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.
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#
- Idempotency: making retries safe
- Rate limits
- Authentication
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.