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, lark) 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 page — https://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 sends the LINK: code (iMessage, WhatsApp, Lark). The connection flips CONNECTED, the pre-bound destination becomes the default binding, and channel.connected fires with your metadata.

Slack and Discord with destination.webhook use the same pending LINK flow; pass your registered app, open the returned hosted URL and send its code in the intended bot conversation. Their session/channel-creation flows still return CONNECTED synchronously and do not accept callback_url.

The hosted page shows pending, connected, expired or disconnected status even without a callback, retries after temporary network failures, and preserves the result when refreshed. Your authenticated app can poll GET /v1/channels/connections/status?token={token}; it returns the effective connection_id, platform, metadata and lowercase status. The token is the part after LINK: or the Telegram start parameter. Treat it as private. Reconnecting a conversation can replace the pending ID with its existing ID. A connected status proves the claim, not that your webhook has finished processing it; honor normal API rate limits.

Migrate an existing verified conversation

An admin-scoped token can call POST /v1/channels/connections with platform, an owned active app, one destination and adopt: {thread_id, author_id}. Use this only when your application already verified the sender binding and consent. AgentSky checks the app-scoped thread and destination; adoption does not challenge the user or send a platform message. Preserve the original identity and conversation metadata.

An identical replay reuses the connection. A different sender, metadata or destination, an inaccessible workspace, or a disconnected record is rejected. Do not combine adoption with callback_url or invite_user. Ordinary new connections should use the LINK ceremony.