---
title: Image Uploads
description: Upload Post images for immediate use with background screening and recover upload progress.
navigationTitle: Images
---

# Image Uploads

`POST /api/v1/posts/:postId/images` receives one inline (`intent=content`) or OG (`intent=og`) image privately and promotes verified received bytes for immediate use while screening runs in the background. [Profile avatar and header uploads](https://docs.subnano.me/v1/api/profile#upload-an-avatar) use the same recoverable operation and become available while checks run in the background.

Returned image URLs are public media, including images placed in paid Markdown. They cannot enforce a purchase entitlement. Use [private ZIP attachments](https://docs.subnano.me/v1/api/attachments) for research kits included in a Post purchase.

## Migration from immediate image URLs

Contract document **2.0.0** introduces a breaking change to image uploads on the existing `/api/v1` routes. The previous 1.0.0 contract returned immediate `avatarUrl`/`headerImageUrl` or Post image URLs. These fields and immediate availability are replaced by the shared operation described below. Other route prefixes and credentials remain the same.

Save an `Idempotency-Key`, accept `202` as an operation receipt, and checkpoint the operation. `safety.received:true` is the durable byte checkpoint; `receiving` can still report false. All image uploads can return `publicUrl` while `safety.state` is `queued` or `checking` and `eligible:false`; use that current URL without waiting for clearance. Poll the existing status action if promotion has not yet produced a URL. A replay may return `200` with the same operation shape; it does not imply completed inspection. Profile uploads require no Post revision; Post uploads retain the revision and target-key fences. Do not retry confirmed bytes because a URL is absent.

All images, including avatars and headers, become public after durable receipt and byte verification. Scanning continues in the background. See [Profile images](https://docs.subnano.me/v1/api/profile#upload-an-avatar) and [registration](https://docs.subnano.me/v1/api/registration).

## Save identity before transfer

Use an active Publishing key with `posts:publish`. Save the owner UUID, target kind/UUID, request key and inline node key before sending bytes. The same owner's rotated active key can recover that operation; expired or revoked credentials cannot.

- `Authorization: Bearer snpk_<keyId>_<secret>`
- Saved `Idempotency-Key`: 1–255 trimmed nonwhitespace characters. One immutable request identity per deliberate upload.
- Post images: `X-Post-Revision` with the latest exact Post `updatedAt`, preserving fractional seconds. Legacy `If-Match` is accepted; both headers must agree if supplied. Profile images require no Post revision.
- Inline images: saved `X-Upload-Target-Key` for this specific current editor node. OG/avatar/header use empty `targetKey`.
- Client-generated multipart boundary and numeric `Content-Length`.

Query `?intent=content|og` takes precedence over the optional multipart `intent` field; omission selects content. Invalid intent returns `422`. Use exactly one multipart file field named `file`, plus only the optional Post intent field. No URLs, other owner IDs, duplicate files or extra parts.

PNG, JPEG, WebP and GIF require matching MIME and file signature. Inline content allows 5 MiB; OG/avatar/header allow 2 MiB. Actual request bytes allow the selected file limit plus 512 KiB overhead. Images must decode completely: at most 32,000,000 canvas pixels per frame and 128,000,000 decoded canvas pixels per upload, with at most 128 distinct source/normalized representations. Animations are inspected frame by frame; there is no independent fixed frame-count allowance. SVG, PDF, Office documents and video are unsupported. Unsupported, malformed or over-budget material never receives partial clearance.

There are ten unfinished image operations and 25 MiB of staged images per Profile; pending physical cleanup counts. ZIP and image transfers share 10/minute, 100/day and 250 MiB/day per Profile, including multipart bytes. New hosted operations also share ten unfinished slots. Inspect saved status before resending bytes and honor `Retry-After`. [Shared rate limits](https://docs.subnano.me/v1/api/rate-limits).

```bash
curl -X POST "https://subnano.me/api/v1/posts/$POST_ID/images?intent=og" \
  -H "Authorization: Bearer $SUBNANO_PUBLISH_KEY" \
  -H "Idempotency-Key: $SAVED_OG_UPLOAD_KEY" \
  -H "X-Post-Revision: $CURRENT_REVISION" \
  -F "file=@/absolute/path/to/og-image.jpg;type=image/jpeg"
```

## Receipt, availability and association

Image POST and individual recovery return `{safety,publicUrl,currentRevision,replayed,actions}`. Receiving/queued/checking returns `202`; terminal or cleared reconciliation returns `200`. Save the receipt before continuing. `x-idempotency-replay:true` marks replay. Owner responses are `private, no-store`.

```json
{
  "safety": {
    "operationId": "11111111-1111-4111-8111-111111111111",
    "targetKind": "og",
    "targetId": "22222222-2222-4222-8222-222222222222",
    "targetKey": "",
    "generation": 1,
    "currentGeneration": 1,
    "associated": false,
    "state": "queued",
    "desired": true,
    "held": false,
    "received": true,
    "eligible": false,
    "policyVersion": "upload-safety-v1",
    "lastError": null,
    "retryAfterSeconds": 5,
    "reuploadAllowed": false
  },
  "publicUrl": null,
  "currentRevision": "2026-10-06T12:00:00.123456Z",
  "replayed": false,
  "actions": {
    "status": "/api/v1/upload-operations/11111111-1111-4111-8111-111111111111",
    "cancel": "/api/v1/upload-operations/11111111-1111-4111-8111-111111111111",
    "associate": "/api/v1/upload-operations/11111111-1111-4111-8111-111111111111/associate"
  }
}
```

`received:true` is a durable byte checkpoint. Recover status without sending those bytes again, including after scanner/provider failure or key rotation. `eligible:true` means received, cleared, desired, not held and the current generation; it does not by itself mean public promotion/association finished. A cleared operation with `publicUrl:null` remains progress. Poll its existing status action; never allocate a replacement to obtain a URL.

Inline completion requires a current `publicUrl`. Insert it only into the still-current matching node/Markdown, then save the current Post body through the existing revision contract. OG/avatar/header additionally require `safety.associated:true`; completed screening is not required to use their current public URL. Reconciliation changes only the current image field. Keep the previously confirmed image until its replacement has a usable URL and association. A late superseded/removed selection cannot restore itself. Inline admission cannot be borrowed for OG; using the same source image requires a separate keyed OG upload.

All images are publicly visible before inspection finishes. Background rejection or exhausted retries remove the exact saved Post references or Profile image field and queue public-copy deletion, even after the caller leaves. Transient scanner/provider failures keep retrying. Downloads and external caches cannot be recalled. ZIP downloads retain their pre-delivery clearance requirement.

## Recover, resume or cancel

| Method | Path                                                                                                            | Effect                                                                       |
| ------ | --------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
| GET    | `/api/v1/upload-operations?postId=<Post UUID>`                                                                  | Owned Post image list, excluding ZIPs; projects status without promotion     |
| GET    | `/api/v1/upload-operations`                                                                                     | Owned Profile image list                                                     |
| GET    | `/api/v1/upload-operations?requestKey=<saved key>&targetKind=<kind>&targetId=<UUID>&targetKey=<saved node key>` | Exact pre-ID recovery and reconciliation; `{operations:[]}` if not yet found |
| GET    | Returned `actions.status`                                                                                       | Reconcile receipt, exact promotion and current association                   |
| POST   | Returned `actions.associate`                                                                                    | Same current reconciliation without new bytes or body                        |
| DELETE | Returned `actions.cancel`                                                                                       | Revoke desired inclusion immediately; does not clear an incident hold        |

For exact recovery use targetKind `inline|og|avatar|header`, the original Post UUID or owner Profile UUID, and the saved inline node key. Omit or send an empty `targetKey` for OG/avatar/header. The exact query returns `{operations:[result]}` rather than the individual-result shape. A reservation-uncertain problem may expose `recovery:{requestKey,statusUrl}` before an operation ID is known; follow that safe URL. Once confirmed it may include `operationId`. ZIPs keep their [attachment actions](https://docs.subnano.me/v1/api/attachments).

Use the current active key on each recovery call. Honor `safety.retryAfterSeconds` with bounded polling (for example 24 calls with at most 30 seconds between calls), then pause honestly and explicitly resume the saved operation. Empty/failed lookup does not prove bytes were never received. Only explicit `reuploadAllowed:true` before durable receipt permits retransferring the same saved intent; elapsed time alone does not.

States are `receiving`, `queued`, `checking`, `cleared`, `rejected`, `failed`, `cancelled`. Stop retrieval/association on rejected, failed, held, cancelled, undesired or obsolete results. `lastError` is a sanitized reason: unsupported/incomplete inspection, unavailable scanner/provider, stale definitions or exhausted attempts. Holds project `screening-blocked`; a match is not an ordinary caller's legal determination. [Errors and recovery](https://docs.subnano.me/v1/api/errors) covers conflicts and safe problem codes. No private path, evidence or provider credential is returned.

Clearance means completed malware and known-material checks under the recorded policy, with coverage limits. It is never a promise of virus-free or CSAM-free content. Shield matches known material; previously unseen abuse can be missed. Remote embeds and old public images are outside the new hosted-upload guarantee: old images remain legacy/unscanned, while new bytes cannot overwrite or borrow a legacy identity.

For current-body/revision coordination see [Posts API](https://docs.subnano.me/v1/api/posts#integration-recipe-post-with-2-images).
