# 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. Source: https://getlaunchlist.com/help/docs/api/errors --- 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. ```json { "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](/help/docs/api/contacts-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](/help/docs/api/openapi) 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](/help/docs/api/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. > [!TIP] A timeout is not a failure > If a write times out, the request may still have succeeded. Do not assume it failed and retry > blindly. Retry with the same `Idempotency-Key` instead: if the first attempt landed you get its > response back, and if it did not, the retry does the work once. ## Next - [Idempotency](/help/docs/api/idempotency): making retries safe - [Rate limits](/help/docs/api/rate-limits) - [Authentication](/help/docs/api/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.