Skip to content

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

Events you can choose
EventWhen it is sent
conversation.handoff_requestedA conversation was passed to a person. Includes the conversation id, the reason and the customer’s question.
tool.executedThe 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.requestedThe agent asked for approval before doing something risky.
approval.decidedSomeone approved or rejected a request.
billing.payment_succeededA payment for your Haven plan went through.
billing.payment_failedA payment for your Haven plan failed.

What arrives

A POST with a JSON body and these headers:

Request headers
HeaderMeaning
Haven-Event-IdA unique id for the event. The same event is always sent with the same id, so you can ignore repeats.
Haven-Event-TypeFor example conversation.handoff_requested.
Haven-SignatureLooks 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 2xx status 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-Id to 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.