Developers
Webhooks
Receive signed HTTPS notifications when calls end, messages are taken, appointments, orders and reservations change, or a campaign finishes, and verify that each one really came from Ringhum.
6 min read Updated 24 September 2026
On this page
A webhook sends an HTTPS request to your server the moment something happens in your workspace. Use webhooks instead of polling the API: when a call ends, you receive the summary, outcome, collected details and transcript straight away. Each delivery is signed with a secret that only you and Ringhum know, so your server can reject anything that did not come from Ringhum.
Add an endpoint
Only the workspace Owner and Admins can add, test and remove endpoints.
- Open Developers in the sidebar.
- In the Webhooks card, click Add endpoint.
- Enter the Endpoint URL. It must start with
https://. - Under Events, tick the events you want.
call.endedis ticked for you. - Click Add endpoint.
- Copy the signing secret from the green box (it starts with
whsec_) and store it on your server. Click Done.
Important: The signing secret is shown only once. If you lose it, remove the endpoint and add it again to get a new secret.
You can add as many endpoints as you need. Webhooks cover every assistant in the workspace. You can also manage endpoints through the API, with POST /webhooks and a key that has the write scope.
Events
| Event | When it is sent |
|---|---|
call.started |
A call was answered by an assistant |
call.ended |
A call ended and was summarised (includes transcript and summary) |
call.transferred |
A call was transferred to a human |
message.taken |
The assistant took a message |
appointment.booked |
The assistant booked an appointment |
appointment.rescheduled |
An appointment was moved to a new time |
appointment.cancelled |
An appointment was cancelled |
order.created |
An order was placed (by an assistant, by hand or through the API) |
order.updated |
An order moved to a new status |
order.cancelled |
An order was cancelled |
reservation.created |
A room was reserved (by an assistant, by hand or through the API) |
reservation.updated |
A reservation moved to a new status (confirmed, checked in, checked out, no-show) |
reservation.cancelled |
A reservation was cancelled |
campaign.completed |
An outbound campaign finished calling its list |
test.ping |
You clicked Send test |
What a delivery looks like
Every delivery is a POST with a JSON body and these headers:
POST https://example.com/hooks/ringhum
Content-Type: application/json
User-Agent: Ringhum-Webhooks/1.0
X-Ringhum-Event: call.ended
X-Ringhum-Timestamp: 1789250000
X-Ringhum-Signature: v1=5f1a…
{
"id": "evt_8f2k…",
"type": "call.ended",
"created_at": "2026-09-24T14:03:11+00:00",
"data": {
"call_id": 4812,
"direction": "inbound",
"from": "+14155550132",
"to": "+14155550100",
"assistant_id": 3,
"status": "completed",
"outcome": "message_taken",
"sentiment": "neutral",
"duration_seconds": 94,
"summary": "Caller asked for a quote for a kitchen repair…",
"extracted": { "name": "Dana Lee", "reason": "Quote" },
"transcript": [
{ "role": "assistant", "content": "Hello, you've reached…" },
{ "role": "user", "content": "Hi, I'd like a quote…" }
]
}
}
The data object depends on the event:
call.endedandmessage.taken: the call fields above.call.startedandcall.transferred:call_id,direction,from,to,assistant_id,statusandstarted_at.appointment.*:appointment_id,title,status,starts_at,ends_at,timezone,customer(name, phone, email),contact_id,service,staff,call_idandnotes.order.*:order_id,number,status,fulfillment,customer,address,itemswith options and prices,currency,subtotal,fee,total,source,call_idand, for cancellations,cancel_reason.reservation.*:reservation_id,number,status,room,check_in,check_out,nights,adults,children,guest,currency,nightly,totalandsource.campaign.completed:campaign_id,nameand the campaign's progress counts.
Note: When the assistant books an appointment during a call, you can receive
appointment.bookedtwice. One is sent as the booking is made, with the appointment fields. The other is sent after the call is summarised, with the call fields. Check whetherdatahasappointment_idorcall_idand use theidfield to avoid processing a delivery twice.
Verify the signature
The signature is an HMAC-SHA256 of the timestamp, a dot, and the raw request body, keyed with your endpoint's signing secret. It is sent as v1= followed by the hex digest.
- Read the raw body exactly as received, before any JSON parsing.
- Build the string
{X-Ringhum-Timestamp}.{raw body}. - Compute the HMAC-SHA256 with your secret and prefix it with
v1=. - Compare it with
X-Ringhum-Signatureusing a constant-time comparison. - Reject the request if the timestamp is more than five minutes old, to block replayed requests.
Node.js:
const crypto = require('crypto');
function isValid(req, rawBody, secret) {
const ts = req.header('X-Ringhum-Timestamp');
const sig = req.header('X-Ringhum-Signature') || '';
const expected = 'v1=' + crypto.createHmac('sha256', secret).update(`${ts}.${rawBody}`).digest('hex');
const fresh = Math.abs(Date.now() / 1000 - Number(ts)) < 300;
return fresh && sig.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}
PHP:
$ts = $_SERVER['HTTP_X_RINGHUM_TIMESTAMP'] ?? '';
$sig = $_SERVER['HTTP_X_RINGHUM_SIGNATURE'] ?? '';
$expected = 'v1=' . hash_hmac('sha256', $ts . '.' . file_get_contents('php://input'), $secret);
if (! hash_equals($expected, $sig) || abs(time() - (int) $ts) > 300) {
http_response_code(400);
exit;
}
Responses, timeouts and retries
- Reply with any
2xxstatus to confirm the delivery. Do slow work after you respond. - Ringhum waits up to 10 seconds for your response.
- If your endpoint does not answer with a
2xx, the delivery is tried up to 3 times in total: at once, again after about 10 seconds, and a last time about 60 seconds later. After that it is dropped. - Deliveries can arrive out of order. Use
created_atand the ids indatato put them in order.
The Webhooks table shows each endpoint's last HTTP status and when it was sent. A failed endpoint stays active; Ringhum does not switch it off for you.
Test, pause and remove
- Send test sends a
test.pingevent straight away, and a message shows the HTTP status your endpoint returned. - The Active switch pauses an endpoint. Events that happen while it is off are not sent later.
- Remove deletes the endpoint and its secret.
Common questions
What is the difference from the Zapier, Make and n8n apps under Integrations? Those connections use the Integrations page and its six event types. Their body is {"event", "workspace", "sent_at", "data"}, and when you set a secret the signature header is X-Ringhum-Signature: sha256=…, an HMAC-SHA256 of the body alone. Developer webhooks, described here, cover more events and use the v1= signature with a timestamp. See Choosing what gets sent.
My endpoint returns 2xx but my code sees nothing. Check that the event is ticked for that endpoint, that the endpoint is active, and that your firewall allows requests from the internet.
Why does signature checking fail? Most often the body was parsed and re-encoded before hashing. Hash the raw bytes exactly as received.
Related articles
API keys and scopes
Create and revoke API keys on the Developers page, choose read or write access, and learn the basics of the Ringhum REST API - base URL, authentication, pagination, errors and rate limits.
Connect Ringhum to Claude and other AI tools (MCP)
Add Ringhum as a connector in Claude, ChatGPT, Cursor, VS Code and other AI apps that support the Model Context Protocol, choose read-only or read-and-change access, and disconnect an app.
Choosing what gets sent
Pick which calls, inbox items, appointments and orders each connected app receives, and limit call updates to the calls that need someone to act.
Connecting an app
How to connect Slack, your CRM, help desk, task tool, spreadsheet, shop or calendar to Ringhum, which kind of connection each app uses, and how to check that it works.
Still stuck?
Email hello@ringhum.com or send us a message. Team and Scale plans get priority support.