Setting a webhook
Set the webhook URL on one of your numbers: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:
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:
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 nestedcall wrapper). Only a curated, whitelisted set of fields is sent; internal data (your system prompt, API key id, database ids) is never included.
Key fields
| Field | Type | Notes |
|---|---|---|
event | string | call.started (call connected) or call.completed (call finished). The dashboard Test button sends webhook.test. See Event types. |
call_id | string | Twilio call SID. Use this as your idempotency key. |
agent_id | string | The agent that handled the call. |
direction | string | inbound or outbound. |
from / to | string | Caller’s number / number dialled. |
status | string | Final call status, e.g. completed. |
ended_reason | string | Why the call ended, e.g. caller_hangup, silence_timeout. |
started_at / ended_at | string | ISO 8601 timestamps (UTC). |
duration_seconds | number | Full connection time in seconds (ring/connect to hang-up). Used for billing. |
transcript | string | Plain text, newline-separated. Each line prefixed Agent: or Caller:. |
summary, sentiment, successful | — | From post-call analysis. See Post-call analysis. |
extracted | object / null | Your custom extraction fields. null if none configured. |
bookings | array | Appointments created, rescheduled, or cancelled during the call. Empty array if none. See Appointment bookings. |
recording_url | string | Public MP3 link. Populated by the 60s delay (see below). |
recording_duration_seconds | number | Length of the recorded conversation in seconds. Use this to show end-users the real talk time. |
voicemail_detected | boolean | Outbound only — whether the call reached voicemail. |
campaign_id | string / null | Set if the call was part of a batch campaign. |
avg_latency_ms | number | Average agent response latency for the call, in milliseconds. |
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 thebookings 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: [].
| Field | Type | Notes |
|---|---|---|
action | string | created, rescheduled, or cancelled. |
booking_uid | string | The calendar’s own reference for the appointment. Stable across reschedule/cancel, so you can match actions to the same appointment. |
start | string | ISO 8601 start time of the appointment. Present for created and rescheduled; omitted for cancelled. |
attendee_name | string | The caller’s name as booked. Present when captured (typically on created). |
attendee_email | string | The email the booking is under. |
service | string | The appointment type. |
timezone | string | IANA timezone the appointment is scheduled in. |
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
Theevent 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, andstarted_at. The post-call fields (transcript,summary,sentiment,successful,extracted,recording_url,duration_seconds,ended_at,avg_latency_ms) arenull, andbookingsis 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.
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:<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.Verifying in Node.js
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, sorecording_url and analysis fields are always current.
4xx (except 408, 429)
Treated as a permanent failure — your endpoint has a bug or rejected the request. No retry.
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).
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 thecall_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 seeSummary, 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 smallwebhook.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.