Call webhooks

Receive signed snapshots for finished calls and later call outcome updates.

Configure your callback

Use your workspace bearer API key with https://api.sonik.ai/v1/webhooks/calls.

MethodPathBehavior
GET/webhooks/callsReturns url, enabled, enabledAt, and signingSecret.
PUT/webhooks/callsAccepts { "url": "https://client.example/sonik", "enabled": true }. Returns settings and signingSecret.
POST/webhooks/calls/testSends one signed test event. Accepts call.ready or call.outcome.updated; defaults to call.ready.
DELETE/webhooks/callsClears webhook settings and cancels undelivered callbacks. Preserves API key and call records.

One callback URL per workspace. URL must use HTTPS and resolve only to public IP addresses. Redirects are not followed. Keep the signing secret server-side; it is separate from your API key.

Repeating PUT with the same settings preserves the secret and activation time. Set enabled to false to stop deliveries. Re-enabling starts a new activation period. Changing URL rotates the secret and cancels undelivered callbacks. API key regeneration preserves webhook configuration; API key revocation stops deliveries. Requests already in flight may still complete.

The test endpoint requires an enabled webhook and saved callback URL. Send { "type": "call.ready" } or { "type": "call.outcome.updated" }; omitting the body sends call.ready. It sends immediately without creating a call or delivery record and does not retry. A 204 response means the callback returned 2xx.

Every test event includes top-level "test": true so your receiver can distinguish synthetic checks from live events. Live deliveries omit the test property.

What gets delivered

Receive one logical call.ready event for each stored inbound or outbound call ending after activation. Later call outcome changes send call.outcome.updated events while the webhook remains enabled. There is no historical replay.

Event types

EventSent when
call.readyA finished call snapshot is ready.
call.outcome.updatedThe saved outcome of an existing call changes.

call.ready example:

{
  "id": "66666666-6666-4666-8666-666666666666",
  "type": "call.ready",
  "occurredAt": "2026-08-20T09:32:06.000Z",
  "workspaceId": "11111111-1111-4111-8111-111111111111",
  "summaryStatus": "ready",
  "data": {
    "id": "33333333-3333-4333-8333-333333333333",
    "userId": "44444444-4444-4444-8444-444444444444",
    "direction": "outbound",
    "from": "+49301234567",
    "to": "+4915112345678",
    "status": "completed",
    "startedAt": "2026-08-20T09:30:00.000Z",
    "endedAt": "2026-08-20T09:32:05.000Z",
    "duration": 125,
    "outcome": "Connected",
    "listName": "Outbound Q1",
    "prospect": {
      "id": "55555555-5555-4555-8555-555555555555",
      "firstName": "Alex",
      "lastName": "Morgan"
    },
    "recordingUrl": "https://api.sonik.ai/v1/calls/33333333-3333-4333-8333-333333333333/recording?workspaceId=11111111-1111-4111-8111-111111111111&expiresAt=1788422400000&signature=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
    "transcription": "Speaker 1: Could we schedule a product demonstration?\nSpeaker 2: Yes, next Tuesday works.",
    "transcriptionSummary": "Prospect requested a product demonstration and a follow-up next Tuesday.",
    "createdAt": "2026-08-20T09:29:58.000Z",
    "updatedAt": "2026-08-20T09:32:06.000Z"
  }
}

call.outcome.updated example:

{
  "id": "77777777-7777-4777-8777-777777777777",
  "type": "call.outcome.updated",
  "occurredAt": "2026-08-20T09:40:00.000Z",
  "workspaceId": "11111111-1111-4111-8111-111111111111",
  "data": {
    "id": "33333333-3333-4333-8333-333333333333",
    "userId": "44444444-4444-4444-8444-444444444444",
    "direction": "outbound",
    "from": "+49301234567",
    "to": "+4915112345678",
    "status": "completed",
    "startedAt": "2026-08-20T09:30:00.000Z",
    "endedAt": "2026-08-20T09:32:05.000Z",
    "duration": 125,
    "outcome": "Meeting booked",
    "listName": "Outbound Q1",
    "prospect": {
      "id": "55555555-5555-4555-8555-555555555555",
      "firstName": "Alex",
      "lastName": "Morgan"
    },
    "recordingUrl": "https://api.sonik.ai/v1/calls/33333333-3333-4333-8333-333333333333/recording?workspaceId=11111111-1111-4111-8111-111111111111&expiresAt=1788422400000&signature=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
    "transcription": "Speaker 1: Could we schedule a product demonstration?\nSpeaker 2: Yes, next Tuesday works.",
    "transcriptionSummary": "Prospect requested a product demonstration and a follow-up next Tuesday.",
    "meetingBookedAt": "2026-08-20T09:39:58.000Z",
    "meetingBookedByUserId": "44444444-4444-4444-8444-444444444444",
    "createdAt": "2026-08-20T09:29:58.000Z",
    "updatedAt": "2026-08-20T09:40:00.000Z"
  }
}
  • Failed, busy, no-answer, canceled, and voicemail calls become ready after they finish.
  • Completed non-voicemail calls wait for their AI summary when transcription and AI notes are enabled.
  • Completed non-voicemail calls wait for their full transcript when transcription is enabled but AI notes are disabled.
  • Calls with transcription disabled are delivered without waiting for transcript or summary.
  • After 30 minutes from call end, a scheduled fallback sends available data if expected transcript or summary is still missing. Queue availability can add delay.

Both JSON envelopes contain id, type, occurredAt, workspaceId, and data. Deliveries contain the same currently available properties as GET /calls; unavailable optional fields are omitted. call.ready also contains summaryStatus. Webhook snapshots include the signed recording URL, prospect, full transcription, and transcription summary when available and enabled by workspace settings.

summaryStatusMeaning
readyVisible summary is available.
not_applicableCall does not need a summary.
disabledWorkspace transcription or AI notes are disabled.
unavailableExpected summary was unavailable when the timeout snapshot was prepared.

Late transcripts, summaries, and edits other than call outcome changes can be retrieved through the calls API; they do not trigger another webhook.

Verify and acknowledge deliveries

The Sonik-Signature header is t=<unix-seconds>,v1=<hex-hmac>. Compute HMAC-SHA256 over the exact timestamp string, a period, and the raw request body, using your signing secret. Each attempt gets a fresh timestamp and signature. Reject timestamps more than five minutes from your clock and compare signatures in constant time.

Express receiver example, before any JSON body parser for this route:

import express from 'express';
import { createHmac, timingSafeEqual } from 'node:crypto';

// persistOnce must durably store the event using event.id as a unique key.
// Duplicate inserts must succeed without processing the same event twice.
export function createSonikReceiver(signingSecret, persistOnce) {
  const router = express.Router();

  router.post('/sonik', express.raw({ type: 'application/json', limit: '1mb' }), async (req, res) => {
    const match = /^t=(\d+),v1=([a-f0-9]{64})$/.exec(req.get('Sonik-Signature') || '');

    if (!match || !Buffer.isBuffer(req.body)) {
      return res.sendStatus(400);
    }

    const [, timestamp, hex] = match;

    if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
      return res.sendStatus(401);
    }

    const expected = createHmac('sha256', signingSecret)
      .update(timestamp + '.')
      .update(req.body)
      .digest();

    if (!timingSafeEqual(expected, Buffer.from(hex, 'hex'))) {
      return res.sendStatus(401);
    }

    let event;

    try {
      event = JSON.parse(req.body.toString('utf8'));
    } catch {
      return res.sendStatus(400);
    }

    if (!event || typeof event.id !== 'string' || !['call.ready', 'call.outcome.updated'].includes(event.type)) {
      return res.sendStatus(400);
    }

    try {
      await persistOnce(event.id, event);

      return res.sendStatus(204);
    } catch {
      return res.sendStatus(503);
    }
  });

  return router;
}

Return a 2xx response after durable acceptance, within ten seconds. Process business work asynchronously from your stored inbox. Network errors and other responses retry with exponential backoff, up to five attempts per delivery. Duplicate delivery is possible after ambiguous network failures; deduplicate by event ID. Ordering is not guaranteed.


Did this page help you?