API Reference
Base URL: https://erased.ink/api/v1
All successful responses are application/json. Errors are application/problem+json (RFC 7807) — see the Error codes page.
POST /uploads
Issue a short-lived presigned R2 PUT URL.
Request
{
"filename": "photo.png",
"content_type": "image/png",
"size": 4823109
}| Field | Type | Notes |
|---|---|---|
filename | string | Used for response headers only; the server assigns the object_key. |
content_type | string | One of image/png, image/jpeg, image/webp, image/avif, image/heic, image/heif, image/jxl. |
size | integer | Max 10485760 (10 MB). |
Response 200
{
"upload_url": "https://...",
"object_key": "in/2026-05-24/job_xxx.png",
"expires_at": "2026-05-24T10:15:00Z"
}You must PUT the file to upload_url before expires_at.
POST /jobs
Create a processing job.
Request body — three shapes
POST /api/v1/jobs accepts three body forms. Pick one based on what you have at hand:
| Content-Type | Required fields | Best for |
|---|---|---|
multipart/form-data | image (file, ≤10MB) or object_key; mode; options (JSON string, optional) | One-shot upload-and-process. Drop-in curl -F use. |
application/json + image_base64 | image_base64 (≤1MB decoded), content_type, mode | JSON-only toolchains (Zapier, n8n). Small images. |
application/json + object_key | object_key (from POST /uploads), mode | Large files / batch — pair with the presigned upload. |
Example (object_key form)
{
"object_key": "in/2026-05-24/job_xxx.png",
"mode": "visible"
}Response — sync (visible, metadata)
{
"id": "job_xxx",
"status": "succeeded",
"mode": "visible",
"result_url": "https://...",
"result_expires_at": "2026-05-25T10:00:00Z",
"processing_ms": 412
}Response — async (invisible, all)
{
"id": "job_xxx",
"status": "queued",
"mode": "invisible",
"poll_url": "/api/v1/jobs/job_xxx",
"poll_after_ms": 2000
}Response — detect (read-only, no result_url)
{
"id": "job_xxx",
"status": "succeeded",
"mode": "detect",
"processing_ms": 487,
"report": {
"visible": {
"detected": true,
"confidence": 0.87,
"region": [1820, 120, 80, 80],
"label": "Gemini sparkle"
},
"metadata": {
"c2pa": { "present": true, "manifest": { "claim_generator": "Gemini ..." } },
"exif_ai_tags": { "Software": "Gemini" },
"png_text_chunks": null,
"xmp_digital_source_type": null
},
"invisible_external": {
"synthid": {
"status": "unsupported_locally",
"verify_url": "https://support.google.com/gemini/answer/16722517"
}
},
"overall_assessment": "Likely AI-generated by Gemini. C2PA Content Credentials present."
}
}detect never writes an output object; the original image is unchanged.
GET /jobs/:id
Poll an async job (and a valid read for sync jobs while their record is still in KV — about a 30-minute TTL).
Response — still processing
{
"id": "job_xxx",
"status": "processing",
"mode": "invisible",
"poll_after_ms": 2000
}Response — succeeded
Same shape as the sync success response above.
Response — failed
{
"id": "job_xxx",
"status": "failed",
"mode": "invisible",
"error_code": "pipeline_error",
"error_message": "..."
}Authentication tiers
detect is the only mode available without an account (100 req/hour unauthenticated). All strip modes require at least a free account.
- Free (signed in) — sign in via Google OAuth or an email magic link at /en/sign-in. Unlocks all strip modes. No API key issued.
- Pro ($9 / month) — issued an API key. Pass it as
Authorization: Bearer uk_.... Quotas are monthly. Includes 100 buffer credits/month for overage, and detect calls are unlimited.
Rate limits
detect is the only mode usable without an account: 100 req/hour unauthenticated, 500/hour for free accounts. All other modes require a free account (signed in) or Pro.
| Quota group | Modes | Free (signed in) | Pro |
|---|---|---|---|
light | visible, metadata | 10 / day | 5000 / month |
heavy | invisible, all | 3 / day | 500 / month |
uploads | POST /uploads | 200 / h | unlimited |
detect | detect | 500 / h | unlimited |
Exceeding a limit returns 429 with a Retry-After header and an RFC 7807 body. Daily quotas reset at 00:00 UTC. Failed jobs automatically refund the bucket charge.