Fetchit

Switch to dark theme

REST API

SIP Calls

Reporting calls observed on SIP: what an endpoint can and cannot see, and how to fill the ingestion body from a SIP dialog.

Mapping a SIP dialog onto the call event vocabulary, and being honest about what the vantage point could not observe.

Published

The SIP reporter watches a practice's own signalling and translates what it sees into the call ingestion body. This page is the mapping, plus the part that surprises people: a SIP vantage point does not see the whole call, and the body has fields for saying so.

Everything here posts to the same endpoint with provider set to sip, which is also its default.

Mapping a dialog

Ingestion fieldComes from
providerCallIdThe SIP Call-ID.
occurredAtWhen the reporter saw the message, on its own clock.
call.from, call.toThe From and To URIs, split into number, name, extension and uri.
call.trunkThe carrier trunk the INVITE arrived on, where the reporter knows it.
call.diversionThe Diversion or History-Info chain, outermost first.
rawThe message itself. Never parsed by us, kept for diagnosis.
sequenceThe reporter's own counter for the dialog.

Numbers should be E.164 where the party has a real one. Normalisation happens server side, so a reporter that can only offer what the PBX said may offer exactly that.

What your vantage point can see

observer is the most consequential field on a SIP report, because it is the difference between "this call was not answered" and "we could not tell".

observerVantage pointSees an answer elsewhere?
endpointRegistered as one extension, typically in a ring group. The default.No.
proxyIn the signalling path for the dialog.Yes.
pbx_apiThe PBX's own event feed.Yes.

An endpoint in a ring group never learns that a human at another extension picked up. The PBX simply sends it a CANCEL. Read observer before concluding anything from a call with no call_answered event: on an endpoint report, that silence is not evidence of a missed call.

Set this honestly. A reporter that claims proxy from an endpoint's vantage point turns "we could not see" into "it did not happen", and the practice's reporting inherits the lie.

Reporting the end of a call

call_ended is where the honesty above gets expressed.

  • disposition is one of answered, missed, busy, failed, cancelled, voicemail or unknown. unknown is a real answer and a ring-group endpoint will use it constantly. Admitting the reporter could not tell beats a guess that reporting will later average.

  • answeredElsewhere is the one thing an endpoint does learn about a call it lost. RFC 3326 puts it on the CANCEL:

    The CANCEL that says somebody else took it
    Reason: SIP;cause=200;text="Call completed elsewhere"

    Without that flag, answered-elsewhere and the caller giving up are the same silence.

  • sipCode and sipReason carry the final response, 486 and Busy Here, for the failures worth keeping verbatim.

  • duration is seconds from the call starting to it ending, ring time included. talkTime is conversation only, and null means the reporter knows the total but not the split.

A call the endpoint lost to a colleague
{
  "type": "call_ended",
  "provider": "sip",
  "providerCallId": "a1b2c3@pbx.clinic.local",
  "sequence": 2,
  "occurredAt": "2026-09-04T14:32:19.700Z",
  "observer": "endpoint",
  "disposition": "unknown",
  "duration": 14,
  "talkTime": null,
  "answeredElsewhere": true,
  "sipCode": 200,
  "sipReason": "Call completed elsewhere",
  "call": {
    "direction": "inbound",
    "from": { "kind": "e164", "number": "+15550104242", "name": "DOLITTLE JANE" },
    "to": { "kind": "extension", "extension": "101" }
  }
}

Callers who withhold their number

A caller sending Privacy: id gives the reporter nothing to store. That is a kind of caller, not a parse failure, and it must still be reported: it is the call the practice would most like a name on, and dropping it means no popup at all.

An anonymous caller still rings the desk
{
  "type": "call_received",
  "providerCallId": "d4e5f6@pbx.clinic.local",
  "occurredAt": "2026-09-04T15:02:41.000Z",
  "call": {
    "direction": "inbound",
    "from": { "kind": "anonymous", "number": null, "name": null },
    "to": { "kind": "extension", "extension": "101" }
  }
}

Use unknown for the parties the reporter genuinely could not classify, and extension for internal ones.

Forwarded calls

The diversion chain is what stops a rolled call from looking like a direct one. A call that rang the main line, went unanswered and rolled to a mobile arrives with two hops, and without them it reads as somebody having dialled the mobile.

Two hops, outermost first
"diversion": [
  { "number": "+15550100000", "reason": "no-answer" },
  { "number": "+15550109999", "reason": "unconditional" }
]

Events that repeat

call_hold is the event type that can legitimately happen more than once in a dialog, since one type covers both directions through released.

The server deduplicates on type:sequence when a reporter offers no providerEventId. So a SIP reporter has two workable options, and must pick one:

  • Keep a monotonic sequence per dialog, incrementing on every event. This is the ordinary approach and needs no providerEventId at all.
  • Mint a providerEventId for each event, if your counter cannot be trusted to be unique.

Sending two holds with the same sequence and no providerEventId means the second is filed as a duplicate of the first.

Where the reporter runs

The SIP reporter runs at the practice, translating on the client and posting this body directly. Its credential is a key federated to that organization and holding org.calls.events, created from the organization's manage page.

Because the reporter is inside the practice's network, it sees the signalling without any provider integration. That is the whole reason this vantage point exists, and it is also why observer defaults to the most limited of the three.