Error codes

Every machine-readable error code we emit

This is the consolidated table of error code values returned by the HTTP API. The text of message is human-readable and may change without notice; the code is stable and is what you should branch on.

For the error-envelope shape see the API overview.

Authentication and authorisation

HTTPcodeWhen
401auth_requiredNo Authorization header on a protected route
401token_invalidToken is malformed or revoked
401token_expiredToken has expired (session tokens only)
403forbiddenToken correct, but scope does not cover route
403device_not_in_tokenDevice token does not own the device in body
403account_suspendedAccount is past-due or admin-suspended

Request validation

HTTPcodeWhen
400bad_jsonBody is not valid JSON
400bad_queryQuery string parameter has wrong type
400missing_fieldRequired field omitted; message names the field
400no_measurementsmeasurements array is empty on ingest

Semantic validation (422)

HTTPcodeWhen
422unknown_kindMeasurement type not in supported set
422out_of_rangeSanity-check bound exceeded (see POST /v1/ingest)
422ts_in_futurets more than 30 s in the future
422ts_too_oldts more than 30 days in the past
422invalid_rangeRule ok_min > ok_max
422grace_too_longRule grace > 6 h
422period_emptyReport period contains no measurements
422period_too_longReport period > 13 months (beyond raw retention)
422unknown_templateReport template not in supported set

Resource state (404 / 409)

HTTPcodeWhen
404account_not_found(Diagnostic only — never leaks existence)
404site_not_found
404device_not_found
404sensor_not_found
404channel_not_found
404rule_not_found
404event_not_found
404report_not_found
409local_id_takenAnother device on the site has the same local_id
409idempotency_conflictSame key, different body, within 24 h

Rate limits

HTTPcodeWhen
429rate_limitedGeneric — see X-RateLimit-* headers
429report_rate_limit> 4 reports / min / account
429ingest_rate_limit> 60 req / min / device
429export_rate_limit> 1 export / 5 min / account

Server

HTTPcodeWhen
500internalCatch-all — request_id in the body
503db_unavailablePostgres unreachable; retry with backoff
503pdf_unavailablePDF service container restarting
504webhook_timeoutCustomer-supplied webhook destination > 5 s

What to do per code

For client code:

# Pseudocode for a defensive ingest client
try:
    post_ingest(payload)
except HTTPError as e:
    code = e.json()['error']['code']
    if code in ('rate_limited', 'ingest_rate_limit'):
        sleep(retry_after_seconds); retry()
    elif code in ('bad_json', 'no_measurements', 'unknown_kind',
                  'out_of_range', 'ts_in_future', 'ts_too_old'):
        log_locally(payload, e); drop()    # do NOT retry, fix client
    elif code in ('auth_required', 'token_invalid', 'token_expired',
                  'device_not_in_token', 'forbidden'):
        alert_operator(e); halt()           # human intervention
    elif code in ('internal', 'db_unavailable'):
        sleep(exponential_backoff); retry()
    else:
        log_locally(payload, e); drop()    # unknown error — fail safe

For your own integration tests, asserting on code rather than HTTP status preserves correctness across our internal refactors.