The SIP capture screen shows the signalling ladder for recent calls: which host sent which SIP message, in what order, and where the exchange failed. It is the screen you open when a call never produced a usable CallEvent and you need the raw wire exchange. The screen renders in two places — the agent suite (SIP capture under admin) and the customer portal — from the same shared body component, so both show the same data for the same workspace.

Where captures come from

Captures are produced by Heplify and stored in Homer (HEP). The console does not query Homer’s own UI or deep-link to it; it reads Homer’s storage through the sip.calls and sip.transaction procedures and renders the ladder inline. Two consequences follow:
  • If Heplify was not running when the call was placed, there is nothing to show for that call, even if the call itself succeeded.
  • Everything on this screen is signalling. The screen shows SIP messages, source/destination hosts, ports, and the raw message bodies. It does not show RTP statistics, codec negotiation results as media metrics, or a media-mode indicator. For those, use the CDR / metrics surfaces described in Telemetry.

How a capture is matched to your workspace

Homer stores captures for every call that traverses the capture point. The console never shows you the raw Homer set — it first resolves which Call-IDs belong to your workspace, then fetches only those. Ownership is resolved as the union of two paths:
  1. CDR-based. ClickHouse cdrs.tenant_id is the authoritative owner for outbound calls and answered inbound calls.
  2. DID-based fallback. For inbound calls whose CDR row has not been written yet, or was written with an empty tenant_id, the console falls back to the called number. Homer’s data_header.to_user carries the dialled DID, and each DID belongs to exactly one workspace via phone_number.org_id. The match is a digit-suffix comparison against your workspace’s DIDs.
The fallback exists because short or failed inbound calls can send BYE before the CDR pipeline writes a row. Without path 2 those calls would be invisible here — which is precisely the case you most often want to debug. Expanding a row applies the same ownership check again before returning message bodies. sip.transaction first counts matching cdrs rows for your tenant and the requested Call-ID; if there are none, it repeats the DID-suffix check. If neither path claims the Call-ID, the procedure returns an empty message list rather than the bodies. A guessed Call-ID from another tenant on a shared Homer therefore returns nothing. Call-IDs are normalised before lookup: any @host suffix on the input is stripped, so the wire-form value shown in the calls list still resolves back to the CDR row.

The time window and refresh behaviour

The date-range picker at the top of the screen sets the capture window. The default window is the last hour (now-1h → now). Both bounds are quantised down to 30-second buckets before they are sent to the server. This is deliberate: the query key stays stable across parent re-renders and tooltip hovers, so incidental renders do not trigger a new fetch. The list also refetches on a 30-second interval, which is in phase with the bucket size. Practical effects:
  • The window edges “snap” — a range you pick mid-bucket is rounded to the bucket boundary, so a call placed in the last few seconds may not appear until the next bucket.
  • The header shows a refreshed <time> stamp that updates whenever the list query returns new data.
  • Refresh forces an immediate refetch. Expanded ladder bodies are cached separately and are not refetched on window focus.
The list is capped by a limit that the screen sets to 50 calls. The procedure accepts a limit between 1 and 200.

Finding a call

Three controls narrow the listing, all applied client-side to the rows already fetched for the window: Because filtering happens after the fetch, narrowing the filters does not reach further back in time. If a call is missing, widen the time range rather than adjusting the filters. The counter above the list reads N of M captured when a filter is active, and M calls captured when it is not.

Reading a call row

Each collapsed row is one captured call: Any of these may render as —. The values come from the captured SIP headers, and a call that failed early may not have populated them. Clicking a row expands it and issues the sip.transaction query for that Call-ID over the same quantised window.

Reading the ladder view

The expanded panel has two parts. Call summary. Four fields: Call-ID, Method (defaults to INVITE when the capture does not carry one), Src Host, and Dst Host. SIP ladder. A scrollable list of the captured messages for that Call-ID, with a count in the header (SIP Ladder (N messages)). Each row is collapsible and carries three columns: Expanding a message row reveals the raw SIP message exactly as captured, including all headers and any body. This is where you read the response code, the Reason header, and the SDP. Messages are listed in the order Homer returns them for the transaction, so you read the ladder top-to-bottom as the exchange occurred. For how to interpret the response codes and Q.850 causes you find here, see the debugging reference.

When a Call-ID returns no messages

A row can appear in the list and still expand to No captured messages. The list and the ladder are resolved by two different queries, so this is expected in a few situations:
  • Heplify was not running at the point in time the leg was signalled, so no HEP frames were stored for it.
  • The leg ran outside the time window. The ladder query uses the same quantised fromMs / toMs as the list. A call whose INVITE landed just before the window start has its messages outside the bounds even though the call itself is listed.
  • Ownership could not be confirmed for the ladder. If neither the CDR tenant_id match nor the DID-suffix fallback claims the Call-ID at the moment you expand it, sip.transaction returns an empty list.
A valid Call-ID returning nothing is therefore not a sign of a broken Call-ID. Widen the time range first, then check whether the call is owned via a DID in your workspace.

The empty state

When the window contains no captures at all, the screen shows No SIP captures in this window. Captures are matched to your workspace’s calls, so the two remedies are to widen the time range or to place a call and let it appear on the next refresh.

Exporting the list

Export CSV downloads the currently filtered list — exactly the rows visible on screen, not the whole window. The button is disabled while a fetch is in flight and when the filtered list is empty. The export runs entirely in the browser from rows already delivered over tRPC; there is no extra server round-trip. The filename is sip-capture-<ISO timestamp>.csv. Columns, in order:
Every field is quoted, and embedded quotes are doubled. These are the same fields the table displays, so the download mirrors what the operator sees.
The CSV contains the call-level summary only. It does not contain the SIP message bodies from the ladder, and the screen does not offer a pcap or HEPv3 export. To inspect raw messages, expand the ladder rows in the console.

Where this differs from call traces

Both screens are diagnostic, but they answer different questions. Use SIP capture when a call fails before any CallEvent reaches the SDK, or when you need to prove what the carrier actually sent on the wire. Use call traces when the call reached the gateway and you want the platform’s view of it.
  • Telemetry — the HEPv3 capture stream, CDR schema, and metrics
  • Telephony Metrics — definitions for the numbers that captures do not carry