Skip to main content
When an interaction finishes, Vodex POSTs interaction.completed to the endpoint set with PUT /v1/webhook-config. It is interaction.completed, not call.completed, on purpose: one stream for every channel, so you write one handler and switch on channel rather than integrating a new webhook when SMS ships. The payload is deliberately complete — a consumer that receives it needs no follow-up GET.

Verifying a delivery

The timestamp is inside the signed string, not merely alongside it: signing the body alone leaves a captured delivery replayable forever, because nothing in what was signed says when it was sent. Reject anything more than 300 seconds off in either direction.
Verify against the raw request bytes. Re-serializing the parsed JSON is not guaranteed to reproduce them, and a signature is over bytes.
One secret signs two things — webhook deliveries, and the tool.call POSTs the agent makes to your own server. Every tenant has one whether or not a webhook endpoint is configured. During a rotation both secrets sign, as repeated v1= values, so you can move without a coordinated deploy; the displaced value stops after 24 hours.

Retries

8 attempts at 10s, 30s, 2m, 10m, 30m, 2h, 6h, each with full jitter — a random point in [0, delay], so a consumer coming back from an outage is not hit by every queued delivery at the same instant. 408, 429, 5xx and transport failures retry. Every other 4xx does not: repeating a request you called wrong just burns the budget a real outage needs. A delivery that exhausts its attempts is the dead letter — status: "failed" with no nextAttemptAt. It stays visible at GET /v1/webhook-deliveries, and GET /v1/webhook-deliveries/{id} returns the exact bytes that were sent, which is what answers “we got something, but the signature does not verify”.

Reading the outcome

outcome is orthogonal facts, never a prose string — you should never have to sniff a string for what happened.
endReason is a fixed wire vocabulary, deliberately separate from Vodex’s internal enum so your histograms do not shift when the internals are tidied. Two things that might be expected there are not: whether anyone picked up is outcome.answered, and a voicemail detection rides in outcome.voicemail — the call still ended for one of the listed reasons. voicemail.confidence is not a probability. The detector votes: three independent signals (acoustic, keyword, llm), deciding on any two, or on a confident LLM verdict alone. confidence is the share that agreed — 0.67 is the ordinary two-vote decision, 1.0 is all three, 0.33 is the LLM deciding alone. The words behind it are in signals. transfer is the transfer that actually happened. A transfer is a sequence — requested, then resolved — and this reports the last one, so a warm attempt that failed and fell back to a cold one that completed reports the cold one. analysis is an open envelope: summary and sentiment land there as additive keys, so a handler written today keeps parsing tomorrow’s payloads. The full schema, field by field, is on Set the webhook endpointInteractionCompleted is the callback body it documents.