Guide · Upload flow

Create a session, send the bytes, wait for ready.

An upload session tells you where to put one file. The bytes go straight to storage, or through the API's upload proxy when you ask for proxy mode. img2site then sanitizes the image and publishes it under the session's file_slug. With JavaScript, the upload router and browser SDK run this whole flow for you; this page is the same flow over plain HTTP.

1. Create the session

POST /v1/projects/{slug}/uploads with an API key that has files:write. The body takes content_type (image/jpeg, image/png, image/webp, image/avif, image/gif, image/tiff, image/bmp or image/svg+xml), size_bytes (the exact size), and optionally file_slug, sha256 (hex, checked after upload), mode (direct or proxy) and parts.

image/svg+xml is accepted only when SVG uploads are enabled on the deployment; otherwise it answers 400 unsupported_content_type. An SVG may be at most 2 MB: a larger size_bytes answers 413 file_too_large at create.

file_slug is lowercased and must match ^[a-z0-9][a-z0-9._-]{0,127}$; without one the upload id is used. Uploading to an existing slug replaces that file and increases its revision. Sessions expire after 15 minutes.

Create

curl -X POST https://api.img2.site/v1/projects/my-store/uploads \
  -H "X-Api-Key: $IMG2SITE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "file_slug": "hero", "content_type": "image/jpeg", "size_bytes": 482133 }'

201 Created
{
  "upload_id": "upl_…",
  "project_slug": "my-store",
  "file_slug": "hero",
  "status": "created",
  "mode": "direct",
  "content_type": "image/jpeg",
  "size_bytes": 482133,
  "error_code": null,
  "expires_at": "2026-09-24T12:15:00.000Z",
  "direct": {
    "put_url": "https://…presigned storage URL…",
    "headers": { "content-type": "image/jpeg", "content-length": "482133" },
    "expires_at": "2026-09-24T12:15:00.000Z"
  },
  …
}

Send and complete

# Send the bytes to direct.put_url with exactly the returned headers.
curl -X PUT "$PUT_URL" \
  -H "Content-Type: image/jpeg" \
  --data-binary @hero.jpg

# Then complete the session (optional for a single PUT, makes it immediate).
curl -X POST https://api.img2.site/v1/projects/my-store/uploads/$UPLOAD_ID/complete \
  -H "X-Api-Key: $IMG2SITE_API_KEY"

2. Send the bytes

The response carries exactly one of three targets:

  • direct: PUT the file to put_url with the returned headers. The URL signs the content type and length, so send exactly those.
  • multipart (when parts > 1): PUT each part to its URL, then complete with the part ETags. Parts are at least 5 MiB except the last.
  • proxy (when mode is proxy): PUT the file to the put_url the session returned (its host depends on the deployment; do not hard-code it) with Content-Length equal to size_bytes and the session's Content-Type. Use it when a network blocks the storage host. It completes the session itself (204).

POST …/uploads/{id}/complete is required for multipart, with every part of the session (part_number 1 to N once each, with its ETag); a partial list answers 400 invalid_parts and the session stays completable. It is optional otherwise: a background job completes sessions whose object has arrived within a few minutes. Calling it checks the object is there with the declared size and makes completion immediate. Completion is idempotent.

3. Wait for ready

Poll GET /v1/projects/{slug}/uploads/{id} every second or two, or subscribe to the upload.ready webhook. When the session is ready, the file is published: read it with GET /v1/projects/{slug}/files/{fileSlug} and build delivery URLs from its slug (see delivery URLs).

Publishing sanitizes the image: it is re-encoded in the same format (BMP and TIFF become PNG), orientation is applied and embedded metadata is removed. Animated images and HEIC are refused. Size limits: JPEG and PNG images (8 bits per channel) up to 128 megapixels; GIF, TIFF, BMP, lossy WebP without transparency and 16-bit images up to 50 megapixels; lossless WebP up to about 49 megapixels (about 32 with transparency); lossy WebP with transparency up to about 38 megapixels; AVIF up to about 28 megapixels. Larger images are refused as too costly to process.

SVGs are sanitized: scripts, event handlers, external references, animation and editor metadata are removed. SVGs with embedded bitmap images or a DOCTYPE are refused, and so are very complex ones (too many elements, layers, text or styles). The sanitized SVG is stored and served as vector on originals; transforms rasterize it on a grid whose long edge is at least 2048 px (up to 2048 × 2048 in area, default output PNG), and the file's width/height report that grid.

Poll and read the file

curl https://api.img2.site/v1/projects/my-store/uploads/$UPLOAD_ID \
  -H "X-Api-Key: $IMG2SITE_API_KEY"

{ "upload_id": "upl_…", "file_slug": "hero", "status": "ready", "ready_at": "…", "error_code": null, … }

# The published file:
curl https://api.img2.site/v1/projects/my-store/files/hero \
  -H "X-Api-Key: $IMG2SITE_API_KEY"

{ "file_slug": "hero", "status": "ready", "revision": 1, "width": 2400, "height": 1600,
  "url": "https://my-store.i2scd.net/originals/f/hero", … }

Session statuses

Upload session statuses
StatusMeaningNext action
createdThe session exists; img2site is waiting for the bytes.Send the bytes before expires_at, then complete.
uploadedThe bytes arrived and the file is queued for processing.Keep polling.
processingThe image is being sanitized and published.Keep polling; this usually takes seconds.
readyThe file is published and served on the delivery host.Use the file slug in delivery URLs.
failedProcessing refused the file; error_code says why.Fix the file and upload again with a new session.
expiredNo bytes arrived before expires_at.Create a new session.

Multipart

// "parts": 3 in the create body selects S3 multipart (direct mode only)
"multipart": {
  "s3_upload_id": "…",
  "part_size": 8388608,
  "parts": [ { "part_number": 1, "url": "…" }, { "part_number": 2, "url": "…" }, … ],
  "complete_url": "/v1/projects/my-store/uploads/upl_…/complete"
}

// PUT each part to its url, keep each response's ETag, then:
POST /v1/projects/my-store/uploads/upl_…/complete
{ "parts": [ { "part_number": 1, "etag": "\"…\"" }, { "part_number": 2, "etag": "\"…\"" } ] }

Processing errors

error_code values of a failed upload session
error_codeCause
unsupported_media_typeNot a supported image format (for example HEIC, compressed SVG, or SVG with embedded images or a DOCTYPE).
animated_not_supportedThe image has more than one frame.
image_too_largeAbove the size limit for its format (see above), or too costly to process: for example a palette PNG that becomes too large once re-encoded.
processing_timeoutProcessing a very large image took too long three times; upload again later, or a smaller image.
decode_failedThe file is corrupt or truncated.
ingest_size_mismatchThe stored object is not size_bytes long.
sha256_mismatchThe bytes do not match the declared sha256.
file_too_largeAbove the upload size limit.
ingest_object_missingThe bytes never arrived.
processing_abandonedProcessing failed repeatedly; upload again.
session_expiredNo bytes arrived before expires_at (status expired); create a new session.

Limits and refusals

  • An upload that would exceed the project's storage limit is refused at create with 403 storage_quota_exceeded.
  • A suspended project refuses uploads with 403 project_suspended.
  • A size_bytes above the upload limit is refused at create with 413 file_too_large (details.max_bytes); for image/svg+xml the limit is 2 MB.
  • Completing a session whose object is missing answers 409 ingest_object_missing; a wrong size answers 409 ingest_size_mismatch.
  • A multipart complete with missing, repeated or not-yet-uploaded parts (or a part other than the last under 5 MiB) answers 400 invalid_parts and leaves the upload completable; send the full list.
  • The proxy PUT answers 401 invalid_signature for an altered URL and 401 signature_expired once the session's URL expired. It accepts cross-origin browser requests (CORS for any origin; the signed URL is the credential).
  • A file blocked by moderation stays blocked when its slug is uploaded again.
  • The full list of codes is on the errors page.