Developers
Webhooks
Act on results as they happen. Get a signed POST when a call ends, an appointment is booked or a message is taken.
#Events
| Event | Sent when |
|---|---|
call.completed | A call has ended and been summarised and analysed. Browser test calls are not sent. |
appointment.created | An appointment is booked by the AI or by your team. |
message.created | The AI takes a message. |
Add webhook URLs under Settings → Webhooks & calendar and choose events for each. Each webhook shows the status and time of its last delivery.
#Payloads
Every delivery has the same envelope, with the event's data inside:
call.completed
{
"event": "call.completed",
"business_id": 1,
"sent_at": "2026-10-06T14:32:08.512Z",
"data": {
"id": 8812,
"assistant_id": 12,
"from": "+15555550142",
"to": "+15555550100",
"direction": "inbound",
"caller_name": "Maya Chen",
"started_at": "2026-10-06T14:28:51Z",
"duration_s": 187,
"outcome": "booked",
"ended_reason": "customer_hung_up",
"summary": "Maya, a new patient, booked a cleaning for Thursday at 2:30 PM.",
"intent": "book a cleaning",
"analysis": { "success": 9, "success_reason": "Booked on the first try", "scale": "score_10",
"data": { "new_patient": true } },
"link": "https://app.bluepenguin.io/calls/8812"
}
}appointment.created data
{ "id": 431, "title": "Cleaning · Maya Chen", "start": "2026-10-08T14:30:00-04:00",
"duration_minutes": 45, "contact_id": 977, "call_id": 8812,
"service": "Cleaning", "created_by": "ai" }message.created data
{ "call_id": 8813, "caller_name": "Tom", "caller_phone": "+15555550199",
"message": "Wants a quote for a new water heater, prefers mornings.", "urgent": false }#Verify signatures
Every delivery has an X-BluePenguin-Signature header: sha256= followed by the hex HMAC-SHA256 of the raw request body, keyed with the webhook's secret. Compute it over the exact bytes you received, before parsing the JSON.
import crypto from 'node:crypto'
export function verify(rawBody, header, secret) {
const want = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex')
return !!header && header.length === want.length &&
crypto.timingSafeEqual(Buffer.from(header), Buffer.from(want))
}import hashlib, hmac
def verify(raw_body: bytes, header: str, secret: str) -> bool:
want = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(header or "", want)Express receiver
import express from 'express'
const app = express()
app.post('/bluepenguin', express.raw({ type: 'application/json' }), (req, res) => {
if (!verify(req.body, req.get('x-bluepenguin-signature'), process.env.BP_WEBHOOK_SECRET)) {
return res.sendStatus(401)
}
const event = JSON.parse(req.body)
if (event.event === 'call.completed') console.log(event.data.summary)
res.sendStatus(204)
})#Delivery
- URLs must use https and resolve to a public address.
- Answer with any
2xx. After a server error or network failure the delivery is tried again with growing delays, up to three attempts in total.4xxanswers are not retried. - Deliveries run in the background and never slow a call down.
- Use the call or appointment
idto ignore a delivery you have already processed.