§ Errors
Match on the code, never on the sentence.
Failures return application/problem+json with a stable machine-readable code and the request id that produced it. 85 of them, and the ones that are our fault are labelled as ours.
{
"type": "about:blank",
"title": "Unprocessable Entity",
"status": 422,
"code": "invalid_pdf",
"detail": "input is not a readable pdf: ...",
"requestId": "host/abc-000021"
}code is the contract. detail is for a human reading a log and will be reworded without warning, so branch on the former. requestId is the same value as the X-Request-Id response header, and it is what we need if you ask us about a call.
What a failure costs
Nothing. Everything you can get wrong is knowable before we charge you, so those requests never reach the ledger at all. A failure after that point — a timeout, a fault of ours, even a client that hung up mid-job — is compensated with the credits put back. You pay for work we actually delivered.
The request itself
Unknown option keys are rejected rather than ignored, so a typo like {"rotaton":90} fails loudly instead of silently doing nothing.
| Status | Code | Cause |
|---|---|---|
| 400 | no_input | No file part at all |
| 400 | input_count | Too few or too many file parts for the operation |
| 400 | too_many_files | More file parts than the operation’s ceiling |
| 400 | unknown_field | A form field the operation does not accept |
| 400 | duplicate_field | options, password, or an auxiliary field sent twice |
| 400 | malformed_body | The multipart body could not be parsed |
| 400 | invalid_request | A malformed request matching nothing more specific |
| 404 | not_found | No operation at that path |
| 405 | method_not_allowed | Operations are POST only |
| 415 | unsupported_media_type | The body was not multipart/form-data |
| 422 | invalid_options | Malformed JSON, or an option key the operation does not have |
Ceilings and capacity
Admission is rationed by estimated memory rather than by request count, so many small jobs run at once while a few large ones do not. An exhausted budget is an immediate 503, not a queue that grows without bound.
| Status | Code | Cause |
|---|---|---|
| 413 | file_too_large | One part exceeds the operation’s per-file cap |
| 413 | request_too_large | The whole body exceeds the request cap |
| 413 | field_too_large | options or password over its byte cap |
| 413 | page_limit_exceeded | More pages than the page ceiling |
| 413 | split_limit_exceeded | A split that would produce more documents than the ceiling |
| 413 | pixel_limit_exceeded | Decoded image pixels over the image caps |
| 413 | extract_budget_exceeded | Extracted images over the count or byte ceiling |
| 413 | job_too_large | Estimated memory exceeds the whole admission budget |
| 503 | busy | Admission budget exhausted. Retry with jittered backoff, honouring Retry-After |
| 504 | job_timeout | The operation exceeded its time budget |
| 500 | internal_error | An unexpected fault. detail is withheld; quote the request id |
pdf/*
| Status | Code | Cause |
|---|---|---|
| 422 | invalid_pdf | The document could not be read |
| 422 | password_required | Encrypted, and the password is missing or wrong |
| 422 | invalid_rotation | Rotation is zero, or not a multiple of 90 |
| 422 | invalid_span | span below 1 |
| 422 | invalid_page_selection | Missing, malformed, or matching no page in the document |
| 422 | invalid_stamp | Contradictory or out-of-range stamp options |
| 422 | invalid_image | An input image could not be decoded |
| 422 | invalid_import_config | Bad /pdf/from-images options |
| 422 | invalid_pipeline | No steps, too many, or a step that cannot be chained |
| 422 | not_encrypted | /pdf/decrypt on a document with no protection |
| 422 | already_encrypted | /pdf/encrypt on an already-protected document |
| 422 | no_images | /pdf/extract-images found nothing on the selected pages |
| 422 | new_password_required | /pdf/change-password without a newPassword part |
| 422 | no_metadata_change | /pdf/set-metadata with nothing to add or remove |
image/*
| Status | Code | Cause |
|---|---|---|
| 415 | unsupported_format | A format outside the allowlist. The error names the ones that are in it |
| 422 | invalid_image | The image could not be decoded |
| 422 | invalid_crop | A crop rectangle that does not fit the image |
| 422 | invalid_composite | Contradictory gravity and offset options |
| 422 | invalid_pipeline | No steps, too many, or a step that cannot be chained |
| 413 | frame_limit_exceeded | An animation with more frames than the ceiling |
| 413 | output_too_large | The requested output exceeds the output pixel cap |
text/*
A detector that is advertised, accepted, and silently finds nothing is the worst failure these endpoints have, so an unrecognised name is refused and the error lists the seventeen that exist.
| Status | Code | Cause |
|---|---|---|
| 415 | invalid_text | The input is not valid UTF-8 |
| 422 | unknown_detector | detect names something not on the allowlist |
| 422 | no_detectors | detect: [] with no terms — a scan that would find nothing |
| 422 | invalid_strategy | strategy is not one of the six |
| 422 | invalid_redaction_format | redactionFormat contains %v, which puts the value back inside its own placeholder |
| 422 | static_replacement_required | strategy is static with no staticReplacement |
| 422 | invalid_mask_character | maskCharacter is not exactly one character |
| 422 | too_many_terms | More inline dictionary terms than the ceiling |
| 422 | term_too_long | A term over the per-term byte cap |
| 422 | empty_term | A term is blank |
| 422 | option_not_applicable | includeMatches sent to /text/redact |
| 422 | invalid_mode | mode is not token or surrogate |
| 422 | invalid_token_format | tokenFormat is blank or has no %n |
| 422 | surrogate_unsupported_for_terms | mode: "surrogate" with terms, which the engine has no generator for |
| 409 | surrogate_collision | Two values generated one stand-in. Retry in token mode, which cannot collide |
| 409 | token_collision | A custom tokenFormat produced one token for two values |
| 413 | too_many_tokens | More distinct values, or a larger mapping, than the ceiling |
| 400 | missing_mapping | /text/restore called with no mapping part |
| 422 | invalid_mapping | The mapping part is not a JSON object of token to value |
| 422 | empty_mapping | The mapping is empty |
| 422 | invalid_token | A mapping key is empty or over the byte limit |
| 500 | redaction_failed | The engine failed on a policy this service built. Ours, not yours |
office/*
| Status | Code | Cause |
|---|---|---|
| 415 | unknown_input_format | The extension is not an accepted input, or there is no extension |
| 415 | invalid_text | A text-shaped input is not valid UTF-8 |
| 422 | container_mismatch | The bytes do not match what the extension claims. Refused before a parser sees it |
| 422 | missing_target | No to |
| 422 | no_conversion | to is the format the input already is |
| 422 | unsupported_target | The pair is not in the matrix, pdf included. The error lists what this input can become |
| 422 | conversion_failed | LibreOffice produced nothing: corrupt, password-protected, or a filter that gave up |
| 503 | office_busy | Every conversion slot is in use. Told to retry rather than queued behind your own deadline |
| 504 | conversion_timeout | The conversion did not finish in its budget |
| 500 | office_engine_error | The work directory or the process could not be set up. Ours, not yours |
Your account
A database failure is deliberately not a 401: telling you the key is invalid because our console was briefly unreachable would send you to rotate a credential that was never the problem.
| Status | Code | Cause |
|---|---|---|
| 401 | unauthenticated | No Authorization: Bearer header |
| 401 | invalid_api_key | No active key matches: revoked, expired, or a suspended account |
| 402 | insufficient_credits | The pack is empty. Nothing written, nothing done |
| 403 | operation_unavailable | No active price row: the operation is switched off or unknown |
| 409 | idempotency_key_reused | That Idempotency-Key has been used before |
| 400 | invalid_idempotency_key | Over 120 characters, or not printable ASCII |
| 429 | rate_limited | Over your account’s requests per second |
| 429 | too_many_concurrent_requests | Too many of your requests in flight at once |
| 429 | too_many_failed_keys | This address has presented too many unrecognised keys |
| 503 | billing_unavailable | The console database is unreachable |
Retrying
Every operation is a pure function of its inputs, so a retry is always safe. A 503 or a 429 is worth retrying with jittered backoff — both carry Retry-After, and on a rate refusal that figure is the real wait until your next token rather than a guess. A 4xx about the file itself will fail again the same way; fix the file.
A refused request never spends a rate token either, so a client that keeps retrying does not push its own next opening further out.
Your first call takes one line of curl.
No plan to choose. No credits to model. No storage to configure.
10,000 calls for $10. No subscription, no expiry.