Skip to main content
A city’s agents usually hear from people through gc or the dashboard. When you want them reachable from somewhere else — a voice front end, a chat app, a bot, another service — you connect that client to the city’s external messaging API. The client posts each turn in; the agent answers with a reply command; the city delivers the answer to a callback URL the client serves. This guide builds a working client for a provider named voice. Substitute your own provider name throughout.

How it works

Every endpoint is city-scoped under /v0/city/{cityName}/extmsg/ on the supervisor API (default http://127.0.0.1:8372). Mutations need the X-GC-Request header — see Supervisor REST API.

Identify the conversation

Every turn and reply carries a conversation reference. Pick fixed values for your client and a stable ID per conversation:

1. Give agents a reply command

When a turn arrives, the agent is told to answer by running gc <provider> reply-current --conversation-id <id> --body-file <path>. That command comes from a pack, so add a small one to the city and import it under your provider name:
run.sh turns the agent’s reply into an outbound publish. It runs inside the agent’s session, so GC_SESSION_ID identifies the replying agent. gc sets GC_CITY_NAME for pack commands, and GC_SUPERVISOR_URL when the supervisor API is plain http on loopback. It needs curl and jq:
Make it executable (chmod +x) and import the pack in the city’s pack.toml. The import binding name — voice here — is the command namespace, so it must equal your provider name:
gc voice --help now lists reply-current. See Create and Share Packs for more on pack commands; the Command Directory section of the pack specification lists the environment they run with.

2. Route conversations to an agent

A turn reaches an agent only when its conversation is routed. The simplest route sends every new voice conversation to one agent:
The first turn of a conversation binds it to that agent, and later turns follow the binding. To route a specific conversation somewhere else, bind it before its first turn with gc extmsg bind or POST /v0/city/{cityName}/extmsg/bind.

3. Serve a callback and register the adapter

Replies arrive as POST {callback_url}/publish:
Answer 2xx with a receipt that echoes the conversation. delivered: true is what tells the agent its reply landed:
Then register the callback for your (provider, account_id):
callback_url is the base URL; omit the trailing slash, because gc appends /publish. The response is 201 Created; registering the same (provider, account_id) again replaces the earlier entry. Registrations live only in the running city’s memory, so register on every client start and again whenever the city starts or restarts — not only when the supervisor restarts. A restart can happen between turns without the client seeing it, so the simplest client registers again before every turn, as the Go example does. Each registration emits an extmsg.adapter_added event, so that client adds one event per turn to the city’s event log.

4. Send a turn

A 200 returns the routing result. TargetAgentName or TargetSessionID names who received the turn; both empty means no route matched and the turn was dropped. The agent is notified in the background, so its reply arrives later on your callback.

Delivery semantics

Sending a turn: The city-scoped routes answer only while the city is running; see City Scope for that readiness boundary. The transcript keeps one entry per provider_message_id, so a retry never duplicates the turn. The agent is notified on every successful attempt, though, so retrying a turn whose 200 was lost — a timeout or a dropped connection — can notify it twice. Receiving a reply — how your /publish response is reported back to the agent’s reply command: To report a reply you received but could not deliver, answer 2xx with "delivered": false and a failure_kind of transient, rate_limited, permanent, auth, not_found, or unsupported. The city does not retry a failed publish and does not record it in the conversation transcript; the reply command prints the failure kind and exits nonzero, so the agent sees why its reply did not land. Delivered replies are recorded, and GET /v0/city/{cityName}/extmsg/transcript (filtered by the conversation fields) lists a conversation’s history — useful for catching up after a client restart.

Go example

A line-oriented client that follows the delivery rules above, except that it gives up on a refused write: it serves /publish, registers itself, and sends each line of stdin as a turn in conversation call-001, waiting out a city restart instead of dropping the turn.
The example sends no X-GC-City-Write grant. On a city that requires one, its first registration is refused with 401 and the client exits; a turn refused later with 401 or 403 is logged as not delivered and skipped. Fixing a refused write needs an operator, so a production client keeps the turn and resends it with the same provider_message_id after the fix.
Last modified on October 10, 2026