API
Errors
Every non-2xx response uses the same error envelope.
#Error envelope
Every non-2xx response returns a JSON object with a single error key:
{
"error": {
"code": "insufficient_credits",
"message": "Insufficient credits: need 10, have 3 remaining."
}
}code is a stable machine-readable identifier. message is a human-readable description that may include contextual detail.
#Error codes
| Code | HTTP | When |
|---|---|---|
invalid_input | 400 | The request body isn't valid JSON, or isn't a JSON object. A malformed request, as opposed to a well-formed one that fails validation (422). |
invalid_key | 401 | Missing, malformed, or revoked API key. |
subscription_past_due | 402 | The account's last payment failed. Update the card to resume API access. |
insufficient_credits | 402 | Not enough credits remaining this billing period. |
deck_limit | 402 | Deck count is at the plan limit. Upgrade to create more. |
brand_limit | 402 | Brand count is at the plan limit. Upgrade to create more. |
pptx_not_on_plan | 403 | POST /v1/decks/{id}/exports with format: "pptx" on a plan without PPTX export. Export PDF, or upgrade to Solo or higher. |
not_found | 404 | The resource doesn't exist or belongs to a different user. |
idempotency_conflict | 409 | An Idempotency-Key on POST /v1/generate was reused with a different body. |
invalid_input | 422 | Well-formed JSON that failed validation: a missing or too-long field, an unknown enum value, more than 12 or an unrecognised keys value on GET /v1/slide-templates. |
rate_limit | 429 | Over the request rate for your plan. Retry-After and the RateLimit-* headers say for how long. |
internal_error | 500 | An unexpected server error. Safe to retry with backoff. |
ai_unavailable | 502 | The AI step on POST /v1/decks/{id}/slides (prompt mode) or POST /v1/decks/{id}/edit failed. Credits are refunded. Retry with backoff. |
generation_failed | 502 | POST /v1/images returned no usable image. Credits for the failed image are refunded. |
The API is available on every plan, including free. Access is gated on the account's billing being in good standing (subscription_past_due), on credits (insufficient_credits), on plan resource limits (deck_limit, brand_limit), and per feature (pptx_not_on_plan) — never on simply having a paid plan.
generation_failed is also the error_code on a failed GET /v1/jobs/{id}. There it covers any failure of the background generation: the job runs after the HTTP response was already sent, so the failure surfaces on the job rather than on the original request. Credits are refunded.
#Rate limits
Every v1 request is counted, not just the expensive ones. There are two windows, and a request may be charged to both:
| Bucket | Window | Applies to | Capacity |
|---|---|---|---|
| standard | 10s | Every v1 request, reads included. | 30 × your plan's per-second rate. |
| ai | 5s | POST /v1/generate, POST /v1/images, POST /v1/brands/extract, POST /v1/decks/{id}/edit, and POST /v1/decks/{id}/slides in prompt mode. | 5 × your plan's per-second rate. |
Free plans are 1 request per second, so in practice: 30 requests per 10 seconds overall, and 5 AI requests per 5 seconds. Paid plans scale from there.
A 429 carries Retry-After (seconds), RateLimit-Limit, RateLimit-Remaining, and RateLimit-Reset. Honour Retry-After instead of retrying immediately. Minting an export or preview link is a standard-bucket request: the render it triggers is charged to nobody, but the mint still counts.
#Handling 402 errors
A 402 covers four distinct situations — always check error.code:
subscription_past_due— a payment failed. Update the card at heydecks.com/dashboard/billing. Retrying without fixing billing will keep failing.insufficient_credits— you've used your monthly allowance. Wait for the next billing period or upgrade.deck_limit— you've reached the maximum deck count for your plan. Delete a deck or upgrade.brand_limit— you've reached the maximum brand count for your plan. Delete a brand or upgrade.
#Retrying
Safe to retry (idempotent reads):
GET /v1/decks,GET /v1/decks/{id},GET /v1/jobs/{id},GET /v1/brands,GET /v1/slide-templates
Not safe to retry without checking first:
POST /v1/generate— each call deducts 100 credits per deck and creates a new job.POST /v1/decks/{id}/publish— idempotent by effect but each call creates a version snapshot.POST /v1/decks/{id}/exportsandPOST /v1/decks/{id}/previews— free and harmless to repeat, but each call mints a new link. The previous one stays valid until it expires, so retrying in a loop leaves live download links behind.
500 internal_error responses are safe to retry with exponential backoff.