API
Errors
Errors are JSON with a coarse error code and an HTTP status. Codes are intentionally coarse — precise internal detail stays server-side. Some responses add context fields (issues, photo_count, balance_cents, price_cents, index). All error responses are Cache-Control: no-store.
Shape
{ "error": "insufficient_credits", "balance_cents": 800, "price_cents": 2500 }Status codes
| HTTP | error | Meaning |
|---|---|---|
| 400 | invalid_json | Body was not valid JSON. |
| 400 | invalid_request | A field failed validation; see issues. |
| 400 | invalid_idempotency_key | Malformed Idempotency-Key header. |
| 400 | consent_required | consent: true missing on render. |
| 400 | photo_limit | Batch would exceed 30 photos per gift. |
| 400 | invalid_photo_url | A photo URL could not be fetched (blocked/failed/redirects). |
| 400 | invalid_photo_data | Inline base64 photo was empty or unreadable. |
| 401 | unauthorized | Missing, malformed, unknown, or revoked key. |
| 402 | insufficient_credits | Balance below the tier price; includes balance_cents, price_cents. |
| 403 | test_key_cannot_render | Test key used to render; test keys are a free sandbox. Use a live key. |
| 404 | not_found | Unknown gift, a gift another key owns, or the API is disabled. |
| 409 | order_locked | Gift is past the content stage; photos no longer accepted. |
| 409 | already_delivered | Film already rendered; read URLs from status. |
| 409 | not_ready | Render requested with no reservation or zero photos. |
| 411 | length_required | Missing Content-Length (chunked bodies are rejected). |
| 413 | payload_too_large | Photo or request body over the size cap. |
| 415 | unsupported_media_type | Bytes were not a supported image (JPEG/PNG/WebP). |
| 429 | rate_limited | Rate limit exceeded; includes retryAfter and a Retry-After header. |
| 502 | trigger_failed | Render could not be started after the debit; charge auto-refunded. |
| 503 | store_unavailable | Transient storage failure; retry. |
| 503 | unavailable | Render backend not configured or a transient failure. |
Refunds — pay per delivery
The one paid call is render, and you are charged for a delivered film, not an attempt. If a render ends held (the content gate needs more or better photos) or failed, the debit is automatically refunded to your prepaid balance and the gift re-opens so you can add photos and render again.
Refunds happen two ways, both automatic: lazily the next time you poll GET /v1/gifts/{id}, and by a reconciliation job that sweeps stranded charges if you never poll. A refund restores balance_cents and reverses lifetime spend; the status then shows refunded_cents and a status_detail of held_refunded or failed_refunded.
If the render could not even be started after the debit (502 trigger_failed), the charge is refunded before the response returns.
External billing (pay.sh)
Gifts billed through an external gateway (pay.sh / USDC) are not charged against internal credits, so a failed delivery is made good with a free re-render rather than an internal refund. See Payments.
Last updated 2026-07-11.