Webhooks
Webhooks allow you to receive real-time notifications about events happening in your Busy Board account. We send an HTTP POST request to your endpoint whenever a subscribed event occurs.
Verification & Security
To ensure that the webhook request came from Busy Board and hasn't been tampered with, we include a signature in the X-Busy-Signature header. This is a SHA-256 HMAC hash of the request payload, signed with your webhook's secret.
const crypto = require('crypto');
const secret = 'whsec_...';
const signature = req.headers['x-busy-signature'];
const payload = JSON.stringify(req.body);
const expectedSignature = crypto.createHmac('sha256', secret)
.update(payload).digest('hex');
if (signature === expectedSignature) { /* Verified */ }
$secret = 'whsec_...';
$signature = $_SERVER['HTTP_X_BUSY_SIGNATURE'];
$payload = file_get_contents('php://input');
$expectedSignature = hash_hmac('sha256', $payload, $secret);
if (hash_equals($signature, $expectedSignature)) { /* Verified */ }
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.codec.binary.Hex;
String secret = "whsec_...";
String signature = request.getHeader("X-Busy-Signature");
String payload = getBody(request);
Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
SecretKeySpec secret_key = new SecretKeySpec(secret.getBytes(), "HmacSHA256");
sha256_HMAC.init(secret_key);
String expected = Hex.encodeHexString(sha256_HMAC.doFinal(payload.getBytes()));
if (signature.equals(expected)) { /* Verified */ }
using System.Security.Cryptography;
using System.Text;
string secret = "whsec_...";
string signature = Request.Headers["X-Busy-Signature"];
string payload = await new StreamReader(Request.Body).ReadToEndAsync();
using (var hmacsha256 = new HMACSHA256(Encoding.UTF8.GetBytes(secret))) {
byte[] hash = hmacsha256.ComputeHash(Encoding.UTF8.GetBytes(payload));
string expected = BitConverter.ToString(hash).Replace("-", "").ToLower();
if (signature == expected) { /* Verified */ }
}
import ("crypto/hmac"; "crypto/sha256"; "encoding/hex"; "io/ioutil")
secret := "whsec_..."
signature := r.Header.Get("X-Busy-Signature")
payload, _ := ioutil.ReadAll(r.Body)
h := hmac.New(sha256.New, []byte(secret))
h.Write(payload)
expected := hex.EncodeToString(h.Sum(nil))
if hmac.Equal([]byte(signature), []byte(expected)) { /* Verified */ }
The Standard Wrapper
Every webhook notification is wrapped in a consistent JSON object. Your integration should first parse these top-level attributes.
| Attribute | Type | Description |
|---|---|---|
| organisationId | Integer | The unique ID of your organization. Useful for multi-tenant integrations. |
| eventType | String | The type of event (e.g., call.hangup). |
| notificationId | String | A unique UUID for this specific notification. Crucial for idempotency. |
| timestamp | ISO 8601 | When this notification was generated. |
| data | Object | The actual event payload. See details below. |
Event Types & Context
Incoming Call (Ringing)
Triggered the moment our gateway receives a call destined for your account. This is a Pre-Answer event, meaning the caller is currently hearing a ringtone.
{
"organisationId": 1,
"eventType": "call.incoming",
"data": {
"callSessionId": "789-abc-123",
"caller": "+2711000222",
"callee": "+2782000111",
"status": "RINGING"
}
}
Important Fields
-
callSessionId
Global unique ID for this specific call attempt. Use this to track the call across
acceptedandhangupevents. -
caller / callee
Phone numbers in E.164 format.
calleris the originator,calleeis the destination.
Note: This event is perfect for looking up customer records in your CRM before the agent answers.
Call Accepted (Answered)
Triggered when the call connects. This could be a human agent answering, or an AI bot picking up the line.
{
"organisationId": 1,
"eventType": "call.accepted",
"data": {
"callSessionId": "789-abc-123",
"status": "ACCEPTED",
"timestamp": "2026-01-23T09:27:00.000Z"
}
}
Payload Context
- status: ACCEPTED Indicates the audio stream is now active. Billing starts exactly at this timestamp.
- timestamp The precise ISO 8601 moment of the "Off-Hook" signal.
Call Hangup
Triggered when any party disconnects. This is the final event for traditional calls and contains billing-ready duration data.
{
"organisationId": 1,
"eventType": "call.hangup",
"data": {
"callSessionId": "789-abc-123",
"duration": 145,
"status": 200,
"reason": "OK",
"callId": 1050
}
}
Fields to Watch
- duration The total billable duration in seconds.
-
status / reason
SIP response codes.
200 / OKis a normal disconnect.486 / Busyor480 / No Answersignify failed attempts. - callId The internal database ID of the call record in Busy Board.
Call Transfer initiated
Triggered when the AI agent decides to hand over the call to a human sip terminal or mobile number.
{
"organisationId": 1,
"eventType": "call.transfer",
"data": {
"referTo": "sip:+2782555000@gateway",
"referredBy": "sip:ai-bot@busyboard",
"referId": "ref_990011"
}
}
Transfer Details
- referTo The destination SIP URI or phone number where the call is being moved.
- referredBy The agent (human or bot) that initiated the transfer.
New Booking
The ultimate success metric! Triggered when the AI successfully books a slot for a client.
{
"organisationId": 1,
"eventType": "booking.created",
"data": {
"bookingId": 55,
"clientName": "John Doe",
"bookingDate": "2026-02-15T00:00:00.000Z",
"bookingTime": "2026-02-15T14:30:00.000Z"
}
}
Success Payload
- bookingId ID of the booking in your Busy Board calendar.
- bookingTime ISO timestamp of the start of the appointment.
Pro Tip: Use this to trigger your internal confirmation SMS or Emails.
Booking Updated
Triggered when an existing booking is modified, usually when a caller reschedules or updates their details via AI.
{
"organisationId": 1,
"eventType": "booking.updated",
"data": {
"bookingId": 55,
"clientEmail": "john.doe@rescheduled.com",
"bookingDate": "2026-02-16T00:00:00.000Z",
"bookingTime": "2026-02-16T10:00:00.000Z",
"notes": "Updated: rescheduled to Monday"
}
}
Change Context
- bookingId The persistent ID of the original booking.
- notes Contains specific details about what was changed during the AI conversation.
Payment Context
Triggered when credits are purchased or subscriptions renew via PayFast.
{
"organisationId": 1,
"eventType": "payment.update",
"data": {
"status": "COMPLETED",
"amount": 499.00,
"metadata": {
"pf_payment_id": "2001556",
"item_name": "Pro Plan Renewal"
}
}
}
Billing Attributes
-
status
Common values:
COMPLETED,FAILED, orPENDING. - metadata Raw pass-through of the PayFast ITN payload for manual auditing.
Widget Call Initialized
Sent when a visitor starts a call from your website voice widget. Includes rich browser metadata.
{
"organisationId": 1,
"eventType": "widget.call.started",
"data": {
"widgetCallId": 123,
"domain": "yoursite.com",
"browser": "Chrome",
"os": "Windows",
"metadata": {
"ip": "102.165.2.1"
}
}
}
Visitor Intelligence
- domain / originUrl Track which page the visitor called from.
- browser / os / device Technical profile of the caller's environment.
Widget Call Completed
The final notification for interactions via the Website Voice Widget.
{
"organisationId": 1,
"eventType": "widget.call.ended",
"data": {
"widgetCallId": 123,
"duration": 120,
"status": "COMPLETED"
}
}
Web-Call Analytics
-
widgetCallId
Link this to the
widget.call.startedevent. - duration Active talk-time on the browser in seconds.
Implementation Guidelines
1. Idempotency
Always store the notificationId. If you receive the same ID again, process it only once and return 200 OK immediately.
2. Timely Acks
Return a 2xx response within 2-3 seconds. If your processing takes longer, move it to a background queue.
3. Retry Strategy
We retry deliveries up to 3 times over 1 hour. Make sure your server doesn't treat retries as new events.