DocsReceive the result

Get started · 4 of 4

Receive the result

When a call ends, Oxumi posts the result to a URL in your system. That request is signed, retried for about seven hours if your server is down, and carries an id you can use to ignore repeats.

Set up an endpoint

Add an endpoint on the project's API page: open your projects, choose a project, then API → Webhooks. The API itself has no endpoints for managing them.

In production the URL has to meet these rules, and is refused otherwise:

  • It uses https and contains no username or password.
  • Its host resolves to a public address. Loopback, private, link-local and carrier-grade NAT ranges are refused. This is checked again on every delivery.
  • Redirects are not followed. A 3xx answer counts as a failed attempt.
Webhook endpoints
Where
In the app: project → API → Webhooks
Per project
Up to 5, each can be switched off
Secret
whsec_…, shown once when the endpoint is created
Event
call.completed
Sent for
Phone calls and API text conversations
Not sent for
Calls made in the app's Test tab

The payload

Every delivery is a POST with a JSON body in the same envelope: id, event, createdAt and data. For call.completed, data.call is the finished call.

HeaderValue
Content-Typeapplication/json
User-AgentOxumi-Webhooks/1
Oxumi-EventThe event name, the same as event in the body.
Oxumi-DeliveryThe delivery id, the same as id in the body.
Oxumi-Signaturet=<unix seconds>,v1=<hex digest>

Note · Key order is not fixed

The sample shows fields in a readable order. On the wire the keys arrive in a different order (for instance data before event). Read fields by name, and compute the signature over the bytes you received, never over a re-serialised copy.

Note · Not the same shape as GET /v1/calls/:id

The call in a webhook is a summary. Its turns use caller, agent and step, where the API's Turn object uses callerText, agentText and nodeId; it adds agentName and durationSeconds and leaves out projectId.

call.completedExample
{
: "c7a1e0d2-3b54-4f6a-9d8e-2f1b0a9c8d7e",
: "call.completed",
: "2026-10-12T14:03:25.871Z",
: {
: {
: "0b9f6c1e-52a7-4d1c-9b6e-3f2a8d4c7e10",
: "81ee2426-7abe-444b-96f8-b8e195ddde49",
: "Driver check-in",
: "phone",
: "+919876543210",
: "ended",
: "completed",
: null,
: {
"current_location": "Adilabad toll",
"on_schedule": false,
"delay_reason": "traffic",
"revised_eta": "around midnight",
"needs_help": false
},
: {
"driver_name": "Ramesh",
"company": "Shakti Logistics",
"route": "Nagpur to Hyderabad",
"planned_eta": "tonight at 9 pm"
},
: {
"trip_id": "TRIP-2291"
},
: "anthropic:claude-haiku-5-5",
: "2026-10-12T14:02:11.412Z",
: "2026-10-12T14:03:25.803Z",
: 74,
: [
{
: 1,
: null,
: "Namaste Ramesh ji, this is the automated assistant from Shakti Logistics, calling for a quick update on your Nagpur to Hyderabad trip. Where have you reached?",
: false,
: "ask_location"
},
{
: 2,
: "Just crossed the Adilabad toll.",
: "Thank you. Will you reach by nine tonight?",
: false,
: "ask_schedule"
},
{
: 3,
: "No, there is a jam on the highway. It will be around midnight.",
: "Understood. Is there any problem with the truck, the load or the paperwork?",
: false,
: "ask_help"
},
{
: 4,
: "No, all fine.",
: "Thank you, your update is recorded. Drive safe.",
: false,
: "goodbye"
}
]
}
}
}

Point at a field, or tab to it, to see what it holds. Click to keep it selected.

Verify the signature

Anyone who learns your URL can post to it. The signature proves a delivery came from Oxumi and was not altered.

Oxumi-Signature
t=1791846956,v1=f7924ed6a561dca0594bd01e5dbf3b398bb0cacda7f58d89b8d074e54ef6c572
  • t is the time the attempt was made, in Unix seconds. Every retry is signed again with a new t.
  • v1 is the HMAC-SHA256, in lowercase hex, of the string <t>.<raw body>, keyed with the endpoint's secret.

To check a delivery, as each sample does:

  • Read the body as raw bytes, before any JSON parsing. Many frameworks parse first by default; turn that off for this route.
  • Split the header into t and v1. Reject the delivery if t is more than five minutes from your clock, so a captured request cannot be replayed later. Oxumi does not enforce a window; the tolerance is yours to choose.
  • Compute the HMAC over t, a full stop, and the raw body, and compare it with v1 using a constant-time comparison.

Note · The cURL tab is a shell script

There is no server to verify in with cURL, so that tab checks one saved delivery by hand with openssl. It is for debugging; verify in your server with one of the other eight.

Verify a deliverycURL
#!/bin/sh# Checks one saved delivery by hand. For debugging only: a shell comparison is# not constant-time, so verify in your server with one of the other languages.## usage: sh verify.sh body.json "t=1791844742,v1=e8e42b..." "whsec_..."body_file="$1"header="$2"secret="$3"
timestamp="${header#t=}"timestamp="${timestamp%%,*}"signature="${header##*v1=}"
# Reject a timestamp more than five minutes from now.age=$(( $(date +%s) - timestamp ))if [ "$age" -gt 300 ] || [ "$age" -lt -300 ]; then  echo "rejected: timestamp is outside the five-minute window"  exit 1fi
# HMAC-SHA256 of "<timestamp>.<raw body>", keyed with the secret.expected=$( { printf '%s.' "$timestamp"; cat "$body_file"; } \  | openssl dgst -sha256 -hmac "$secret" -r | cut -d' ' -f1 )
if [ "$expected" = "$signature" ]; then  echo "valid"else  echo "invalid"  exit 1fi

Respond quickly

Answer with any 2xx status within 10 seconds, then do slow work afterwards, on a queue or in the background.

A receiver that updates three systems before answering will sooner or later time out and be sent the same event again.

What counts as delivered
Success
Any 2xx status
Time limit
10 seconds
Response body
Ignored
Redirect (3xx)
Failed attempt; not followed
Other status, timeout, no connection
Failed attempt

Retries

A failed delivery is tried again, up to six attempts in all over about seven hours. Each wait is counted from the attempt that failed.

Retries are picked up on a 15-second cycle, so each can run up to 15 seconds later than the schedule says.

The app's Webhooks tab lists the project's 50 most recent deliveries with the status your server returned, and can send any of them again.

Attempt → when
1
As soon as the call ends
2
30 seconds after attempt 1 fails
3
2 minutes after attempt 2 fails
4
10 minutes after attempt 3 fails
5
1 hour after attempt 4 fails
6
6 hours after attempt 5 fails
then
Marked failed; not sent again automatically

Handle repeats

Your server can receive the same delivery more than once: when its answer was slow or lost, or when someone re-sends it from the app. Make handling it twice harmless.

  • The delivery id (id in the body, Oxumi-Delivery in the headers) is the same on every attempt of one delivery. Record the ids you have handled and skip any you have seen.
  • Each endpoint gets its own delivery, with its own id, for the same call. If two endpoints feed one system, de-duplicate on data.call.id instead.
The delivery id, in a real delivery's headersCaptured
Oxumi-Event: pingOxumi-Delivery: 5c164866-5bf6-412c-bceb-c6f9ec1c0cdb

Test deliveries

The app can send a test delivery to an endpoint at any time. It is a real, signed request with the event ping, so it exercises your signature check and your 2xx answer without placing a call.

A ping is not something you subscribe to: it goes only to the endpoint you test. Have your receiver accept it and do nothing.

Outside production, http URLs and local addresses are accepted, so a receiver on localhost works against a development server.

A test delivery as receivedCaptured
POST /hooks/oxumi HTTP/1.1Content-Type: application/jsonUser-Agent: Oxumi-Webhooks/1Oxumi-Event: pingOxumi-Delivery: 5c164866-5bf6-412c-bceb-c6f9ec1c0cdbOxumi-Signature: t=1791846956,v1=f7924ed6a561dca0594bd01e5dbf3b398bb0cacda7f58d89b8d074e54ef6c572
{"id":"5c164866-5bf6-412c-bceb-c6f9ec1c0cdb","data":{"message":"This is a test delivery from Oxumi."},"event":"ping","createdAt":"2026-10-12T23:15:56.219Z"}
Its bodyCaptured
{  "id": "5c164866-5bf6-412c-bceb-c6f9ec1c0cdb",  "data": {    "message": "This is a test delivery from Oxumi."  },  "event": "ping",  "createdAt": "2026-10-12T23:15:56.219Z"}