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.
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 endpoint —
InteractionCompleted is the callback body it documents.