Skip to main content
Almost every failure carries the same body:
Some failures add a machine-readable code. Two places use a different shape entirely, and both are documented below.
error is a message for a human, not a stable identifier. Log it, show it to an operator, branch on the HTTP status or on code — but do not build control flow on the exact string.

Status codes

A request to a path the API does not serve answers 404 with a plain-text body rather than the JSON envelope. If you get text back, the URL is wrong.

Coded errors

Four failures carry a code alongside the message. These are the ones worth branching on, because each has a specific fix and none of them will resolve on a retry. A typical body:
In POST /executions/bulk these codes appear per item instead of on the response. The call answers 201, and each rejected item shows up in failed with its deviceId, an error, and a code when there is one:

Validation failures

A body, query string or path parameter that fails schema validation does not use the { error } envelope. The validator returns its raw result, so you get a 400 shaped like this:
The issues array is Zod’s, verbatim, and its exact contents depend on how the value was wrong. Read path to find the offending field. A client that parses error bodies has to handle both this shape and { error }.
Not every 400 is a validation failure. Creating an execution can also answer 400 with a plain { error } — an account that is not linked to the device, an account on the wrong platform for the automation, or a container that has moved or been deleted. Check for error before assuming issues exists.

The upload endpoint’s variant

POST /content/upload is multipart, so its form fields are validated by hand. A field failure answers 400 with a third shape:
details is a flattened Zod error: fieldErrors keyed by field name, plus formErrors for anything that did not belong to one field. A request with no file at all answers 400 with { error: "No file provided" } and no details.

401 and 403 are different problems

401 is a credential problem: the header is missing or malformed, or the key matches nothing. Retrying with the same key is pointless, and so is refreshing anything — there is nothing to refresh. 403 is an authorization problem: the key is good, and the user behind it is not allowed to do this. That covers a role without the required permission, a disabled user, a user in no organization, and the two entitlement codes above. Never sign a user out or re-authenticate on a 403; surface it to a human. Full bodies for both are in Authentication.

Not found, or not yours

Most resources are scoped to the key owner. A row that exists but belongs to a teammate answers exactly like a row that does not exist — 404, same message. You cannot tell the two apart, and that is deliberate. The same goes for admin-only automations: GET /automations/{id} answers 404 rather than 403 for a key that may not see them.

What to retry

Scheduling is not idempotent. There is no idempotency key, so a create that timed out may or may not have landed — read GET /executions filtered by device and time range before sending it again.