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 handful of terms show up across the Platform API, APIchat, Widgets SDK, and Core SDK docs, and they always mean the same thing — but they're easy to confuse the first time around (especially "Field" vs "customData" vs "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) that grants the same access via the Platform API as their dashboard login. A customer can be assigned to an agent (PUT /customers/{id}/assign/) so that human 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 visual blocks, branches, and Code blocks that decide what to ask and how to respond. Bots have two IDs: a numeric one the Platform API uses, and an H-… one the browser SDKs use. A single bot can be exposed through multiple channels (webchat + WhatsApp + APIchat, say) — same flow, different surfaces.

In the Platform API, a customer can be assigned to a bot via PUT /customers/{id}/assign_bot/{bot_id}/. Two optional body parameters make this far more useful than "start a bot":

  • node — a reference to the block to start from, instead of the flow's start node. Get one by right-clicking a block in the builder and choosing Copy reference.
  • launchtrue (the default) runs the bot immediately; false waits 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 hit Publish.

That distinction bites when you test through the API. Because preview creates no API-addressable customer (see Customer), API testing has to happen on a published channel — which means you are exercising the published flow, not the edits you just 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 part of what live channels serve yet, so publish before pointing assign_bot at it.

Flows are built in the builder, not through the API. There are no endpoints for creating a bot or for reading or modifying a flow's blocks, branches, or logic. The Platform API's entire relationship to a bot is assignment — pointing a customer at a flow, optionally at a specific node — plus the conversation that results. If the integration you're building needs a flow that doesn't exist yet, building it is a builder task, not an API call. The builder's AI features — Build it for me and Copilot — take a natural-language description of the flow and assemble it for you, so "describe the flow, then wire up the API" is usually the fastest path.

Bot ID

An integer — e.g. 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/.

Note The H-XXXXXX-YYYYYYY string 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 a bot_id belongs. If assign_bot is 404ing, that swap is the first thing to check.

Three IDs, and which is which

The identifiers get confused constantly, so:

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

Channel

A connection between Landbot and a messaging surface — webchat, whatsapi (WhatsApp), facebook, apichat, etc. Channels have an integer ID and a type. Every customer belongs to exactly one channel; that's how Landbot knows which surface to deliver messages on. Channels are managed via the Channels endpoints. Setting up a channel itself 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. There's no persistent connection driving the flow, so it simply 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 rather than restarting the flow, and 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 — POST /channels/{channel_id}/message_hooks/ and friends. 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 it's no use to assign_bot.

Prefer GET /channels/ anyway. Reading the ID out of the URL only works for web channels, and it's a string format rather than a contract. The endpoint works for every channel type, hands you 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). Distinct per APIchat channel — if you have three APIchat channels, you have 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 via window.fn(). See JavaScript execution.

Not available in WhatsApp or Messenger flows — those run server-side without a JS 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 by passing it to the constructor (new Landbot.Popup({ configUrl: '...' })) or by fetching the JSON manually and handing the parsed object to new Core(config).

Conversation

The message stream between a customer and whoever is currently assigned (bot or agent). Not a first-class API resource — there's no /conversations/ endpoint. Every message-related API endpoint addresses the customer, not the conversation. A customer who's been reassigned between bots and agents has had multiple "logical" conversations, but the API surfaces them as a single message history per customer.

customData

An object passed at widget construction (or set later via setCustomData(...)) that populates Fields on the customer. Same primitive as a Field, just a different on-ramp. 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_fields in API responses).
  • Meta state: archived, unread, last_message, etc.

Customers are managed via 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 itself executes normally — logic evaluates, Webhook blocks fire, integrations call out — but no API-addressable customer is created. The direction that breaks is inbound: with no customer_id in existence, nothing outside can reach into 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 rather than as an escaped string. It's the type the builder canvas presents as array/list. Reach for it whenever a Field holds anything that isn't a scalar.

Five ways to set a Field:

  1. Inside the flow, with a Set-Variable block.
  2. From a Code block, with this.setCustomData({...}).
  3. From the parent page (Widgets SDK), with myLandbot.setCustomData({...}) or passing customData at construction.
  4. From a URL query parameter (Hidden Field).
  5. 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; read from a Code block with the same syntax (literal substitution before the JS 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 doing its job, but 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 404served as an HTML page, not JSON
GET 200 404 — HTML page
DELETE 204 404 — HTML page

A PUT returning 404 therefore means this Field was never created, not this endpoint is broken. Because that 404 arrives as HTML with Content-Type: */*, a client that calls .json() on it gets a parse error instead of a useful message — which makes the endpoint look far more broken than it is. 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 migrate a Field's type in place — a string Field carrying escaped JSON can be PUT as "type": "object" and will round-trip 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 — 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 itself 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 is arriving at your server as an escaped string, the fix is on the sending side — store it in an object Field — not a second JSON.parse() on the receiving side.

So the fix for an escaped-string value is to store it in an object Field rather than a text one — in the builder canvas that's the array/list type, and over the Platform API it's "type": "object".

Warning Unrecognised type strings are not rejected. POSTing "type": "array", "list", or "json" returns 200, but the value is silently coerced and stored as a string[1,2,3] comes back as "[1, 2, 3]", and {"a":1} as "{'a': 1}", which isn't even valid JSON. The only type that preserves structure is object. Read the type in the response to confirm what was actually stored.

Hidden Fields

A declarative way to capture URL query parameters into named Fields. Declared in the bot's Settings → Hidden Fields panel. 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.

Iframe-embedded widgets don't inherit the parent page's query params — that's the most common gotcha.

Message hook (Webhook)

A URL Landbot POSTs to when messages move through a channel. Registered via POST /channels/{channel_id}/message_hooks/ on the Platform API. The payload shape is documented at Webhook payload. Optionally signed with a shared token Landbot echoes in the Authorization header so your endpoint can verify the request.

Distinct from APIchat's webhook, which delivers from-bot messages to your middleware in a slightly different shape — see the APIchat webhook payload.

Question block (Ask a Question)

A flow block that pauses and waits for user input. Comes in flavours: text input, buttons (with optional payloads), buttons-multiple, rating, date, file upload, etc. Each Question block can save the answer to a Field by name. The most common building 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. No pause for user input — execution flows straight through. The other half of the most common pair: Send Message → Question → Send Message → Question.

Webhook

See Message hook.