Errors
Error bodies and status codes for every Landbot API, what each one means and which ones to retry.
Every Landbot API answers an error with a 4xx or 5xx status and, most of the time, a JSON body. The shape of that body depends on the API and on where the request was stopped.
Check the status code first
Branch on the status code, then read the body. A 404 or a 5xx always arrives as an HTML page, whatever the Accept header says, so parse the body only when Content-Type says it is JSON:
const res = await fetch(url, { method, headers, body });
if (!res.ok) {
const type = res.headers.get("content-type") || "";
const detail = type.includes("json") ? await res.json() : await res.text();
throw new Error(`HTTP ${res.status}: ${JSON.stringify(detail).slice(0, 200)}`);
}
Error bodies
Platform API
The Platform API uses two shapes.
A per-field map, for validation and state errors the endpoint checks itself (400, 402, 403, 412, 422):
{
"errors": {
"customer_id": ["The customer is blocked"]
}
}
A single detail string, for requests stopped before the endpoint runs: a missing or bad token (401), a body that isn't valid JSON (400), a wrong method (405) and rate limiting (429):
{
"detail": "Invalid token."
}
A customer, channel or hook that belongs to another workspace answers 404, the same as one that doesn't exist.
APIchat
Errors the endpoint checks itself (409 and 422) use success and a single error string:
{
"success": false,
"error": "Invalid message type"
}
Everything else uses detail, the same as the Platform API. That includes a missing, wrong or inactive channel token, which answers 403:
{
"detail": "Invalid token."
}
POST /file/{customer_token}/ reports validation errors as a per-field map under errors.
Bots API, AI Agents API and Channels API
The Bots API answers named refusals as {"error": {"code": "...", "message": "..."}}. See When a call fails for every code. The AI Agents and Channels APIs document their error bodies on each operation in the AI Agents API and Channels API references.
On all three, a 404 or 5xx currently arrives as the HTML page described in Check the status code first, not with those JSON bodies. Branch on the status code before you read error.code.
Status codes
| Code | Meaning | Retry |
|---|---|---|
400 |
The body isn't valid JSON. | After fixing the request. |
401 |
Platform API: the token is missing or invalid. See Authentication. | After fixing the token. |
402 |
The workspace's subscription has expired, or its plan doesn't include the feature. | After the plan changes. |
403 |
Platform API: your agent's role lacks the permission. APIchat: the channel token is missing, invalid or its channel is inactive. | After fixing the role or the token. |
404 |
The resource doesn't exist, belongs to another workspace, or the URL matches no route. | After fixing the ID or the path. |
405 |
Wrong HTTP method for the path. | After fixing the request. |
409 |
APIchat: a customer with that token already exists. | No. Read the existing customer instead. |
412 |
The request is valid, but the customer, channel or Field is in a state that blocks it. See 412 Precondition Failed. | After the state changes. |
413 |
The body is too large. | After shrinking it. |
422 |
The JSON parsed but a field is missing or invalid. | After fixing the request. |
429 |
Too many requests. See Rate limiting. | Yes, after Retry-After or a backoff. |
5xx |
Landbot failed. | Yes, with backoff. |
A 402 body names the cause. An expired subscription answers {"errors": {"brand_id": ["Payment required"]}}. A plan without the feature answers <plan> required under brand_id, naming one of the plans listed under plans that include the feature.
On the Platform API, the only 403 from a role is {"errors": {"token": ["You don't have enought permissions"]}}. The agent token acts as its agent, so ask an admin to give that agent's role the permission, or use the token of an agent who has it.
412 Precondition Failed
A 412 body is a per-field map, and each request reports one cause at a time.
| Message | Returned by | Cause | Fix |
|---|---|---|---|
This customer has no chats |
send_*, assign, unassign, archive, unarchive |
The customer has no conversation to act on. | None through the API. The customer has to start a conversation first. |
The channel is deleted |
send_* |
The customer's channel was deleted. | None. Reach the customer on another channel. |
The channel is not active |
send_* |
The customer's channel is turned off. | Turn the channel back on in the dashboard. |
Is not possible to send messages with API to WhatsApp test numbers |
send_* |
The customer is on a WhatsApp test number. | Use a production WhatsApp number. |
The customer is blocked |
send_* |
The customer is blocked. | PUT /customers/{customer_id}/unblock/. |
The customer is assigned to other agent |
send_* |
The customer is assigned to an agent other than the token's. | Send with the assigned agent's token, or unassign the customer first. |
You can't send messages by WhatsApp 24h after the client's last message, try sending a template |
send_text, send_image, send_location |
The customer last wrote more than 24 hours ago on WhatsApp. | POST /customers/{customer_id}/send_template/. |
The customer has not made the opt in |
send_template, opt_out |
The customer has no active WhatsApp opt-in. | Collect an opt-in first. |
This customer is assigned to other agent |
assign, unassign, archive |
The token's agent isn't an admin, and the customer is assigned to another agent. assign also refuses a customer who already has any agent, and unassign one who has none. |
Use an admin agent's token, or PUT /customers/{customer_id}/assign/{agent_id}/. |
The field <name> already exists |
POST /customers/{customer_id}/fields/{field_name}/ |
The Field exists. | PUT the same URL instead. |
Inactive channel |
POST /channels/{channel_id}/message_hooks/ |
The channel is turned off. | Turn the channel back on in the dashboard. |
Any other message under channel_id |
send_* |
The channel's provider refused the message, for example Temporary failure sending message to Facebook, please retry later. |
Read the message. Retry the temporary ones with backoff. |
send_* covers send_text, send_image, send_location and send_template. Sending to an unassigned customer assigns them to the token's agent, so a second agent's token gets The customer is assigned to other agent from then on.
Trailing slash
Every Platform API and APIchat path ends with /. A request without it is redirected with a 301 to the same path with the slash. Most HTTP clients follow that redirect as a GET and drop the body, so a POST without the slash fails in confusing ways. Write the slash.
Retries
- Retry
429and5xxwithout changing the request. Wait forRetry-Afteron a429(or back off when it is missing), and back off exponentially with jitter on a5xx. - Retry
412once the state it names has changed. - Fix the request before retrying
400,404,405,409,413and422. - Fix the token, the role or the plan before retrying
401,402and403.
async function call(url, options, attempts = 5, maxWaitSeconds = 60) {
for (let i = 0; i < attempts; i++) {
const res = await fetch(url, options);
const retryable = res.status === 429 || res.status >= 500;
if (!retryable || i === attempts - 1) return res;
const retryAfter = Number(res.headers.get("retry-after"));
if (retryAfter > maxWaitSeconds) return res;
const wait = retryAfter > 0
? retryAfter * 1000
: Math.min(2 ** i * 100, 5000) + Math.random() * 100;
await new Promise(r => setTimeout(r, wait));
}
}
Duplicate requests
No Landbot API takes an idempotency key. A POST that times out on your side may or may not have run, and retrying it can send the same message twice.
Most state changes on the Platform API are PUT requests (archive, block, assign), and repeating one leaves the same state. For the send endpoints, decide whether a possible duplicate is worse than a possible gap, and retry only when it isn't.