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: independent signals (acoustic — the far end monologued, beep — a record tone, keyword — greeting phrasing in the transcript, and llm), deciding on any two, or on a confident LLM verdict alone. confidence is how many agreed over the three it takes to be unanimous, capped at 1.0 — 0.67 is the ordinary two-vote decision, 0.33 is the LLM deciding alone. The words behind it are in signals, which is not a closed set: the detector has gained a signal before, so match the ones you care about rather than exhaustively. 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 endpoint — InteractionCompleted is the callback body it documents.

Report events

The same endpoint also receives report events when you have reports configured through the /v1/reports API. Switch on x-vodex-event — a handler written only for interaction.completed should ignore the rest rather than fail on them. report.instant fires as a call reaches an outcome an instant report covers:
report.digest fires once a day for a scheduled report, rolling up the window’s matching calls. A call joins the window by when it was analysed (analyzedAt), not when it started (at) — an outcome lands after the call ends, so at can fall before the window. window is the nominal span, but the real lower bound is since (anchoring to the previous run can push it earlier), so check analyzedAt against since, not window. total is the full match count; calls is capped, so it can be shorter than total:
number, outcome, summary, at and analyzedAt are null when absent (a web call has no number; both times are ISO-8601), so both report events read the same way.

Payment events

When an agent takes a card payment by keypad, the same endpoint receives one event when the attempt settles. Switch on x-vodex-event.
amountMinor and balanceMinor are in the currency’s smallest unit (4950 is 49.50 USD). result is Twilio’s own result for the attempt, such as success or payment-connector-error. The card number is never part of an event.