API Reference

Every snapnedit endpoint is described by a single OpenAPI 3.1 document, served live at:

https://snapnedit.com/api/openapi.json

It is generated, not hand-written: every request schema in it is the same zod schema the api validates that request with, and every response schema is pinned to a real response by a test. So the document cannot drift from the running service — if a route changes its contract, the document changes with it.

Point any OpenAPI toolchain at that URL:

# Read it
curl -s https://snapnedit.com/api/openapi.json | jq '.paths | keys'

# Lint it
npx @redocly/cli lint https://snapnedit.com/api/openapi.json

# Generate a client in your language
npx @openapitools/openapi-generator-cli generate \
  -i https://snapnedit.com/api/openapi.json \
  -g python \
  -o ./snapnedit-python

The same bytes are committed in the repository at docs/openapi.json, so you can generate a client offline and diff the document between releases.

Authentication

Three credentials, all sent as Authorization: Bearer <token>. The api reads no cookies.

| Scheme | What it is | Billed | | --- | --- | --- | | ApiKey | A secret sk_… key created in your dashboard. The only credential that may manage storage destinations or point a job at an external URL. | Yes, in credits | | EmbedToken | A short-lived signed token for one embedded-editor session, from POST /embed/sessions or POST /embed/tokens. Can be scoped to an end user, a credit cap and an operation allowlist. | Yes, like an API key | | Session | A website session token, minted by the site for a signed-in visitor. | No — site usage is free |

Uploading and running jobs also works with no credential at all, metered by a small daily free tier per IP.

See API authentication for how to create and rotate a key.

Errors

Every failure has the same body:

{ "error": { "code": "invalid_input", "message": "params.factor: Invalid option" } }

Branch on code, never on message. The closed set is:

invalid_input, unsupported_mime, too_large, not_found, input_fetch_failed, provider_failed, provider_exhausted, rate_limited, bot_check_failed, unauthorized, forbidden, payment_required, internal.

Two deliberate behaviours worth knowing before you write a client:

  • A job id or a destination id that belongs to someone else answers 404, never 403 — an id must not become an existence oracle.
  • A presigned destination URL and its headers are never echoed back by any endpoint or webhook. A presigned URL is a bearer credential for your bucket.

Credits

Each operation has a fixed integer cost, published in the document twice: as a map on the GET /operations operation (x-credit-cost), and per operation on its params schema (components.schemas["OperationParams.<id>"]["x-credit-cost"]).

A cost of 0 means the operation is free (today: resize-image). A cache hit is never billed, and a job that fails terminally is refunded.

Endpoints

Operations

The catalog of image operations and their params contracts.

| Endpoint | What it does | | --- | --- | | GET /operations | List every operation and its params contract |

Uploads

Registering image bytes as an asset a job can consume.

| Endpoint | What it does | | --- | --- | | POST /uploads | Register an asset and get a presigned PUT url | | POST /uploads/{assetId}/confirm | Confirm uploaded bytes and backfill the content hash |

Jobs

Creating and polling image jobs.

| Endpoint | What it does | | --- | --- | | POST /jobs | Create an image job | | GET /jobs/{id} | Poll a job |

Destinations

Saved S3-compatible buckets: CRUD, connectivity test, and signed exports.

| Endpoint | What it does | | --- | --- | | GET /destinations | List saved storage destinations | | POST /destinations | Save an S3-compatible bucket | | PATCH /destinations/{id} | Update a saved destination | | DELETE /destinations/{id} | Delete a saved destination | | POST /destinations/{id}/test | Prove the credentials can write | | POST /destinations/{id}/presign | Mint a one-shot signed PUT into your own bucket |

Embed

Minting short-lived tokens for the embeddable editor.

| Endpoint | What it does | | --- | --- | | POST /embed/sessions | Exchange a publishable key for an embed token | | POST /embed/tokens | Mint a scoped embed token from your server |

Designs

Compiling and rendering declarative designs.

| Endpoint | What it does | | --- | --- | | POST /designs | Compile a declarative design spec into editor documents | | POST /designs/render | Render a design to image bytes or a PDF |

Meta

This document.

| Endpoint | What it does | | --- | --- | | GET /openapi.json | This document |

Not in the document, deliberately: the signed storage-proxy URLs the api mints for you (/_local/*), the editor's stock-photo proxy (/stock/*), the editor's RAW-develop bridge (/develop-raw) and the collaboration websocket (/collab). Those are internals of the website, not a product surface, and they can change without notice.

One shape difference to watch

POST /jobs nests the job status under a status key; GET /jobs/{id} flattens it to the top level:

// POST /jobs -> 202
{ "jobId": "…", "status": { "state": "queued" }, "input": { "kind": "asset" }, "destination": null, "delivery": null }

// GET /jobs/{id} -> 200
{ "state": "succeeded", "outputAssetId": "…", "download": { "url": "…", "expiresAt": "…" },
  "input": { "kind": "asset" }, "destination": null, "delivery": null }

Both carry the same input / destination / delivery envelope. The document describes both shapes exactly, so a generated client gets this right for free.

Building an SDK

If you are writing a snapnedit client in a language we do not ship one for, the OpenAPI document gives you the surface — and the repository ships a conformance suite that gives you the behaviour: a runnable local server and a language-neutral list of scenarios every client is expected to pass, including cache hits, credit debits and refunds, and the webhook signature algorithm with a fixed test vector.

Start with docs/sdk-conformance.md in the repository.