Errors & troubleshooting
Most errors from the /v2 API are machine-readable problem+json with a stable top-level code. A few have an empty body instead, and one class of validation failure uses a different, code-less shape. This guide covers all of it: the shapes, the code registry by status, and how to react to each.
When to use this
- You're writing error handling and want to branch on a stable
code, not a status code alone. - You hit a
400/401/403/404/409/410/413/415/422/429and need to know what it means and what to do.
The problem+json shape
Most errors follow RFC 9457 (application/problem+json) with a stable top-level code. For field-level problems, errors[] lists each offending field, but read the subsection below before you parse errors: its shape is not always the same.
{
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
"title": "Validation failed.",
"status": 400,
"code": "validation_failed",
"errors": [
{ "pointer": "Cargo[0].Geometry.Type", "code": "validation_failed", "message": "Only 'box' geometry is supported." }
]
}
| Field | Notes |
|---|---|
type |
URI for the problem type. Not customized per error: it points at the response's HTTP status, so two failures with different code values share one type. Usually the RFC 9110 section for that status, but the 422 points at RFC 4918 instead, because RFC 9110 defines no 422, and the three 429s carry no type at all. Switch on code, never on this field. |
title |
Short, human-readable summary. |
status |
HTTP status code (also on the response). |
detail |
Human-readable explanation for this occurrence, where present. |
code |
Stable machine-readable code; branch on this when it's present. |
errors[] |
Per-field problems on this shape: pointer, code (always validation_failed per entry), message. |
Only title and status are guaranteed present; type/code/detail/errors appear where they apply. type is on every problem body except the three 429s, and, as the next section explains, some 400s have no code at all.
What a client actually has to handle
errors[].pointeris a PascalCase C# property path with[i]indexers (e.g.Carriers[0].ExternalId). It is not a JSON Pointer and not camelCase. Lowercase the first letter of each segment yourself if you map it back to a form field.- A second, code-less
400shape exists. Some validation failures onPOST /v2/jobsare caught only after the request is mapped onto the internal job shape; those come back as a standard ASP.NETValidationProblemDetails, whereerrorsis an object map keyed by property name (each value a list of messages) and there is nocodefield at all. Treat any400without a top-levelcodeas a validation failure and fall back totitle/errors. - Some responses have no body.
401, the idempotency409(reusedIdempotency-Keywith a different body) and the400for an oversizedIdempotency-Keyare all empty. The409s forjob_already_terminalandjob_not_succeededdo carryproblem+jsonwith acode.
The error code registry, by status
| HTTP | code |
When |
|---|---|---|
400 |
validation_failed |
Request body invalid: errors[] lists the fields (see above for the shapes a 400 can take) |
401 |
none (empty body) | Key missing, invalid or revoked |
403 |
customer_api_not_entitled |
Key valid, but the plan has no customer API access. You can create a key on any plan, so this is the response an integration built ahead of an upgrade will see. The detail field names where to upgrade |
403 |
request_body_not_supported |
You sent a request body to POST /v2/jobs/{id}/instructions or POST /v2/jobs/{id}/share. Neither takes one; send the request without a body. Note this shares the 403 status with the plan check above, so branch on code, not on the status |
403 |
mixed_palletizing_not_entitled |
POST /v2/jobs submitted a MixedPalletizing job and the plan does not include that problem type. Distinct from the three 422s below: this means the capability is missing, not that the job is too big |
403 |
load_planning_not_entitled |
The symmetric case: POST /v2/jobs submitted, or defaulted to, a LoadPlanning job and the plan does not include it. Rare in practice, since every tier ships load planning enabled |
403 |
link_sharing_not_entitled |
POST /v2/jobs/{id}/share and the plan does not include link sharing. Checked before the job lookup, so an unentitled caller learns about the plan rather than about the job. Revoking a link is never gated this way |
404 |
job_not_found |
Unknown job id, or a job that belongs to another account |
404 |
result_not_ready |
Job exists but hasn't succeeded yet |
404 |
artifact_not_ready |
The instructions job is yours, but its PDF hasn't rendered yet, or was cleaned up by retention. Either way this is 404; keep polling |
409 |
job_already_terminal |
POST .../cancel on a job that already finished |
409 |
job_not_succeeded |
POST .../instructions or POST .../share on a job that hasn't succeeded |
409 |
none (empty body) | Idempotency-Key reused with a different body |
413 |
payload_too_large |
The request body is larger than the API will read. Permanent for that body, so retrying it unchanged cannot succeed: split the order across several jobs instead. A very large cargo array is the usual cause |
415 |
unsupported_media_type |
A body sent to POST /v2/jobs or POST /v2/jobs/validate under a content type other than application/json, or with none at all: these two read JSON only. An empty body with a stray content type is not an error. The endpoints that take no body answer 403 request_body_not_supported instead, whatever the content type |
422 |
max_items_per_job |
The job has more cargo units than your plan allows. Send fewer items, or upgrade the plan. detail names the limit's value |
422 |
max_carriers_per_job |
The job uses more distinct carrier types than your plan allows. Note this counts carrier types, not how many of each: one carrier entry with quantity: 20 is one type. Combine types, or upgrade the plan |
422 |
max_carrier_instances_per_job |
The carrier quantities add up to more carriers than your plan allows. That is the sum over all entries, where an entry without a quantity counts as one. Lower the quantities, or upgrade the plan |
429 |
rate_limit_exceeded |
Too many requests, too fast: the account may make 300 requests per minute across /v2. This is the one response that tells you how long to wait, in a Retry-After header |
429 |
too_many_active_jobs |
Concurrent-job limit reached |
429 |
monthly_quota_exceeded |
The monthly job quota is used up for the job's problem type. Each problem type meters its own pot, so mixed palletizing running out says nothing about load planning, and the other pot keeps accepting jobs. See Account limits |
500 |
internal_server_error |
Something failed that the API did not anticipate. Treat it as transient: retry, and if it persists contact support. Not the same as the failed-job error.code internal_error further down this page, which reports a job that was accepted and then did not solve |
A 500 carries internal_server_error and gives you nothing else to branch on: the cause is recorded internally and never appears in the body. It is the one entry in this registry that does not tell you what to change, because there is nothing on your side to change. Retry it.
A 429 is retryable, but how you wait depends on which one you hit. rate_limit_exceeded carries a Retry-After header with the number of seconds; honour it. too_many_active_jobs and monthly_quota_exceeded carry no such header, so back off on a schedule of your own choosing.
Validate first to catch 400s without queuing a job: POST /v2/jobs/validate runs the same two-stage validation as POST /v2/jobs, but always answers with the code-bearing shape above, even for a post-mapping failure. The code-less ValidationProblemDetails shape only shows up on POST /v2/jobs itself. Validate never returns 404, 422, too_many_active_jobs or monthly_quota_exceeded: it does no job lookup, entitlement check or quota check of its own. It is not exempt from the request rate limit, though, so a tight validation loop can still earn a 429 rate_limit_exceeded. It is not exempt from 401/403, though: authentication and the plan's customer-API gate run for every /v2 route, including this one, before any handler gets to see the request. See Async & validate.
Failed jobs aren't HTTP errors
A job that fails during solving still returns 200 from GET /v2/jobs/{id}; the failure is in the body: status: failed, a raw internal errorCategory string, and an error object (code + message). Likewise, cargo that couldn't be placed in a succeeded job appears in unplacedItems, not as an error. See Async & validate.
error.code is a closed set:
error.code |
Meaning |
|---|---|
invalid_input |
The job couldn't be solved as submitted; have the user adjust the input. |
solver_error |
The solve didn't complete; retry. |
render_error |
Document rendering failed; retry. |
internal_error |
Everything else: retry, or contact support. |
error.message is a safe, human-readable sentence and never contains exception detail. errorCategory is the raw internal category behind it: diagnostic only, and not a closed set. Switch on error.code, not errorCategory.
Notes & caveats
- Branch on
code, nottitle/detail. Codes are stable; the human-readable text may change, and, per above, some400s have nocodeat all. 403is permanent,429is transient. Retrying a403won't help: the account's plan needs customer API access before the call will succeed.429is worth retrying after a wait. Onlyrate_limit_exceededtells you how long, inRetry-After; for the other two, pick your own interval.404hides existence on purpose. A job that belongs to another account looks exactly like a job that doesn't exist: both arejob_not_found.- Two registries, one field name.
internal_server_erroris a top-levelcodeon a500: the request did not complete, and the job may not exist.internal_erroris anerror.codeinside a200body: the job does exist, was accepted, and then failed while solving. Keeping one lookup table for both fields conflates the two. - Validate to shift errors left.
POST /v2/jobs/validatereturns the same400shapes as a real submit, without queuing a job or triggering a solve.
Related
- Authentication:
401/403/404auth behaviour and account limits. - Async & validate: dry-run validation, idempotency
409, failed-job detail.