01 Getting started
Overview
Snap Fix is an outcome-oriented image transformation API. A client sends an image plus a natural-language instruction — “crop into a circle,” “keep it under 5 MB” — and Snap Fix plans the processing steps, runs them, and re-verifies the real output against every measurable constraint before returning it. Payment only settles on a verified success.
| Method | Path | Description |
|---|---|---|
| POST | /v1/transform | Transform an image from an instruction. x402-paid. |
| GET | /v1/jobs/:id | Get a job's status and metadata. |
| GET | /v1/jobs/:id/download | Download a completed job's output. |
02 Getting started
Authentication (x402)
POST /v1/transform is protected by the x402 protocol — no API key, no account, no subscription. Pricing, network, and facilitator are configured server-side, kept separate from the transformation logic.
Nothing is charged unless the transformation actually succeeds and passes constraint verification. The gate verifies your payment header (checks you can pay — no funds move) before your request is processed; it only settles the real on-chain charge after a successful (< 400) response. Validation failures, ambiguous or unsupported instructions, execution errors, and unmet constraints are all free. The API negotiates x402 version 2; the v1 X-PAYMENT header is accepted as a fallback.
curl -i -X POST https://api.snapfix.media/v1/transform \
-F "instruction=convert to jpeg" \
-F "file=@photo.png"PAYMENT-REQUIRED: <base64-encoded JSON>
// decoded, the header carries the payment requirements:
{
"accepts": [{
"scheme": "exact",
"network": "eip155:8453",
"maxAmountRequired": "50000",
"resource": "https://api.snapfix.media/v1/transform",
"payTo": "0x...",
"asset": "0x...",
"description": "Outcome-verified image transformation"
}]
}Sign a payment authorization for those requirements and retry the identical request with a PAYMENT-SIGNATURE header (an x402-aware client does this for you). A valid payment authorization lets it through to processing — it's only settled once the response succeeds, and the settlement receipt (with the on-chain transaction hash) comes back in the PAYMENT-RESPONSE response header.
curl -i -X POST https://api.snapfix.media/v1/transform \
-H "PAYMENT-SIGNATURE: <base64-encoded payment payload>" \
-H "Idempotency-Key: 3f29f6c2-...-a-client-generated-uuid" \
-F "instruction=convert to jpeg" \
-F "file=@photo.png"import { wrapFetchWithPayment } from "@x402/fetch";
import { privateKeyToAccount } from "viem/accounts";
const fetchWithPay = wrapFetchWithPayment(fetch, privateKeyToAccount(PRIVATE_KEY));
const form = new FormData();
form.set("file", new Blob([bytes], { type: "image/png" }), "photo.png");
form.set("instruction", "convert to jpeg");
const res = await fetchWithPay("https://api.snapfix.media/v1/transform", {
method: "POST",
body: form,
headers: { "Idempotency-Key": crypto.randomUUID() },
});import uuid
import requests
from x402.clients.requests import x402_requests
session = x402_requests(requests.Session(), private_key=PRIVATE_KEY)
with open("photo.png", "rb") as f:
response = session.post(
"https://api.snapfix.media/v1/transform",
headers={"Idempotency-Key": str(uuid.uuid4())},
data={"instruction": "convert to jpeg"},
files={"file": f},
)
with open("output.jpg", "wb") as out:
out.write(response.content)Any x402-aware client works the same way — the snippets above illustrate the shape of the call; check your chosen library's current docs for exact function names.
03 Making a request
Transform an image
Two input modes:
Multipart upload (multipart/form-data) — shown above:
| Field | Required | Description |
|---|---|---|
file | Yes | The image — jpeg, png, webp, gif, tiff, avif — detected from the actual bytes, not the filename. |
instruction | Yes | Natural-language description of the desired outcome. |
JSON + remote URL (application/json):
{
"instruction": "resize to 1200x630 and convert to webp",
"imageUrl": "https://example.com/photo.jpg"
}The remote URL is fetched with SSRF defense-in-depth: https-only, private/loopback/link-local/reserved IP literals rejected, redirects re-validated per hop (capped), streaming size cap, fetch timeout.
Optional headers:
| Header | Description |
|---|---|
Idempotency-Key | See Idempotency below. Strongly recommended. |
Accept: application/json | Returns full JSON metadata + a download link instead of the binary. See Success responses. |
Optional query param ?mode=async plans synchronously — so a rejected instruction still returns its real error immediately and is never charged as a false 202 — then returns 202 Accepted with a Link to GET /v1/jobs/:id for polling, instead of blocking for the result:
curl -s -X POST "https://api.snapfix.media/v1/transform?mode=async" \
-H "PAYMENT-SIGNATURE: <payment>" \
-F "instruction=convert to jpeg" -F "file=@photo.png"
# -> 202 {"jobId":"3f29f6c2-...","status":"processing"}
curl -s https://api.snapfix.media/v1/jobs/3f29f6c2-...
# poll until "status" is "completed" (or "failed"), then:
curl -sO https://api.snapfix.media/v1/jobs/3f29f6c2-.../downloadImages are fast enough that this is rarely needed — the seam exists for future, slower media types.
04 Making a request
Idempotency
Send an Idempotency-Key header — any client-generated unique string — with every request you might need to retry.
| Retry lands on… | Result |
|---|---|
| A completed job, same key | The cached result is returned immediately, without re-entering the payment gate at all. A true retry can never charge you twice. |
| A job still processing (within a short staleness window) | 409 Conflict with Retry-After and a Link to GET /v1/jobs/:id. |
| A failed or rejected job | Reclaimed and reprocessed — a prior failed attempt was never charged, so this is a normal, free retry. |
No Idempotency-Key header | No dedup — each request is independent. A client responsibility, not a server-enforced requirement. |
05 Making a request
Success responses
Default — the binary, directly:
Content-Type: image/jpeg
Content-Disposition: attachment; filename="snapfix-<job-id>.jpeg"
X-Job-Id: 3f29f6c2-...
X-Snapfix-Width: 1200
X-Snapfix-Height: 630
X-Snapfix-Format: jpeg
Link: </v1/jobs/3f29f6c2-...>; rel="metadata"
<binary bytes>With Accept: application/json — full metadata + a download reference instead of inline binary:
{
"jobId": "3f29f6c2-...",
"status": "completed",
"instruction": "resize to 1200x630, convert to webp, keep under 500KB",
"plan": {
"status": "ok",
"operations": [
{ "op": "resize", "width": 1200, "height": 630, "fit": "cover" },
{ "op": "convertFormat", "format": "webp" }
],
"constraints": { "exactWidth": 1200, "exactHeight": 630, "format": "webp", "maxBytes": 500000, "forbidUpscaling": true }
},
"validation": {
"overallPassed": true,
"checks": [
{ "constraint": "exactWidth", "required": 1200, "measured": 1200, "passed": true },
{ "constraint": "exactHeight", "required": 630, "measured": 630, "passed": true },
{ "constraint": "format", "required": "webp", "measured": "webp", "passed": true },
{ "constraint": "maxBytes", "required": 500000, "measured": 421887, "passed": true }
],
"iterations": 3
},
"output": {
"contentType": "image/webp",
"format": "webp",
"byteSize": 421887,
"width": 1200,
"height": 630,
"hasAlpha": false,
"filename": "snapfix-3f29f6c2-....webp"
},
"downloadUrl": "/v1/jobs/3f29f6c2-.../download"
}06 Making a request
Error responses
Every failure is a structured { "error": { "code", "message", "unmetRequirement"? } } body. None of these are charged.
| Status | When | Example error.code |
|---|---|---|
| 400 | Malformed request — bad multipart, bad JSON, missing fields, SSRF-blocked/unreachable remote URL | invalid_request, remote_fetch_blocked |
| 402 | No or invalid payment | x402-native 402 body, not the error envelope above |
| 409 | An Idempotency-Key retry landed while the original request is still processing | already_processing |
| 413/415 | Input too large, or not a recognized image format | input_too_large, unsupported_media_type |
| 422 | The planner rejected the instruction, or the output failed a measurable constraint after iteration | ambiguous, unsupported_operation, unsafe, impossible_constraints, unclear_target, input_mismatch, constraint_unmet |
| 500/502 | Internal error, or the container failed unexpectedly | internal_error, execution_failed |
| 504 | The container exceeded its execution time limit | execution_timeout |
{
"error": {
"code": "ambiguous",
"message": "The instruction doesn't specify what outcome is wanted.",
"unmetRequirement": null
}
}{
"error": { "code": "constraint_unmet", "message": "Output did not satisfy all requested constraints after iteration" },
"validation": {
"overallPassed": false,
"checks": [
{ "constraint": "maxBytes", "required": 5000, "measured": 41230, "passed": false }
]
}
}A constraint that can't be satisfied even after iteration is reported this way — never returned as a silently oversized file.
07 Jobs
Get job status
GET /v1/jobs/:id — metadata for any job, queryable regardless of status:
{
"jobId": "3f29f6c2-...",
"status": "completed",
"instruction": "convert to jpeg",
"plan": { "...": "..." },
"validation": { "...": "..." },
"error": null,
"createdAt": "2026-01-01T00:00:00.000Z",
"completedAt": "2026-01-01T00:00:02.000Z",
"downloadUrl": "/v1/jobs/3f29f6c2-.../download"
}404 (job_not_found) if the id doesn't exist.
08 Jobs
Download output
GET /v1/jobs/:id/download streams the output binary for a completed job. 410 Gone (result_expired) once the R2 lifecycle TTL has removed it — outputs are short-lived by design. See Limits.
09 Reference
Limits
| Limit | Default | Configured via |
|---|---|---|
| Max input size | 25 MB | MAX_INPUT_BYTES |
| Max output size | 25 MB | MAX_OUTPUT_BYTES |
| Max input pixels (decompression-bomb guard) | 50 MP | MAX_INPUT_PIXELS |
| Container execution timeout | 45s | CONTAINER_EXEC_TIMEOUT_MS |
| Remote URL fetch timeout | 10s | REMOTE_FETCH_TIMEOUT_MS |
| Remote URL max redirects | 5 | REMOTE_FETCH_MAX_REDIRECTS |
| Result retention (R2 TTL) | 1 hour | JOB_RESULT_TTL_SECONDS |
| Idempotency staleness window | 90s | IDEMPOTENCY_STALE_MS |
10 Reference
Supported operations
The planner chooses from these 25 operations to satisfy an instruction:
| Operation | Notes |
|---|---|
resize | Width/height/fit; respects forbidUpscaling (default true) |
convertFormat | jpeg, png, webp, avif |
crop | Explicit x/y/width/height rectangle, or per-edge pixel insets |
cropToAspectRatio | Crop (never stretch) to a target aspect ratio |
cropCircle | Centered circle crop; requires an alpha-capable target format |
rotate | Arbitrary degrees |
autoOrient | Applies EXIF orientation, then drops it |
removeBackground | Self-hosted ONNX (u2netp) — no third-party API call |
trim | Removes a uniform-color border |
stripMetadata | Explicit no-op marker — metadata is stripped by default on every operation |
compress | The target for maxBytes iteration |
adjust | Brightness/contrast/saturation (multipliers around 1.0) and gamma (1.0–3.0) |
grayscale | Convert to grayscale |
invert | Invert colors |
sepia | Sepia tone |
normalize | Auto-levels / contrast stretch |
blur | Gaussian blur (whole image) |
sharpen | Sharpening (whole image) |
flattenBackground | Flatten transparency onto a solid background color |
threshold | Pure black/white at a cutoff (1–254, default 128) |
pad | Extends the canvas to an aspectRatio or explicit dimensions, filling new area with background — never crops |
border | Solid-color border of a given width on every side |
flip | Mirror horizontally or vertically |
scale | Resize by a relative factor (e.g. 0.5, 2) — resolved against real decoded dimensions, container-side |
inspect | Read-only metadata report; no pixel changes. See below. |
roundedCorners | Rounds the image's corners; requires an alpha-capable target format |
watermarkText | Overlays semi-transparent text at a position/opacity |
Measurable constraints (maxBytes, exactWidth/exactHeight, maxWidth/maxHeight, format, requireTransparency, forbidUpscaling, preserveAnimation) are always re-verified against the real output. Subjective preferences (qualityHint, free-text notes) inform defaults but are never a pass/fail gate — there's no objective check for "high quality."
11 Reference
Read-only inspection
inspect doesn't transform pixels — its findings are returned as an inspection object alongside the normal output/validation fields, with Accept: application/json:
{
"inspection": {
"width": 1200, "height": 800, "format": "jpeg", "hasAlpha": false,
"hasExif": true, "hasIccProfile": false, "isAnimated": false,
"hasGpsData": false
}
}12 Reference
Not supported (by design, this pass)
The planner rejects these with unsupported_operation rather than attempting them:
- AI upscaling / super-resolution
- Generative edits — inpainting, outpainting, generating new content, AI deblurring
- OCR / text extraction
- PDF rendering
- Camera RAW processing
- Barcode / QR decode
- Face / object / subject detection
- Any operation requiring a second input image — compositing, logo overlay, image-as-mask
- Multi-file / bundle output — icon packs, zips, batched variants in one response
- Real ICC-managed print color conversion
- Per-frame animation editing — frame-rate change, reordering, time-based trimming — beyond simple format-preserving passthrough via
preserveAnimation