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": "..."
}GET /clean — URL input, image output
One-shot synchronous endpoint: pass a public image URL, get the processed image back as the response body. Designed for <img src="..." /> embeds and CDN caching.
GET /api/v1/clean?image=<encoded-https-url>&mode=visible
→ 200 OK
Content-Type: image/png
Cache-Control: public, max-age=86400
<bytes>- Modes:
visible,metadata,invisible,all.detectis rejected (returns JSON, not an image). - Auth: Anonymous / Free only. Pro users should use
POST /jobs. - Size limit: 10MB (HEAD-checked before fetch).
- Errors: 4xx/5xx responses return a placeholder PNG body; the real error type is in the
X-Error-Typeheader. - SSRF: only
https://URLs accepted; private IP ranges blocked.
Authentication tiers
- Anonymous — no credentials required. Subject to per-hour quotas.
- Free — sign in via Google OAuth or an email magic link at /en/sign-in. Higher hourly caps, still no API key.
- 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
| Quota group | Modes | Anonymous | Free | Pro |
|---|---|---|---|---|
light | visible, metadata | 30 / h | 100 / h | 5000 / month |
heavy | invisible, all | 5 / h | 15 / h | 500 / month |
uploads | POST /uploads | 60 / h | 200 / h | unlimited |
detect | detect | 100 / h | 500 / h | unlimited |
heavy also has a per-day cap of 12/day (Anonymous & Free), on top of the hourly limit — Anonymous/Free by ip+fp, logged-in users by account. Pro is governed by its monthly quota.
Exceeding a limit returns 429 with a Retry-After header and an RFC 7807 body. Failed jobs automatically refund the bucket charge.