Docs
GuidesAPI referenceWebhooksWeb widget

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

EventSent when
call.completedA call has ended and been summarised and analysed. Browser test calls are not sent.
appointment.createdAn appointment is booked by the AI or by your team.
message.createdThe 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))
}
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. 4xx answers are not retried.
  • Deliveries run in the background and never slow a call down.
  • Use the call or appointment id to ignore a delivery you have already processed.