Webhooks: call_completed events & signatures
Babelbeez can send a webhook at the end of each call so you can push outcomes into tools like n8n, Zapier, Make, or your own backend.
Each webhook delivers:
- A normalized call status (how the call ended)
- A human‑readable summary of the conversation
- A
dataobject with optional structured fields (name, email, appointment_time, etc.)
This page is for developers, agencies, and power‑users who want to:
- Verify webhook signatures
- Inspect payload shape
- Wire Babelbeez into automation tools or custom code
Webhook configuration lives in the Integrations → Webhooks tab for each voice agent. The high‑level “Configure Webhooks” help entry simply links here for the technical details.
1. When webhooks fire
Babelbeez currently sends two event types:
call_completed– sent at the end of a real call.webhook_test– sent when you click “Send test webhook” from the Webhooks tab in the dashboard.
You control which voice agents send webhooks by configuring per‑agent settings in the dashboard. A webhook is sent when:
- Your account/profile tier has the webhooks feature, and
- The chatbot’s Webhook section has:
- a non‑empty URL
- a secret
- Webhook Enabled toggled on
Monitored entities are optional. They only affect the data object (see below). Webhooks will still fire even when you have no monitored entities.
2. HTTP request structure
Babelbeez sends a standard POST request with:
- Method:
POST - Body: JSON payload (see examples below)
- Headers: includes
X-Babelbeez-*headers for event name, timestamp, and signature
Platforms like n8n, Zapier, or Make will typically show you a wrapped view of the request (for example, an object with headers and body fields). The contract that matters is:
- The JSON body that Babelbeez sends.
- The HTTP headers (especially
X-Babelbeez-*).
2.1 Example body: call_completed
This is a representative example of what the JSON body looks like for a real call_completed webhook (field names and types are accurate; values are illustrative):
jsonc
{
"event": "call_completed",
"session_id": "6c518bb4-eca9-4dd3-a466-1862e597a11d",
"public_chatbot_id": "ba52827d-3ff0-4f3e-bab6-b11d83a4ac87",
"user_id": "e6d0bc49-411d-4807-9740-312d6e136263",
"timestamp": "2026-01-14T10:57:09.176173+00:00",
"duration_seconds": 75,
"status": "ended_ai_tool", // normalized end status
"summary": "...human readable summary of the call...",
"data": {
"name": "Jane Doe" // optional structured fields
}
}Notes:
statusis a normalized value derived from how the client ended the session. Examples include:ended_user– user hung upended_timeout– inactivity timeoutended_ai_tool– the AI used its end‑conversation toolended_handoff_whatsapp– user chose WhatsApp handoffaborted_mic_denied– microphone access was denied
summaryis a short natural‑language recap of the call.datais derived from the backend’s entity extraction pipeline:- If you configured monitored entities for this chatbot and the model successfully extracted them, they are returned here as
{ [entityKey]: string | null }. - If no entities are configured or nothing was reliably captured,
datawill be an empty object:{}.
- If you configured monitored entities for this chatbot and the model successfully extracted them, they are returned here as
2.2 Example body: webhook_test
When you click “Send test webhook” in the dashboard, Babelbeez sends a synthetic webhook_test payload. It uses the same signing logic and headers but with placeholder data:
jsonc
{
"event": "webhook_test",
"session_id": "8a7d0f92-fc7d-4e9a-b049-b4d62d0c0b8f",
"public_chatbot_id": "ba52827d-3ff0-4f3e-bab6-b11d83a4ac87",
"user_id": "e6d0bc49-411d-4807-9740-312d6e136263",
"timestamp": "2026-01-14T11:02:00.000000+00:00",
"duration_seconds": 0,
"status": "test",
"summary": "This is a test webhook from Babelbeez.",
"data": {
"email": "test_value",
"appointment_time": "test_value"
}
}Use webhook_test events to:
- Teach tools like n8n, Zapier, or Make about the payload shape.
- Verify that your endpoint URL, secret, and headers are wired correctly.
3. Headers & signature scheme
Each webhook includes the following key headers:
Content-Type: application/jsonX-Babelbeez-Event: call_completed | webhook_testX-Babelbeez-Timestamp: <ISO8601 timestamp>X-Babelbeez-Signature: v1=<hex>User-Agent: Babelbeez-Webhook/1.0
3.1 How the signature is computed
Serialize the JSON payload to a string (this becomes the raw HTTP body).
Compute the signing base string:
text<timestamp>.<json_payload>where:
<timestamp>is the value from theX-Babelbeez-Timestampheader.<json_payload>is the exact request body as sent (raw bytes), not a re‑serialized object.
Compute the HMAC using your per‑chatbot webhook secret:
- Algorithm:
HMAC-SHA256 - Key: the Webhook secret you configured in the dashboard.
- Message: the signing base string above, as bytes.
- Algorithm:
Hex‑encode the resulting digest.
The final header value is:
textX-Babelbeez-Signature: v1=<hex_digest>
On your side, you should repeat this process and compare your computed signature to the value from X-Babelbeez-Signature.
3.2 Replay protection (optional but recommended)
Babelbeez includes a timestamp in X-Babelbeez-Timestamp but does not enforce any clock skew policy on your behalf.
On your receiver, you can add an extra check such as:
- Parse
X-Babelbeez-Timestampas UTC. - Reject requests where the timestamp is more than, e.g., 5–10 minutes away from your current time.
This helps reduce the risk of a signed request being replayed later.
4. Verifying signatures in Node.js (Express example)
The crucial part is to get access to the raw request body bytes. Many frameworks will automatically parse JSON and discard the original bytes, which breaks signature verification if you re‑serialize.
Below is an example using Express:
ts
import express from 'express';
import crypto from 'crypto';
const app = express();
// 1) Use express.raw() so we keep the raw body for HMAC
app.post(
'/webhooks/babelbeez',
express.raw({ type: 'application/json' }),
(req, res) => {
const webhookSecret = process.env.BABELBEEZ_WEBHOOK_SECRET!;
const signatureHeader = req.header('x-babelbeez-signature') || '';
const timestamp = req.header('x-babelbeez-timestamp') || '';
// Expect format: v1=<hex>
const expectedPrefix = 'v1=';
if (!signatureHeader.startsWith(expectedPrefix)) {
return res.status(400).send('Invalid signature format');
}
const receivedSignature = signatureHeader.slice(expectedPrefix.length);
const rawBody = req.body as Buffer;
const signingBase = `${timestamp}.${rawBody.toString('utf8')}`;
const hmac = crypto
.createHmac('sha256', webhookSecret)
.update(signingBase, 'utf8')
.digest('hex');
const computedSignature = hmac;
// Constant‑time comparison to avoid timing attacks
const valid = crypto.timingSafeEqual(
Buffer.from(computedSignature, 'hex'),
Buffer.from(receivedSignature, 'hex')
);
if (!valid) {
return res.status(401).send('Signature mismatch');
}
// At this point, the request is authentic; now parse the JSON
let payload: any;
try {
payload = JSON.parse(rawBody.toString('utf8'));
} catch (err) {
return res.status(400).send('Invalid JSON');
}
// Example: route by event type
const event = payload.event;
if (event === 'call_completed') {
// Handle real call outcomes
// e.g. upsert into your DB, push into a queue, etc.
} else if (event === 'webhook_test') {
// Handle test events (optional)
}
res.status(200).send('ok');
}
);
app.listen(3000, () => {
console.log('Listening for Babelbeez webhooks on port 3000');
});Key points:
- Use
express.raw()for the route so you get the raw body as aBuffer. - Build the signing base string from the header timestamp + raw body.
- Use your chatbot’s webhook secret as the HMAC key.
- Only parse JSON after the signature has been verified.
5. Verifying signatures in Python (FastAPI example)
Here is a similar example using FastAPI:
py
from fastapi import FastAPI, Request, HTTPException
import hmac
import hashlib
import os
app = FastAPI()
def timing_safe_compare(a: str, b: str) -> bool:
return hmac.compare_digest(a, b)
@app.post('/webhooks/babelbeez')
async def handle_babelbeez_webhook(request: Request):
webhook_secret = os.environ['BABELBEEZ_WEBHOOK_SECRET']
signature_header = request.headers.get('x-babelbeez-signature', '')
timestamp = request.headers.get('x-babelbeez-timestamp', '')
prefix = 'v1='
if not signature_header.startswith(prefix):
raise HTTPException(status_code=400, detail='Invalid signature format')
received_sig = signature_header[len(prefix):]
raw_body = await request.body()
signing_base = f"{timestamp}.{raw_body.decode('utf-8')}"
computed_sig = hmac.new(
key=webhook_secret.encode('utf-8'),
msg=signing_base.encode('utf-8'),
digestmod=hashlib.sha256,
).hexdigest()
if not timing_safe_compare(computed_sig, received_sig):
raise HTTPException(status_code=401, detail='Signature mismatch')
# Signature ok – now parse JSON
try:
payload = await request.json()
except Exception:
raise HTTPException(status_code=400, detail='Invalid JSON body')
event = payload.get('event')
if event == 'call_completed':
# Handle real call outcomes
...
elif event == 'webhook_test':
# Handle test events (optional)
...
return {"status": "ok"}Key points:
- Use
await request.body()to get raw bytes for HMAC calculation. - Build the signing base string exactly as described.
- Only parse JSON after verification succeeds.
6. Working with the payload
Once you’ve verified the signature and parsed the JSON body, a typical handler looks like this:
- Check
payload.event:call_completed– real call outcome.webhook_test– test payload triggered from the dashboard.
- Read high‑level metadata:
session_idpublic_chatbot_iduser_idtimestampduration_secondsstatussummary
- Consume
data:datais a flat object:jsonc{ "email": "[email protected]", "name": "Jane Doe", "appointment_time": "2026-01-20T15:30:00Z" }Keys come from the entity keys you configured on the chatbot (e.g.
email,name,appointment_time).Values are strings or
nullif the model could not reliably extract a value.
Common patterns:
- Insert each
call_completedinto your owncalls/conversationstable. - Use
datafields to:- Create or update contacts in your CRM.
- Create a ticket or task in your helpdesk.
- Schedule follow‑ups based on captured appointment times.
7. Using Babelbeez webhooks with n8n, Zapier, or Make
No‑code/low‑code tools usually handle HTTP parsing for you. A typical setup is:
- Create a Webhook trigger in your tool (n8n, Zapier, Make, etc.).
- Copy the generated URL into your agent’s Webhook URL field in Babelbeez and set your Webhook secret.
- Click “Send test webhook” in the Babelbeez dashboard.
- Back in your tool, inspect the received payload:
- Confirm the
X-Babelbeez-*headers are present. - Look at
body.datato see your monitored entity keys.
- Confirm the
- Map fields in
body.datainto actions:- e.g. create a contact using
data.emailanddata.name. - e.g. create a calendar event or task using
data.appointment_time.
- e.g. create a contact using
Many of these tools do not require you to implement signature verification manually, but you can often add a custom code step or HTTP middleware if you want to enforce HMAC verification there as well.
8. Security recommendations
- Always use HTTPS for your webhook endpoints.
- Keep your webhook secret private and rotate it periodically; update the secret in the Babelbeez dashboard when you do so.
- Treat webhook payloads as sensitive data (they may include personal information such as names, email addresses, or appointment details).
- Implement signature verification for any production or security‑sensitive workflows.
With these patterns in place, you can safely use Babelbeez webhooks as the bridge between real customer conversations and your automation stack.
