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
httpsand 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
3xxanswer counts as a failed attempt.
- 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.
| Header | Value |
|---|---|
Content-Type | application/json |
User-Agent | Oxumi-Webhooks/1 |
Oxumi-Event | The event name, the same as event in the body. |
Oxumi-Delivery | The delivery id, the same as id in the body. |
Oxumi-Signature | t=<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.
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.
t=1791846956,v1=f7924ed6a561dca0594bd01e5dbf3b398bb0cacda7f58d89b8d074e54ef6c572tis the time the attempt was made, in Unix seconds. Every retry is signed again with a newt.v1is 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
tandv1. Reject the delivery iftis 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 withv1using 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.
#!/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 1fiRespond 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.
- 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.
- 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 (
idin the body,Oxumi-Deliveryin 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.idinstead.
Oxumi-Event: pingOxumi-Delivery: 5c164866-5bf6-412c-bceb-c6f9ec1c0cdbTest 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.
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"}{ "id": "5c164866-5bf6-412c-bceb-c6f9ec1c0cdb", "data": { "message": "This is a test delivery from Oxumi." }, "event": "ping", "createdAt": "2026-10-12T23:15:56.219Z"}