Channel routing

A binding routes a connection — or one thread of it — to a destination: an AgentSky session, or your webhook endpoint. Which agent handles a conversation is a binding you control; with webhook destinations it's entirely your code.

Bindings

bash
# route the whole connection to a session (or {"webhook": "whk_..."})
curl -s -X PUT https://api.agentsky.dev/v1/channels/connections/{id}/binding \
  -H "Authorization: Bearer $AST_TOKEN" -H 'content-type: application/json' \
  -d '{"destination": {"session": "sess-..."}}'

# route one thread to your webhook endpoint instead
curl -s -X POST https://api.agentsky.dev/v1/channels/bindings \
  -H "Authorization: Bearer $AST_TOKEN" -H 'content-type: application/json' \
  -d '{"connection_id": "...", "thread_id": "telegram:app_...:12345", "destination": {"webhook": "whk_..."}}'

A destination names exactly one of session or webhook. Connection-level routing is a single slot: PUT .../binding replaces whatever the connection routed to before, so there is never a second competing default. POST /bindings creates thread-scoped rules only, and thread-scoped bindings win over the connection default. route_key is a free label for organizing multiple binding rows. An inbound message on a connected surface with no active binding emits channel.unmapped_conversation instead of dropping.

Session mode

Session-bound inbound becomes agent turns automatically: the platform shows a working marker while the agent runs, the reply posts back into the thread as rendered markdown, and turn.refused reaches your endpoints when a turn is rejected.

Re-point a surface with one call — the connection keeps a single active session:

bash
curl -s -X PUT https://api.agentsky.dev/v1/channels/connections/{id}/binding \
  -H "Authorization: Bearer $AST_TOKEN" -H 'content-type: application/json' \
  -d '{"destination": {"session": "sess-other"}}'
# posts "You're now talking to ..." into the surface and emits channel.rebound

Webhook mode

Bind to a webhook endpoint and the service delivers message.received for every inbound; you reply through POST /v1/channels/threads/{thread_id}/messages. Mapping threads to agents — yours or AgentSky sessions you drive via the sessions API — is your code: look the thread up, create what's missing, send the turn.

Proactive deliveries

bash
# fan out to every thread bound to a destination
curl -s -X POST https://api.agentsky.dev/v1/channels/deliveries \
  -H "Authorization: Bearer $AST_TOKEN" -H 'content-type: application/json' \
  -d '{"destination": {"session": "sess-..."}, "parts": [{"type": "text", "text": "nightly report ready"}]}'

The response carries per-thread results; failures also emit delivery.failed.

The connect flow

POST /v1/channels/connections on a link-flow platform (telegram, imessage, whatsapp) returns a PENDING connection with a connect ceremony — a deep link, and for code flows a phone number + LINK: code. Links expire after 15 minutes.

  • Hosted connect pagehttps://connect.agentsky.dev/connect/{token} renders the ceremony for you; agents can hand the link to an end user mid-conversation (no credentials transit the page).
  • callback_url — pass it on the connect call and the hosted page polls GET /connect/{token}/status, then redirects the end user to your URL with connection_id, status, and your metadata echoed as query params once the claim lands.
  • Claim — the end user taps Start (Telegram) or texts the LINK: code (iMessage, WhatsApp). The connection flips CONNECTED, the pre-bound destination becomes the default binding, and channel.connected fires with your metadata.

Slack and Discord connect synchronously instead: the call creates a per-agent channel under your app's identity and returns CONNECTED directly — no pending ceremony, so callback_url doesn't apply there.