Send vCons to vcon store - NormB/sipnab GitHub Wiki
vcon.store is a hosted store for
vCons. This page sends the containers sipnab writes to it.
It uses sipnab --vcon-forward, the forwarder that
Deliver the spool to a store
describes.
Status on 2026-10-11: vcon.store accepts the two forms it refused on 2026-10-07:
-
extensionsas the array of strings both vCon drafts define. - the
recordingDialog Object withoutpartiesthat sipnab writes for a call whose container carries no audio.
More about the store:
- It answers
409for a uuid it already holds. -
PUT /v1/vcons/{uuid}replaces the held copy. - The forwarder sends each container byte for byte.
- Not measured: a
recordingthat carries audio but noparties.- sipnab writes that form when an audio channel came from an address no party advertised in its SDP, such as a media relay's.
The measurements are the evidence.
A capture carries personal data:
- telephone numbers,
- addresses,
- the headers your platform sets,
- with
--retain-audio, the call audio.
vcon.store is another party's system. Decide what may leave the machine first:
-
--redactreplaces identities, addresses and correlation identifiers with keyed tokens. It also withholds the audio.- It works on a capture file (
-I), not on a live capture.
- It works on a capture file (
- A redacted container carries no audio.
- So its
recordingDialog Object has nopartiesand no content. - vcon.store accepted that form when measured on 2026-10-09.
- So its
- A container exported without
--redactand with--retain-audiocarries the call audio and the identifiers the signaling carried, as captured. - What may you conclude lists what a container carries.
Export the calls from a capture file with their audio.
-
--retain-audiokeeps each call's audio whole, from the first frame. - It makes sipnab put the audio in its
recordingDialog Object. - The object names the parties when each audio channel came from the media
address and port that one party advertised in its own SDP.
- Otherwise the object has no
parties.
- Otherwise the object has no
- This container is not redacted. It carries the audio and the identifiers as captured.
sipnab -N --no-cli-print -I calls.pcap --retain-audio \
--export-vcon-when "state == 'Completed'" --export-vcon-dir spoolAdd --redact and remove --retain-audio. The same command then writes
redacted containers without audio.
sipnab prints Wrote N vCon container(s) to 'spool'.
vcon.store authenticates with a bearer token.
- Put the token alone in a file only you can read.
- The
vcon-storekind sends it asAuthorization: Bearer <token>. - The forwarder refuses a file that other users can read:
# Run all of these, in order.
umask 077
printf '%s\n' "$VCON_STORE_TOKEN" > vcon-store.key$VCON_STORE_TOKEN is the token vcon.store issued you.
- The forwarder never prints it.
- The forwarder removes it from any answer it keeps.
- Instead of a file, the
SIPNAB_VCON_FORWARD_AUTHenvironment variable can hold the token.
# Run all of these, in order.
STORE_URL=https://api.vcon.store
sipnab --vcon-forward spool --vcon-forward-kind vcon-store \
--vcon-forward-url "$STORE_URL" --vcon-forward-auth-file vcon-store.key \
--vcon-forward-once
echo "exit $?"--vcon-forward-kind vcon-store does two things:
- It adds
/v1/vconsto the base URL. - It forms the header from the token.
It does not change the container. The same settings can live in sipnab.toml:
[vcon_forward]
kind = "vcon-store"
url = "https://api.vcon.store"
auth_file = "/etc/sipnab/vcon-store.key"For each container the forwarder:
- logs a
deliveredline with the store's status, - moves the file to
spool/delivered.
The exit status is 0 when the store accepted every container.
- A container the store refused is in
spool/failed.- Beside it is a
.error.jsonrecord that says why.
- Beside it is a
-
sipnab --vcon-spool-status spoolprints:- the pending, delivered and failed counts,
- the store's answer from each
.error.jsonrecord. - It reads these from the files without sending anything.
- A capture run with
--mcpor--apianswers the same through:- the MCP tool
vcon_spool_status, -
GET /v1/vcon/spool.
- the MCP tool
A 401 or 403 is different. It refuses the token or the client for every
container. So the forwarder:
- stops,
- moves nothing,
- logs the status and the start of the answer,
- exits
3.
sipnab's tests run the vcon-store kind against two stand-ins, not
against vcon.store itself:
- one that answers as the maintainer measured vcon.store answering on 2026-10-11,
- one that answers as it did on 2026-10-07.
The tests are in
tests/vcon_forward_test.rs.
The maintainer measured these against vcon.store's API. This page's commands did not contact the store.
On 2026-10-09, the maintainer used the sipnab 0.5.209 release binary and a
container exported from
tests/pcap-samples/sip-rtp-g711.pcap:
| Sent | vcon.store answered |
|---|---|
POST /v1/vcons, the container with audio, "extensions": ["sip-signaling", "CC"], sent unchanged |
201; a GET returned extensions as the same array |
the recording Dialog Object sipnab writes for a call without audio (no parties, no content) |
201, stored as sent |
a POST of a uuid the store already held |
500, a problem document of type https://vcon.store/errors/internal-error, An unexpected error occurred, twice. Changed by 2026-10-11; see below |
a GET with Python's default HTTP User-Agent, with Go's, with none, and with sipnab/0.5.209
|
200 for each |
a GET of a stored vCon |
the content, with a top-level _meta member added |
DELETE /v1/vcons/{uuid} |
200, {"deleted":true,...}; a GET after it 404, {"error":"vCon not found"}
|
On 2026-10-11, with a container exported from the same capture, sent with curl:
| Sent | vcon.store answered |
|---|---|
POST /v1/vcons, a new uuid |
201, {"uuid":...,"stored_at":...}
|
a second POST of the same uuid |
409, {"error":"vCon already exists","details":[{"path":["uuid"],"message":"a vCon with uuid ... already exists; use PUT to replace it"}]}
|
PUT /v1/vcons/{uuid} with a changed subject, same uuid |
200; a GET returned the changed subject
|
the recording Dialog Object without parties and content |
201 |
DELETE /v1/vcons/{uuid} |
200; a GET after it 404
|
On 2026-10-07, kept as history:
| Sent | vcon.store answered |
|---|---|
POST /v1/vcons with "extensions": ["sip-signaling", "CC"]
|
400, extensions: Expected object, received array
|
the same with extensions as an object of names mapped to true
|
201, stored as sent |
a vCon with no audio, whose Dialog Object is {}
|
400: the store required type and parties
|
the same with extensions removed |
201 |
a request with Python's default HTTP User-Agent
|
403, error code: 1010, from the Cloudflare front. The front accepted a curl User-Agent
|
A 409 for a held uuid is a refusal:
- The container goes to
spool/failed. - The store's answer is in its
.error.jsonrecord. -
--vcon-forward-onceexits1.
To replace the held copy instead, name the PUT endpoint with
--vcon-forward-replace-url. The forwarder then sends the container there
after the 409:
sipnab --vcon-forward spool --vcon-forward-kind vcon-store --vcon-forward-url https://api.vcon.store --vcon-forward-auth-file vcon-store.key --vcon-forward-replace-url 'https://api.vcon.store/v1/vcons/{uuid}'Replacing is off by default.
- It overwrites what the store holds.
- That is the operator's decision.
The forwarder retries a 500 after a back-off, as it retries every 5xx.
On each create the store wrote one entry to its transparency log (SCITT). The entry holds:
- the event
vcon_created, - a SHA-256 hash of the payload,
- a COSE signed statement,
- a receipt.
Each stored vCon also comes back with a _meta envelope. It says:
-
form: unsigned, -
consentStatus: unknown, -
retentionAction: redact.
What these mean:
-
The signature is the store's, not sipnab's. It says what vcon.store
received.
- The container it covers is an observation from a tap.
- sipnab never signs that.
- So the signature does not make the container a recording.
-
consentStatus: unknownis accurate. sipnab records no consent, and a container carries none.
sipnab --vcon-fetch reads a container back by its uuid, with the same key
file. The vcon-store kind:
- reads
/v1/vcons/{uuid}, - sends the key as
Authorization: Bearer <key>, - removes the
_metamember the store adds.
So the file holds the container alone:
sipnab --vcon-fetch 018bcfe5-6800-8a6b-a667-78f1c5213800 --vcon-fetch-kind vcon-store --vcon-fetch-url https://api.vcon.store --vcon-fetch-auth-file vcon-store.key --vcon-fetch-out fetchedThe container is the copy the store accepted, as sipnab sent it.
Measured on 2026-10-09:
- sipnab exported a container from
tests/pcap-samples/sip-rtp-g711.pcap. -
--vcon-forward-kind vcon-storesent it (201). -
--vcon-fetchread it back.- The file held
extensionsas the array and no_meta.
- The file held
- A delete then removed it (
200). - A read after the delete answered
404.
More: Fetch a stored vCon.
-
Every container lands in
spool/failedwith status400andextensions: Expected object, received array.- The store refuses the form the drafts define, as vcon.store did on 2026-10-07.
- sipnab no longer changes the copy it sends.
- So such a store does not accept sipnab's containers.
-
A container lands in
spool/failedwith status409andvCon already exists.- The store already holds that uuid (
--vcon-fetchreads it back). - To replace the held copy, move the file back into
spool. - Then run the forwarder with
--vcon-forward-replace-url 'https://api.vcon.store/v1/vcons/{uuid}'.
- The store already holds that uuid (
-
A container stays in
spool, and the log saysanswered 500; will retry.- The store is in trouble.
- The forwarder tries again after its back-off.
-
The forwarder exits
3and logs403witherror code: 1010.- The Cloudflare front refused the request's client.
- It refused Python's default
User-Agentthe same way on 2026-10-07. - The forwarder sends
User-Agent: sipnab/<version>. - Check that nothing between it and the store replaces that header.
- It moved no file, so run it again once you fix that.
-
The forwarder exits
3and logs401. The token invcon-store.keyis wrong or expired. Every container is still inspool.