Inbound payload
This is the format of the request the Gateway sends to your URL.
Request
POST <your webhook URL>
Content-Type: application/json
X-Bridge-Signature: v0=<hex>
X-Bridge-Event: <kind>
X-Bridge-Timestamp: <unix seconds>
X-Bridge-Delivery-Id: <uuid>
Body — the "signal"
{
"brand": "mybrand",
"userKey": "Ab3d_x9Kq2Lm",
"kind": "message",
"seq": 42
}
| Field | Type | Description |
|---|---|---|
brand | string | Brand (channel) key |
userKey | string | Consultation customer key. The same correlation key as userKey in the outbound API |
kind | string | Event type (see the table below) |
seq | number | Monotonically increasing sequence in the brand journal. The basis for ordering and idempotency |
The payload is a signal that "something happened" and does not include the message body (text).
If you need the actual conversation content, fetch it with the Content Retrieval API. The X-Bridge-Event header is identical to kind in the body.
Event types (kind)
kind | Meaning | Typical handling |
|---|---|---|
message | The customer sent a message | Fetch content → process / reply |
reference | A new consultation was connected (session start) | Handle session start |
expired | The session expired | Handle session end |
ended | The consultation ended | Handle session end |
agent | Your (or a human agent's) outbound send was applied (echo) | Usually ignore |
deleted | A sent message was deleted (Cancel a send) — match it by serial | Mark that bubble as deleted in your UI |
personal_info | The customer consented to share personal info — the values are in the body (below) | Store immediately |
kind: "agent" is the echo of an outbound message you sent. If your reply logic reacts to this event, you get an infinite loop — filter out agent.
The agent echo arrives twice
Every outbound send produces two kind: "agent" signals. Tell them apart by their fields — do not infer from whether seq is present.
| Order | When | Distinguishing fields | Content |
|---|---|---|---|
| ① Acceptance signal | Right after the outbound API returns success | phase: "accepted" · serial | No seq. serial equals the one in the send response — you can match it exactly to an optimistically rendered bubble |
| ② Journal entry | About a second later, when it is written to the brand journal | seq · serial | Same shape as other events. Part of the (brand, seq) idempotency key. For an attachment send split into several bubbles, serials carries the full list |
{ "brand": "mybrand", "userKey": "Ab3d_x9Kq2Lm", "kind": "agent", "phase": "accepted", "serial": "bw-1757400000000-a1b2" }
{ "brand": "mybrand", "userKey": "Ab3d_x9Kq2Lm", "kind": "agent", "seq": 43, "serial": "bw-1757400000000-a1b2" }
Recommended handling: do not store ①, use it only as a UI acknowledgement; deduplicate ② by seq. Ignoring both has no effect on the consultation flow. serial may be absent on ① with an older core, but phase is always present.
Message deleted (deleted)
When a Cancel a send (delete) call succeeds, a kind: "deleted" signal arrives. Its serial is the serial of the deleted send, so you can match it against the agent signal or the send response to find the bubble.
{ "brand": "mybrand", "userKey": "Ab3d_x9Kq2Lm", "kind": "deleted", "seq": 45, "serial": "bw-1757400000000-a1b2" }
Clients that did not request the deletion (for example, another agent tool on the same brand) can use this signal to stay in sync. Deduplicate by (brand, seq) like any other event. Deleting does not restore the consultation count.
Personal-info consent (personal_info)
Sent when the customer agrees to a personal-info consent request. Unlike other signals, the values are in the body.
{
"brand": "mybrand", "userKey": "Ab3d_x9Kq2Lm", "kind": "personal_info",
"sessionId": "29153", "consentedAt": 1757400000000,
"personalInfo": { "phoneNumber": "821012345678", "nickname": "customer-nickname" }
}
| Field | Description |
|---|---|
sessionId | The Kakao consultation session in which consent was given |
consentedAt | When the customer consented (Unix ms) |
personalInfo.phoneNumber | Kakao account phone number, with country code (e.g. 8210…). Empty string if not registered with Kakao |
personalInfo.nickname | Kakao profile nickname. Empty string if not registered |
- There is no
seq. This signal is not written to the brand journal, so deduplicate byX-Bridge-Delivery-Idinstead of(brand, seq). - TalkBridge relays these values without storing them. They do not appear in message retrieval and cannot be delivered again. If your webhook does not respond, delivery is retried briefly and then discarded — store the data as soon as it arrives.
- You are responsible for storing, using and deleting this information. Describe the collected items and purpose in your own privacy policy.
- Discord and Slack integrations receive only a "the customer consented to share personal info" notice, without the values.
Consultation connect metadata (extra)
Values planted in a connect button or link, or through a chatbot handoff (extra), arrive inside the signal itself. Fields appear only when a value exists.
kind | Field | Content |
|---|---|---|
reference | reference.extra | The value planted for this connection |
reference | lastReference.{extra, bot, botEvent, createdAt} | The most recent metadata, which Kakao provides when there is none for this connection |
message | extra | The value of a message the customer sent by tapping a button (BK/MD) |
{ "brand": "mybrand", "userKey": "Ab3d_x9Kq2Lm", "kind": "reference", "seq": 44, "reference": { "extra": "goods_1024" } }
See Using Connect Metadata for how to plant values, the order of reading, and a routing example.
Test signal
The Inbound test in the "Connection test" card of TalkBridge Center Developer mode sends the signal below with the same headers and the same signature as a real delivery.
{ "brand": "mybrand", "userKey": "tb-test-1a2b3c4d", "kind": "message", "seq": 0, "test": true }
test: trueis present only on test signals. Real signals never carry this field.userKeyis a virtual customer (it starts withtb-test-), so content retrieval and replies do not work. Verify the signature and return 2xx, then skip your storage and reply logic.seqis always0, so it can collide with the(brand, seq)idempotency key — branch ontest: truebefore idempotency handling.
Correlation
userKeyin the inbound signal =userKeyin the reply send (POST /api/agent/send).brand=brandKeyin outbound and retrieval calls.- No session-level identifier is needed — a single
userKeythreads the conversation.