Errors and limits
Every status code the API returns, what your plan allows per minute, and the headers that tell you where you stand.
Keys
Send the key as a header. Only render accepts one in the URL, and only a publishable key.
curl https://pixloom.dev/api/v1/styles -H "Authorization: Bearer sk_live_..."Secret keys (sk_live_) reach everything. Publishable keys (pk_live_) reach only what a browser needs: render, job, styles, embed resolve, analytics and the image proxy. Use one anywhere else and you get:
403 { "error": "Publishable keys cannot access this endpoint. Use a secret key (sk_live_)." }Account status
The beta is invite-only and every account is approved by hand. Until yours is, every call returns:
403 { "error": "Account pending approval", "code": "pending_approval" }pending_approval is the one error code worth branching on. Retrying does not help; wait for the email. A suspended or banned account gets a 403 too, with Account suspended or Account banned and no code.
Domains
Each key carries a list of domains it may run on. We check the browser's Origin header, or its Referer when there is no Origin, as with an image load.
- An empty list means any domain. That is what you want for a server-side secret key.
*.example.comcoversexample.comand every subdomain.- Localhost always passes, so development needs no setup.
- A secret key with no origin at all passes, which is every server-to-server call.
- A publishable key with no origin at all is refused. Browsers always say which page they are on; a script copying your key from page source does not.
A blocked origin gets 403 { "error": "Origin not allowed" }.
Sites
Your plan sets how many sites your keys can span together. A site is one root domain: shop.example.com, *.example.com and example.com are the same site, example.co.uk is its own, and localhost and *.vercel.app previews never count.
| Plan | Sites |
|---|---|
| Free | 1 |
| Hobby | 2 |
| Pro | 5 |
| Business | Unlimited |
Creating a key, or editing one's domains, past that gets a 403 naming your plan and the count. Settings shows how many you are using next to your keys. Upgrading lifts the cap at once.
Rate limits
Per minute, on a sliding window. A secret key is counted as a whole. A publishable key sits on a public page, so each visitor to your site gets the full limit below, and the key as a whole gets twenty times that. A popular page keeps working; one visitor hammering it does not.
| Plan | Requests per minute |
|---|---|
| Free | 30 |
| Hobby | 60 |
| Pro | 120 |
| Business | 300 |
| Enterprise | 600 |
The limit applies to generate, render, embed resolve and creating or cartoonifying a character. Embed resolve only holds a key to its whole-site ceiling when it would start a new image, so images that already exist keep showing. Analytics has the same ceiling in a separate bucket, so telemetry never spends your generation budget. Polling a job, listing styles and listing characters are not limited at all.
Responses carry X-RateLimit-Limit and X-RateLimit-Remaining. Over the line:
429 { "error": "Rate limit exceeded", "retryAfterMs": 60000 }Back off for retryAfterMs and try again. The window slides, so a steady pace recovers faster than a burst.
Generations
Your plan includes a number of generations a month. The counter resets on a rolling monthly date, not the first of the month.
Only fresh images count. Cache hits, instant mode, permalinks serving something they already have, and everything in the library are all free.
A generation counts the moment it starts, and comes back if it fails or turns out to be a cache hit. So a burst of requests can never go past your limit, and X-Quota-Used on a 202 already includes the job you just started:
X-Quota-Used: 42
X-Quota-Limit: 100
X-Quota-Reset: 2026-10-01T00:00:00.000ZOut of generations:
402 {
"error": "Generation quota exceeded",
"used": 100,
"limit": 100,
"resetAt": "2026-10-01T00:00:00.000Z"
}Render tries harder before giving up: if you already made an image from that exact prompt, it redirects there instead of failing. A page that has been live for a while keeps working after you hit your limit.
Status codes
| Status | Meaning |
|---|---|
200 | Done. |
202 | Accepted, work continues in the background. Generate and analytics. |
302 | Redirect to an image. Render, image, proxy. |
400 | Bad input. { "error": "Invalid request", "details": { ... } }, with details naming the fields. |
401 | Missing API key or Invalid API key. |
402 | Out of generations. |
403 | Account not approved, wrong key type, or a domain that is not on the key. |
404 | Not found, or not yours. We do not distinguish the two. |
429 | Over the rate limit. |
500 | { "error": "Internal server error" }. Ours, not yours. Retry, and tell us if it persists. |
502 | Generation failed at the model. Retry. |
503 | New images are paused for a while. Existing images keep serving; retry after Retry-After seconds. |
Validation errors
A bad body comes back with the field that was wrong:
400 {
"error": "Invalid request",
"details": {
"formErrors": [],
"fieldErrors": { "width": ["Number must be a multiple of 64"] }
}
}A body that is not JSON at all gets { "error": "Invalid JSON" }.
CORS
Every /api/v1/ endpoint answers preflight and allows GET, POST and OPTIONS with Content-Type and Authorization. When your key has a domain list, we echo back the matching origin. When it does not, we allow any. We never reflect an origin we have not checked.