Webhooks

Set up a webhook, receive your first call event, and build on it

Webhooks push events out of dialnote the moment they happen. When a call ends, a recording finishes processing, someone texts one of your numbers, or a teammate writes a note, dialnote POSTs a JSON payload to a URL you control. No polling, no scheduled sync.

This guide takes you from nothing to a working webhook, then covers what to build on top of it. If you just want the field list, jump to the Event Reference.

What You'll Need#

Two things before you start:

  • A destination URL that accepts a POST over HTTPS and is reachable from the public internet. The next section covers your options.
  • Access to Settings. Webhooks live under Settings → Webhook Management in dialnote.

Choose Your Destination#

Decide this before you create anything, because the provider you pick changes the payload your endpoint receives.

DestinationProvider to selectPick this when
ZapierZapierYou want prebuilt actions for 5,000+ apps and don't want to write code
Make or n8nCustom WebhookYou want a visual builder with branching, and full access to dialnote's payload
Your own serverCustom WebhookYou need the raw payload, signature verification, or custom business logic

In Zapier, Make, or n8n, create a scenario with a webhook trigger and copy the URL it generates. For your own server, deploy an endpoint that accepts a POST and returns 200.

Create Your First Webhook#

With your destination URL in hand, go to Settings → Webhook Management and click Add Webhook. Budget about five minutes.

  1. Name it something you'll recognize later, like "CRM Call Sync" or "Lead Alerts". Up to 100 characters.
  2. Pick the provider you settled on above: Zapier for a hooks.zapier.com URL, or Custom Webhook for Make, n8n, and your own servers.
  3. Paste your webhook URL. It has to start with https://. dialnote won't send events to an HTTP endpoint.
  4. Tick the events you want under Events to send. Start with Call logs, which fires on every completed call. The full list is just below.
  5. Save.
  6. Click Test on the new webhook card. A sample payload goes out immediately, so you can confirm the endpoint is reachable without waiting for a real call.
  7. Place a real call to your dialnote number and hang up. Within seconds the call.completed event lands at your URL.

If step 6 worked but step 7 didn't, check that Call logs is actually ticked. See Troubleshooting.

Event Types#

Events to send is a checkbox list grouped into three categories. Tick any combination you want on a single webhook:

CategoryCheckboxEvent typeWhat fires
CallsCall logscall.completedA call ends, whether answered, missed, or failed
CallsCall recordingscall.recording.completedA recording is ready. Needs recording enabled on the number.
MessagesInbound SMSsms.receivedSomeone texts one of your numbers
MessagesInbound MMSmms.receivedAn inbound message carries photos or other media
NotesNotesnote.createdA teammate adds a note to a conversation (not to a contact)

That's the complete list. One webhook can carry all five if you want, and a webhook with nothing ticked stays saved but receives no events. Field lists for each are in the Event Reference.

A few rules that catch people out on the two message events:

  • sms.received and mms.received are mutually exclusive for any given message. Classification is by whether media is attached, not by Twilio's NumMedia count. An international MMS that arrives as a p.twil.io link in the message body gets unwrapped into real attachments and delivered as mms.received.
  • Inbound only. Messages your team sends don't fire either event.
  • SMS only. Inbound WhatsApp produces neither event.
  • Both default to off, including on webhooks you created before they existed, so nothing you already built started receiving new traffic.

If You Chose Zapier#

Zapier URLs work as soon as you save the webhook. One extra step wires it to your Zap:

  1. Click Show Key in the Zapier Integration section to get your Zapier API key
  2. Use that key in the X-DIALNOTE-API-KEY header when setting up webhook subscriptions in Zapier

Your Zap also receives an X-DialNote-Provider: zapier header, so it can tell dialnote traffic apart from other sources.

Zapier handles parsing and delivery acknowledgement for you, so skip the next section and continue at Delivery and Reliability.

Building Your Endpoint#

This section is for Custom Webhook destinations, including Make and n8n when you want to verify signatures yourself.

dialnote POSTs JSON to your URL. Every event has its own sample payload and field table in the Event Reference: call.completed, call.recording.completed, sms.received, mms.received, and note.created. Since one webhook can carry several events, branch on the envelope's type before you touch data.

Verifying the signature#

Each webhook gets a unique secret, starting with whsec_ and shown beside the webhook in Settings → Webhook Management. dialnote signs every request with HMAC-SHA256 over the exact bytes it sends, and puts the result in the X-DialNote-Signature header as sha256=<hex>.

Always verify that signature before you process a payload. Without the check, anyone who learns your URL can post fake calls into your systems.

A complete receiver#

Putting it together: verify the signature, acknowledge fast, then process. Here's the whole thing in Express.

javascript
const crypto = require('crypto');
const express = require('express');

const app = express();

// Event ids you've already handled. In production this belongs in Redis or a
// table, not memory — a restart would forget everything and reprocess.
const seen = new Set();

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'));

  // Acknowledge BEFORE doing any real work. dialnote gives you 30 seconds and
  // never retries, so a slow database write would cost you the event.
  res.sendStatus(200);

  // Every event carries a unique `id`, so a repeat is safe to drop.
  if (!seen.has(event.id)) {
    seen.add(event.id);
    setImmediate(() => handleEvent(event));
  }
});

Three rules that account for most broken integrations:

  1. Hash the raw request body, not a re-serialized object. Parsing and re-stringifying JSON can reorder keys and change whitespace, which changes the hash.
  2. Strip the sha256= prefix before comparing.
  3. Answer 200 before you process, then do the real work asynchronously. The next section explains why this one matters so much.

Delivery and Reliability#

Read this section before you go live. dialnote's delivery model is deliberately simple, and it puts real constraints on your endpoint.

One attempt, no retry#

A second consequence worth knowing: 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#

Three consecutive failures disable a webhook, so a dead endpoint doesn't get hammered forever. Any successful delivery resets the counter to zero. The usual causes are an endpoint that moved, a server returning non-2xx, or a handler that ran past the 30 second limit.

A 410 Gone response is treated differently: it disables the webhook immediately, on the first response. Zapier returns 410 when a Zap has been deleted, and that's a deliberate unsubscribe rather than a fault.

Failure alerts#

You don't have to watch the settings page for any of this.

  • At the second consecutive failure, one short of the cutoff, you get a warning while it's still fixable. With no retries, the next failed event would be the last one.
  • When a webhook is auto-disabled, you get a second notification.

Alerts go out by email and as an in-app alert, to the organization's owners and admins plus whoever created the webhook. The person who built the integration is often not an admin, so both get told. A 410 Gone shutdown is recorded in-app only, with no email, because nothing is actually broken.

What the webhook card shows#

Each webhook in Settings → Webhook Management carries its own delivery status: Last sent (the most recent success), Failure count (consecutive failures, reset by any success), and Last error (the most recent error message). A disabled webhook also shows an Enable button. Fix the underlying problem before you click it.

What You Can Build#

With delivery working, here are patterns worth copying. Each is a single webhook plus a bit of logic on your side.

  • Text back every missed call. Subscribe to call.completed, branch when data.status is not_picked or failed, and fire an SMS. This is the highest-value automation most teams build first, because it turns a lost call into a live conversation.
  • Log every call to a warehouse or spreadsheet. Write data.callId, direction, status, duration, and fromNumber on each call.completed. You get a queryable call history that outlives any dashboard.
  • Route notes into your ticketing system. Subscribe to note.created, and use data.conversationId to attach the note to the right customer record.
  • Pull recordings for QA review. Subscribe to call.recording.completed and pass data.callId to the API recordings endpoints to fetch audio. The event itself carries a link to the conversation, not the audio file.
  • Alert on long or expensive calls. Branch on data.duration to ping a channel when a call runs past a threshold.
  • Route inbound texts into your helpdesk. Subscribe to sms.received and open a ticket from data.message.body, using data.conversationId to thread replies onto the same conversation.
  • Save photo attachments customers text in. Subscribe to mms.received and download each data.message.attachments[].url to your own storage. Those links are signed and expire, so re-host anything you need to keep.

Troubleshooting#

SymptomLikely causeFix
Test works, real calls don'tThe event you're waiting on isn't tickedOpen the webhook and check Events to send. Test fires regardless of what's selected.
Nothing arrives at allThe webhook is Inactive, or nothing is ticked under Events to sendCheck the card's Active/Inactive badge and click Enable, then confirm at least one event is ticked
Recordings never arriveCall recordings isn't ticked, or recording isn't on for that numberTick it, then check recording is enabled on the number itself
Inbound texts never arriveInbound SMS and Inbound MMS both default to offTick them. A text with media fires mms.received, not sms.received.
Signature check always failsHashing parsed JSON instead of the raw body, or not stripping sha256=See Building Your Endpoint
Events stop after a few daysThree consecutive failures disabled the webhookCheck your email for the failure alert, then fix and re-enable
Deliveries stopped right after a Zap changeA deleted Zap returns 410 Gone, which disables immediatelyRecreate the Zap, then re-enable the webhook
Wrong phone number in your CRMReading data.phoneNumber.numberUse data.phoneNumberId or data.toNumber
Missed calls never triggerThere's no missed-call eventBranch on call.completed with status not_picked or failed
A note you wrote didn't fireIt was a note on a contact, not on a conversationOnly conversation notes send note.created
An MMS arrived as sms.receivedIt didn't actually carry mediaClassification is by attachments present, not by Twilio's NumMedia

Managing Webhooks#

Each webhook card offers Edit (name, URL, or event), Test (send a sample payload), and Delete (permanent, no undo).

Changing the event on an existing webhook is the usual way to repoint an integration, and it takes effect on the next event. Deleting is the only way to change the secret: it can't be regenerated from the app, so delete the webhook and create a new one if you need to rotate it.

Event Reference#

Every field dialnote sends is listed below. "Always" means the field is in the payload on every delivery of that event (it may still be null where the type says so); "Optional" means it's omitted entirely when there's nothing to send, so read it defensively.

Event envelope#

Every custom-webhook event shares the same outer shape:

FieldTypePresenceNotes
idstring (UUID)AlwaysUnique per event. Use it as your idempotency key.
typestringAlwaysWhich event this is, e.g. call.completed
timestampstring (ISO 8601)AlwaysWhen dialnote emitted the event
organizationIdstring (UUID)AlwaysYour dialnote organization
sourcestringOptionaltwilio or system
dataobjectAlwaysThe event body, detailed below

call.completed#

Fires when a call ends, whether it was answered, missed, or failed.

json
{
  "id": "01924f8e-1234-7000-8000-000000000006",
  "type": "call.completed",
  "timestamp": "2026-08-04T10:03: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-1234-7000-8000-000000000002", "number": "+15551234567" },
    "status": "answered",
    "createdAt": "2026-08-04T10:00:00.000Z",
    "duration": 180,
    "hasRecording": true,
    "endedBy": "contact",
    "initiatedAt": "2026-08-04T10:00:00.000Z",
    "answeredAt": "2026-08-04T10:00:05.000Z",
    "endedAt": "2026-08-04T10:03:00.000Z",
    "contactId": "01924f8e-1234-7000-8000-000000000005"
  }
}

Every field in the data object:

FieldTypePresenceNotes
phoneNumberIdstring (UUID)AlwaysYour dialnote line. The reliable way to tell which number took the call.
conversationIdstring (UUID)AlwaysGroups calls, notes, and messages with the same contact
callIdstring (UUID)Alwaysdialnote's call record. Use it against the API recordings endpoints.
callSidstringAlwaysProvider-side call ID (Twilio CA…)
directionstringAlwaysinbound or outbound
fromNumberstring (E.164)AlwaysCalling party
toNumberstring (E.164)AlwaysCalled party. On inbound calls, this is your dialnote line.
phoneNumberobjectAlways{ id, number }. Read id; see the warning above about number.
statusstringAlwaysanswered, not_picked, failed, voicemail, or disconnected
createdAtstring (ISO 8601) or nullAlwaysCan be null
durationinteger (seconds)Optional0 or more. Absent on calls that never connected.
hasRecordingbooleanOptionalWhether a recording exists. Processing finishes in a separate event.
endedBystringOptionaluser, contact, specialist, ai, or system
fromNamestringOptionalDisplay name for the calling party, when dialnote knows it
toNamestringOptionalDisplay name for the called party, when dialnote knows it
initiatedAtstring (ISO 8601) or nullOptionalWhen the call was placed
answeredAtstring (ISO 8601) or nullOptionalOn a call nobody picked up it's either absent or null, so check for both
endedAtstring (ISO 8601) or nullOptionalWhen the call terminated
contactIdstring (UUID)OptionalPresent when the call matched a contact
transcriptionSummarystringOptionalAI summary, only if transcription finished before this event was sent
failureCodestringOptionalFailed calls only
failureMessagestringOptionalFailed calls only
failureProviderstringOptionalFailed calls only
failureDocsUrlstringOptionalFailed calls only

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:

javascript
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.

call.recording.completed#

Fires when a call recording has finished processing. This is a separate webhook from call.completed, so subscribe to it on its own if you want recordings.

json
{
  "id": "01924f8e-1234-7000-8000-000000000007",
  "type": "call.recording.completed",
  "timestamp": "2026-08-04T10:03:45.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-1234-7000-8000-000000000002", "number": "+15551234567" },
    "conversationUrl": "https://app.dialnote.com/conversations/01924f8e-1234-7000-8000-000000000003",
    "recordingSid": "RE00000000000000000000000000000001",
    "duration": 180,
    "transcriptionSummary": "Caller asked about weekend availability and booked Saturday at 10am.",
    "transcriptionStatus": "completed",
    "isVoicemail": false,
    "initiatedAt": "2026-08-04T10:00:00.000Z",
    "endedAt": "2026-08-04T10:03:00.000Z",
    "contactId": "01924f8e-1234-7000-8000-000000000005",
    "tags": ["booking"]
  }
}

Two fields you might expect are not here: this event carries no status (the call is over by definition) and no createdAt. Every field in the data object:

FieldTypePresenceNotes
phoneNumberIdstring (UUID)AlwaysYour dialnote line
conversationIdstring (UUID)AlwaysGroups the recording with the rest of the conversation
callIdstring (UUID)AlwaysPass this to the API recordings endpoints to fetch audio
callSidstringAlwaysProvider-side call ID
directionstringAlwaysinbound or outbound
fromNumberstring (E.164)AlwaysCalling party
toNumberstring (E.164)AlwaysCalled party
phoneNumberobjectAlways{ id, number }
conversationUrlstringAlwaysOpens the conversation in dialnote. Not a downloadable audio file.
recordingSidstringOptionalProvider-side recording ID
durationinteger (seconds)OptionalRecording length
fromNamestringOptionalDisplay name for the calling party
toNamestringOptionalDisplay name for the called party
transcriptionSummarystringOptionalAI summary of the call
transcriptionStatusstringOptionalpending, completed, or failed
initiatedAtstring (ISO 8601) or nullOptionalWhen the call was placed
endedAtstring (ISO 8601) or nullOptionalWhen the call terminated
isVoicemailbooleanOptionalWhether the recording is a voicemail rather than a conversation
contactIdstring (UUID)OptionalPresent when the call matched a contact
tagsarray of stringOptionalCall tags applied by dialnote

note.created#

Fires when a team member adds a note to a conversation.

json
{
  "id": "01924f8e-1234-7000-8000-000000000008",
  "type": "note.created",
  "timestamp": "2026-08-04T10:05:00.000Z",
  "organizationId": "01924f8e-1234-7000-8000-000000000001",
  "data": {
    "phoneNumberId": "01924f8e-1234-7000-8000-000000000002",
    "conversationId": "01924f8e-1234-7000-8000-000000000003",
    "note": {
      "id": "01924f8e-1234-7000-8000-000000000009",
      "content": "Booked for Saturday 10am. Wants a quote on the second unit too.",
      "authorId": "01924f8e-1234-7000-8000-00000000000a",
      "author": {
        "id": "01924f8e-1234-7000-8000-00000000000a",
        "firstName": "Jordan",
        "lastName": "Lee"
      },
      "createdAt": "2026-08-04T10:05:00.000Z"
    },
    "mentionedUserIds": []
  }
}

There are no call fields and no contact fields on this event. Link a note back to a call through conversationId. Every field in the data object:

FieldTypePresenceNotes
phoneNumberIdstring (UUID)AlwaysThe line the conversation belongs to
conversationIdstring (UUID)AlwaysYour join key back to the call
noteobjectAlways{ id, content, authorId, author: { id, firstName, lastName }, createdAt }
mentionedUserIdsarray of UUIDOptionalTeammates @-mentioned in the note

sms.received#

Fires when a text-only SMS arrives on one of your numbers.

json
{
  "id": "019fef0c-5b0f-73cf-a28f-1ab61fbbf971",
  "type": "sms.received",
  "timestamp": "2026-08-11T04:19:52.207Z",
  "organizationId": "019fef0c-5b0f-73cf-a28f-1f6af60dae5f",
  "source": "twilio",
  "data": {
    "phoneNumberId": "019fef0c-5b0f-73cf-a28f-2379b5cbc85b",
    "conversationId": "019fef0c-5b0f-73cf-a28f-26f4590ac49f",
    "fromNumber": "+15551234567",
    "toNumber": "+15559876543",
    "message": {
      "id": "019fef0c-5b0f-73cf-a28f-2922d0daff6a",
      "conversationId": "019fef0c-5b0f-73cf-a28f-2f2cd09a6546",
      "messagingChannelId": "019fef0c-5b0f-73cf-a28f-3103431dfff7",
      "messagingParticipantId": "019fef0c-5b0f-73cf-a28f-365b54ac444e",
      "channel": "SMS",
      "direction": "INBOUND",
      "contentType": "TEXT",
      "body": "Can I move my Saturday appointment to Monday?",
      "status": "DELIVERED",
      "authorType": "EXTERNAL",
      "createdAt": "2026-08-11T04:19:52.207Z"
    },
    "participant": {
      "id": "019fef0c-5b0f-73cf-a28f-3a1d7c2ee9b4",
      "channelAddress": "+15551234567"
    }
  }
}

Every field in the data object:

FieldTypePresenceNotes
phoneNumberIdstring (UUID)AlwaysThe dialnote line that received the message
conversationIdstring (UUID)AlwaysTies the message to the same conversation as calls and notes
fromNumberstringAlwaysSender as the carrier reported it. Not always E.164: short codes (12345) and alphanumeric sender IDs (MyBank) arrive here too, so don't parse it as a phone number.
toNumberstring (E.164)AlwaysYour number that received it
messageobjectAlwaysThe message itself, fields below
participantobjectAlwaysThe external sender, fields below

Inside message:

FieldTypePresenceNotes
idstring (UUID)AlwaysMessage record
conversationIdstring (UUID)AlwaysSame conversation as data.conversationId
messagingChannelIdstring (UUID)AlwaysThe channel that received it
messagingParticipantIdstring (UUID)AlwaysMatches participant.id
channelstringAlwaysAlways SMS
directionstringAlwaysAlways INBOUND
contentTypestringAlwaysAlways TEXT on this event
bodystringAlwaysThe message text. Required here, since a message with no body and no media isn't emitted.
statusstringAlwaysDelivery status as recorded
authorTypestringAlwaysAlways EXTERNAL
createdAtstring (ISO 8601)AlwaysWhen the message was recorded
externalIdstringOptionalProvider-side message ID (Twilio SM…)

Inside participant:

FieldTypePresenceNotes
idstring (UUID)AlwaysThe external party on this channel
channelAddressstringAlwaysTheir address, same caveat as fromNumber
displayNamestringOptionalName from the channel profile or a matched contact
contactIdstring (UUID)OptionalPresent when the sender matched a contact

mms.received#

Fires when an inbound message carries at least one media attachment.

The envelope, participant, and most of message match sms.received. The differences: data.numMedia is added, message.attachments is added, message.body becomes optional (MMS is often media with no caption), and message.contentType reflects the media type rather than always being TEXT.

json
{
  "id": "019fef0c-5b0f-73cf-a28f-41b0c7de5512",
  "type": "mms.received",
  "timestamp": "2026-08-11T04:22:10.412Z",
  "organizationId": "019fef0c-5b0f-73cf-a28f-1f6af60dae5f",
  "source": "twilio",
  "data": {
    "phoneNumberId": "019fef0c-5b0f-73cf-a28f-2379b5cbc85b",
    "conversationId": "019fef0c-5b0f-73cf-a28f-26f4590ac49f",
    "fromNumber": "+15551234567",
    "toNumber": "+15559876543",
    "numMedia": 1,
    "message": {
      "id": "019fef0c-5b0f-73cf-a28f-4693ba01cd77",
      "conversationId": "019fef0c-5b0f-73cf-a28f-2f2cd09a6546",
      "messagingChannelId": "019fef0c-5b0f-73cf-a28f-3103431dfff7",
      "messagingParticipantId": "019fef0c-5b0f-73cf-a28f-365b54ac444e",
      "channel": "SMS",
      "direction": "INBOUND",
      "contentType": "IMAGE",
      "body": "Here's the photo of the unit",
      "status": "DELIVERED",
      "authorType": "EXTERNAL",
      "createdAt": "2026-08-11T04:22:10.412Z",
      "attachments": [
        {
          "type": "IMAGE",
          "url": "https://storage.googleapis.com/bucket/inbound/org/a.jpg?X-Goog-Signature=abc",
          "mimeType": "image/jpeg"
        }
      ]
    },
    "participant": {
      "id": "019fef0c-5b0f-73cf-a28f-3a1d7c2ee9b4",
      "channelAddress": "+15551234567"
    }
  }
}

Fields beyond what sms.received carries:

FieldTypePresenceNotes
numMediaintegerAlwaysAt least 1. dialnote's own count, which can differ from Twilio's NumMedia when links in the body were unwrapped into attachments.
message.attachmentsarrayAlwaysAt least one entry. Each is { type, url, mimeType }, where type is IMAGE, VIDEO, AUDIO, DOCUMENT, or LOCATION.
message.bodystringOptionalOptional here, unlike on sms.received. Media often arrives with no caption.

Zapier payload shape#

This isn't a separate event you can subscribe to. It's the shape your endpoint receives whenever the webhook's provider is set to Zapier, and event_type tells you which of the two call events produced it. There's no note.created equivalent, and none of the dialnote UUIDs (conversationId, callId, phoneNumberId) survive the reshape. Pick Custom Webhook if you need them.

json
{
  "id": "01924f8e-1234-7000-8000-000000000006",
  "event_type": "call_log",
  "contact_id": "01924f8e-1234-7000-8000-000000000005",
  "timestamp": "2026-08-04T10:03:00.000Z",
  "call": {
    "sid": "CA00000000000000000000000000000001",
    "direction": "inbound",
    "phone_number": "+15551234567",
    "from_number": "+15551234567",
    "to_number": "+15559876543",
    "from_name": null,
    "to_name": "Main Line",
    "status": "answered",
    "duration": 180,
    "initiated_at": "2026-08-04T10:00:00.000Z",
    "answered_at": "2026-08-04T10:00:05.000Z",
    "ended_at": "2026-08-04T10:03:00.000Z",
    "recording_url": null,
    "transcription_summary": null,
    "transcription_status": null
  }
}
FieldTypePresenceNotes
idstringAlwaysEvent ID
event_typestringAlwayscall_log (from call.completed) or call_recording (from call.recording.completed)
contact_idstringAlwaysContact the call was matched to
timestampstring (ISO 8601)AlwaysWhen the event was emitted
callobjectAlwaysCall details, fields below

Inside call, every field is always present, though many can be null:

FieldTypeNotes
sidstringProvider-side call ID
directionstringinbound or outbound
phone_numberstringSee the identifying-your-number warning above
from_numberstringCalling party
to_numberstringCalled party
from_namestring or nullDisplay name for the calling party
to_namestring or nullDisplay name for the called party
statusstringSame values as data.status on call.completed
durationinteger (seconds)0 or more
initiated_atstring (ISO 8601)When the call was placed
answered_atstring (ISO 8601) or nullnull when nobody picked up
ended_atstring (ISO 8601) or nullWhen the call terminated
recording_urlstring or nullPopulated on call_recording events
transcription_summarystring or nullAI summary of the call
transcription_statusstring or nullpending, completed, or failed

To try these against live data, or to generate types from the raw schemas, open the Outbound Webhooks section of the API reference. It's built from the same schemas the delivery code uses, so it never drifts from the tables above.

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
  • API Access: pull data on your own schedule, and fetch recording audio by callId

We use cookies for analytics, ads, and to remember your preferences. Privacy Policy