Product Use cases Become a partner Pricing Help centre Contact Book a demo Nederlands
Email us Try 30 days free

Then from €79 a month, excl. VAT. Card up front, cancel any time.

Help centre

Connecting your CRM with a webhook

After every call VoiceHelden sends a signed message to an address you set, so each call lands in the system you already use, through Zapier, Make or n8n.

Where to set it up. In your dashboard under Koppelingen you will find the Webhook (CRM) card: enter the address, save, copy the signing secret once, and use the test button to check that your system receives the message. If you do not see that card yet, the connection is not switched on for your account; email info@voicehelden.nl. Below is version 1 of the message; a later version may add fields but will not remove any.

What you need

  • An https URL that accepts a POST and replies within ten seconds with a status code in the 200 range. Reply first, process afterwards — otherwise you will hit the timeout.
  • A secret. You would get it from us, once. Keep it the way you keep a password: whoever has it can forge a message.
  • Something that checks the signature. Zapier, Make and n8n can all three do it; see the recipes at the bottom.

The message

One POST goes to your URL after every completed call, with these headers:

Content-Typeapplication/json
X-VoiceHelden-Eventcall.completed
X-VoiceHelden-Deliverya uuid; the same on a retry
X-VoiceHelden-Timestampunix time in seconds
X-VoiceHelden-Signaturesha256=<hex> — HMAC-SHA256 over timestamp + "." + body

And this is the body. The values below are made up; the field names are not.

{
  "event": "call.completed",
  "version": 1,
  "delivery_id": "b1f0c6e2-8f1a-4a35-9a0e-2f5b7c3d9e41",
  "sent_at": "2026-09-08T09:14:07Z",
  "account_id": "kZ8yQ2mVbNfR7pLx3TcW",
  "agent_id": "agent_01",
  "call": {
    "id": "call_9f3a2b7c",
    "started_at": "2026-09-08T09:11:52Z",
    "ended_at": "2026-09-08T09:13:58Z",
    "duration_seconds": 126,
    "direction": "inbound",
    "from": "+31612345678",
    "to": "+31851234567",
    "outcome": "appointment"
  },
  "caller": {
    "name": "Jeroen Bakker",
    "phone": "+31612345678",
    "email": null,
    "reason": "Leak under the kitchen sink"
  },
  "summary": "Caller reports a leak under the kitchen sink and wants someone out as soon as possible. Appointment set for Tuesday 9 September at 10:00.",
  "transcript_url": "https://app.voicehelden.nl/calls/call_9f3a2b7c",
  "appointment": {
    "starts_at": "2026-09-09T08:00:00Z",
    "ends_at": "2026-09-09T08:30:00Z",
    "calendar_event_id": "5k1q7v9m3b2n8x0c"
  }
}

What the fields mean

  • version — always 1 for now. If the shape of the message changes, this number goes up and version 1 stays alive as long as anyone is listening to it.
  • call.outcome — one of appointment, message, transfer, no_action. This is what you branch your automation on.
  • call.direction — always inbound for now. The assistant does not call out.
  • caller — everything the caller volunteered. Every field can be null; someone who does not give their name has no name. Build your integration so an empty field is not an error.
  • appointmentnull when no appointment was made. calendar_event_id is null when no calendar is connected; the call and the summary still arrive.
  • agent_idnull while you are still on the default assistant.
  • Times are ISO 8601 in UTC. Convert to Europe/Amsterdam yourself before showing them to anyone.

Checking the signature

Do this before you do anything with the message. Without the check your URL is a form anyone who knows it can fill in.

Compute the HMAC over the raw body, not over the body your framework has already parsed and turned back into text. One space of difference and the signature no longer matches.

raw       = the bytes of the body, exactly as they arrived
time      = header["X-VoiceHelden-Timestamp"]
base      = time + "." + raw
expected  = "sha256=" + hex(hmac_sha256(secret, base))

if NOT constant_time_equals(expected, header["X-VoiceHelden-Signature"]):
    respond 401 and stop

if abs(now_in_seconds() - time) > 300:
    respond 401 and stop          # older than five minutes: replayed

process(json_parse(raw))
respond 200

The five-minute window is our recommendation, not part of the message: it stops someone replaying a valid message from last week.

If you do not answer

If no reply arrives in the 200 range, or no reply at all, we retry after 1 minute, 10 minutes, 1 hour, 6 hours and 24 hours. After that we stop; that is five attempts at most.

  • Every attempt carries the same delivery_id. Store it and ignore a message you already know, or the same appointment ends up twice in your CRM.
  • Reply with HTTP 410 and we switch the webhook off and stop retrying. Use that when you take the integration down for good.
  • A 4xx that is not 410 counts as a failed attempt and is retried.

The test message

The test button sends one message with an extra field at the top:

  • "test": true — otherwise the message is identical in shape.

Make your automation stop on that, or you will have an invented leak in your CRM. In n8n that is an IF node, in Zapier a filter.

Recipe: n8n

  • New workflow, node Webhook, method POST. Set Respond to Immediately so n8n replies within ten seconds.
  • In the same node, under Options, switch on Raw Body. Without the raw body you cannot check the signature.
  • Node Crypto → Action Hmac, Type SHA256, Encoding hex. Value: the timestamp header, a dot, and the raw body. Secret: from a credential, not typed into the node.
  • Node IF: compare sha256= plus the result with the x-voicehelden-signature header. Not equal → end the workflow.
  • A second IF on test: if it is true, stop as well.
  • Node Switch on call.outcome, and your own follow-up per outcome — a CRM node, or an HTTP Request to your own API.
  • Copy the Production URL (not the test URL, which only listens once) and email it to us.

Recipe: Zapier

  • New Zap, trigger Webhooks by ZapierCatch Raw Hook. Pick the raw variant deliberately: the ordinary Catch Hook parses the body, and after that your signature no longer matches.
  • Copy the Custom Webhook URL and email it to us. We send the test message so Zapier learns the fields.
  • Step Code by Zapier (JavaScript): recompute the HMAC with the built-in crypto module and throw if it does not match. Put the secret in an input data field, not in the code.
  • Step Filter by Zapier: continue only when test is absent, and for instance only when outcome equals appointment.
  • Final step: your CRM.

Recipe: Make

Same pattern: Webhooks → Custom webhook, with Get request headers on and without automatic JSON parsing, so the raw body survives. Check the signature in a Tools → Set variable step using the sha256 function, and only then convert the body with JSON → Parse JSON.

What is not in it

  • No audio file. The message carries a summary and a link, not a recording.
  • No outbound calls. The webhook reports what happened; it cannot make the assistant call anyone.
  • No email or WhatsApp to you after a call. Apart from this webhook, a call does not reach any other system automatically: you read it back in your dashboard.

Stuck? Email info@voicehelden.nl or use the contact page. Add your 085 number and we can look along straight away.

Then from €79 per month, excl. VAT. Card up front, cancel any time.