The HTTP Bot adapter integrates any backend system with a LangBot pipeline over plain HTTP. Ticketing systems, CRMs, internal tools, custom web apps — all can drive a pipeline through it:
- Inbound: your backend POSTs a signed message to a fixed LangBot URL;
- Outbound: LangBot POSTs replies to a callback URL you configure.
It needs no long-lived connection and fully preserves two native pipeline capabilities:
- Message aggregation (N→1): a user fires several messages in a row, merged into one turn;
- Multi-part replies (1→M): one turn may produce several replies (function calls, multi-message plugins, streamed chunks).
If you want an in-browser, real-time chat widget, use the Web Page Bot instead. HTTP Bot is designed for backend-to-backend integration.
How it works
Your backend ──(1) POST signed message──► LangBot /bots/<bot_uuid>
(pipeline: aggregate → think → reply)
Your callback ◄─(2) POST signed reply(s)── LangBot one POST per reply part
- (1) Inbound is fire-and-collect: LangBot returns
202 Accepted immediately and does not carry the pipeline result on that response;
- (2) Outbound replies arrive later as separate signed POSTs to your
callback_url; a single turn may produce several callbacks;
- Everything is keyed by a
session_id you choose (e.g. a ticket number); each session_id maps to one isolated session.
Create the bot
In the LangBot WebUI, go to Bots > Create Bot, fill in a name, and pick HTTP Bot as the platform/adapter.
Configuration
| Field | Required | Notes |
|---|
| Inbound Signing Secret | yes | Your backend signs inbound requests with this; LangBot verifies every inbound request. |
| Outbound Callback URL | yes | Where LangBot POSTs replies. Config-only — cannot be overridden per message (SSRF protection). |
| Outbound Signing Secret | no | LangBot signs callbacks with this; defaults to the inbound secret if blank. |
| Default Session Type | no | person (default) or group. |
| Require Inbound Signature | no | Keep enabled in production. |
| Callback Timeout (seconds) | no | Per-callback HTTP timeout, default 15. |
| Callback Max Retries | no | Retries on timeout or 5xx with exponential backoff, default 3. |
After binding a pipeline and enabling the bot, the config page shows the Inbound Webhook URL, like https://your-langbot/bots/<bot_uuid>. Copy it.
Signature scheme
Both directions use the same dependency-free HMAC-SHA256 scheme:
signing_string = "{timestamp}." + raw_body_bytes
signature = "sha256=" + hex(HMAC_SHA256(secret, signing_string))
Sent as headers:
| Header | Meaning |
|---|
X-LB-Timestamp | Unix seconds. Rejected if more than ±300s from server time. |
X-LB-Signature | sha256=<hex> over "{timestamp}." + body. |
X-LB-Idempotency-Key | (optional, inbound) dedup key; a repeat returns 409. |
Verify outbound callbacks the same way, using the outbound secret (or the inbound secret if left blank).
Send your first message (curl)
BOT="https://your-langbot/bots/<bot_uuid>"
SECRET="your-inbound-secret"
BODY='{"session_id":"ticket-10293","message":[{"type":"Plain","text":"Export keeps failing on the dashboard."}]}'
TS=$(date +%s)
SIG="sha256=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -r | cut -d' ' -f1)"
curl -sS -X POST "$BOT" \
-H "Content-Type: application/json" \
-H "X-LB-Timestamp: $TS" \
-H "X-LB-Signature: $SIG" \
-d "$BODY"
# -> 202 {"code":0,"msg":"accepted","data":{"session_id":"ticket-10293","accepted_message_id":"in_...","aggregating":true}}
Replies will be POSTed to your configured callback URL shortly after.
POST /bots/{bot_uuid}
{
"session_id": "ticket-10293",
"session_type": "person",
"sender": { "id": "user-5567", "name": "Alice" },
"message": [
{ "type": "Plain", "text": "Export keeps failing on the dashboard." },
{ "type": "Image", "url": "https://example.com/screenshot.png" }
]
}
session_id (required): your stable id, mapped 1:1 to a LangBot session;
message (required): a LangBot message chain. Text uses {"type":"Plain","text":"..."}, images use {"type":"Image","url":"..."} (or base64); other types: Voice, File, At, Quote.
The callback URL is not accepted in the body — it is taken only from bot config. This is deliberate: even if the inbound secret leaks, an attacker cannot redirect replies to an arbitrary host.
Aggregation (N → 1)
If the pipeline has message aggregation enabled, send several messages with the same session_id inside the aggregation window and they merge into one turn. No special flag — just reuse the session_id.
LangBot POSTs each reply part to your callback URL:
{
"session_id": "ticket-10293",
"reply_to": "in_01H...",
"sequence": 1,
"is_final": false,
"stream": false,
"message": [ { "type": "Plain", "text": "Looking into it…" } ],
"timestamp": "2026-06-22T09:00:01Z"
}
Your endpoint should return 2xx quickly. Non-2xx / timeout → LangBot retries with exponential backoff.
Multi-part replies (1 → M)
One turn may emit multiple callbacks, delivered in sequence order for a given session:
seq=1 is_final=false "Checking your export logs…"
seq=2 is_final=false "Found 2 failed exports."
seq=3 is_final=true "Fixed — please try again."
Stitch by session_id + sequence; the turn is complete when is_final: true arrives.
Reset a session
Start a fresh conversation for a session_id (drops history):
POST /bots/{bot_uuid}/reset
{ "session_id": "ticket-10293", "session_type": "person" }
→ 200 { "code":0, "msg":"reset", "data": { "session_id":"ticket-10293", "removed": true } }
Signed exactly like an inbound message.
Synchronous convenience mode
If you don’t need streaming/multi-part and just want one reply back on the same HTTP call, POST to /sync. LangBot waits for the turn to finish and returns all reply parts collapsed into one array:
POST /bots/{bot_uuid}/sync
{ "session_id": "ticket-10293", "message": [ { "type":"Plain", "text":"hi" } ] }
→ 200 { "code":0, "msg":"ok",
"data": { "session_id":"ticket-10293", "reply_to":"in_...", "message": [ ... ] } }
Sync mode is lossy (you lose sequence and streaming boundaries) and blocks up to callback_timeout × 4 seconds. Prefer the callback model for anything real-time or multi-part. Only one in-flight /sync per session_id.
Error codes
{ "code": 40101, "msg": "invalid signature: signature_mismatch", "data": null }
| HTTP | code | meaning |
|---|
| 202 | 0 | accepted |
| 400 | 40001 | malformed body / missing session_id or message |
| 401 | 40101 | bad/expired signature |
| 409 | 40901 | duplicate idempotency key |
| 413 | 41301 | message too large (>1 MiB) |
| 500 | 50001 | internal error |
Reference clients & 5-minute demo
The main repo’s examples/http-bot/ ships an interactive playground plus Python and TypeScript reference clients.
Interactive playground (run this first)
playground.py is a single-file web app: type a message in your browser → it is signed and POSTed to a running http_bot bot → replies stream back into the page, with a debug panel showing the signature, the 202 ack, and each callback’s sequence / verification.
# From the LangBot repo root, with the backend running:
PUBLIC_IP=<your-host-ip> ./.venv/bin/python examples/http-bot/playground.py
# then open http://<your-host-ip>:8920/
On startup it reads the API key + the http_bot bot from data/langbot.db and points that bot’s callback_url + secrets back at itself via the LangBot API (live reload, no restart). Requires an enabled http_bot bot bound to a working pipeline.
Command-line reference clients
examples/http-bot/ also ships Python and TypeScript reference clients (with a callback receiver):
cd examples/http-bot
pip install flask requests
# Terminal 1: callback receiver (point the bot's callback_url here; use cloudflared / ngrok locally)
python client.py serve --port 8900 --secret SHARED_SECRET
# Terminal 2: push a message
python client.py push \
--url https://your-langbot/bots/<bot_uuid> \
--secret SHARED_SECRET \
--session ticket-1 \
--text "hello"
Terminal 1 prints each reply part ([part ] / [FINAL]) with its sequence number — that’s 1→M multi-reply with signature verification, live.
A machine-readable contract is in the main repo at docs/http-bot-openapi.json.
Security checklist
- Keep Require Inbound Signature on in production;
- Use HTTPS callback URLs, set only in config (no per-message override);
- Treat secrets like passwords; rotate via the dashboard;
- The inbound route is unauthenticated at the framework level by design — security comes entirely from the HMAC signature, so never disable it on a public deployment.