Skip to content

Output Formats

github-actions[bot] edited this page Jul 29, 2026 · 1 revision

sipnab has four output modes: interactive TUI (default), per-message CLI text (-N), structured NDJSON (--json), and the MCP server. This page documents the machine-readable formats.

Prerequisites: --json requires non-interactive mode (-N / --no-tui) — sipnab refuses to start otherwise (exception: --call-report implies non-interactive output). No extra build feature is needed: NDJSON output is part of the default build. Full flag reference: CLI reference.

NDJSON (--json)

--json emits one JSON object per SIP message — newline-delimited, so each line is independently parseable and the stream is pipe-friendly:

sipnab -N -I capture.pcap --json | jq .

Message record (fields with no value are omitted, not null):

{
  "schema_version": 1,
  "timestamp": "2026-06-12T14:03:21.412345+00:00",
  "src": "192.0.2.1",
  "src_port": 5060,
  "dst": "192.0.2.2",
  "dst_port": 5060,
  "transport": "UDP",
  "is_request": true,
  "method": "INVITE",
  "call_id": "abc123@192.0.2.1",
  "from": "\"Alice\" <sip:1001@192.0.2.1>;tag=1c145053",
  "to": "<sip:1002@192.0.2.2>",
  "contact": "<sip:1001@192.0.2.1:5060>",
  "ua": "FreePBX-16",
  "sdp": "v=0\r\no=- 1 1 IN IP4 192.0.2.1\r\ns=-\r\nc=IN IP4 192.0.2.1\r\nt=0 0\r\nm=audio 40000 RTP/AVP 0 96\r\na=rtpmap:0 PCMU/8000\r\na=rtpmap:96 telephone-event/8000\r\n",
  "cseq": { "number": 1, "method": "INVITE" }
}

A response record carries is_request: false plus status_code, reason, and response_context instead of method:

{
  "schema_version": 1,
  "timestamp": "2026-06-12T14:03:21.598204+00:00",
  "src": "192.0.2.2",
  "src_port": 5060,
  "dst": "192.0.2.1",
  "dst_port": 5060,
  "transport": "UDP",
  "is_request": false,
  "status_code": 180,
  "reason": "Ringing",
  "call_id": "abc123@192.0.2.1",
  "from": "\"Alice\" <sip:1001@192.0.2.1>;tag=1c145053",
  "to": "<sip:1002@192.0.2.2>;tag=782609",
  "cseq": { "number": 1, "method": "INVITE" },
  "response_context": "1 INVITE"
}

from and to are the full From / To header values — display name, URI and tags included — not the bare user part. (The aggregated dialog object below is the one that carries the user part.) Match them with a regex or a substring test, not with equality.

cseq (the parsed CSeq header — { number, method }) is emitted on every message, requests included, so re-requests within a dialog (e.g. two REGISTERs with CSeq 1 and 2) stay distinguishable. It is omitted only when the CSeq header is absent or unparseable.

contact is the Contact header value when present (routing-critical, omitted otherwise). sdp is the raw SDP body, emitted only when Content-Type is application/sdp and the body is valid UTF-8 — it lets a consumer verify the negotiated media (connection / m= / a=rtpmap) that dynamic-PT decode depends on; omitted for non-SDP or non-UTF-8 bodies.

response_context ("<num> <method>", responses only — what the response answers) is a deprecated alias of cseq, retained for backward compatibility under schema_version 1. Prefer cseq.

malformed is a list of structural-defect diagnostics, present only when a message is malformed (a well-formed message omits the field). It surfaces crafted or broken input rather than silently accepting it: missing mandatory headers (Call-ID/CSeq/From/To/Via), an unparseable CSeq, a Content-Length larger than the body actually present (truncated/lying length), and control/NUL bytes in a header. Example: "malformed": ["missing mandatory header: Call-ID"].

schema_version increments on breaking field changes — pin your consumers to it.

jq recipes

One recipe per question, each a complete pipeline — run the one you want, not the whole list.

Keep only the INVITEs:

sipnab -N -I capture.pcap --json | jq 'select(.method == "INVITE")'

Find the calls placed by one user. from carries the whole From header, so test it for a substring rather than comparing it for equality:

sipnab -N -I capture.pcap --json | jq 'select(.from // "" | test("sip:1001@"))'

Count the messages per method, to see what a capture is made of before reading any of it:

sipnab -N -I capture.pcap --json \
  | jq -s 'group_by(.method) | map({method: .[0].method, n: length})'

Pull the failed responses (4xx/5xx/6xx) with the reason each one carries:

sipnab -N -I capture.pcap --json \
  | jq 'select(.status_code != null and .status_code >= 400)
        | {ts: .timestamp, code: .status_code, reason, call_id}'

List the distinct Call-IDs seen — this is the list you feed into --call-report:

sipnab -N -I capture.pcap --json | jq -r '.call_id' | sort -u

Build a response-code histogram, counting how many of each status code the capture holds:

sipnab -N -I capture.pcap --json \
  | jq -r 'select(.is_request == false) | .status_code' \
  | sort | uniq -c | sort -rn

Summary-only output

--json prints a line per message. For end-of-run summaries instead, combine the report flags with --no-cli-print (which suppresses the per-message stream but not the report).

For the aggregate report over the whole capture, and nothing else:

sipnab -N -I capture.pcap --report --no-cli-print

For a deep dive into a single call, named by its Call-ID:

sipnab -N -I capture.pcap --call-report 'abc123@192.0.2.1' --no-cli-print

Dialog / stream JSON

The richer aggregated dialog object — state, timing (PDD / setup / ring / teardown milliseconds, retransmit counts), sdp_timeline, streams with jitter and loss, and the diagnosis flags (one_way_audio, nat_mismatch, no_media) plus hints — is one shape produced by a single serializer. It is documented once, with a full worked example, under GET /v1/dialogs/{call_id} in the REST API reference.

The same document is what you get from:

  • the REST GET /v1/dialogs/{call_id} and GET /v1/dialogs/{call_id}/report endpoints,
  • MCP tool responses (get_dialog_report with format: "json" — see the MCP server reference), and
  • SIPNAB_JSON in --on-dialog-exec hooks. Note the dialog hook fills streams: [] and a default diagnosis — it fires on a dialog event, when media analysis for that call may not be complete, so the stream and diagnosis fields are placeholders there rather than populated data.

--on-quality-exec is separate: it passes the stream object on its own, under its own variable name — SIPNAB_STREAM_JSON, not SIPNAB_JSON.

pcap / pcapng

-O <file> writes captured packets; --pcapng selects PCAP-NG. With TLS decryption, --pcap-export-mode controls whether decryption secrets (DSBs) are embedded for Wireshark. Rotation: --split filesize:N / --split duration:N, or SIGUSR1 on demand.

pcapng timestamps are nanosecond-resolution, declared via if_tsresol=9 in the Interface Description Block. Files written by sipnab <= 0.5.0 stored nanosecond ticks but omitted that declaration, so other readers inflated every time value ×1000 (capinfos reporting year 58484); scripts/repair_pcapng_tsresol.py repairs such old captures in place without touching packet data.

See also

Clone this wiki locally