Webhooks
How Landbot posts messages to your server: the two kinds of webhook, how to verify and acknowledge a delivery, retries, and the payloads.
Landbot posts messages to your server in two ways:
| Platform message hooks | APIchat webhook | |
|---|---|---|
| Set up | POST /channels/{channel_id}/message_hooks/ on the Platform API, on any channel |
The webhook URL in an APIchat channel's settings |
| Fires for | Every message in the channel's conversations: customer, bot, agent and system. Not for test conversations, such as the builder preview | Messages the bot or a human agent sends to the customer |
Authorization header |
Token <token>, with the token you set on the hook |
None |
| Timeout | 10 seconds | 60 seconds |
Both send one message per request, as a JSON POST with User-Agent: landbot/webhooks.
Verify a delivery
A Platform message hook sends Authorization: Token <token> with the token you set when you created it. Compare the whole header with the value you expect and reject anything else. If you set no token, the header is still sent, with nothing after Token. There is no signature to check.
To change a hook's token, send PATCH /channels/{channel_id}/message_hooks/{hook_id}/ with the new token.
An APIchat webhook sends no Authorization header. Put a long random secret in the webhook URL (for example as a query parameter) and check it on every request, or accept requests only through a gateway you control.
Acknowledge and retries
Answer any 2xx to acknowledge a delivery. Landbot ignores the response body.
Any other status is retried, 4xx included. So is a 3xx, because Landbot doesn't follow redirects. Retries come about 5, 10, 15, 20 and 25 seconds apart, for 6 attempts in total. To drop a message for good, answer 2xx and discard it on your side.
Landbot doesn't retry when it can't connect or when your server takes longer than the timeout. Answer first and do the slow work afterwards:
app.post("/landbot/hook", (req, res) => {
if (req.get("authorization") !== `Token ${process.env.LANDBOT_HOOK_TOKEN}`) {
return res.sendStatus(403);
}
res.sendStatus(200);
handle(req.body).catch(console.error);
});
Duplicates and order
A retry sends the same body again, and deliveries can arrive out of order. Make your handler idempotent and sort by timestamp.
- Platform message hooks:
_raw.uuididentifies the message and is the same on every retry. Skip auuidyou have already handled. - APIchat webhook: there is no message ID. Use
customer.tokenand the message'stimestamptogether.
Hooks follow your plan
Message hooks need a plan that includes them. Creating or changing one without it answers 402. If your plan loses the feature, Landbot turns every message hook off, and turns them back on when the plan regains it.
Platform payload
{
"messages": [
{
"type": "text",
"timestamp": 1780054017.039184,
"data": { "body": "Hi, I need help with my order" },
"sender": { "id": 529094641, "name": "Visitor #1234", "type": "customer" },
"customer": { "id": 529094641, "agent_id": null, "archived": false, "name": "Visitor #1234" },
"channel": { "id": 3440710, "name": "Web chat", "type": "landbot" },
"_raw": { "uuid": "5f0c6a8e-6f1b-4a8e-9a3c-2b7d1e4f9c10", "author_type": "user" }
}
]
}
messagesalways has exactly one entry.typedecides the shape ofdata. Expand themessagewebhook in the Platform API reference for every type.sender.typeiscustomer,bot,agentorsys._raw.author_typenames the same thing asuser,bot,agentorsys. Pick one and use it everywhere.customercarries the customer's Fields as extra keys. On web channels,is_externalis the string"True"or"False", not a boolean.timestampis in seconds since the epoch.
APIchat payload
{
"messages": [
{
"type": "dialog",
"title": "How can I help?",
"buttons": ["Track an order", "Talk to a person"],
"payloads": ["$0", "$1"],
"timestamp": 1780054017.039184
}
],
"customer": { "id": 2000, "token": "my-customer-42", "name": "Peter", "phone": null, "email": null, "register_date": 1780053990.12 },
"channel": { "id": 100, "name": "Android App" },
"agent": { "id": 1450, "type": "bot" }
}
messagesalways has exactly one entry.customer,channelandagentsit next to it, not inside it.- Message fields are flat: there is no
datawrapper and no_raw. agent.typeisbotfor the bot, orhumanfor an agent writing from the inbox. Ahumanagent also carriesnameandemail.channelhas notype. Its absence tells an APIchat payload apart from a Platform one.customercarries the customer's Fields as extra keys, andtokenis the customer token you use with/send/.
type |
Fields |
|---|---|
text |
message |
dialog |
title, buttons, payloads |
media_dialog |
url, media_type, buttons, payloads |
image, video, audio, document (decided by the file's extension) |
url, message (caption) |
location |
latitude, longitude |
Every message also has type and timestamp. The APIchat API reference has the full schema, and Bridge a channel to Landbot consumes this webhook end to end.