Skip to main content
Set a webhook URL on a phone number and Nixflex will POST a clean, flat JSON payload to your server about 60 seconds after every call on that number ends. Use this to sync transcripts to your database, trigger downstream workflows, or pipe events into Zapier / Make. Webhooks are configured per phone number, so each number can send to its own endpoint.

Setting a webhook

Set the webhook URL on one of your numbers:
curl -X PUT https://api.nixflex.com/v1/integrations/webhook/number/+447700900123 \
  -H "Authorization: Bearer KEY_ID:KEY_SECRET" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://api.yourapp.com/nixflex/calls"}'
url is required and must be https://. Every call on that number — inbound, outbound, or batch — will POST to it. Read the current URL (returned in full, since it is your own endpoint), or DELETE to remove it:
curl https://api.nixflex.com/v1/integrations/webhook/number/+447700900123 -H "Authorization: Bearer KEY_ID:KEY_SECRET"
# -> { "phone_number": "+447700900123", "webhook_url": "https://api.yourapp.com/nixflex/calls", "is_set": true }

curl -X DELETE https://api.nixflex.com/v1/integrations/webhook/number/+447700900123 -H "Authorization: Bearer KEY_ID:KEY_SECRET"

Second webhook URL (optional)

Each number can have a second webhook URL in addition to the primary one. Both URLs receive the same payload, for both events (call.started and call.completed). Use it to send call data to a second endpoint — for example, your own system alongside a primary destination. The second URL is API-only (there is no dashboard field for it). It mirrors the primary webhook routes, using webhook2 in the path:
# Set the second URL
curl -X PUT https://api.nixflex.com/v1/integrations/webhook2/number/+447700900123 \
  -H "Authorization: Bearer KEY_ID:KEY_SECRET" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://api.yourapp.com/nixflex/calls-2"}'

# Read it
curl https://api.nixflex.com/v1/integrations/webhook2/number/+447700900123 -H "Authorization: Bearer KEY_ID:KEY_SECRET"
# -> { "phone_number": "+447700900123", "webhook_url_2": "https://...", "is_set": true }

# Remove it
curl -X DELETE https://api.nixflex.com/v1/integrations/webhook2/number/+447700900123 -H "Authorization: Bearer KEY_ID:KEY_SECRET"
url is required and must be https://. The primary webhook (set via /webhook/number/) is unaffected — the two URLs are independent. A number with both set delivers to both; with only one set, only that one fires.

Payload structure

The body is a flat JSON object — every field is at the top level (no nested call wrapper). Only a curated, whitelisted set of fields is sent; internal data (your system prompt, API key id, database ids) is never included.
{
  "event": "call.completed",
  "call_id": "CA1b5c0bfad4b56635b773938f16db2db7",
  "agent_id": "agent_f2b06c0e0bd877e9",
  "direction": "inbound",
  "from": "+447386172392",
  "to": "+447446466847",
  "status": "completed",
  "ended_reason": "silence_timeout",
  "started_at": "2026-05-28T02:59:26.296Z",
  "ended_at": "2026-05-28T03:00:36.410Z",
  "duration_seconds": 70,
  "transcript": "Agent: Hello! How can I help?\nCaller: Hi, my name is Amir...",
  "summary": "Amir called inquiring about the business and requested a callback.",
  "sentiment": "neutral",
  "successful": true,
  "extracted": {
    "caller_name": "Amir",
    "wants_callback": true
  },
  "bookings": [
    {
      "action": "created",
      "booking_uid": "scf821a4pcuq3oglf8rfh0tc8k",
      "start": "2026-05-29T09:00:00.000Z",
      "attendee_name": "Amir",
      "attendee_email": "amir@example.com",
      "service": "appointment",
      "timezone": "Europe/London"
    }
  ],
  "recording_url": "https://storage.nixflex.com/recordings/CA1b5c....mp3",
  "recording_duration_seconds": 64,
  "voicemail_detected": false,
  "campaign_id": null,
  "avg_latency_ms": 901
}

Key fields

FieldTypeNotes
eventstringcall.started (call connected) or call.completed (call finished). The dashboard Test button sends webhook.test. See Event types.
call_idstringTwilio call SID. Use this as your idempotency key.
agent_idstringThe agent that handled the call.
directionstringinbound or outbound.
from / tostringCaller’s number / number dialled.
statusstringFinal call status, e.g. completed.
ended_reasonstringWhy the call ended, e.g. caller_hangup, silence_timeout.
started_at / ended_atstringISO 8601 timestamps (UTC).
duration_secondsnumberFull connection time in seconds (ring/connect to hang-up). Used for billing.
transcriptstringPlain text, newline-separated. Each line prefixed Agent: or Caller:.
summary, sentiment, successfulFrom post-call analysis. See Post-call analysis.
extractedobject / nullYour custom extraction fields. null if none configured.
bookingsarrayAppointments created, rescheduled, or cancelled during the call. Empty array if none. See Appointment bookings.
recording_urlstringPublic MP3 link. Populated by the 60s delay (see below).
recording_duration_secondsnumberLength of the recorded conversation in seconds. Use this to show end-users the real talk time.
voicemail_detectedbooleanOutbound only — whether the call reached voicemail.
campaign_idstring / nullSet if the call was part of a batch campaign.
avg_latency_msnumberAverage agent response latency for the call, in milliseconds.
The payload contains caller data — the caller’s phone number (from), the full transcript, and any extracted fields can include personal information. Store and process it in line with your own privacy policy and applicable law.

Appointment bookings

If the agent books, reschedules, or cancels an appointment during a call (via a connected calendar), each action is captured as a structured object in the bookings array. This is the reliable way to know an appointment changed — read it directly instead of parsing the transcript or summary. The array is per call: every booking action taken on that one call is listed, in order. A call with no booking activity sends bookings: [].
"bookings": [
  {
    "action": "created",
    "booking_uid": "scf821a4pcuq3oglf8rfh0tc8k",
    "start": "2026-05-29T09:00:00.000Z",
    "attendee_name": "Amir",
    "attendee_email": "amir@example.com",
    "service": "appointment",
    "timezone": "Europe/London"
  }
]
FieldTypeNotes
actionstringcreated, rescheduled, or cancelled.
booking_uidstringThe calendar’s own reference for the appointment. Stable across reschedule/cancel, so you can match actions to the same appointment.
startstringISO 8601 start time of the appointment. Present for created and rescheduled; omitted for cancelled.
attendee_namestringThe caller’s name as booked. Present when captured (typically on created).
attendee_emailstringThe email the booking is under.
servicestringThe appointment type.
timezonestringIANA timezone the appointment is scheduled in.
Because each action carries the booking_uid, you can mirror these straight into your own appointments table: insert on created, update on rescheduled, mark cancelled on cancelled. Drive your own confirmations and reminders from there.
Bookings are also stored on the call record, so if a webhook delivery is missed you can still fetch them later via the call’s API record — they are never lost.

Event types

The event field tells you which event the payload represents:
  • call.started — fires when a call connects, about 2 seconds after it begins. Use it for live tracking (a call just started on one of your numbers). Only the early fields are populated: event, call_id, agent_id, direction, from, to, status, and started_at. The post-call fields (transcript, summary, sentiment, successful, extracted, recording_url, duration_seconds, ended_at, avg_latency_ms) are null, and bookings is an empty array, because the call has not happened yet.
  • call.completed — fires about 60 seconds after a call ends, with the full payload (transcript, summary, recording, bookings, and analysis).
  • webhook.test — sent by the dashboard “Test” button so you can verify reachability. Not a real call.
Both call.started and call.completed are delivered to the same webhook URL. Switch on the event field to tell them apart. call.started uses the same delivery, retry, and logging as call.completed.

Signing and verification

Every webhook request includes a signature header so you can verify it came from Nixflex:
X-Nixflex-Signature: t=1779500000,v1=abc123...
The signature is HMAC-SHA256 of <timestamp>.<raw_body> using the API key secret (key_secret) tied to the call’s API key as the signing secret. Your key_secret is the part of your API key after the colon. API keys are formatted nxf_<id>:nxfs_<secret>, so the signing secret is the nxfs_... half only (not the full key, and not the nxf_ id).
If signature generation fails on our side, the webhook is still delivered without the X-Nixflex-Signature header (delivery is never blocked). Treat a missing signature header as unsigned and skip verification for that request rather than erroring.
Always verify webhook signatures in production. Without verification, anyone who knows your URL could spam your endpoint with fake data.

Verifying in Node.js

const crypto = require('crypto');

// The signing secret is the part of your API key AFTER the colon (the nxfs_ half).
// If your key is nxf_abc:nxfs_xyz, pass "nxfs_xyz" as the secret.
// e.g. const secret = process.env.NIXFLEX_API_KEY.split(':')[1];

function verifyNixflexSignature(rawBody, signatureHeader, secret) {
  const parts = Object.fromEntries(
    signatureHeader.split(',').map(p => p.split('='))
  );
  const timestamp = parts.t;
  const sig = parts.v1;

  if (Math.abs(Date.now() / 1000 - parseInt(timestamp)) > 300) {
    return false;
  }

  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${timestamp}.${rawBody}`)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(sig),
    Buffer.from(expected)
  );
}
Always use the raw request body bytes when computing the HMAC. If your framework parses JSON before you see it (Express body-parser does this by default), you’ll get a signature mismatch. Use express.raw() middleware on the webhook route.

Delivery and retries

The webhook fires roughly 60 seconds after the call ends — this gives Nixflex time to upload the recording, run post-call analysis, and include everything in one payload. The worker fetches fresh call data at fire time, so recording_url and analysis fields are always current.
1

2xx or 3xx response

Delivered. No retry.
2

4xx (except 408, 429)

Treated as a permanent failure — your endpoint has a bug or rejected the request. No retry.
3

5xx, 408, 429, or timeout/network error

Retried automatically. Backoff between attempts: 5 min, 5 min, 5 min, 60 min (5 attempts total over ~76 min).
4

After 5 failed attempts

Given up. Logged in your dashboard so you can investigate.
Request timeout is 10 seconds per attempt — keep your handler fast.

Viewing delivery history

Go to Logs → Webhooks in your dashboard. Every delivery is logged with status, HTTP code, attempt count, last attempt time, the URL, and any error message. The last 5,000 successful and 5,000 failed deliveries are retained.

Idempotency

The same call may arrive twice if a retry succeeds after the original was actually delivered (rare but possible). Store the call_id from each payload and skip if you’ve already processed it.

Connecting to Zapier / Make

Both platforms support generic webhook triggers. Point them at the URL Zapier/Make gives you, and every call becomes a trigger. Because the payload is flat, fields map cleanly — you’ll see Summary, Transcript, Sentiment, Duration Seconds, etc. directly, with no Call Call ... prefixes. From there, send data to Google Sheets, HubSpot, Salesforce, GoHighLevel, Slack, Notion, Airtable, email, or anything else those platforms support.

Testing your webhook

In the dashboard, go to Integrations → Webhooks, pick the number, and click Test. Nixflex sends a small webhook.test payload to that number’s URL and shows the HTTP status and response time. This only checks that your endpoint is reachable — it does not send a full call payload.