Webhooks let you send call events from dialnote to your own systems or third-party services in real-time. When a call ends or a recording completes, dialnote can instantly notify your server, CRM, or automation platform.
You can set up webhooks to Zapier or any custom HTTPS endpoint. This opens up possibilities like syncing call data to your database, triggering follow-up workflows, or updating external systems automatically. Prefer a no-code tool? Make and n8n both accept dialnote webhooks too. Webhooks push events out of dialnote; to pull data in from your own code, see API Access, or connect an AI assistant over MCP.
Setting Up a Webhook#
Go to Settings → Webhook Management to manage your webhook configurations. Click Add Webhook to create a new one.
Each webhook needs:
- Name: A descriptive label, up to 100 characters (e.g., "CRM Call Sync" or "Lead Alerts")
- Provider: Choose Zapier or Custom Webhook
- Webhook URL: The HTTPS endpoint where dialnote sends events
- Event Settings: Which event triggers the webhook

HTTPS Required
Webhook URLs must use HTTPS. dialnote won't send events to HTTP endpoints for security reasons.
Event Types#
Each webhook sends one event type. Under Event Settings, pick the option you want:
| Option (in app) | Event type | What fires |
|---|---|---|
| Send call logs | call.completed | A call ends — answered, missed, or failed |
| Send call recordings | call.recording.completed | A call recording has finished processing |
| Send notes | note.created | A team member adds a note to a conversation |
| Disabled | — | Nothing is sent (handy for pausing a webhook without deleting it) |
Because each webhook carries a single event, create one webhook per event type if you need calls, recordings, and notes going to the same or different destinations.
Events fire after the fact, not while a call is ringing
call.completed fires when a call ends. There is no pre-answer or ringing webhook, so webhooks can't drive a screen-pop while the phone is still ringing.
One event per webhook
The event picker is a single choice, not a set of checkboxes. Switching from "Send call logs" to "Send call recordings" replaces the event — it doesn't add a second one.
Zapier Integration#
If you're using Zapier, dialnote provides built-in support. The Zapier webhook URLs (starting with hooks.zapier.com) work automatically.
To connect with Zapier:
- Create a new webhook in dialnote with provider set to Zapier
- Get your Zapier API key by clicking Show Key in the Zapier Integration section
- Use this API key in the
X-DIALNOTE-API-KEYheader when setting up webhook subscriptions in Zapier
Zapier-provider webhooks also carry an X-DialNote-Provider: zapier header so your Zap can tell dialnote traffic apart from other sources.
Zapier API key
The Zapier API key is unique to your organization. Keep it secure and don't share it publicly. For the full Zap setup walkthrough, see the Zapier integration guide.
Custom Webhooks#
For custom integrations, set the provider to Custom Webhook and enter your server's endpoint URL. dialnote sends JSON payloads with the event data.
Webhook Security#
Each webhook gets a unique secret (starting with whsec_). dialnote includes an HMAC signature in the X-DialNote-Signature header with every request.
To verify webhook authenticity on your server:
const crypto = require('crypto');
function verifySignature(rawBody, header, secret) {
if (!header?.startsWith('sha256=')) return false;
const received = Buffer.from(header.slice('sha256='.length), 'hex');
const expected = crypto.createHmac('sha256', secret).update(rawBody).digest();
// timingSafeEqual throws when lengths differ — check first.
return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}
// express.raw() preserves the exact bytes we signed. express.json() would parse
// them away, and re-serializing can produce a different string.
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const header = req.get('X-DialNote-Signature');
if (!verifySignature(req.body, header, process.env.DIALNOTE_WEBHOOK_SECRET)) {
return res.sendStatus(401);
}
const event = JSON.parse(req.body.toString('utf8'));
res.sendStatus(200); // acknowledge first, process afterwards
});
Two things matter here: hash the raw request body, not a re-serialized object, and strip the sha256= prefix before comparing.
Always verify signatures before processing webhook payloads in production.
No replay protection
The signature has no timestamp or nonce, so it stays valid indefinitely. Rely on TLS, and use the event id as an idempotency key.
Payload Format#
Zapier webhooks receive a different shape
Everything in this section describes Custom Webhook payloads. Webhooks with the provider set to Zapier receive a reshaped body — { id, event_type, contact_id, timestamp, call } — because that's what the Zapier app parses. That shape has no conversationId, callId or phoneNumberId. If you need those identifiers, use Custom Webhook.
A call.completed payload:
{
"id": "01924f8e-1234-7000-8000-000000000006",
"type": "call.completed",
"timestamp": "2026-08-04T10:00:00.000Z",
"organizationId": "01924f8e-1234-7000-8000-000000000001",
"source": "twilio",
"data": {
"phoneNumberId": "01924f8e-1234-7000-8000-000000000002",
"conversationId": "01924f8e-1234-7000-8000-000000000003",
"callId": "01924f8e-1234-7000-8000-000000000004",
"callSid": "CA00000000000000000000000000000001",
"direction": "inbound",
"fromNumber": "+15551234567",
"toNumber": "+15559876543",
"phoneNumber": { "id": "01924f8e-…-0002", "number": "+15551234567" },
"status": "answered",
"createdAt": "2026-08-04T10:00:00.000Z",
"duration": 180
}
}
Always present on call.completed: phoneNumberId, conversationId, callId, callSid, direction, fromNumber, toNumber, phoneNumber, status and createdAt. Optional and omitted when unavailable: duration, hasRecording, endedBy, fromName, toName, initiatedAt, answeredAt, endedAt, contactId, transcriptionSummary, and — when the call failed — failureCode, failureMessage, failureProvider and failureDocsUrl.
call.recording.completed carries the same call fields except status, which is absent because the call is finished by definition, plus a required conversationUrl. That URL opens the conversation in dialnote; it is not a downloadable audio file.
note.created carries phoneNumberId, conversationId and a note object — no call fields. Link it to a call through conversationId.
For the complete field-by-field reference, including types and every optional field, see the webhooks section of the API reference, which is generated from the same schemas the delivery code uses.
Identifying your dialnote number
Use data.phoneNumberId, or data.toNumber on inbound calls. Don't use data.phoneNumber.number — on inbound calls it currently holds the caller's number, not your dialnote line, even though data.phoneNumber.id is your line. This is a known inconsistency.
Call Status Values#
data.status is one of answered, not_picked, failed, voicemail or disconnected.
There is no separate missed-call event. A missed call arrives as call.completed with a status of not_picked or failed:
const MISSED = new Set(['not_picked', 'failed']);
if (event.type === 'call.completed' && MISSED.has(event.data.status)) {
// handle the missed call
}
Raw provider statuses like no-answer, busy, completed and canceled are translated before the event is sent and never appear in data.status.
Testing and Troubleshooting#
Click the Test button on any active webhook to send a sample payload. This helps verify your endpoint is reachable and properly configured.
Test payload shape
The test payload is a fixed sample with event_type set to test.webhook and a test: true flag. Its shape differs slightly from real event payloads, so use it to confirm connectivity — not to model your parsing logic. Build your parser against the real payload format shown above.
Delivery Guarantee#
Events are sent once — there is no retry
dialnote attempts each delivery exactly once. If your endpoint is unreachable, returns a non-2xx status, or takes longer than 30 seconds, that event is lost — it is not queued or redelivered later.
Design your endpoint to acknowledge first and process afterwards: return 200 as soon as you've durably stored the payload, then do the real work asynchronously. That way a slow downstream system never costs you an event.
A related consequence: if a call's status is corrected after the fact — recorded as not_picked, then later promoted to answered — the event is not re-sent, so a status you already received can become stale.
Auto-Disable on Failures#
dialnote tracks delivery failures for each webhook. If a webhook fails 3 consecutive times, it's automatically disabled to prevent repeated failed requests.
When a webhook is disabled due to failures:
- The card shows the failure count and last error message
- An Enable button appears to reactivate it
- Fix the underlying issue before re-enabling
Common failure reasons:
- Endpoint URL changed or is unreachable
- Server returning non-2xx status codes
- Request timeout (30 second limit)
Monitoring Deliveries#
Each webhook card shows:
- Last sent: When the last successful delivery occurred
- Failure count: How many consecutive failures (resets on success)
- Last error: The most recent error message if any
Timeout limit
Your endpoint must respond within 30 seconds. For long-running processes, acknowledge the webhook quickly and process asynchronously.
Managing Webhooks#
From the webhook management page you can:
- Edit: Update the name, URL, or event settings
- Test: Send a test payload to verify connectivity
- Delete: Remove a webhook permanently (can't be undone)
- Enable: Reactivate a webhook that was auto-disabled
The webhook secret can't be changed from the app after creation. If you need a new secret, delete the webhook and create a new one.
What's Next#
- Zapier integration — connect calls and recordings to 5,000+ apps without writing code
- Make and n8n — build visual automations on top of dialnote webhooks
- CRM integrations — push call activity straight into HubSpot, Salesforce, or Pipedrive without a custom endpoint