KardinalError and can be imported from the package root.
Error families
A code the SDK doesn’t know yet falls back to the family matching its HTTP status, so a new code added by the API doesn’t go unnoticed. For the meaning of each code, see the
Error schema in the API Reference.
Common error attributes
Every exception carries the same attributes, named in each language’s case:retryable is true for a 429, and for a 5xx or a connection error — except when a submit may have reached the server, since the plan may then already exist.
Validation errors
A400 means the API refused the plan as sent. errors holds every problem found, each with its code and, in properties.path, the field concerned: fix them all before submitting again rather than one per attempt. Most structural mistakes never reach the API: the SDKs catch them locally first (see Validating locally before you spend credits).
Quotas and rate limits
403: a limit was hit. For a builder account, a 403 always means a limit, never a permission refusal: the SDKs raise KardinalLimitError for every one of them, and details says which.
- Plan-size limit (too many resources, orders, or stops, or more than 4 stops in an order without
successiveStops):errorsalso holds oneINVALID_VALUEentry per exceeded limit, naming the field and the allowed value. Shrink the plan. - Daily or monthly optimization quota: wait, or ask for a higher quota.
- Insufficient credits: top up your credits in the Console.
429: a temporary overload, not a quota. It comes with an empty body. The SDKs retry it on their own, honouring a Retry-After header if the API sends one, and backing off otherwise. See Temporary overload (429).
What the SDKs retry
The number of retries is set by
max_retries / maxRetries (3 by default), with a growing, randomized delay between attempts. submit is never retried because the API has no idempotency key: a retried submit could create the plan twice. After a failed submit, check with plans.list whether the plan exists before submitting again.
Client-side errors
Some errors are raised before any request is sent:-
UnknownFieldError—submitrefuses a plan containing a key no model declares, and lists each one with its path and the closest real field name. The API would drop such a key without an error: a misspelledoperationDurationnwould leave the stop with no service time, and the plan would still look valid.updatesorts unknown keys by origin: a key the server sent, from a field added after your SDK version, is sent back untouched; a key you added is refused as a typo. To send a field the API supports but your SDK version doesn’t know yet, name it explicitly:allow_unknown=["aNewField"](Python) or{ allowUnknown: ["aNewField"] }(TypeScript). A name listed there is allowed anywhere in the plan, so prefer distinctive names. -
Model validation — a missing required field or a wrong type fails when the plan is built or parsed:
pydantic.ValidationErrorin Python,ZodErrorin TypeScript. -
Missing key —
MissingApiKeyErrorandEmptyApiKeyError, raised when creating the client — see API key resolution.
- Python
- TypeScript
On a plan this SDK didn’t read from the API (built by hand, or loaded from a file),
update can’t tell where an unknown key came from: it raises an UnknownFieldWarning through Python’s warnings module, only for a key within two edits of a real field, and sends the plan.Warnings on successful calls
When the API accepts a plan but amends part of it — amaxOptimizationDuration clamped to your agency’s cap, crow-fly distances forced in the sandbox, predictive traffic turned off — submit and update return the amendments in warnings:
get never returns them, only the amended values. Solutions, objective lists, and the references returned by start and stop carry a warnings field too. The Warning schema in the API Reference lists what each one means.
