message.comDevelopers

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

POST/api/v1/webhooks/calls/inboundAuth: Bearer

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

EventEffect in message.com
ringingCreate a channel='call' conversation. Fan out call:ringing on the agent namespace.
answeredRecord the agent that picked up. Fan out call:answered.
endedPersist duration_ms and recordingUrl. Fan out call:ended.
voicemail_leftCreate a voicemails row with the recording URL. Fan out call:voicemail.

ringing payload

ringing
{
  "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

answered
{
  "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

ended
{
  "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

voicemail_left
{
  "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 on callId + 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 ringing as the start of an answered call. The call may never be answered (drops to voicemail or abandons). Use answered to 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.