Calls inbound Live
Our carrier-grade SIP infrastructure posts call lifecycle events to message.com. We normalise the payloads, create a channel='call' conversation in the unified inbox, and fan out the corresponding Socket.io events so agent dashboards ring in real time.
For the agent-side Socket.io events that fan out from this webhook, see Call events. For REST control of the resulting conversation see Calls API.
Endpoint
HMAC-signed per workspace. The secret is stored in workspaces.call_webhook_secret_encrypted (AES-256-GCM at rest) and rotated by your account manager on request. The same endpoint serves every workspace; we identify the target workspace by the DID the call hit.
Status note
The HMAC verification path is shipped and tested. We are currently in carrier qualification for production traffic. Until that is done, only test calls flow through this endpoint in production. The shape below is stable.
Events
| Event | Effect in message.com |
|---|---|
ringing | Create a channel='call' conversation. Fan out call:ringing on the agent namespace. |
answered | Record the agent that picked up. Fan out call:answered. |
ended | Persist duration_ms and recordingUrl. Fan out call:ended. |
voicemail_left | Create a voicemails row with the recording URL. Fan out call:voicemail. |
ringing payload
{
"event": "ringing",
"callId": "call-uuid",
"from": "+14155550100",
"to": "+18005550100",
"didId": "did-uuid",
"timestamp": "2026-05-13T15:30:00Z"
}We resolve the didId to a workspace via Phone numbers API. If no workspace owns the DID, the call is rejected at the carrier level (we never receive the webhook).
answered payload
{
"event": "answered",
"callId": "call-uuid",
"answeredBy": "+12025550199", // PSTN bridge leg
"timestamp": "2026-05-13T15:30:08Z"
}The carrier reports the leg that bridged. Our internal mapping turns the bridge target into an agent ID (via the agent's registered SIP endpoint or app token).
ended payload
{
"event": "ended",
"callId": "call-uuid",
"endReason": "hangup" | "carrier_error" | "no_answer",
"duration_ms": 84000,
"recordingUrl": "https://recordings.message.com/...",
"timestamp": "2026-05-13T15:31:32Z"
}The recording URL is initially carrier-hosted. We copy the file into our own object storage asynchronously and update conversation.calls.recordingUrl to point at our copy within ~60 seconds. The widget API and REST API always return our copy, never the carrier URL.
voicemail_left payload
{
"event": "voicemail_left",
"callId": "call-uuid",
"recordingUrl": "https://recordings.message.com/...",
"duration_ms": 32000,
"timestamp": "2026-05-13T15:31:32Z"
}Voicemails trigger an automatic transcription pass. The transcript appears on the conversation within ~5 seconds. Retrieve via Voicemails API.
Signature header
The carrier posts a header X-Message-Signature: sha256=<hex>. The signature is HMAC-SHA256 of the raw request body using the per-workspace secret. Verification logic mirrors the inbound email scheme; see Signature verification.
Response codes
200 OK, event accepted (or duplicate; idempotent oncallId + event).401 invalid_signature, signature failed.404 unknown_did, the DID is not registered to any workspace.400 invalid_body, payload shape unrecognised.
Recording retention
Recordings are retained for 90 days by default. Workspaces on the regulated-industry tier can extend retention or disable recording entirely. Retention policy is per phone number, configurable in Phone numbers API.
Common pitfalls
- Polling the carrier recording URL. Once we have copied the recording, the carrier URL may expire. Always use the URL on the conversation, not the one in the webhook.
- Treating
ringingas the start of an answered call. The call may never be answered (drops to voicemail or abandons). Useansweredto flip your “in progress” state. - Missing the carrier qualification window. While we are in qualification, production traffic is gated. Test calls only.
- Hard-coding the carrier. The webhook shape is normalised from the upstream carrier, but the abstraction is owned by us. If we add a second carrier, the normalised events stay the same; the upstream changes do not leak.