Key concepts
The vocabulary used across these docs (bots, channels, customers, fields, code blocks), defined once and cross-referenced from everywhere else.
Read this once if Landbot is new to you. The same terms show up across the Platform API, APIchat, Widgets SDK and Core SDK docs, and they always mean the same thing. They're easy to confuse the first time, especially "Field", "customData" and "Hidden Field", which are three names for largely the same primitive.
How the pieces fit
┌──────────────────────────────────────────────┐
│ YOUR WORKSPACE │
│ │
│ ┌────────┐ ┌──────────┐ │
│ │ Bot │───▶│ Channel │◀── Webhook ──▶ Your server
│ │ (flow) │ │ (e.g. │ │
│ └────────┘ │ webchat) │ │
│ └──────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────┐ │
│ │ Customer │ │
│ │ ── Fields ── │ │
│ │ email: … │ │
│ │ utm_source: … │ │
│ └────────────────┘ │
│ │ │
│ assigned to │ │
│ ▼ │
│ ┌────────┐ │
│ │ Agent │ (you, a human) │
│ └────────┘ │
└──────────────────────────────────────────────┘
A bot runs in one or more channels. Each conversation is between a customer and either the bot or an agent. Every customer has a bag of Fields that the bot can read and write. A webhook lets your server be notified when messages move through the channel.
Terms
Agent
A human team member in your workspace. Each agent has an agent token (Authentication). Calls made with it act as that agent, with the permissions of the agent's role. A customer can be assigned to an agent (PUT /customers/{id}/assign/) so that person gets the conversation instead of the bot. The agent identifier shows up on customer records as agent_id.
Agent token
The credential the Platform API uses. One per agent. Sent as Authorization: Token <agent_token> against api.landbot.io. Find yours at app.landbot.io/gui/settings/account. Distinct from the channel token.
Bot
A conversation flow you built in the Landbot builder: the blocks, branches and Code blocks that decide what to ask and how to respond. The Platform API identifies a bot by a numeric bot ID. The H-… string the browser SDKs use identifies the channel the bot is served on, not the bot. A single bot can be exposed through several channels (webchat, WhatsApp and APIchat, say) with the same flow.
In the Platform API, a customer can be assigned to a bot with PUT /customers/{id}/assign_bot/{bot_id}/. Two optional body parameters make this far more useful than "start a bot":
nodeis a reference to the block to start from, instead of the flow'sstartnode. Get one by right-clicking a block in the builder and choosing Copy reference.launchset totrue(the default) runs the bot immediately;falsewaits until the customer sends a message.
Together they let you jump a customer to any point in a flow at any time, including a flow they're already in. That's the basis for handing a conversation to your own backend and resuming it later. See Park a conversation and resume it.
A bot exists in two states. The draft is the builder canvas, autosaved as you work; preview runs the draft. The published version is what live channels serve. Editing a flow changes the draft only. Real conversations keep hitting the last published version until you click Publish.
That distinction matters when you test through the API. Preview creates no API-addressable customer (see Customer), so API testing has to happen on a published channel, and you are exercising the published flow, not the edits you made. If a change doesn't seem to take effect, check that you published it. Likewise, a node reference copied from a block that exists only in your draft isn't served by live channels yet, so publish before pointing assign_bot at it.
Flows are built in the builder or through the Bots API. The Platform API's relationship to a bot is assignment: pointing a customer at a flow, optionally at a specific node, plus the conversation that results. Creating a bot and reading or changing its blocks, branches and logic belongs to the Bots API, which is in alpha: it writes the same draft the builder edits, deploys it for testing and publishes it. If you'd rather not build the flow in code, the builder's AI features (Build it for me and Copilot) take a natural-language description of the flow and assemble it for you.
Bot ID
An integer, for example 4043569, and the only thing the Platform API means by bot_id. Read it out of the builder URL:
https://app.landbot.io/gui/bot/4043569/builder
▲
the bot_id the API wants
It's typed as an integer throughout: PUT /customers/{id}/assign_bot/4043569/.
The Bots API does not use this id: it identifies a bot by a uuid, which GET /v0-alpha/bots lists.
Note The
H-XXXXXX-YYYYYYYstring in a bot's hosted URL is not the bot ID. It identifies the channel the bot is served on (see Channel ID), and the Platform API won't accept it where abot_idbelongs. Ifassign_botreturns404, check that first.
Three IDs, and which is which
| ID | Looks like | Where you get it | Used for |
|---|---|---|---|
| Customer ID | 529021057 |
the @id system Field, or GET /customers/ |
every /customers/{id}/… call |
| Bot ID | 4043569 |
the builder URL | assign_bot/{bot_id}/ |
| Channel ID | 3440710 |
GET /channels/, or on web channels the middle segment of the H-… string |
/channels/{id}/message_hooks/, and the channel_id on every customer |
All three are integers. A path that expects one of them and gets something else, such as a customer UUID, returns 404.
Channel
A connection between Landbot and a messaging surface. Channels have an integer ID and a type: landbot (web), whatsapp or whatsapi (WhatsApp), facebook, apichat and others. Every customer belongs to exactly one channel; that's how Landbot knows which surface to deliver messages on. Channels are managed through the Channels endpoints. Setting up a channel is a dashboard task. See the Help Center for WhatsApp and API Chat.
What happens when a flow is waiting on your server
If a flow hands off to your backend and waits (a Webhook block that leads nowhere while you do slow work), what the user can do to break it depends on the channel:
| Channel | User types while the flow waits |
|---|---|
| Web (webchat) | Nothing happens. No persistent connection drives the flow, so it sits until you resume it. |
| APIchat, Messenger, WhatsApp | The submitted message restarts the flow from the beginning, silently dropping the user back to the first block mid-wait. |
On anything but web, a bare wait is unsafe. Park the user in an Ask a Question block that loops back to itself, so any message they send is absorbed by the holding loop instead of restarting the flow. Break them out of it with assign_bot when your work is done. See Park a conversation and resume it.
Channel ID
The integer identifying a channel. Every customer carries one as channel_id, and it's the path parameter for channel-scoped endpoints such as POST /channels/{channel_id}/message_hooks/. List them with GET /channels/.
On a web channel, it's also the middle segment of the H-… string in the bot's hosted URL, the same string that appears in the configuration URL:
H-3440710-YQ702MGG35NCYIND
▲
the channel ID, on web channels
That string identifies the channel the bot is served on, not the bot itself, which is why assign_bot doesn't accept it.
Prefer GET /channels/ anyway. Reading the ID out of the URL only works for web channels, and the URL format isn't a contract. The endpoint works for every channel type, returns the type alongside the ID, and won't break if the hosted-URL format changes.
Channel token
The credential APIchat uses (not the Platform API). Each APIchat channel has its own: three APIchat channels means three independent tokens. Sent as Authorization: Token <channel_token> against chat.landbot.io. Find it in the APIchat channel's settings panel. See Authentication.
Code block
A flow block that runs JavaScript inline in the conversation. In web channels, the code executes in the bot's render context (the iframe, in most widget formats). Inside a Code block, this is the widget instance: you can call this.setCustomData(...), read @{field} interpolations, and reach the parent page through window.fn(). See JavaScript execution.
Not available in WhatsApp or Messenger flows, which run server-side without a JavaScript runtime.
Configuration URL (configUrl)
The JSON representation of a bot, fetched at:
https://chats.landbot.io/u/H-XXXXXX-YYYYYYY/index.json
or (newer hosting)
https://landbot.pro/v3/H-XXXXXX-YYYYYYY/index.json
The H-XXXXXX-YYYYYYY segment identifies the channel the bot is served on (see Channel ID), not the bot's API bot_id.
Replace index.json with index.html to get the rendered hosted page. The Widgets and Core SDKs both take this URL, either passed to the constructor (new Landbot.Popup({ configUrl: '...' })) or fetched manually and handed as a parsed object to new Core(config).
Conversation
The message stream between a customer and whoever is currently assigned (bot or agent). It isn't an API resource: there's no /conversations/ endpoint, and every message-related endpoint addresses the customer. A customer reassigned between bots and agents has had several "logical" conversations, but the API returns them as a single message history per customer.
customData
An object passed at widget construction (or set later with setCustomData(...)) that populates Fields on the customer. Same primitive as a Field, with a different entry point. See Setting variables.
Customer
A person who has spoken with the bot through one channel. Customers have:
- An integer
id, unique in your workspace. - A
channel_id: exactly one channel per customer. - Optionally an
agent_id, if assigned to a human. - A bag of Fields (
custom_fieldsin API responses). - State such as
archived,unreadandlast_message.
Customers are managed through the Customers endpoints.
Preview and test sessions don't create customers. A customer record exists only for a conversation on a published channel. When you run a bot from the builder's preview or Test this AI agent mode, the flow executes normally (logic evaluates, Webhook blocks fire, integrations call out), but no API-addressable customer is created. What breaks is inbound: with no customer_id, nothing outside can reach that conversation, so send_text, assign_bot, Field writes and the rest have nothing to target. Test anything that calls into Landbot against a published channel.
Field (Landbot Field)
A typed variable stored on a customer. Field types: string, integer, float, boolean, date (ISO string), datetime (Unix timestamp), and object.
object holds arbitrary JSON, either an object or an array, and round-trips as real structure instead of an escaped string. The builder canvas presents it as array/list. Use it whenever a Field holds anything that isn't a scalar.
Five ways to set a Field:
- Inside the flow, with a Set-Variable block.
- From a Code block, with
this.setCustomData({...}). - From the parent page (Widgets SDK), with
myLandbot.setCustomData({...})or by passingcustomDataat construction. - From a URL query parameter (Hidden Field).
- From the Platform API, with
POST/PUT/DELETE /customers/{id}/fields/{name}/. See Creating vs updating a Field, because neither verb is an upsert.
Read from the flow with @{field_name} interpolation in Send Message blocks. Code blocks use the same syntax (literal substitution before the JavaScript runs).
The system Field that holds the customer ID
Every conversation carries a system Field, @id, holding that customer's customer_id: the same integer the Platform API addresses in /customers/{customer_id}/….
That's what lets a flow tell your backend who it's talking about: interpolate @id into a Webhook block's body and your server receives the identifier it needs to call back in.
{ "customer_id": "@id", "email": "@email" }
Without it, a Webhook block's request is anonymous and there's no way to answer it. See Park a conversation and resume it.
@id is numeric, so it arrives as a JSON number even if you quote it in the body template: "customer_id": "@id" is delivered as "customer_id": 529094641. That's type-aware interpolation, and it catches out receivers that expect a string.
Creating vs updating a Field over the API
Neither verb is an upsert. POST creates and refuses to overwrite; PUT updates and refuses to create:
| Verb | Field exists | Field doesn't exist |
|---|---|---|
POST |
412, {"errors": {"field_name": ["The field <name> already exists"]}} |
200, creates it |
PUT |
200, updates the value and may also change the type |
404 |
GET |
200 |
404 |
DELETE |
204 |
404 |
A PUT returning 404 usually means the Field was never created on this customer. Field names must match [a-z0-9_]+, and a name with any other character also gets a 404. The 404 body is an HTML page, so check the status code; see Errors.
To set a Field without knowing whether it exists, try POST first and fall back to PUT on 412:
async function setField(customerId, name, payload) {
const url = `https://api.landbot.io/v1/customers/${customerId}/fields/${name}/`;
const opts = {
headers: { Authorization: `Token ${TOKEN}`, "Content-Type": "application/json" },
body: JSON.stringify(payload),
};
const created = await fetch(url, { method: "POST", ...opts });
if (created.status !== 412) return created; // created, or a real failure
return fetch(url, { method: "PUT", ...opts }); // already existed — update instead
}
await setField(529021057, "preferences", { type: "object", value: { plan: "pro" }, extra: {} });
PUT can also change a Field's type in place: a string Field carrying escaped JSON can be PUT as "type": "object" and round-trips as structure from then on.
Field type decides the shape of interpolated JSON
When a Webhook block interpolates a Field into a JSON request body, the Field's declared type determines whether the value arrives as a string or as structure. The same value, {"a":1}, stored in two Fields of different types and sent in one body:
{
"from_a_string_field": "{\"a\":1}",
"from_an_object_field": {"a": 1}
}
The string Field arrives as a JSON string containing JSON, so your receiver must parse it a second time to reach a. The object Field arrives as a real nested object.
Neither case is double-encoding: the request body is always a well-formed JSON object with Content-Type: application/json. Only the value is escaped, and only when the Field holding it is typed as text. If structured data arrives at your server as an escaped string, fix it on the sending side by storing it in an object Field. In the builder canvas that's the array/list type; over the Platform API it's "type": "object".
Warning Unrecognized type strings are not rejected.
POSTing"type": "array","list"or"json"returns200, but the value is silently coerced and stored as astring:[1,2,3]comes back as"[1, 2, 3]", and{"a":1}as"{'a': 1}", which isn't valid JSON. The only type that preserves structure isobject. Read thetypein the response to confirm what was stored.
Hidden Fields
A declarative way to capture URL query parameters into named Fields. Declared in the Hidden Fields panel of the bot's Settings. When a user lands on the bot URL with ?name=Jane&utm_source=newsletter, those values populate Fields named name and utm_source before the first flow block runs. See Setting variables, from URL query params.
Widgets embedded in an iframe don't inherit the parent page's query params. That's the most common surprise.
Message hook (Webhook)
A URL Landbot POSTs to when messages move through a channel, registered with POST /channels/{channel_id}/message_hooks/ on the Platform API. If you set a token on the hook, Landbot sends it as Authorization: Token <token> on every delivery; there's no signature. APIchat has its own webhook with a different payload. Both are described in Webhooks.
Question block (Ask a Question)
A flow block that pauses and waits for user input. It comes in several kinds: text input, buttons (with optional payloads), multiple-choice buttons, rating, date, file upload and others. Each Question block can save the answer to a Field by name. It's the most common block when you need data from the user.
samurai field
The discriminator that distinguishes bot messages from user messages on the receive side. Bot messages carry samurai (usually a small negative integer identifying the bot or persona); user messages don't. See Messages, bot vs user messages.
Send Message block
A flow block where the bot says something: pre-written text (with @{field} interpolation), an image, a card, a media block. It doesn't pause for user input. Paired with the Question block, it makes up most flows: Send Message, Question, Send Message, Question.
Webhook
See Message hook.