# imgd.dev > Image hosting your agent can sign up for. No email, no captcha, no dashboard. > One API call creates an account. $1 = 1 GB of storage, one-time, stacking. ## Guides Markdown tutorial index: https://imgd.dev/blog/index.md HTML tutorial index: https://imgd.dev/blog/ ## Pricing $1 buys 1 GB of permanent storage. Payments are one-time and stack: five $1 payments give you 5 GB. There is no subscription and no metered balance to run down — you buy space, you keep it. There is no free tier. Creating an account is free and instant, but it starts with no storage and cannot upload until you buy some. An upload attempted before payment returns 402 payment_required with a checkout URL in the body. If you were given a coupon code, redeem it instead of paying: curl -X POST https://imgd.dev/v1/redeem \ -H "Authorization: Bearer $IMGD_KEY" \ -d '{"code": "YOUR-CODE"}' A code adds its storage immediately and works at most once per account. ## Quickstart (three calls, no human required) 1. Create an account. No email, no captcha. curl -X POST https://imgd.dev/v1/accounts Returns {"api_key": "imgd_...", "activation_url": "https://checkout.stripe.com/..."}. Save the key: it is shown exactly once and is never recoverable. Store it outside your repository — an environment variable or a secret manager. Never put it in a config directory that is version-controlled or synced, which is the easiest way to leak it. The account has no storage until you complete step 2. 2. Buy storage. $1.00 per GB, one-time, stacks. curl -X POST https://imgd.dev/v1/credit \ -H "Authorization: Bearer $IMGD_KEY" \ -d '{"gb": 1}' Returns {"checkout_url": "https://checkout.stripe.com/...", "gb": 1}. Works for new AND already-paid accounts — top up any time. 3. Upload. curl -X POST https://imgd.dev/v1/upload \ -H "Authorization: Bearer $IMGD_KEY" \ -F "file=@photo.jpg" Returns 202 {"hash","url","status":"processing"}. Moderation runs async and usually finishes in a few seconds. Poll GET /v1/images/:hash for the verdict and the generated alt_text. ## Endpoints POST /v1/accounts create account, returns api key + activation url optional body {"coupon":"CODE"} to start with storage POST /v1/redeem redeem a coupon code; body {"code":"CODE"} POST /v1/images/:hash/publish set/clear an expiry, or republish POST /v1/images/:hash/unpublish hide the link now, keep the bytes POST /v1/credit buy storage; body {"gb": N}, returns a checkout url GET /v1/me quota, usage, remaining bytes, strike count POST /v1/upload multipart (field: file) or raw image bytes GET /v1/images list your images. Returns {"images":[...],"count":N,"next_cursor":N|null}. ?limit=1-100 (default 25), ?cursor=. Keep paging while next_cursor is not null. GET /v1/images/:hash metadata for one image, including alt_text DELETE /v1/images/:hash unlink an image from your account GET /i/:hash serve the image; ?w=800&fmt=webp to transform GET /health liveness GET /openapi.json machine-readable spec GET /evaluate.md paste-at-an-agent: is this service a fit? GET /integrate.md paste-at-an-agent: step-by-step integration ## Things worth knowing - Uploads are content-addressed by sha256. The same bytes always return the same hash and url, are never billed twice, and are never re-scanned by the moderation model. Retry freely; it is idempotent. - Links are permanent unless you ask otherwise. Add unpublish_after_hours (or unpublish_at) to an upload and the link stops serving at that time. The bytes are kept, so POST /v1/images//publish brings the SAME url back, with or without a new window. Retained bytes still count toward your storage quota; DELETE is what actually frees space. - Storage quota counts every image you keep, including one that deduplicated against another account's copy. Delete an image and its bytes come straight back. Check GET /v1/me before a large upload: storage_remaining_bytes tells you whether it will fit. - Over quota returns 402 quota_exceeded with a topup_url and a suggested_gb. Buy the storage or delete something, then retry the same bytes. - alt_text is generated during moderation and returned for free. Use it. - Every 4xx body has a "fix" field in plain language saying what to do next. Read it before retrying. - A fresh upload is status=processing, then live | review | blocked. The url works immediately: while the check runs it serves a heavily blurred placeholder (header X-Imgd-Moderation: processing-blurred), which sharpens to the real image once the check clears. Send Accept: application/json to that url instead if you want a 409 status body to poll. Nothing recognisable is served before a verdict, and a blocked image's link goes dead. - Max upload 20MB. Accepted: jpeg, png, gif, webp, avif. - Re-uploading identical bytes REPLACES your expiry window with whatever the new upload asks for, including no window at all (which makes the link permanent again). Send unpublish_after_hours on every upload of the same bytes if you want the window kept. - unpublish_after_hours must be greater than 0. Omitting it means permanent; 0 is rejected rather than silently meaning permanent, so a computed 0 is a bug you hear about instead of a link that never expires. - Storage figures are recomputed on write, not on read. If GET /v1/me looks wrong immediately after a delete, re-read it; the claims in GET /v1/images are the source of truth. ## Moderation, and what it means for you Every upload is classified by an automated check. Verdicts: - live — public, nothing more to do. - review — the check was not confident, so the image is served BLURRED at its normal url. This is a terminal state: it does not clear on its own, and polling will not change it. It is NOT an accusation against your account and carries no strike. If it is wrong, email abuse@imgd.dev with the hash and a person will look. - blocked — the bytes were deleted and the link is dead. A blocked or review verdict is reported in GET /v1/images/ under "moderation", and every one of them is contestable at abuse@imgd.dev with the hash — a person will look at it. Blocking requires TWO independent checks to agree. A single bad read holds the image in review instead, and only a corroborated block adds an abuse strike. 3 strikes suspends the account. ## Recent changes Behaviour changes, newest first. Dates are ISO. - 2026-08-07 — Screenshots of terminals, code and logs are no longer misread as violent because of the words in them. Blocking now needs two independent checks to agree, and a single check can no longer cost an abuse strike. Blocked and review responses state the outcome and how to contest it. - 2026-08-07 — A fresh upload's url works IMMEDIATELY, serving a blurred placeholder while the check runs instead of returning 409. If you built a wait-then-fetch loop against the old behaviour, you no longer need it. Send Accept: application/json to keep getting the 409 status body. - 2026-08-06 — Added scheduled unpublish and republish: unpublish_after_hours and unpublish_at on upload, plus POST /v1/images//publish and /unpublish. Bytes are retained while unpublished and still count toward quota. ## Contact Abuse, takedowns, and contesting a moderation verdict: abuse@imgd.dev