Concepts

Webhooks

Webhooks push events to your server the moment they happen: a call is answered, a call ends and its analysis is ready, a call fails or is transferred. No polling.

Setting up an endpoint

  1. Build an HTTPS endpoint on your server that accepts POST with a JSON body. It must be reachable from the internet: private and internal addresses are refused.
  2. Register it — in the dashboard under Webhooks, or with POST /webhooks — and choose the events it should receive.
  3. Copy the signing secret from the response. It is shown once. Store it like a password.
  4. Verify the signature of every delivery, then answer 200 quickly.

Event types

EventWhen it is sent
call.startedA call was answered and the agent is on the line.
call.completedA call has ended and its analysis is ready. data.object is the call, with analysis filled in.
call.failedA call out was never connected. data.reason says why: no_answer (it rang out or was declined), busy, or not_connected (the number or the network refused it). Sent once per call.
call.transferredThe agent transferred the caller to a person or another number.
webhook.testSent by "Send test event" in the dashboard and by POST /webhooks/{webhook_id}/test.

webhook.test needs no subscription: it is only sent when you ask for a test. Subscribe an endpoint to the others one by one; it receives nothing else.

The event payload

Every event has the same envelope. For call events, data.object is the full call — the same object GET /calls/{call_id} returns — including the metadata you sent when you started it.

  • id — unique per event, and kept when the same event is retried. De-duplicate on it.
  • type — the event type.
  • api_version — always v1 for this API.
  • created_at — when the event happened (ISO 8601, UTC).
  • data — the event’s content.
call.completed
{
  "id": "evt_5f0c2b9a7d3e4c1f8a6b2d9e0c4f7a13",
  "object": "event",
  "type": "call.completed",
  "api_version": "v1",
  "created_at": "2026-10-12T10:07:51.000Z",
  "data": {
    "object": {
      "object": "call",
      "id": "3c27f11c-8a1e-4d55-9b0e-1f4c2a7d9e10",
      "agent_id": "7c3f1e90-5a44-4b21-9c07-2ed5b8f41a66",
      "direction": "outbound",
      "channel": "phone",
      "status": "completed",
      "from": "+4721000000",
      "to": "+4712345678",
      "started_at": "2026-10-12T10:04:12.000Z",
      "ended_at": "2026-10-12T10:07:33.000Z",
      "duration_seconds": 201,
      "end_reason": "caller_hangup",
      "metadata": {
        "crm_contact_id": "48213"
      },
      "has_recording": true,
      "analysis": {
        "summary": "The caller booked a check-up on Tuesday 14 October at 12:00.",
        "outcome": "resolved",
        "sentiment": "positive",
        "action_items": [
          "Send booking confirmation by SMS"
        ],
        "created_at": "2026-10-12T10:07:51.000Z"
      }
    }
  }
}

Delivery and retries

  • Each event is a POST with a JSON body. Your endpoint has 10 seconds to answer.
  • Any 2xx status acknowledges the event. Anything else, or no answer in time, counts as a failure.
  • A failed delivery is retried, up to 5 attempts in total, waiting longer before each one (exponential backoff).
  • The same event can arrive more than once — a retry after your server processed it but answered too late, for example. Store the event id and skip ids you have seen.
  • Events can arrive out of order. When order matters, fetch the call with GET /calls/{call_id} and trust its status.
  • Answer first, work later: put the event on a queue and return 200, then process it.

Headers

HeaderValue
Content-Typeapplication/json
Voximate-Signaturet=1760263671,v1=5257a869e7…
Voximate-Event-IdThe event id, the same as id in the body.
Voximate-Event-TypeThe event type, the same as type in the body.
User-AgentVoximate-Webhooks/1.0

Verifying signatures

Anyone can send a request to your endpoint. The signature proves a delivery came from Voximate and was not changed on the way. The Voximate-Signature header has two parts:

  • t — when Voximate signed the delivery, in Unix seconds.
  • v1 — the signature: HMAC-SHA256, hex-encoded.

To verify a delivery:

  1. Read the raw request body exactly as it arrived. Do not parse and re-serialise the JSON first — a single changed space breaks the signature.
  2. Build the signed message: t, a dot, then the raw body — ${t}.${rawBody}.
  3. Compute HMAC-SHA256 of that message, using the endpoint secret exactly as shown as the key (including its whsec_ prefix), and hex-encode it.
  4. Compare it to v1 with a constant-time comparison.
  5. Reject the delivery if t is more than 300 seconds from your clock. That stops an old, captured delivery from being replayed.

Verification code

// npm install express
import crypto from 'node:crypto';
import express from 'express';

const app = express();
const SECRET = process.env.VOXIMATE_WEBHOOK_SECRET; // "whsec_…", shown once

// The signature covers the RAW body, so read it as bytes, not parsed JSON.
app.post('/voximate/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
  const header = req.get('Voximate-Signature') ?? '';
  const parts = Object.fromEntries(header.split(',').map((p) => p.trim().split('=')));
  const t = Number(parts.t);
  const v1 = parts.v1 ?? '';

  // Refuse old or future-dated deliveries (replay protection).
  if (!Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > 300) {
    return res.status(400).send('stale signature');
  }

  const expected = crypto
    .createHmac('sha256', SECRET)
    .update(`${t}.`)
    .update(req.body) // Buffer — the exact bytes received
    .digest('hex');

  const a = Buffer.from(expected, 'hex');
  const b = Buffer.from(v1, 'hex');
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
    return res.status(400).send('bad signature');
  }

  const event = JSON.parse(req.body.toString('utf8'));
  // Events can arrive more than once: skip ids you have already handled.
  console.log(event.id, event.type);

  res.sendStatus(200); // answer fast; do slow work after acknowledging
});

app.listen(3000);

Older integrations

Voximate also sends X-Voximate-Signature: sha256=… — an HMAC-SHA256 of the body alone, with no timestamp. It is kept for integrations built before v1. New code should verify Voximate-Signature, which also protects against replays.

Secrets and rotation

A secret is whsec_ followed by 64 hex characters, and it is shown only when the endpoint is created and when you rotate it. To replace a secret — because it leaked, or as routine — call POST /webhooks/{webhook_id}/rotate-secret. Deliveries are signed with the new secret from the next event, so update your server straight away. Deleting an endpoint deletes its secret.

Testing

  • In the dashboard, open Webhooks and click Send test event, or call POST /webhooks/{webhook_id}/test. You get a signed webhook.test event and see the status your server answered.
  • Every delivery attempt is logged, with your server’s status code: GET /webhooks/{webhook_id}/deliveries.
  • Pause an endpoint while you work on it with PATCH /webhooks/{webhook_id} and {"status": "paused"}.
  • While developing on your own machine, expose it with a tunnel such as ngrok or cloudflared — the endpoint must be public https.

Checklist

  • The endpoint is HTTPS and public.
  • The signature is checked on the raw body, with a constant-time compare and the 300-second window.
  • Event ids are stored, and duplicates skipped.
  • The endpoint answers 2xx within 10 seconds and does slow work afterwards.
  • The secret lives in a secret store, not in code.