Developer docs
Webhooks
Haven can call your own server when something happens. Each message is signed, so you can be sure it came from Haven and was not changed on the way.
Set it up
Open Webhooks in your dashboard (account owners only), add an https:// address on your server and choose the events you want. Copy the signing secret that starts with whsec_; it is shown once. You can add up to five addresses, switch them off, send a test and replace the secret at any time.
Events
| Event | When it is sent |
|---|---|
conversation.handoff_requested | A conversation was passed to a person. Includes the conversation id, the reason and the customer’s question. |
tool.executed | The agent made a change in your system, such as creating an order or a booking. Includes the tool, its input and what your system returned. |
approval.requested | The agent asked for approval before doing something risky. |
approval.decided | Someone approved or rejected a request. |
billing.payment_succeeded | A payment for your Haven plan went through. |
billing.payment_failed | A payment for your Haven plan failed. |
What arrives
A POST with a JSON body and these headers:
| Header | Meaning |
|---|---|
Haven-Event-Id | A unique id for the event. The same event is always sent with the same id, so you can ignore repeats. |
Haven-Event-Type | For example conversation.handoff_requested. |
Haven-Signature | Looks like t=1760000000,v1=9f2c…: the time it was sent and the signature. |
JSON
Example body (JSON){
"id": "6f1c9a52-8a0e-4c0b-9a47-1d2f6a3b9e10",
"type": "conversation.handoff_requested",
"createdAt": "2026-10-21T09:15:00.000Z",
"data": {
"handoffId": "b3a8…",
"conversationId": "9d41…",
"reason": "requested",
"question": "I want to talk to a person about my refund"
}
}Check the signature
The signature is an HMAC-SHA256 of the text {t}.{raw body} using your signing secret, written in hex. Use the raw body exactly as received (before any JSON parsing), compare in constant time, and refuse messages older than five minutes.
Verify a webhook (Node.js)import { createHmac, timingSafeEqual } from 'node:crypto';
export function verify(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
const age = Math.abs(Date.now() / 1000 - Number(parts.t));
if (!parts.t || !parts.v1 || age > 300) return false;
const expected = createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(parts.v1);
return a.length === b.length && timingSafeEqual(a, b);
}Answer quickly, and expect repeats
- Answer with any
2xxstatus within 8 seconds. Do slow work after you have answered. - Anything else (or no answer) counts as a failure. Haven tries again after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 24 hours, then gives up. You can send a failed event again by hand from the delivery list in your dashboard.
- An event can arrive more than once. Use
Haven-Event-Idto ignore repeats. - If an address fails 30 times in a row Haven switches it off and emails the account owners. Fix your server, then switch it back on.
- Haven only calls public
https://addresses on the standard port. Addresses inside private networks are refused.