Skip to main content

Errors, limits and stability

Everything your integration needs in order to fail well: the shape of an error, the codes you can branch on, the headers that let you pace yourself, and what we promise about changes.

This page covers the three visual endpoints: POST /v1/visual and the status and download endpoints.

The OAuth endpoints under /v1/oauth are different: their own handlers follow RFC 6749 and answer error with error_description, so read them as an OAuth client would. Four refusals are shared with every other endpoint and reach an OAuth caller in the envelope below instead — an oversized body, a rejected origin, a path that routes nowhere, and a method an endpoint does not serve. See OAuth user authentication.

The error body​

Every failed call to a visual endpoint answers application/json with the same two fields.

{
"error": "validation_failed",
"message": "validation failed: format is required; content is required"
}
  • error is the machine-readable code. Branch on this.
  • message is for a human. It is not stable; do not parse it, match on it, or show it to an end user as-is without checking it reads well in your product.

A code is part of the contract once published: we may reword a message at any time, and we will not change the meaning of a code or silently drop one. We may add new codes, so treat a code you do not recognise as its HTTP status class and carry on.

Some failures add fields to the envelope. A 429 carries the limit that was hit; see Rate limits below.

Error codes​

CodeStatusWhat happenedWhat to do
invalid_request400The body is not readable JSON.Fix the request. Retrying is pointless.
validation_failed400The body parsed, but a field is missing or out of range. message lists every field at fault.Fix the request. Retrying is pointless.
invalid_request_id400A path parameter is not the identifier form the route expects.Use the id returned by POST /v1/visual, and a file_id taken from generated_files.
request_too_large413The request body exceeds the accepted size.Split the content into smaller requests.
method_not_allowed405The path exists and does not answer that method.Check the method against the API reference.
origin_not_allowed403The browser origin is not one this API answers.Call the API from your server, not from a browser page.
missing_authorization403No usable Authorization header was sent.Send Authorization: Bearer YOUR_TOKEN.
invalid_token403The bearer token is not one this API accepts.Check the token, and that you are calling the right environment.
token_expired401An OAuth access token is past its expiry.Refresh with the refresh_token grant, then retry once.
token_revoked401The token was revoked.Mint a new token. Retrying the same one never succeeds.
insufficient_scope403The OAuth grant does not carry the scope this route needs.Ask for the scope at authorization time.
forbidden403The resource exists and belongs to another caller.Use a request id your own token created.
not_found404The request, or that file of it, does not exist.Check the id. A request that never completed has no files yet. A file that once existed may have expired.
request_expired410The request completed more than 30 minutes ago. Its status and files are gone.Create a new request. Download files as soon as a request completes.
no_credits402The account has no credits left.Top up or upgrade. Retrying without doing so never succeeds.
rate_limit_exceeded429One of your windows is full.Wait Retry-After seconds, then retry. See below.
generation_limit_exceeded429The account's lifetime generation allowance is spent.Contact us. Waiting does not help.
too_many_failed_attempts429Repeated bad credentials from your address are being refused.Stop retrying, fix the token, and come back after a pause.
internal_error500The call failed on our side.Retry with exponential backoff. If it persists, send us the request id.
service_unavailable503A dependency is down or slow — the database, the queue, or the storage a download reads from.Retry with exponential backoff. A download that answers this has not lost your file.

Which failures are worth retrying​

Retry internal_error, service_unavailable, rate_limit_exceeded and token_expired. Every other code describes something that will be true again on the next call, so a retry loop around it only burns your rate limit.

Rate limits​

Limits are enforced per second, per minute, per hour and per day, and the numbers depend on your plan, so the reliable way to know yours is to read them off a response rather than hard-code a figure. POST /v1/visual carries them on any answer that reached its limiter:

HeaderMeaning
X-RateLimit-LimitRequests allowed in the window named by X-RateLimit-Window.
X-RateLimit-RemainingRequests left in that window.
X-RateLimit-ResetUnix timestamp when that window resets.
X-RateLimit-WindowWhich window the two counts above describe: second, minute, hour or day.
Retry-AfterSeconds to wait. Sent only with a rate_limit_exceeded, not with the other two 429 codes.

A refusal that answers before the limiter carries none of them: 403 for a credential or an origin, 405 for the wrong method, 413 for a body that declared itself oversized, and the 429 for repeated bad credentials all return earlier in the chain. Read the headers where they are present rather than requiring them.

All of these are listed in Access-Control-Expose-Headers, so a call from an allowed browser origin can read them too, alongside X-API-Version.

X-RateLimit-Window names the window with the fewest requests left in absolute terms, not the one nearest its own limit proportionally. Because the per-second allowance is the smallest number on every tier, a successful call usually reports second even when your day quota is nearly gone. So read these headers as "you may send this many right now", which is what they reliably tell you, and do not read them as naming the quota that will stop you next. To know how much of your daily budget is left, count your own calls; the headers will not tell you until that window is the smallest one remaining.

Whichever window is reported, X-RateLimit-Remaining and X-RateLimit-Reset describe that same window, so pacing on the pair is always correct for the next few requests.

If the limiter itself cannot be reached, the call is let through and the headers read X-RateLimit-Window: fallback with zeroes. That means no reading is available, not that your limit is zero.

A 429 from the rate limiter looks like this:

{
"error": "rate_limit_exceeded",
"message": "Too many requests. Limit: 10 per minute. Try again in 45 seconds.",
"limit": 10,
"window": "minute",
"reset_time": 1704067260,
"retry_after": 45
}

The other two 429 codes are not the same thing and are not worth waiting out: generation_limit_exceeded carries limit and used and means the account's lifetime allowance is spent, and too_many_failed_attempts means bad credentials are being refused. Branch on error rather than on the status.

The status and download endpoints are not rate limited, so polling a request costs you nothing against your generation quota, and they send no rate limit headers. The only 429 they answer is too_many_failed_attempts, which carries no Retry-After and none of the window fields. Poll with exponential backoff anyway: most requests finish in 10 to 30 seconds.

Need more headroom? Contact us — higher limits and volume pricing are what custom plans are for.

Errors inside a completed request​

The codes above describe a call that failed. A call can also succeed while the generation behind it does not: POST /v1/visual returns 201, and the status endpoint later reports status: failed with an error object, or status: completed with warnings.

CodeKindMeaning
no_creditserrorThe account ran out of credits while the request was being processed.
no_visualserrorNothing could be generated from this content. Simplify it, or drop visual_ids.
upstream_unavailableerrorGeneration was temporarily unavailable on our side. The content was fine; retry the same request.
not_enough_visualswarningFewer visuals than you asked for. The ones that worked are in generated_files.
missing_visual_idswarningSome requested layouts did not fit the content.
some_visuals_failed_orientation_controlwarningSome visuals missed the orientation constraint. They are returned last.
invalid_style_idwarningThe style was not found; the default style was used.
visual_id_with_random_sortwarningsort_strategy: random was ignored because specific visual_ids were requested.

A warning is not a failure: files are still there, and the request is still billed for what it produced.

A status: failed with no error object means the failure was ours and does not map to any of the codes above. Retry once; if it happens again, send us the request id.

What we promise about changes​

The API is generally available and supported for production use. We have not committed to a formal versioning scheme yet, so rather than point you at a rule, here is what we actually do:

  • Additive changes ship without warning. New fields on a response, new optional request fields, new error codes, new styles. Write your client so an unknown field or code is ignored rather than fatal — this is the single thing that will keep your integration working through everything below.
  • Breaking changes are announced in the changelog with the version they land in, and we mail integrators we know about. Read X-API-Version on any response to see which version answered it.
  • Deprecated fields keep working until a version says otherwise. context_before, context_after and inverted_color are deprecated today and still answered.
  • The error codes on this page are stable. A message may be reworded at any time; a published code will not change meaning or quietly disappear.
  • Availability is not guaranteed. Production support means we run the API as a product and take its failures seriously, not that we promise uptime. The Terms of Service provide it as-is without an uptime guarantee, and interruptions can happen without notice. Design for that: retry the codes worth retrying, and cache what you have already generated.

If you need an availability or support commitment, talk to us.

The terms that govern your use of the API are in the Terms of Service.

Getting help​

Send us the request id and the error code — those two together are usually enough to find what happened. api@napkin.ai.