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.uuid identifies the message and is the same on every retry. Skip a uuid you have already handled.
  • APIchat webhook: there is no message ID. Use customer.token and the message's timestamp together.

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" }
    }
  ]
}
  • messages always has exactly one entry.
  • type decides the shape of data. Expand the message webhook in the Platform API reference for every type.
  • sender.type is customer, bot, agent or sys. _raw.author_type names the same thing as user, bot, agent or sys. Pick one and use it everywhere.
  • customer carries the customer's Fields as extra keys. On web channels, is_external is the string "True" or "False", not a boolean.
  • timestamp is 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" }
}
  • messages always has exactly one entry. customer, channel and agent sit next to it, not inside it.
  • Message fields are flat: there is no data wrapper and no _raw.
  • agent.type is bot for the bot, or human for an agent writing from the inbox. A human agent also carries name and email.
  • channel has no type. Its absence tells an APIchat payload apart from a Platform one.
  • customer carries the customer's Fields as extra keys, and token is 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.