Errors

One error envelope for the whole API: every status, every failure class, the same shape. Here is what each field means and what to do about each code.

Updated ·6 min read

Every non-2xx response on the API uses one envelope. A validation failure, a missing key, a 404 and a 500 all parse the same way, so a client needs one error path rather than five.

{
    "error": {
        "type": "invalid_request_error",
        "code": "parameter_invalid",
        "message": "One of email, phone or identifier is required.",
        "param": "email",
        "errors": {
            "email": ["One of email, phone or identifier is required."]
        },
        "request_id": "req_01k2p8w3q6z9y0abcdefgh7jkm"
    }
}

The fields#

Field What it is
type The broad class. Branch on this
code The specific failure. Log this
message Human-readable. Do not parse it; wording can change
param The field at fault, when one field is at fault
errors Per-field messages, on validation errors
request_id This exact call, in our logs. Also on the Request-Id header

param and errors are omitted entirely when they do not apply, rather than sent as null. Only type, code and message are always present.

Branch on type and code. Never on message, which is written for a person and will change.

Types#

Seven, and this list is closed. Anything else is a bug on our side.

Type Meaning
invalid_request_error The request was wrong. Fix it and retry
authentication_error The key is missing, malformed, revoked or expired
permission_error The key is real but is not allowed to do this
not_found_error No such object, or it belongs to another workspace
rate_limit_error Slow down and retry after the given delay
idempotency_error The Idempotency-Key clashed with an earlier request
api_error Something failed on our side. Retrying is reasonable

Codes#

Status Type Code What to do
400 invalid_request_error parameter_missing A required parameter was not sent. param names it
400 invalid_request_error parameter_invalid A parameter was sent but is not usable. param names it
400 invalid_request_error idempotency_key_too_long Keep Idempotency-Key to 255 characters or fewer
401 authentication_error missing_api_key No bearer token, or one that is not an llk_ key
401 authentication_error invalid_api_key No such key, or it is revoked or expired
403 permission_error insufficient_scope The key lacks the scope this route needs
403 permission_error permission_denied The key may not perform that action
403 permission_error test_mode_unsupported A test key called something that would reach real data. Use a live key for it
403 invalid_request_error plan_required The action needs a higher plan. Note the type here is invalid_request_error, not permission_error
404 not_found_error resource_missing No such object, or it is not yours
405 invalid_request_error method_not_allowed Right path, wrong verb
4xx invalid_request_error bad_request A client error with no more specific code
409 idempotency_error idempotency_key_reused Same Idempotency-Key, different request
409 idempotency_error idempotency_key_in_flight The first request with that key is still running. Retry
422 invalid_request_error parameter_invalid The body did not validate. Check errors for the field
429 rate_limit_error rate_limit_exceeded Back off for Retry-After seconds
429 rate_limit_error submission_rate_limited One signup is being submitted too fast. Honor Retry-After
429 rate_limit_error too_many_requests Too many failed key attempts from one IP address
500 api_error internal_error Ours. Retry, then quote the request_id

Two of these are worth a closer look, because each covers two different cases:

parameter_invalid arrives as a 400 and as a 422. The 400 is a query-string or cursor parameter we rejected before touching the body. The 422 is the request body failing validation, and only that one carries errors. Branch on the status, not the code alone.

401 splits into missing_api_key and invalid_api_key. The first means nothing usable arrived in the Authorization header, which is almost always a client bug. The second means a key arrived and we do not accept it, which is almost always a revoked, expired or mistyped key. Neither response says which of "no such key", "revoked" or "expired" applies, deliberately: an attacker holding a candidate token has no business learning that.

Codes a single endpoint can add#

Some endpoints refuse for reasons only they know about. These all use invalid_request_error and follow the same envelope, so no extra parsing is needed:

submission_rejected · external_id_taken · form_without_identity · no_capture_form · contact_not_created · contact_not_pending · contact_hidden · invitation_not_issued · test_audience_full · contact_unsubscribed_themselves · contact_cannot_be_emailed · contact_held · broadcast_state_conflict

contact_hidden is a 403 on PATCH /v1/contacts/{contact}: the contact is past your plan's contact size, so it is hidden and cannot be edited until the workspace moves up a size. Nothing is written, and nothing is deleted.

contact_unsubscribed_themselves (409), contact_cannot_be_emailed (422) and contact_held (409) are the refusals of POST /v1/contacts/{contact}/resubscribe, explained on verification and subscription. broadcast_state_conflict is a 409 from a broadcast call the broadcast's current state does not allow, such as editing one that has already gone out.

test_audience_full is a 403 on POST /v1/contacts with a test key: the project already holds 100 test contacts. Nothing is created, and nothing is captured as a real contact instead. Clear test data, or delete a test contact, and try again.

Invitation redemption adds its own reason as the code on a 409 or 422, one of:

expired · revoked · exhausted · already_redeemed · domain_not_allowed · invalid · not_invited

The OpenAPI document lists which of these each operation can return.

Why cross-tenant is 404 and not 403#

Asking for an object in someone else's workspace returns 404, never 403.

A 403 would be an admission that the id exists somewhere. That is a real leak: given a list of ids, you could learn which are real by watching which return 403 rather than 404. So the API answers identically whether an object does not exist or is simply not yours.

Handling errors well#

Retry 429 and 5xx, not 4xx. A validation failure will fail identically forever; retrying it just burns your rate limit. The one 4xx worth retrying is 409 idempotency_key_in_flight, which says an earlier attempt of this exact request is still running, so wait briefly and send it again.

Use an Idempotency-Key on writes. Then a retry after a timeout is safe, because a replay returns the original response instead of creating a second contact. See idempotency.

Log the request_id on every failure. It is the difference between a support conversation that takes one message and one that takes six.

Next#

Common questions

What does a 404 from the LaunchList API mean?

Either the object does not exist, or it belongs to another workspace. We return 404 rather than 403 across a tenant boundary on purpose, because a 403 would confirm the id is real somewhere else.

Why am I getting 401 with a key I just created?

Read the error code. "missing_api_key" means nothing usable reached the Authorization header, so check the "Bearer" prefix and look for whitespace copied along with the key. "invalid_api_key" means a key arrived and we do not accept it, so check it has not been revoked, expired or mistyped.

What is the request_id in an error response for?

It identifies that exact call in our logs. Quote it in support and we can find precisely what happened rather than guessing from a description.

Was this page helpful?