Skip to main content
Wire your own application to an agent through the conversation bridge’s API door: your system POSTs a message, the bridge runs an agent turn as the route’s execution key, and the answer comes back by a signed HTTPS callback (or inline on a bounded wait). See client conversations for the model.

Two identities: who-may-send vs what-the-agent-may-do

An API route separates two authorities, and you provision both:
  • The invoking key — the API key your system authenticates with when it POSTs a message. The access-control gate authorizes it for the route’s send door; it answers who may send.
  • The execution key — the identity the turn runs as. Its live grants are the turn’s entire authority; it answers what the agent may do.
They are independent. The sender never inherits the agent’s authority, and the agent never runs as the sender.
1

Mint the invoking key

A scoped key your system will send with. Scope it to reach the route and no more.
2

Mint the execution key

A least-privilege key the agent runs as (see owned keys).

Create the API route

An api route may carry an HTTPS callback_url — the sink the answer is POSTed to; this walkthrough uses one. When set, the URL must be absolute HTTPS; an http:// or relative URL is rejected at create. A route with no callback_url reads its answers back from the poll door instead (below).
The response carries a callback_secret — shown once, here, and never readable again. It signs every callback for this route. Capture it now and store it where your callback receiver can read it.

Send a message

POST to the send door with your invoking key. external_user_id is your handle for the end user — it becomes the conversation’s thread key, so the agent remembers that user’s earlier turns.
The default answer is 202 with the id to correlate the callback against:
Without a callback_url on the route, read the answer back once the turn finishes from GET /api/conversations/{route_name}/messages/{message_id}, keyed by the message_id this 202 returned. The door refuses loudly rather than silently: 404 unknown route, 429 the address rate cap, 503 the thread queue is full, 501 no conversations backend configured, 400 a malformed body, 401 an unauthorized caller.

Sync variant: wait_seconds

Add wait_seconds to wait inline for a fast turn. A turn that finishes within the window answers 200 with the answer embedded and its callback suppressed (so it never double-fires); a turn still running when the wait elapses falls back to 202 and the answer arrives by callback as usual. A turn that finishes silent resolves the same way — 200 inline with status: "silent" and no answer text. The window is clamped to the deployment’s sync_wait_max_seconds cap.

Receive the callback

The bridge POSTs the answer to your callback_url as JSON. The body is the answer payload; correlate it by message_id:
On a turn that errored, the shape is identical but status is error and answer is generic client-safe text — never an internal detail:
On a silent turn — a tool target whose mapped reply was null or blank, or an ordinary inbound on a manual-mode thread whose suppressed target turn produced no reply — the callback carries status: "silent" and no answer key at all:
Every turn therefore resolves to one of answered, error, or silent, so your receiver always gets a terminal callback (or inline reply) and never waits on a turn that quietly went nowhere.

Verify the X-Tai-Signature

Every callback carries an X-Tai-Signature header: sha256=<hex HMAC-SHA256(callback_secret, raw_request_body)>. Recompute it over the raw bytes and compare in constant time — reject any mismatch. This is what proves the callback came from your deployment and was not tampered with.
Compute the HMAC over the exact bytes you received, before any JSON re-encoding — re-serializing the body changes the bytes and breaks the signature. Reject a request whose signature does not verify; never act on an unverified callback.
Delivery is retried with backoff if your receiver is down, and a permanently undelivered answer ends failed (listed on tai conversations failed) rather than being lost.

See also