Listen service

View as Markdown

The listen service is a background daemon that keeps a live connection to your account’s event stream and appends every event — message.received, call.ended, and more — to a local log as JSON lines. It manages its own subscription token and renews it automatically.

This is the machine-friendly way to capture events: a long-running agent can tail the log, and dial wait-for reads from it directly when the daemon is up.

The stream behind the daemon is presence-based, not a durable at-least-once queue. If the daemon stops, it replays missed events on reconnect only within 2 minutes — longer downtime can drop events. For guaranteed delivery, register an at-least-once webhook — Dial POSTs each event to your HTTPS endpoint, signed and retried.

Manage the daemon

$dial listen install # install and start the daemon
$dial listen status # report state and recent events
$dial listen uninstall # stop and remove it

install registers a user-level service — a launchd agent on macOS or a systemd user unit on Linux — and prints the unit path. You must be signed in first (dial signup + dial onboard).

To run the worker in the foreground instead (for debugging), use:

$dial listen

Staying current after CLI updates

On startup the daemon records the version it is running in ~/.local/state/dial/listen-version.v1.json. About once a minute it compares that against the installed CLI, and when an update lands — manual or automatic — it drains, exits, and lets the supervisor relaunch it on the new code. You never need to restart it after updating.

Where events land

The daemon appends events to ~/.local/state/dial/listen.log (honoring XDG_STATE_HOME), one JSON object per line. Tail it like any log:

$tail -f ~/.local/state/dial/listen.log

Event payload shapes

Every event shares one envelope — id, object ("event"), type, version, createdAt, relatedObject — and a data payload whose shape the type selects. All field names are camelCase, matching the REST surface.

message.received

1{
2 "id": "evt_3f9a…",
3 "object": "event",
4 "type": "message.received",
5 "version": 1,
6 "createdAt": "2026-05-28T18:32:11Z",
7 "relatedObject": { "id": "clxxx", "type": "message", "url": null },
8 "data": {
9 "messageId": "clxxx",
10 "from": "+14155559876",
11 "to": "+14155550123",
12 "channel": "sms",
13 "body": "Your code is 123456",
14 "source": "external"
15 }
16}

data.source is "external" for real carrier-delivered SMS and "internal" for messages synthesized by Dial (e.g. dashboard test tools). Gate on source === "external" if you only care about real-world traffic.

call.ended

1{
2 "id": "evt_7c1b…",
3 "object": "event",
4 "type": "call.ended",
5 "version": 1,
6 "createdAt": "2026-05-28T18:32:11Z",
7 "relatedObject": { "id": "clxxx", "type": "call", "url": "/api/v1/calls/clxxx" },
8 "data": {
9 "callId": "clxxx",
10 "from": "+14155559876",
11 "to": "+14155550123",
12 "direction": "outbound",
13 "durationSeconds": 42,
14 "status": "completed",
15 "canceled": false,
16 "transcriptAvailable": true
17 }
18}

call.transcribed

A thin event — it carries only the call id. Fetch the transcript from the call record. Emitted only for calls that produced a transcript, and after the call’s call.ended.

1{
2 "id": "evt_9d2e…",
3 "object": "event",
4 "type": "call.transcribed",
5 "version": 1,
6 "createdAt": "2026-05-28T18:32:19Z",
7 "relatedObject": { "id": "clxxx", "type": "call", "url": "/api/v1/calls/clxxx" },
8 "data": { "callId": "clxxx" }
9}

Fetch the full transcript with dial call get <callId> when data.transcriptAvailable is true on call.ended, or on the call.transcribed event.

How it pairs with wait-for

dial wait-for consumes events from this log when the daemon is installed and running — so multiple waits share one connection and resolve instantly from already-captured events. If the daemon isn’t running, wait-for falls back to calling the REST API directly, so it still works either way; the daemon just makes it cheaper and shared.