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.