Skip to content

API errors

A failure carries an error object where a success would have carried data:

{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "customer.id is required for jobs. Look it up via GET /api/v1/customers.",
"field": "customer.id"
},
"request_id": "req_abc123",
"timestamp": "2026-09-07T12:00:00Z"
}

Read error.code, not error.message. The code is the contract; the message is written for a human reading a log and can be reworded. On a validation failure, field names the offending field, which is usually enough to fix it without guessing.

StatusMeaning
201Job or request created
200A lookup succeeded, or an idempotent replay
401Missing or invalid credentials, or a key that has expired or been revoked
403The credential lacks permission — or the request came over HTTP
409Duplicate external ID, or an Idempotency-Key whose original call is still running
422Valid JSON, invalid contents — or an idempotency conflict
429Rate limit exceeded, by the minute or by the hour
500Something broke on the server
501The endpoint is out of scope, reserved or not implemented

200 on a create is not a mistake. It means your idempotency key has been seen before and you are getting the original answer back — the job exists, and it was not created twice. See idempotency.

error.codeStatusMeaning
VALIDATION_ERROR422A field is missing or invalid — field says which
AUTH_ERROR401Missing, invalid, expired or revoked credential
PERMISSION_DENIED403The credential lacks permission
HTTPS_REQUIRED403The request came over HTTP — see if a key is exposed
DUPLICATE_EXTERNAL_ID409That external_id already exists for this resource
IDEMPOTENCY_CONFLICT422 / 409The key was reused with a different body (422), or while the first call was still running (409)
RATE_LIMITED429Too many requests — see Retry-After
NOT_IMPLEMENTED501Out of scope or not implemented
INTERNAL_ERROR500Server or database error

This table is the whole list. Do not assume a code exists because it is a common convention elsewhere — handle these, and treat anything unexpected as a generic failure rather than matching on a code you hoped for.

501 is an answer, not a fault. It means you have called something this API does not do — reading a job back, updating one, deleting one. The scope is short on purpose.

Rate limited:

{
"success": false,
"error": {
"code": "RATE_LIMITED",
"message": "Minute rate limit exceeded. Retry after 42 seconds."
},
"request_id": "req_abc123",
"timestamp": "2026-09-07T12:00:00Z"
}

Sent over HTTP:

{
"success": false,
"error": {
"code": "HTTPS_REQUIRED",
"message": "This API is available over HTTPS only"
},
"request_id": "req_e9fb04dd8728f7c6",
"timestamp": "2026-09-09T09:19:05Z"
}

Always send the request_id. It identifies the exact call in the server’s logs, and without it a support conversation starts with trying to find the request rather than fixing it. That is why it is worth storing alongside whatever your integration created.

Never put an API key or secret in a ticket. Support does not need it, and a credential in a ticket thread is a credential to revoke.

Contact support too when you need something the API does not do — the OpenAPI 3.0 specification, or a status-visibility workflow. Ask before you design around the gap rather than after.