Skip to content

Webhooks

A webhook is an address in your systems where Caller sends the result of each call as soon as it is analyzed. It is how you get scores, criteria and summaries into your CRM, your data warehouse or a dashboard of your own without anyone exporting anything.

  1. In the app, go to Integrations and, under Notices, click Set up on the Webhooks card.

  2. Enter the Address that will receive the deliveries and leave Send the results checked.

  3. Save. Caller shows the signing secret, which starts with whsec_. Copy it: it is not shown again.

  4. Click Send a test to check that your address responds.

There is one webhook per account. The address has to meet three conditions:

  • It starts with https://.
  • It is public. localhost, private IP addresses and internal names such as server.local are not accepted.
  • It has no user name or password in it (https://user:password@…).

To pause deliveries without losing the configuration, uncheck Send the results.

Each delivery is a POST with a JSON body and these headers:

Header Value
Content-Type application/json
User-Agent Caller-Webhooks/1
X-Caller-Event The event type: call.analyzed or ping.
X-Caller-Signature The signature of the body. See verify the signature.

Sent every time the analysis of a call finishes, whether the call came in through the API or was uploaded by hand. If a call is analyzed again, the event is sent again with the new result.

{
"event": "call.analyzed",
"sent_at": "2026-10-09T08:51:37.204Z",
"call": {
"id": "k57e2xq9m4hc8w1t6b0z5y2e97c4n1ad",
"external_id": "pbx-000123",
"url": "https://app.caller.ee/calls/k57e2xq9m4hc8w1t6b0z5y2e97c4n1ad",
"title": "Asks to cancel over the price and accepts a discount",
"score": 76,
"band": "mid",
"topic": "Service cancellation",
"outcome": "resolved",
"customer_sentiment": { "start": "negative", "end": "neutral" },
"agent": {
"id": "jd7b4m1n8p2q5r9s3t6v0w4x7y1z8c2e",
"name": "Lucía Romero",
"code": "AG1143"
},
"campaign": "Retention",
"started_at": "2026-10-09T08:42:11.000Z",
"duration_seconds": 312,
"needs_review": true,
"review_reasons": ["required_failed"],
"criteria": [
{ "key": "greeting", "name": "Corporate greeting", "score": 10, "max": 10, "required": false },
{ "key": "verification", "name": "Identity verification", "score": 0, "max": 10, "required": true },
{ "key": "alternative", "name": "Offers an alternative to canceling", "score": 20, "max": 20, "required": false },
{ "key": "closing", "name": "Call closing", "score": 8, "max": 10, "required": false }
],
"summary": "The customer calls to cancel because of the latest price increase. The agent offers her a discount for six months and she accepts it. Her identity is not verified before her contract is looked up.",
"next_step": "Apply the discount to the next invoice."
}
}
Field Type Description
event string call.analyzed.
sent_at date When this delivery was made, in ISO 8601 and UTC. It changes on every retry.
call.id string The identifier of the call in Caller.
call.external_id string or null The identifier you sent through the API. null for calls uploaded by hand.
call.url string The address of the call in the app.
call.title string The title the analysis gave the call.
call.score number or null The score, from 0 to 100. null if the template has no scored criteria.
call.band string or null The score band: good (80 or more), mid (60 to 79) or bad (below 60).
call.topic string or null The main topic.
call.outcome string or null How the call ended: resolved, follow_up, escalated or unresolved.
call.customer_sentiment object or null The customer’s sentiment at the start (start) and at the end (end): positive, neutral or negative.
call.agent object or null The agent: id, name and code. Any of the three can be null.
call.campaign string or null The name of the campaign.
call.started_at date or null When the call started, in UTC.
call.duration_seconds number or null The length of the recording.
call.needs_review boolean Whether the call is in the To review queue at the time of the delivery.
call.review_reasons array The reasons the call should be listened to: required_failed, critical_score, customer_upset, unresolved, long_silence. Empty if there are none. It can contain reasons while needs_review is false if someone has already marked the call as reviewed.
call.criteria array One item per criterion in the template: its key (key), its name, the points earned (score), the points possible (max) and whether it is mandatory (required).
call.summary string or null The summary of the call.
call.next_step string or null The agreed next step, if there is one.

The key of a criterion does not change even if you rename the criterion in the template. Use it to match one week’s results with the next week’s.

The body does not include the transcript or the audio. To see them, open call.url.

This is what the Send a test button sends. Use it to check the connection and the signature without waiting for a real call.

{ "event": "ping", "sent_at": "2026-10-09T08:40:02.118Z" }

Respond with a 2xx code in less than 10 seconds. The content of your response is not read.

Do the minimum before you respond: verify the signature, store the body and reply. Do the slow work, such as writing to a CRM or recalculating a report, afterward. A receiver that takes more than 10 seconds counts as a failed delivery, even if it processes the event in the end.

Your address is public, so anyone can send it a POST. The signature proves that a delivery comes from Caller and that nobody has altered its content.

The X-Caller-Signature header looks like this:

X-Caller-Signature: sha256=5a1f0c…

What follows sha256= is the HMAC-SHA256 of the request body, computed with your signing secret and written in hexadecimal. To verify it, compute the same HMAC over the body you received and compare.

import hashlib
import hmac
import os
from flask import Flask, abort, request
app = Flask(__name__)
SECRET = os.environ["CALLER_WEBHOOK_SECRET"].encode()
def valid_signature(body: bytes, header: str) -> bool:
expected = "sha256=" + hmac.new(SECRET, body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, header)
@app.post("/hooks/caller")
def receive():
body = request.get_data() # the bytes exactly as they arrived
if not valid_signature(body, request.headers.get("X-Caller-Signature", "")):
abort(401)
payload = request.get_json()
if payload["event"] == "call.analyzed":
save(payload["call"]) # your code
return "", 204

Compare with a constant-time function (hmac.compare_digest, timingSafeEqual, hash_equals), not with ==.

In the webhook configuration, click Make a new secret. Caller shows the new one once, and the previous secret stops working at that moment, so deliveries that arrive before you update your system will fail verification. Do it at a time of low traffic.

A delivery counts as failed when your address:

  • does not respond within 10 seconds or cannot be reached,
  • responds with a 5xx code,
  • responds with 429.

In those cases Caller tries two more times: after 1 minute and, if that fails too, 10 minutes later. After the third attempt, that delivery is dropped.

Any other response is treated as final and is not retried. That includes 4xx codes and also redirects: Caller does not follow a 301 or a 302, so configure the final address.

The Webhooks card in Integrations shows the result of the last delivery: accepted, no answer, or the code your address returned.

  • Treat each delivery as “this is the current state of the call.” Store the result with call.id as the key and overwrite it if it already exists. That way, a retry that arrives twice or a new analysis of the same call leaves your data correct with no special logic.
  • Do not assume the order. A retry can arrive after the delivery for a later call.
  • Ignore events you do not know. If event is not one you handle, respond with 2xx and do nothing.
  • Have a plan for what gets lost. If your system is down for more than a few minutes, the deliveries from that time are dropped and are not sent again. The results are still in the app: you can recover them with a CSV export or by analyzing those calls again, which uses analyses.

The webhook is sent when a call is analyzed. Nothing is sent if:

  • The call could not be transcribed.
  • The account has no template, or there are no analyses left in the period. The event will arrive when the call is analyzed.
  • Someone corrects a score by hand in the app. The correction does not produce a new delivery.

Because the address has to be public and https, a server on localhost does not work directly. While you develop, use a tunnel that exposes your local port at a temporary https address, set that address in Caller and click Send a test.