Skip to content

Real-time events: follow calls as they happen

A CRM that pops the caller’s record, a wallboard, a live transcript next to a supervisor, a compliance monitor: each needs to know what happens on calls while they happen. MoreVoice gives you the same events three ways:

  • Webhooks. MoreVoice POSTs each event to your HTTPS endpoint, signed, and retries for 72 hours. Best for server-side work that must not miss an event: CRM updates, billing, follow-ups.
  • The event log. You list past events with GET /v1/events (kept 30 days). Best for catching up after downtime, reconciling and backfills.
  • Event streams. You hold a connection open and events arrive as they happen (Server-Sent Events). Best for live screens, and for tools behind a firewall without a public endpoint.

Every way carries the same event envelope and the same event types, so one handler serves all three.

Besides the events of a call’s life (call.created, call.answered, call.ended, call.analyzed…), three live events follow a call turn by turn:

Event While the call runs
transcript.final A final line of the transcript: the caller’s or the assistant’s.
call.tool_called An assistant’s tool call finished, with its name, arguments and result or error.
call.dtmf The caller pressed a key. Keys typed into a question (an account number, an ID) arrive masked as *.

They are opt-in: an endpoint receives them only when it names them. * and call.* don’t include them, so an existing endpoint never starts receiving a delivery per sentence by surprise:

Terminal window
# An endpoint for a live screen: every final transcript line and tool call while calls run, then the end of the call.
# Live events are opt-in: name them; "*" and "call.*" don't include them.
curl https://api.morevoice.ai/v1/webhook_endpoints \
-H "Authorization: Bearer $MOREVOICE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://hooks.example.com/morevoice/live",
"enabled_events": ["transcript.final", "call.tool_called", "call.ended"],
"description": "Live call screen"
}'

A live event is delivered as soon as it happens, but like every webhook it can arrive late or twice when your endpoint is slow or a delivery is retried. Order lines by the event’s created time and skip IDs you have seen.

Events are kept for 30 days. After your endpoint was down, or to rebuild a report, list what you missed by type and time:

Terminal window
# Back after downtime: the calls that ended since your last processed event.
curl -g "https://api.morevoice.ai/v1/events?types[]=call.ended&types[]=call.analyzed&created[gte]=2026-11-03T08:00:00Z&limit=100" \
-H "Authorization: Bearer $MOREVOICE_API_KEY"

type takes one type or a group (call.*), types[] up to 20 of them, and created[gte] / created[lt] a time range. The list is newest first and pages with starting_after (the next_cursor of the previous page). A missed webhook can also be redelivered: POST /v1/webhook_endpoints/{id}/replay sends an endpoint the events of a period again, and POST /v1/webhook_deliveries/{id}/retry retries one delivery.

GET /v1/calls/{id}/events · scope calls:read · API reference

Server-Sent Events for one call: call.state_changed, transcript.partial, transcript.final, call.tool_called, and call.ended, after which the stream closes. The stream starts with what already happened on the call. Reconnect with Last-Event-ID (EventSource does it for you) to get only what you missed. An ended call’s stream sends call.ended at once. A call that has not started yet (a web call before the browser connects) is a 404: subscribe once it starts, or follow it on GET /v1/events/stream. Counts against the key’s stream limit (concurrency_limit, limit_type: api_streams).

Query parameter What it does
types Only these event types (comma-separated or repeated): call.state_changed, transcript.partial, transcript.final, call.tool_called, call.ended, transcript.*, call.* or *. Default: all of them.
last_event_id Resume after this event, for clients that cannot send the Last-Event-ID header (the header wins when both are sent).
Terminal window
curl -N https://api.morevoice.ai/v1/calls/call_7Hk2Lm9Qp/events \
-H "Authorization: Bearer $MOREVOICE_API_KEY" \
-H "Accept: text/event-stream"
What the stream looks like
retry: 3000
id: call_8tRPaZp5hLMbrGqdJ9AmNa:1
event: call.state_changed
data: {"id":"call_8tRPaZp5hLMbrGqdJ9AmNa:1","object":"event","type":"call.state_changed","created":"2026-11-03T09:14:22Z","api_version":"2026-11-01","livemode":true,"org_id":"org_0000000000000000000001","data":{"object":{"call_id":"call_8tRPaZp5hLMbrGqdJ9AmNa","state":"listening"}}}
id: call_8tRPaZp5hLMbrGqdJ9AmNa:2
event: transcript.final
data: {"id":"call_8tRPaZp5hLMbrGqdJ9AmNa:2","object":"event","type":"transcript.final","created":"2026-11-03T09:14:24Z","api_version":"2026-11-01","livemode":true,"org_id":"org_0000000000000000000001","data":{"object":{"call_id":"call_8tRPaZp5hLMbrGqdJ9AmNa","utterance_id":"u1","speaker":"customer","role":"user","text":"Hi, where is my order?","final":true,"interrupted":false}}}

GET /v1/calls/{id}/transcript/stream · scope calls:read · API reference

Server-Sent Events with what is said on a call as it is said: transcript.partial while a line is spoken (its text so far) and transcript.final when it is complete. A final supersedes the partials with the same utterance_id. speaker is customer (the caller), assistant (the AI), agent (a human agent), supervisor or external (a transfer party); AI and human calls alike. start_ms and end_ms are when the line began and became final, in milliseconds from the call’s start, as observed live (null for lines spoken before the stream connected; the call’s transcript has the exact times after the call). The stream starts with the conversation so far and ends with call.ended. Reconnect with Last-Event-ID to get only what you missed. Send partials=false for complete lines only. Counts against the key’s stream limit (concurrency_limit, limit_type: api_streams).

Query parameter What it does
partials false: only complete lines (transcript.final), no partials. Default true.
last_event_id Resume after this event, for clients that cannot send the Last-Event-ID header (the header wins when both are sent).
Terminal window
curl -N https://api.morevoice.ai/v1/calls/call_7Hk2Lm9Qp/transcript/stream \
-H "Authorization: Bearer $MOREVOICE_API_KEY" \
-H "Accept: text/event-stream"
What the stream looks like
retry: 3000
id: call_8tRPaZp5hLMbrGqdJ9AmNa:2
event: transcript.partial
data: {"id":"call_8tRPaZp5hLMbrGqdJ9AmNa:2","object":"event","type":"transcript.partial","created":"2026-11-03T09:14:23Z","api_version":"2026-11-01","livemode":true,"org_id":"org_0000000000000000000001","data":{"object":{"call_id":"call_8tRPaZp5hLMbrGqdJ9AmNa","utterance_id":"u1","speaker":"customer","role":"user","text":"Hi, where is","final":false,"interrupted":false,"start_ms":4120,"end_ms":null,"language":"en-US"}}}
id: call_8tRPaZp5hLMbrGqdJ9AmNa:3
event: transcript.final
data: {"id":"call_8tRPaZp5hLMbrGqdJ9AmNa:3","object":"event","type":"transcript.final","created":"2026-11-03T09:14:24Z","api_version":"2026-11-01","livemode":true,"org_id":"org_0000000000000000000001","data":{"object":{"call_id":"call_8tRPaZp5hLMbrGqdJ9AmNa","utterance_id":"u1","speaker":"customer","role":"user","text":"Hi, where is my order?","final":true,"interrupted":false,"start_ms":4120,"end_ms":5310,"language":"en-US"}}}

GET /v1/cli/listen · scope events:read · API reference

The stream behind the CLI’s listen command: your organisation’s events for this key’s mode (each with the exact body and Standard Webhooks headers to POST to your local endpoint, signed with the session’s secret) and, with forward_tools=true, tool calls to answer from your machine — tools whose URL is cli://<name> and, in test mode, every webhook tool of your test calls. The session ends when the stream closes. Use a test key; a live key needs live=true and the webhooks:write scope. Counts against the key’s stream limit.

Query parameter What it does
events Event types to forward: a type (call.ended), a group (call.*) or *; comma-separated or repeated. Default: every event (opt-in live types only when named).
forward_tools true: this session answers tool calls — tools whose URL is cli://<name> and, in test mode, every webhook tool of the organisation’s test calls. Needs the tools:write scope.
live Required (true) with a live key: live events and tool calls of live calls reach your machine. Needs the webhooks:write scope.
last_event_id Resume after this event (evt_…), for clients that cannot send Last-Event-ID.
Terminal window
curl -N https://api.morevoice.ai/v1/cli/listen \
-H "Authorization: Bearer $MOREVOICE_API_KEY" \
-H "Accept: text/event-stream"
What the stream looks like
retry: 3000
event: cli.session
data: {"object":"cli_session","id":"clis_5bH8jK3mN6pQ9rT2vW4xYz","livemode":false,"events":["call.*"],"forward_tools":true,"forward_all_tools":true,"secret":"whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw","created":"2026-11-03T09:14:20.000Z"}
id: evt_3fT9kq2ZpLr8YwVbN1cX0a
event: webhook
data: {"object":"cli_webhook","id":"evt_3fT9kq2ZpLr8YwVbN1cX0a","type":"call.ended","headers":{"content-type":"application/json","user-agent":"MoreVoice-Webhooks/1.0 (cli)","webhook-id":"evt_3fT9kq2ZpLr8YwVbN1cX0a","webhook-timestamp":"1762161262","webhook-signature":"v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4="},"body":"{\"id\":\"evt_3fT9kq2ZpLr8YwVbN1cX0a\",\"object\":\"event\",\"type\":\"call.ended\"}"}
event: tool_call
data: {"object":"cli_tool_call","id":"clitc_7cJ9kL4mP2qR5sT8uV1wXy","tool":"order_status","call_id":"call_8tRPaZp5hLMbrGqdJ9AmNa","timeout_ms":15000,"url":"cli://orders","headers":{"content-type":"application/json","webhook-id":"tc_2bN…","webhook-timestamp":"1762161263","webhook-signature":"v1,…"},"body":"{\"tool\":\"order_status\",\"arguments\":{\"order\":\"A-1\"}}"}

GET /v1/events/stream · scope events:read · API reference

Server-Sent Events: the same event envelopes as GET /v1/events and webhooks, live, for this key’s mode. Filter with types (call.*, campaign.completed, …). Reconnect with Last-Event-ID (EventSource does it for you) to replay what you missed, up to 24 hours back; delivery is at-least-once, so dedupe on id. Counts against the key’s stream limit (concurrency_limit, limit_type: api_streams).

Query parameter What it does
types Only these event types: a type (call.ended), a group (call.*) or "*"; comma-separated or repeated. Default: every event.
last_event_id Resume after this event, for clients that cannot send the Last-Event-ID header (the header wins when both are sent).
Terminal window
curl -N https://api.morevoice.ai/v1/events/stream \
-H "Authorization: Bearer $MOREVOICE_API_KEY" \
-H "Accept: text/event-stream"
What the stream looks like
retry: 3000
id: evt_3fT9kq2ZpLr8YwVbN1cX0a
event: call.ended
data: {"id":"evt_3fT9kq2ZpLr8YwVbN1cX0a","object":"event","type":"call.ended","created":"2026-11-03T09:14:22Z","api_version":"2026-11-01","livemode":true,"org_id":"org_0000000000000000000001","data":{"object":{"id":"call_7Hk2","object":"call","status":"ended"}},"request":{"id":null,"idempotency_key":null}}

GET /v1/request_logs/stream · scope api_keys:read · API reference

Server-Sent Events: each new request log (same filters as the list, without created) a few seconds after the request finished. Counts against the key’s stream limit.

Query parameter What it does
status A status class (2xx, 3xx, 4xx, 5xx), error (any 4xx or 5xx), or one status code (404).
method Only this HTTP method.
path Only paths starting with this (/v1/calls matches /v1/calls/call_…).
key_id Only requests made with this API key (key_…).
request_id Only the request with this X-Request-Id (req_…).
operation_id Only this operation (calls_create).
Terminal window
curl -N https://api.morevoice.ai/v1/request_logs/stream \
-H "Authorization: Bearer $MOREVOICE_API_KEY" \
-H "Accept: text/event-stream"
What the stream looks like
retry: 3000
id: rlog_4Gk2LmN9pQ4rS6tV8wX0yZ
event: request_log
data: {"id":"rlog_4Gk2LmN9pQ4rS6tV8wX0yZ","object":"request_log","livemode":false,"request_id":"req_7Pq2Rk9sT1uV3wX5yZ8aBc","created":"2026-11-03T09:14:22.135Z","method":"POST","path":"/v1/calls","query":null,"route":"/v1/calls","operation_id":"calls_create","api_version":"2026-11-01","status":400,"error_code":"parameter_invalid","latency_ms":42,"key_id":"key_3fT9kq2ZpLr8YwVbN1cX0a","ip":"203.0.113.7","user_agent":"morevoice-node/0.4.0","idempotency_key":"8e0f3a2c-5d1b-4c7e-9f6a-2b4d8c1e7a90","request_body":{"to":"+97250000000","assistant_id":"asst_8tRPaZp5hLMbrGqdJ9AmNa"},"response_body":{"error":{"type":"invalid_request_error","code":"parameter_invalid","message":"to must be an E.164 number","param":"to"}}}

The streams are standard Server-Sent Events, so any SSE client reads them:

  • Read them from your server. A stream is opened with your secret API key, which must never reach a browser. Relay what your page needs from your own server, or give the page a web call of its own.
  • Resume, don’t restart. Each event has an id. When the connection drops, reconnect with the last ID you processed in the Last-Event-ID header (or last_event_id), and the stream carries on from there; standard clients do this for you.
  • Expect repeats. After a reconnect you may see an event again: skip IDs you have already handled.
  • Keep it light. Comment lines (: ping) keep the connection alive through proxies; ignore them. Hand slow work to a queue so reading never falls behind, and close streams you no longer need: each key may hold only a few open at once.