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

call.incoming
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 accepted and hangup events.
  • caller / callee Phone numbers in E.164 format. caller is the originator, callee is the destination.

Note: This event is perfect for looking up customer records in your CRM before the agent answers.

call.accepted
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
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 / OK is a normal disconnect. 486 / Busy or 480 / No Answer signify failed attempts.
  • callId The internal database ID of the call record in Busy Board.
call.transfer
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.
booking.created
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
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.update
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, or PENDING.
  • metadata Raw pass-through of the PayFast ITN payload for manual auditing.
widget.call.started
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.ended
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.started event.
  • 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.