Webhooks allow you to receive real-time HTTP notifications when call events occur in your application, such as incoming calls, call completions, recordings, and voicemails.
For general webhook concepts, see Webhooks .
Creating a Webhook
Subscribe to calling events by creating a webhook for your application.
Via Dashboard
Navigate to your application’s Webhooks page
Click “Create Webhook”
Enter your webhook URL
Select the calling events you want to receive
(Optional) Add custom headers for authentication
Save the webhook
Via API
Use the POST /v1/webhooks endpoint with calling event types:
curl -X POST https://api.buildwithchirp.com/v1/webhooks \
-H "Authorization: Bearer YOUR_APP_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.com/webhooks/chirp",
"events": [
"calls.initiated",
"calls.answered",
"calls.completed",
"calls.failed",
"calls.recording.completed",
"calls.voicemail.received"
],
"headers": {
"X-Webhook-Secret": "your-secret-key"
}
}'
Webhook Payload Structure
Every calling webhook includes these top-level fields:
Field Type Description eventstring The event type (e.g., calls.completed) eventIdstring Unique event identifier for idempotency timestampstring ISO 8601 timestamp of when the event was generated dataobject Event-specific data including call and app objects
Call Data
The data.call object is present in all calling webhooks and includes:
Field Type Description idstring Unique call identifier (e.g., call_2DbBs7GWhGvVNJGrDXr5RG0mBWI) directionstring inbound or outboundfromstring Phone number or identifier that initiated the call tostring Phone number or identifier that received the call fromChannelstring Channel of the originator: pstn, whatsapp, or webrtc toChannelstring Channel of the recipient: pstn, whatsapp, or webrtc livekitRoomNamestring LiveKit room name (present when channel is webrtc) metadataobject Custom metadata attached to the call (if any)
Call Lifecycle Events
These events track the progress of a call from initiation to completion.
calls.initiated
Fired when a call is created (outbound) or an incoming call is received (inbound).
{
"event" : "calls.initiated" ,
"eventId" : "evt_2DbBs7GWhGvVNJGrDXr5RG0mBWI" ,
"timestamp" : "2024-01-15T10:30:00.000Z" ,
"data" : {
"call" : {
"id" : "call_2DbBs7GWhGvVNJGrDXr5RG0mBWI" ,
"direction" : "outbound" ,
"from" : "+15551234567" ,
"to" : "+15559876543" ,
"fromChannel" : "pstn" ,
"toChannel" : "pstn"
},
"app" : {
"id" : "app_2DbBs7GWhGvVNJGrDXr5RG0mBWI"
}
}
}
calls.ringing
Fired when an outbound call is ringing at the destination.
{
"event" : "calls.ringing" ,
"eventId" : "evt_3EcCt8HXiHwWOKHsEYs6SH1nCXJ" ,
"timestamp" : "2024-01-15T10:30:02.000Z" ,
"data" : {
"call" : {
"id" : "call_2DbBs7GWhGvVNJGrDXr5RG0mBWI" ,
"direction" : "outbound" ,
"from" : "+15551234567" ,
"to" : "+15559876543" ,
"fromChannel" : "pstn" ,
"toChannel" : "pstn"
},
"app" : {
"id" : "app_2DbBs7GWhGvVNJGrDXr5RG0mBWI"
}
}
}
calls.answered
Fired when the call is answered. Includes answeredAt and answeredBy fields.
{
"event" : "calls.answered" ,
"eventId" : "evt_4FdDu9IYjIxXPLItFZt7TI2oDYK" ,
"timestamp" : "2024-01-15T10:30:05.000Z" ,
"data" : {
"call" : {
"id" : "call_2DbBs7GWhGvVNJGrDXr5RG0mBWI" ,
"direction" : "outbound" ,
"from" : "+15551234567" ,
"to" : "+15559876543" ,
"fromChannel" : "pstn" ,
"toChannel" : "pstn" ,
"answeredAt" : "2024-01-15T10:30:05.000Z" ,
"answeredBy" : "+15559876543"
},
"app" : {
"id" : "app_2DbBs7GWhGvVNJGrDXr5RG0mBWI"
}
}
}
calls.completed
Fired when a call ends normally. Includes duration, endReason, and endedAt fields.
{
"event" : "calls.completed" ,
"eventId" : "evt_5GeEv0JZkJyYQMJuGAu8UJ3pEZL" ,
"timestamp" : "2024-01-15T10:32:00.000Z" ,
"data" : {
"call" : {
"id" : "call_2DbBs7GWhGvVNJGrDXr5RG0mBWI" ,
"direction" : "outbound" ,
"from" : "+15551234567" ,
"to" : "+15559876543" ,
"fromChannel" : "pstn" ,
"toChannel" : "pstn" ,
"duration" : 120 ,
"endReason" : "hangup" ,
"endedAt" : "2024-01-15T10:32:00.000Z"
},
"app" : {
"id" : "app_2DbBs7GWhGvVNJGrDXr5RG0mBWI"
}
}
}
calls.failed
Fired when a call fails. Includes an error object with details about the failure.
{
"event" : "calls.failed" ,
"eventId" : "evt_6HfFw1KAlKzZRNKvHBv9VK4qFAM" ,
"timestamp" : "2024-01-15T10:30:03.000Z" ,
"data" : {
"call" : {
"id" : "call_2DbBs7GWhGvVNJGrDXr5RG0mBWI" ,
"direction" : "outbound" ,
"from" : "+15551234567" ,
"to" : "+15559876543" ,
"fromChannel" : "pstn" ,
"toChannel" : "pstn"
},
"app" : {
"id" : "app_2DbBs7GWhGvVNJGrDXr5RG0mBWI"
},
"error" : {
"type" : "provider_error" ,
"code" : "call_failed" ,
"message" : "The call could not be connected. The destination number may be unreachable."
}
}
}
calls.busy
Fired when the destination is busy.
{
"event" : "calls.busy" ,
"eventId" : "evt_7IgGx2LBmLAASONwICw0WL5rGBN" ,
"timestamp" : "2024-01-15T10:30:04.000Z" ,
"data" : {
"call" : {
"id" : "call_2DbBs7GWhGvVNJGrDXr5RG0mBWI" ,
"direction" : "outbound" ,
"from" : "+15551234567" ,
"to" : "+15559876543" ,
"fromChannel" : "pstn" ,
"toChannel" : "pstn"
},
"app" : {
"id" : "app_2DbBs7GWhGvVNJGrDXr5RG0mBWI"
}
}
}
calls.no_answer
Fired when no one answers the call.
{
"event" : "calls.no_answer" ,
"eventId" : "evt_8JhHy3MCnMBBTPOxJDx1XM6sHCO" ,
"timestamp" : "2024-01-15T10:30:30.000Z" ,
"data" : {
"call" : {
"id" : "call_2DbBs7GWhGvVNJGrDXr5RG0mBWI" ,
"direction" : "outbound" ,
"from" : "+15551234567" ,
"to" : "+15559876543" ,
"fromChannel" : "pstn" ,
"toChannel" : "pstn"
},
"app" : {
"id" : "app_2DbBs7GWhGvVNJGrDXr5RG0mBWI"
}
}
}
calls.canceled
Fired when a call is canceled before it connects.
{
"event" : "calls.canceled" ,
"eventId" : "evt_9KiIz4NDoPCCUQPyKEy2YN7tIDP" ,
"timestamp" : "2024-01-15T10:30:03.000Z" ,
"data" : {
"call" : {
"id" : "call_2DbBs7GWhGvVNJGrDXr5RG0mBWI" ,
"direction" : "outbound" ,
"from" : "+15551234567" ,
"to" : "+15559876543" ,
"fromChannel" : "pstn" ,
"toChannel" : "pstn"
},
"app" : {
"id" : "app_2DbBs7GWhGvVNJGrDXr5RG0mBWI"
}
}
}
Participant Events
These events track participants joining and leaving a call.
calls.participant_joined
Fired when a participant joins the call.
calls.participant_joined payload
{
"event" : "calls.participant_joined" ,
"eventId" : "evt_0LjJA5OEpQDDVRQzLFz3ZO8uJEQ" ,
"timestamp" : "2024-01-15T10:30:05.000Z" ,
"data" : {
"call" : {
"id" : "call_2DbBs7GWhGvVNJGrDXr5RG0mBWI" ,
"direction" : "outbound" ,
"from" : "+15551234567" ,
"to" : "+15559876543" ,
"fromChannel" : "pstn" ,
"toChannel" : "pstn"
},
"participant" : {
"id" : "part_abc123" ,
"type" : "phone" ,
"displayName" : "John Doe" ,
"phoneNumber" : "+15559876543" ,
"joinedAt" : "2024-01-15T10:30:05.000Z"
},
"app" : {
"id" : "app_2DbBs7GWhGvVNJGrDXr5RG0mBWI"
}
}
}
calls.participant_left
Fired when a participant leaves the call. Includes the participant’s duration and leftAt timestamp.
calls.participant_left payload
{
"event" : "calls.participant_left" ,
"eventId" : "evt_1MkKB6PFqREEWSRAMGA4AP9vKFR" ,
"timestamp" : "2024-01-15T10:32:00.000Z" ,
"data" : {
"call" : {
"id" : "call_2DbBs7GWhGvVNJGrDXr5RG0mBWI" ,
"direction" : "outbound" ,
"from" : "+15551234567" ,
"to" : "+15559876543" ,
"fromChannel" : "pstn" ,
"toChannel" : "pstn"
},
"participant" : {
"id" : "part_abc123" ,
"type" : "phone" ,
"displayName" : "John Doe" ,
"phoneNumber" : "+15559876543" ,
"joinedAt" : "2024-01-15T10:30:05.000Z" ,
"duration" : 115 ,
"leftAt" : "2024-01-15T10:32:00.000Z"
},
"app" : {
"id" : "app_2DbBs7GWhGvVNJGrDXr5RG0mBWI"
}
}
}
Recording and Voicemail Events
calls.recording.completed
Fired when a call recording has finished processing and is available for download.
calls.recording.completed payload
{
"event" : "calls.recording.completed" ,
"eventId" : "evt_2NlLC7QGrSFFXTSBNHB5BQ0wLGS" ,
"timestamp" : "2024-01-15T10:33:00.000Z" ,
"data" : {
"call" : {
"id" : "call_2DbBs7GWhGvVNJGrDXr5RG0mBWI" ,
"direction" : "outbound" ,
"from" : "+15551234567" ,
"to" : "+15559876543" ,
"fromChannel" : "pstn" ,
"toChannel" : "pstn"
},
"recording" : {
"id" : "call_rec_2DbBs7GWhGvVNJGrDXr5RG0mBWI" ,
"duration" : 120 ,
"url" : "https://storage.buildwithchirp.com/recordings/call_rec_2DbBs7GWhGvVNJGrDXr5RG0mBWI.mp4?signed=..." ,
"format" : "mp4" ,
"size" : 1048576
},
"app" : {
"id" : "app_2DbBs7GWhGvVNJGrDXr5RG0mBWI"
}
}
}
calls.voicemail.received
Fired when a voicemail is left. Includes the voicemail audio URL and transcription (when available).
calls.voicemail.received payload
{
"event" : "calls.voicemail.received" ,
"eventId" : "evt_3OmMD8RHsTGGYUSCOIC6CR1xMHT" ,
"timestamp" : "2024-01-15T10:31:30.000Z" ,
"data" : {
"call" : {
"id" : "call_2DbBs7GWhGvVNJGrDXr5RG0mBWI" ,
"direction" : "inbound" ,
"from" : "+15559876543" ,
"to" : "+15551234567" ,
"fromChannel" : "pstn" ,
"toChannel" : "pstn"
},
"voicemail" : {
"id" : "call_vm_2DbBs7GWhGvVNJGrDXr5RG0mBWI" ,
"duration" : 30 ,
"url" : "https://storage.buildwithchirp.com/voicemails/call_vm_2DbBs7GWhGvVNJGrDXr5RG0mBWI.mp4?signed=..." ,
"transcription" : "Hi, please call me back when you get a chance."
},
"app" : {
"id" : "app_2DbBs7GWhGvVNJGrDXr5RG0mBWI"
}
}
}
The transcription field may be null if transcription is not enabled or still processing. You can retrieve the transcription later using the voicemail API endpoints .
The url fields in recording and voicemail webhook payloads are time-limited signed URLs. If you need to access the file later, use the Recordings API to generate a fresh URL.
Event Summary
Event Description calls.initiatedCall created (outbound) or incoming call received (inbound) calls.ringingOutbound call is ringing at the destination calls.answeredCall was answered calls.completedCall ended normally calls.failedCall failed with an error calls.busyDestination was busy calls.no_answerNo one answered the call calls.canceledCall was canceled before connecting calls.participant_joinedA participant joined the call calls.participant_leftA participant left the call calls.recording.completedRecording finished processing and is available calls.voicemail.receivedA voicemail was left
Handling Calling Webhooks
Here is an example of how to handle calling webhooks in your application:
app . post ( "/webhooks/chirp" , ( req , res ) => {
// Acknowledge immediately
res . status ( 200 ). send ( "OK" );
const { event , eventId , data } = req . body ;
switch ( event ) {
case "calls.initiated" :
console . log ( `Call ${ data . call . id } initiated ( ${ data . call . direction } )` );
break ;
case "calls.completed" :
console . log ( `Call ${ data . call . id } completed after ${ data . call . duration } s` );
break ;
case "calls.failed" :
console . error ( `Call ${ data . call . id } failed: ${ data . error . message } ` );
break ;
case "calls.recording.completed" :
console . log ( `Recording available: ${ data . recording . url } ` );
break ;
case "calls.voicemail.received" :
console . log ( `Voicemail from ${ data . call . from } : ${ data . voicemail . transcription ?? "(transcription pending)" } ` );
break ;
default :
console . log ( `Unhandled event: ${ event } ` );
}
});
Use the eventId field to implement idempotency. Track processed event IDs to prevent handling the same event twice if it is delivered more than once.