{"openapi":"3.1.0","info":{"title":"Peelaway API","version":"1.0.0","description":"Production image editing for automated workflows. Submit an image and instruction; Peelaway routes it through object removal, background removal, or generative editing and returns the finished image with structured metadata."},"servers":[{"url":"https://api.peelaway.io"}],"security":[{"bearerAuth":[]}],"paths":{"/api/process":{"post":{"summary":"Submit an image-editing job","description":"Submit one image (or a batch of up to 50) with a natural-language prompt. Returns a job_id immediately; processing runs asynchronously. Supports the standard `Idempotency-Key` header so retries don't double-charge.","parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string"},"description":"Client-supplied UUID. Retrying with the same key returns the original job_ids without re-charging credits."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProcessRequest"}}}},"responses":{"200":{"description":"Job(s) accepted for processing","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProcessResponse"}}}},"400":{"description":"Invalid request (code: batch_exceeds_plan_limit when a batch is larger than the plan's concurrency limit)"},"402":{"description":"Insufficient credits"},"403":{"description":"Email not verified (code: email_unverified) — verify before spending"},"429":{"description":"Throttled — code: rate_limited (honor Retry-After) or too_many_in_flight. Limits scale with plan."},"502":{"description":"Upstream provider failure"}},"callbacks":{"webhook":{"{$request.body#/webhook_url}":{"post":{"summary":"Signed job-settlement callback","description":"Sent once when the job settles (done or error). Headers: X-Peelaway-Timestamp (unix ms) and X-Peelaway-Signature (sha256=<hex> HMAC over `${timestamp}.${rawBody}` with the secret from GET /api/webhooks/secret). Respond 2xx to acknowledge; non-2xx or 3xx is retried with backoff. Dedupe on job_id.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookPayload"}}}},"responses":{"200":{"description":"Acknowledged"}}}}}}}},"/api/process/status":{"get":{"summary":"Poll job or batch status","description":"Returns the current status of a single job (via `job_id`) or every job in a batch (via `batch_id`). Exactly one of the two query parameters is required.","parameters":[{"name":"job_id","in":"query","required":false,"schema":{"type":"string"}},{"name":"batch_id","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Current status","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusResponse"}}}},"400":{"description":"Missing or invalid id"},"404":{"description":"Job or batch not found"}}}},"/api/process/result":{"get":{"summary":"Fetch the result of a completed job","description":"Returns a signed URL to the finished image. Returns 202 if the job is still processing; poll `/api/process/status` first.","parameters":[{"name":"job_id","in":"query","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Signed URL to the output image","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResultResponse"}}}},"202":{"description":"Job is still processing"},"404":{"description":"Job not found"},"500":{"description":"Job failed"}}}},"/api/webhooks/secret":{"get":{"summary":"Fetch the caller's webhook signing secret","description":"Returns the per-user HMAC secret used to sign webhook callbacks, so receivers can verify X-Peelaway-Signature. Authenticated; read-only.","responses":{"200":{"description":"The signing secret","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookSecretResponse"}}}},"401":{"description":"Authentication required"}}}},"/mcp":{"post":{"summary":"MCP Streamable-HTTP JSON-RPC endpoint","description":"Model Context Protocol server (JSON-RPC 2.0 over Streamable HTTP), authed by the same pk_ bearer key. Methods: initialize, tools/list, tools/call. Tools: edit_image, get_job_status, get_job_result — the same behavior and credit debit as the HTTP endpoints. See /mcp.json for the manifest.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","description":"A JSON-RPC 2.0 request object (or batch array).","properties":{"jsonrpc":{"type":"string","enum":["2.0"]},"id":{"type":["string","number","null"]},"method":{"type":"string","enum":["initialize","tools/list","tools/call"]},"params":{"type":"object"}}}}}},"responses":{"200":{"description":"A JSON-RPC 2.0 response object (or batch array)."},"202":{"description":"Accepted (JSON-RPC notification, no response body)."},"401":{"description":"Authentication required"}}}},"/auth/sign-up/email":{"post":{"summary":"Create an account (email + password)","description":"Self-serve account signup. Creates a user (10 free credits), emails a verification link, and returns a session immediately. An account owner must complete email verification before key creation or editing (not before sign-in). Public (no auth).","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignUpRequest"}}}},"responses":{"200":{"description":"Account created and signed in","headers":{"Set-Cookie":{"description":"Session cookie. Resend it (Cookie header) on POST /api/keys.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthSessionResponse"}}}},"400":{"description":"Invalid input"},"422":{"description":"Email already registered or validation error"},"429":{"description":"Rate limited (per IP) — retry after the Retry-After header"}}}},"/auth/sign-in/email":{"post":{"summary":"Sign in (email + password)","description":"Self-serve sign-in for an existing account. Returns the session as a Set-Cookie header; resend it on POST /api/keys. Public (no auth).","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignInRequest"}}}},"responses":{"200":{"description":"Signed in","headers":{"Set-Cookie":{"description":"Session cookie. Resend it (Cookie header) on POST /api/keys.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthSessionResponse"}}}},"401":{"description":"Invalid credentials"}}}},"/api/keys":{"get":{"summary":"List the caller's API keys","description":"Returns metadata for every key (never the secret). Authenticated by the session cookie from sign-up/sign-in or by a pk_ bearer key.","security":[{"bearerAuth":[]},{"sessionCookie":[]}],"responses":{"200":{"description":"The caller's keys","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeyList"}}}},"401":{"description":"Authentication required"}}},"post":{"summary":"Create an API key","description":"Mints a new pk_ key and returns it ONCE (never shown again — store it). Send the session cookie from sign-in. Requires a verified email (else 403 code:email_unverified). Use the returned key as `Authorization: Bearer pk_...` on every /api/process and /mcp call.","security":[{"sessionCookie":[]},{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateApiKeyRequest"}}}},"responses":{"201":{"description":"Key created (secret returned once)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateApiKeyResponse"}}}},"400":{"description":"Name is required"},"401":{"description":"Authentication required"},"403":{"description":"Email not verified (code: email_unverified)"}}}},"/api/keys/{id}":{"delete":{"summary":"Revoke an API key","description":"Permanently revokes the key with the given id. Authenticated by the session cookie or a pk_ bearer key.","security":[{"bearerAuth":[]},{"sessionCookie":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Revoked","content":{"application/json":{"schema":{"type":"object","required":["ok"],"properties":{"ok":{"type":"boolean"}}}}}},"401":{"description":"Authentication required"},"404":{"description":"Key not found"}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"API key (pk_...) — issued from the dashboard or self-served via POST /api/keys."},"sessionCookie":{"type":"apiKey","in":"cookie","name":"better-auth.session_token","description":"Session cookie issued by /auth/sign-up/email or /auth/sign-in/email. Resend the exact Set-Cookie value you received (over HTTPS the cookie name may carry a __Secure- prefix)."}},"schemas":{"SignUpRequest":{"type":"object","required":["email","password","name"],"properties":{"email":{"type":"string","format":"email"},"password":{"type":"string","minLength":8,"description":"At least 8 characters."},"name":{"type":"string"}}},"SignInRequest":{"type":"object","required":["email","password"],"properties":{"email":{"type":"string","format":"email"},"password":{"type":"string"}}},"AuthSessionResponse":{"type":"object","description":"Session payload. The session token is ALSO set as a Set-Cookie header — resend that cookie on POST /api/keys.","properties":{"token":{"type":"string","description":"Session token (also set as a cookie)."},"user":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"},"name":{"type":"string"}}}}},"CreateApiKeyRequest":{"type":"object","required":["name"],"properties":{"name":{"type":"string","description":"A human label for the key (e.g. 'agent-prod')."}}},"CreateApiKeyResponse":{"type":"object","required":["key","id","prefix","name"],"properties":{"key":{"type":"string","description":"The full secret key (pk_...). Returned ONCE — store it now. Send as `Authorization: Bearer <key>`."},"id":{"type":"string"},"prefix":{"type":"string","description":"Display prefix (first 15 chars + '...')."},"name":{"type":"string"}}},"ApiKey":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"prefix":{"type":"string"},"lastUsedAt":{"type":["string","null"],"format":"date-time"},"createdAt":{"type":"string","format":"date-time"}}},"ApiKeyList":{"type":"object","required":["keys"],"properties":{"keys":{"type":"array","items":{"$ref":"#/components/schemas/ApiKey"}}}},"ProcessRequest":{"oneOf":[{"$ref":"#/components/schemas/SingleProcessRequest"},{"$ref":"#/components/schemas/BatchProcessRequest"}]},"SingleProcessRequest":{"type":"object","required":["image","prompt"],"properties":{"image":{"type":"string","description":"Base64-encoded image bytes (JPEG, PNG, or WebP)."},"prompt":{"type":"string","description":"Natural-language edit instruction. Use 'remove X' for object removal, 'remove the background' for background removal, anything else for generative editing."},"negative_prompt":{"type":"string"},"format":{"type":"string","enum":["jpeg","png","webp"],"description":"Output format. Background removal always returns PNG to preserve transparency; other workflows default to JPEG."},"quality":{"type":"integer","minimum":1,"maximum":100,"description":"Output quality for lossy formats."},"mask":{"type":"string","description":"Optional base64-encoded PNG mask. When set, auto-detection is skipped and the mask guides the removal target. The mask is resized to the processed image dimensions. The provider returns and Peelaway re-encodes a complete frame, so pixels outside the mask are not guaranteed unchanged."},"detect":{"type":"string","description":"Optional detection query (e.g. 'car . person . license plate') that overrides the auto-derived removal vocabulary and routes to the removal workflow."},"webhook_url":{"type":"string","format":"uri","description":"Optional public https URL. Peelaway POSTs a signed callback here when the job settles (done or error), with retry backoff. The body carries job_id, status, the metadata block, and result_url (on done) or a generic error (on error). Verify X-Peelaway-Signature (sha256=HMAC over `${X-Peelaway-Timestamp}.${rawBody}`) with the secret from GET /api/webhooks/secret, and dedupe retries on job_id."}}},"BatchProcessRequest":{"type":"object","required":["images"],"properties":{"images":{"type":"array","minItems":1,"maxItems":50,"items":{"$ref":"#/components/schemas/SingleProcessRequest"}}}},"ProcessResponse":{"oneOf":[{"type":"object","required":["job_id","credits_remaining"],"properties":{"job_id":{"type":"string"},"credits_remaining":{"type":"integer"}}},{"type":"object","required":["batch_id","jobs","credits_remaining"],"properties":{"batch_id":{"type":"string"},"jobs":{"type":"array","items":{"type":"object","properties":{"job_id":{"type":"string"},"status":{"type":"string","enum":["pending","error"]},"error":{"type":"string"}}}},"credits_remaining":{"type":"integer"}}}]},"JobMetadata":{"type":"object","description":"Structured job metadata, present on status/result once the job is terminal and in the webhook payload.","properties":{"width":{"type":"integer"},"height":{"type":"integer"},"format":{"type":"string"},"credits_used":{"type":"integer"},"credits_remaining":{"type":"integer"},"processing_time_ms":{"type":["integer","null"]}}},"StatusResponse":{"oneOf":[{"type":"object","required":["status"],"properties":{"status":{"type":"string","enum":["pending","done","error"]},"width":{"type":"integer"},"height":{"type":"integer"},"format":{"type":"string"},"credits_used":{"type":"integer"},"credits_remaining":{"type":"integer"},"processing_time_ms":{"type":["integer","null"]}}},{"type":"object","required":["batch_id","jobs"],"properties":{"batch_id":{"type":"string"},"jobs":{"type":"array","items":{"type":"object","properties":{"job_id":{"type":"string"},"status":{"type":"string","enum":["pending","done","error"]}}}}}}]},"ResultResponse":{"type":"object","required":["job_id","url","format"],"properties":{"job_id":{"type":"string"},"url":{"type":"string","format":"uri","description":"Signed URL valid for 1 hour."},"format":{"type":"string"},"width":{"type":"integer"},"height":{"type":"integer"},"credits_used":{"type":"integer"},"credits_remaining":{"type":"integer"},"processing_time_ms":{"type":["integer","null"]}}},"WebhookPayload":{"type":"object","required":["job_id","status"],"description":"Body Peelaway POSTs to your webhook_url when a job settles. Signed via X-Peelaway-Signature / X-Peelaway-Timestamp.","properties":{"job_id":{"type":"string"},"status":{"type":"string","enum":["done","error"]},"width":{"type":"integer"},"height":{"type":"integer"},"format":{"type":"string"},"credits_used":{"type":"integer"},"credits_remaining":{"type":"integer"},"processing_time_ms":{"type":["integer","null"]},"result_url":{"type":"string","format":"uri","description":"Signed URL to the output image. Present only on status=done."},"error":{"type":"string","description":"Generic failure message ('Processing failed'). Present only on status=error."}}},"WebhookSecretResponse":{"type":"object","required":["secret"],"properties":{"secret":{"type":"string","description":"Per-user HMAC signing secret (hex). Recompute X-Peelaway-Signature with it to verify callbacks."}}}}}}