ResoneraDocs

Webhooks

Get each call's result pushed to your server the moment it ends.

Instead of polling, register an HTTPS URL and Resonera sends it an event whenever something happens: a call ends, its recording is ready, or a campaign finishes.

Events

EventSent whendata contains
call.endedA call finishes, whether it connected or not.The call, as GET /calls returns it: status, outcome, summary, duration and your contact.extra.
call.recording_readyThe call's recording has been saved, usually within a minute of call.ended.The same call, now with has_recording: true.
campaign.completedEvery contact in a campaign has been called.The campaign, as GET /campaigns/{id} returns it, with contacts_by_status.

Transcripts aren't included, to keep deliveries small. Fetch GET /calls/{call_id} when you need one.

Set up an endpoint

Create a key

Give it the webhooks:manage scope.

Register your URL

curl -X POST https://api.resonera.ai/webhooks/endpoints \
  -H "X-API-Key: $RESONERA_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://crm.example.com/resonera/webhook", "events": ["call.ended"]}'

The response includes secret (whsec_…). Store it now: it's shown only once, and you need it to verify deliveries. Leave out events to receive all of them.

Send a test

curl -X POST https://api.resonera.ai/webhooks/endpoints/$ENDPOINT_ID/test \
  -H "X-API-Key: $RESONERA_KEY"

Your endpoint receives a ping event. Check the outcome with GET /webhooks/endpoints/{endpoint_id}/deliveries.

The URL must use HTTPS on a public host. Addresses on private networks are refused.

What a delivery looks like

A POST with a JSON body:

{
  "id": "evt_7b74f0680dbb4562b2ca6d6a526fa1cd",
  "type": "call.ended",
  "created_at": "2026-10-05T09:14:13.412Z",
  "data": {
    "call_id": "88433d99-ead1-4ee2-86c1-7a2cbfd6d064",
    "campaign_id": "3f1d9a52-7c0e-4b8a-a1f3-5e2c9d8b7a64",
    "agent_id": "8b8e3307-2c1a-4f6e-9a51-0d7c2b9e4f10",
    "agent_name": "Neha",
    "direction": "outbound",
    "phone_number": "+919876543210",
    "status": "completed",
    "end_reason": "completed",
    "outcome": "requirements_gathered",
    "summary": "Wants a demo on Friday afternoon.",
    "error": null,
    "duration_seconds": 93.2,
    "start_time": "2026-10-05T09:12:40+00:00",
    "end_time": "2026-10-05T09:14:13+00:00",
    "has_recording": false,
    "contact": { "name": "Asha", "extra": { "crm_id": "42" } }
  }
}

With these headers:

HeaderValue
Resonera-Event-IdThe event's id. The same on every retry of the same event.
Resonera-Event-TypeThe event's type.
Resonera-Signaturet=<unix seconds>,v1=<hex HMAC-SHA256>. See below.

Verify the signature

Check every delivery before trusting it. The signature is an HMAC-SHA256, keyed with your endpoint's secret, over the timestamp, a ., and the raw request body:

v1 = hex( HMAC_SHA256( secret, t + "." + raw_body ) )

Compare it in constant time, and reject timestamps more than five minutes old so a captured request can't be replayed.

import hashlib, hmac, os, time
from flask import Flask, request, abort

SECRET = os.environ["RESONERA_WEBHOOK_SECRET"].encode()
app = Flask(__name__)

def verify(raw_body: bytes, header: str) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    t, v1 = parts.get("t", ""), parts.get("v1", "")
    if not t.isdigit() or abs(time.time() - int(t)) > 300:
        return False
    expected = hmac.new(SECRET, f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, v1)

@app.post("/resonera/webhook")
def webhook():
    if not verify(request.get_data(), request.headers.get("Resonera-Signature", "")):
        abort(400)
    event = request.get_json()
    if event["type"] == "call.ended":
        save_call(event["data"])          # your code; keep it quick
    return "", 204

Verify against the raw bytes you received. Parsing the JSON and serializing it again changes spacing and key order, and the signature won't match.

Responding and retries

Return any 2xx status within 10 seconds. Do slow work, such as calling your CRM, after you've responded or in a background job.

Anything else (an error status, a timeout, a refused connection) is retried with increasing gaps: after 1 minute, 5 minutes, 30 minutes, 2 hours, 5 hours, 10 hours and 18 hours. That's 8 attempts over about a day and a half, after which the delivery is marked failed. Redirects are not followed, so register the final URL.

GET /webhooks/endpoints/{endpoint_id}/deliveries shows each delivery's status, attempts, last response code and next retry time.

Handling duplicates and order

  • Duplicates: a delivery can arrive more than once, for example if your server took the request but timed out before responding. Use Resonera-Event-Id to ignore events you've already processed.
  • Order: events can arrive out of order, especially around retries. call.recording_ready can, rarely, arrive before call.ended. Use the timestamps in data, and treat each event as an update to the record rather than a step in a sequence.

Managing endpoints

  • Pause with PATCH {"enabled": false}. Events that happen while it's disabled aren't queued for it.
  • Rotate the secret with POST /webhooks/endpoints/{endpoint_id}/rotate-secret if it leaks. Deliveries signed after that use the new secret, so update your receiver promptly.
  • Delete with DELETE /webhooks/endpoints/{endpoint_id}. Pending retries are dropped too.

Up to 10 endpoints per account. Each receives the events it subscribes to.

On this page