# 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://agentsky.dev/api/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://agentsky.dev/api/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://agentsky.dev/api/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 /api/v1/channels/threads/{thread_id}/messages`. Mapping threads to agents — yours or AgentSky sessions you drive via the [sessions API](/docs/api.md) — 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://agentsky.dev/api/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 /api/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 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 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.
