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 acode 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:
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:
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 }.
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.