Guides
Errors & rate limits
One envelope, stable codes, honest status — build your retry logic once.
The error envelope
Every non-2xx response carries the same JSON shape:
{
"error": {
"code": "rate_limited",
"message": "Rate limit exceeded. Slow down and retry."
}
}code is machine-readable and stable across releases; message is for humans and may change. Branch on the code, log the message.
Error codes
| Code | HTTP | Meaning |
|---|---|---|
| unauthorized | 401 | Missing, invalid, or revoked API key. |
| forbidden | 403 | The key's account can't act on this resource. |
| not_found | 404 | The resource doesn't exist — or belongs to another account (never disclosed which). |
| invalid_request | 400 | Malformed JSON or missing/invalid fields; the message says which. |
| rate_limited | 429 | Too many requests — honor the Retry-After header and back off. |
| quota_exceeded | 429 | No AI credits left this period. Upgrade the plan or redeem a coupon. |
| internal_error | 500 | Something failed on our side; safe to retry with backoff. |
Rate limits
- Each API key has a token-bucket limit: a burst of ~20 requests, refilling at ~120 requests/minute sustained.
- A per-IP bucket applies on top, so one noisy host can't starve your other integrations.
- On
429, theRetry-Afterheader says how many seconds to wait. Honor it, add jitter, and back off exponentially on repeats.
rate_limited and quota_exceeded both use HTTP 429 but mean different things: the first is about request pace (wait and retry), the second is about AI credits (retrying won't help — upgrade or redeem a coupon at curateone.ai → Settings).Idempotency & retries
- All GETs are safe to retry.
POST /sitesreserves a credit per call — on a network timeout, poll your recent jobs before re-creating, or you may start two generations.DELETE /jobs/{id}is idempotent — cancelling twice is harmless.internal_error(500) is transient; retry with backoff.