# Integrating imgd.dev

You are integrating imgd.dev into a project. Follow this in order. Step 3
costs money, so handle it according to whatever spending authority you have.

If you have not yet decided whether this service fits, read
https://imgd.dev/evaluate.md first.

## 1. Create an account

    curl -X POST https://imgd.dev/v1/accounts

Returns 201:

    {
      "account_id": "acct_...",
      "api_key": "imgd_...",
      "status": "unpaid",
      "activation_url": "https://checkout.stripe.com/...",
      "storage_quota_bytes": 0,
      "gb_price_usd": "1.00",
      "next_step": "...",
      "docs": "https://imgd.dev/llms.txt"
    }

No email, no captcha, no confirmation step. The account exists immediately and
has zero storage.

## 2. Store the key safely — check the destination FIRST

`api_key` is shown exactly once and is **unrecoverable**. There is no reset,
because there is no email address on the account. If it is lost, the only
remedy is buying a new account.

**Before writing the key anywhere, verify the destination is not version
controlled and not synced.** A real integrator nearly committed theirs because
the config directory they chose happened to be a git repository. Check:

    git -C <target-directory> rev-parse --is-inside-work-tree 2>/dev/null

If that prints `true`, pick somewhere else, or confirm the file is ignored and
that the ignore rule is committed.

Prefer, in order:
1. A secret manager the project already uses.
2. An environment variable, set outside the repository.
3. A file outside the working tree with restrictive permissions.

Never: a tracked config file, a directory that syncs to cloud storage, source
code, or anything you are about to commit. Do not print the key into logs, and
do not echo it back into a chat transcript that gets stored.

## 3. Fund the account

The account cannot upload until it has storage. There are two ways to get it.
If you are not authorised to spend, hand the checkout link to whoever is.

**If you have a coupon code**, redeem it — no payment involved:

    curl -X POST https://imgd.dev/v1/redeem \
      -H "Authorization: Bearer $IMGD_KEY" \
      -d '{"code": "THEIR-CODE"}'

    -> {"redeemed": true, "gb": N, "storage_quota_bytes": ..., "status": "active"}

**Otherwise it costs money.** $1.00 per GB, one-time, stacking:

    curl -X POST https://imgd.dev/v1/credit \
      -H "Authorization: Bearer $IMGD_KEY" \
      -d '{"gb": 1}'

    -> {"checkout_url": "https://checkout.stripe.com/...", "gb": 1,
        "crypto_checkout_url": "https://usevig.com/pay/c/chk_...",
        "crypto_options": [{"network": "base", "chain_id": 8453,
                            "token": "USDC", "decimals": 6,
                            "amount": "1000000", "amount_display": "1",
                            "address": "0x...",
                            "pay": "send exactly 1 USDC ... to 0x... ON BASE ...",
                            "quote_expires_at": <unix>},
                           ...one entry per chain and token...]}

There are two ways to pay that, and which one you can use depends on what you
are:

- `checkout_url` is a hosted card page. It needs a person with a browser and a
  card. If you are not that, hand the link to whoever is.
- `crypto_options` is machine-payable and needs no human at all. **Pick ONE
  entry** and follow its `pay` field, which states the network, the exact
  amount and the address as a single instruction. Paying it is an ordinary token
  transfer from any wallet or exchange — no signature, no contract call, no
  account with anyone.

The list spans several chains (base, optimism, arbitrum, polygon, and ethereum
on larger purchases) and several tokens, so pay from whichever chain already
holds your stablecoins. Each entry is a different destination, not a different
formatting of the same one.

Three things about the crypto option that will otherwise cost you money:

1. **Each entry's address exists on that entry's network and nowhere else.**
   Addresses are derived per chain, so sending on a different chain than the one
   in the SAME entry is unrecoverable — by you, by us, by anyone. Never mix the
   address from one entry with the network of another, and never infer a chain
   from the token name. Copy `network`, `address` and `amount` from one
   entry together.
2. **`amount` is already in the token's smallest unit** and `decimals` tells
   you which unit that is (`amount_display` is the same figure for a human).
   Send `amount` verbatim rather than recomputing it: DAI uses 18 decimals
   where USDC uses 6, and assuming 6 for DAI underpays by a factor of 10^12.
3. **The quote expires** at `quote_expires_at`, about 15 minutes out. Ask for
   the checkout when you are ready to pay, not before.

Sending less than the quoted amount does **not** buy storage. Sending more marks
it paid without crediting the excess, so send the exact amount.

Either way, a checkout is not a purchase — do not treat the account as funded
until step 4 confirms it. Payment is detected on-chain, which takes a few
seconds after the transfer confirms.

## 4. Verify funding before uploading

    curl https://imgd.dev/v1/me -H "Authorization: Bearer $IMGD_KEY"

Returns:

    {
      "account_id": "acct_...",
      "status": "active",
      "paid": true,
      "uploads": 0,
      "storage_bytes": 0,
      "storage_quota_bytes": 1073741824,
      "storage_remaining_bytes": 1073741824,
      "storage_used_pct": 0,
      "storage_human": "0 bytes of 1 GB used",
      "abuse_strikes": 0,
      "strike_limit": 3
    }

Proceed only when `paid` is true and `storage_remaining_bytes` is above zero.
An upload before that returns 402 carrying both payment options in the body, so
you can pay from the error itself and retry.

Check `storage_remaining_bytes` before a large upload; over-quota returns 402
`quota_exceeded` rather than storing a partial result.

## 5. Upload

    curl -X POST https://imgd.dev/v1/upload \
      -H "Authorization: Bearer $IMGD_KEY" \
      -F "file=@image.png"

Raw bytes work too — POST the body with an `image/*` Content-Type instead of a
multipart form.

Returns 202:

    {
      "hash": "<sha256 hex>",
      "url": "https://i.imgd.dev/i/<sha256 hex>",
      "status": "processing",
      "mime": "image/png",
      "bytes": 8795,
      "width": 760,
      "height": 300,
      "alt_text": null,
      "unpublish_at": null,
      "note": "..."
    }

Identical bytes that already exist return **200** with `"deduplicated": true`
instead, and are not billed again.

## 6. Confirm it is live

    curl https://imgd.dev/v1/images/<hash> -H "Authorization: Bearer $IMGD_KEY"

`status` moves from `processing` to one of `live`, `review` or `blocked`.
`alt_text` is populated once the check completes — it is generated for you and
costs nothing extra. Use it.

## Operational facts that will otherwise cost you time

- **The URL works immediately. Do not build a wait loop.** From the moment
  upload returns, the URL serves a blurred placeholder (response header
  `X-Imgd-Moderation: processing-blurred`) and sharpens to the real image on
  its own when the check finishes. Polling before using the link is wasted
  work. If you specifically want a machine-readable status from that URL
  instead of image bytes, send `Accept: application/json` and you will get a
  409 status body while it is still processing.
- **Uploads are idempotent.** The URL is the sha256 of the bytes. Retry a
  failed or ambiguous upload freely: same bytes, same URL, stored once, billed
  once, checked once.
- **Re-uploading identical bytes REPLACES any existing expiry.** If the first
  upload set a 24-hour window and a later one sets none, the link becomes
  permanent. Send the window on every upload of those bytes if you want it
  kept.
- **`unpublish_after_hours` must be greater than zero.** Omitting it means
  permanent. Zero is rejected rather than silently meaning permanent, so a
  computed zero surfaces as an error instead of a link that never expires.
- **`review` is terminal.** It means the automated check was not confident, so
  the image is served blurred. It does **not** resolve on its own and polling
  will never change it. It carries no penalty against the account.
- **Unpublished images still consume quota.** The bytes are retained so the
  same URL can come back. `DELETE` is what frees space.
- **Storage figures are recomputed on write.** If `/v1/me` looks stale right
  after a delete, re-read it; `GET /v1/images` is the source of truth for what
  is stored.

## Listing images

    curl "https://imgd.dev/v1/images?limit=25" -H "Authorization: Bearer $IMGD_KEY"

Returns exactly this shape — do not write defensive code for alternatives:

    {"images": [...], "count": 1, "next_cursor": null}

Each entry: `hash`, `url`, `mime`, `bytes`, `width`, `height`, `status`,
`alt_text`, `filename`, `created_at`, `unpublish_at`, `published`.

`?limit=` is 1-100, default 25. Page with `?cursor=<next_cursor>` and keep
going while `next_cursor` is not null.

## Errors

Every 4xx body has this shape:

    {"error": "invalid_api_key",
     "fix": "this key is not recognized; create a new account with POST /v1/accounts"}

`fix` is written to be actionable. **Surface it to the user verbatim.** Do not
paraphrase it into something vaguer, and do not swallow it and retry blindly.

Ones you will actually hit:

- `402 payment_required` — the account has no storage. Body carries both a
  `checkout_url` for a human and machine-payable `crypto_options`, so you can
  settle it from the error itself. Go back to step 3.
- `402 quota_exceeded` — out of space. Body carries `topup_url` and a
  `suggested_gb`.
- `429 rate_limited` — over 60 uploads/minute. Wait for the next minute.
- `400` — bad file type or a malformed parameter. Read `fix`.

## If an image is blocked

A `blocked` status means the automated check flagged it against the acceptable
use policy; the bytes are removed and the link is dead. `GET /v1/images/<hash>`
returns a `moderation` object explaining the state and how to contest it.

**Do not silently retry, and do not re-upload the same bytes** — the result
will be the same, and repeated violations add strikes (3 suspends the account).

Tell the user what happened and that it is contestable: email abuse@imgd.dev
with the hash and a person will review it. That is the correct response to a
verdict you think is wrong.

## Verification checklist

Prove the integration end to end before reporting success:

1. `GET /v1/me` returns `"paid": true` and non-zero `storage_remaining_bytes`.
2. An upload returns 202 with a `hash` and a `url`.
3. Fetching that `url` returns image bytes with a 200 immediately.
4. `GET /v1/images/<hash>` eventually shows `"status": "live"` and a non-null
   `alt_text`.
5. `GET /v1/images` includes the hash, and `storage_bytes` in `/v1/me` has
   grown.
6. Re-uploading the identical file returns `"deduplicated": true` and does not
   increase `storage_bytes`.
7. The key is stored outside version control, and you have confirmed that.

Full reference: https://imgd.dev/llms.txt and https://imgd.dev/openapi.json
