Developer docs
Webhooks
Use a webhook to send every call your team makes and receives to a system CallBix has no built-in connector for: Pipedrive, Freshsales, LeadSquared, Odoo, your own CRM or ERP, or an automation tool like Zapier, Make or n8n. Each event is an HTTPS POST of signed JSON.
Set up
- Open CRM integrations and choose Connect on the Webhook card. Webhooks are included in the Growth plan and above, and only people who can manage integrations can set them up.
- Enter your endpoint. It must be
https://and reachable from the internet. - Set a signing secret of at least 16 characters, then store the same value on your server. CallBix never shows it again.
- Pick the events you want, then save.
- Press Send test event to check that your endpoint answers with a
2xx.
The card's switches also apply: with Log calls automatically off nothing is sent, and with Log missed calls off, missed calls are left out.
Events
| Event | Sent when |
|---|---|
call.created | A call from an agent's phone is logged in CallBix. Sent once per call, as the first delivery for it. |
call.updated | Someone saves the call's outcome, notes, follow-up or custom fields, in the app or on the web. |
recording.added | The call's recording finishes uploading (or is replaced). call.recordingUrl is set. |
ping | You press “Send test event”. Always sent, whatever you subscribe to. The body has "test": true and a sample call. |
A new call is sent about two minutes after it ends, so the agent's notes and the recording usually arrive in the same call.created. Changes made close together are combined. If you don't subscribe to call.created, the first event you get for a call is its first update.
Payload
Every event carries the whole current state of the call, not only what changed, so you can always overwrite your copy with what you receive.
{
"event": "call.updated",
"deliveryId": "8b0f6c1e-3a52-4c55-9d6a-0b7d3e2f9a41",
"sentAt": "2026-10-02T06:15:31.204Z",
"call": {
"id": "66fc1f5e9b1d4a0012a3b4c5",
"phoneNumber": "+91 98765 43210",
"normalizedNumber": "+919876543210",
"contactName": "Priya Sharma",
"direction": "OUTGOING",
"startedAt": "2026-10-02T06:09:12.000Z",
"durationSec": 252,
"disposition": "Interested",
"notes": "Wants a site visit on Saturday.",
"followUpAt": "2026-10-04T04:30:00.000Z",
"tags": [],
"recordingUrl": "https://api.callbix.app/public/recordings/66fc1f5e…?exp=…&sig=…",
"fields": {
"budget": 7500000,
"site_visit_booked": true,
"product_interest": ["2 BHK", "3 BHK"]
},
"agent": {
"id": "66f9a2b1c3d4e5f601234567",
"name": "Ravi Kumar",
"email": "ravi@acme.in",
"phone": "+91 99887 76655"
}
},
"contact": null
}Field reference
| Field | Type | Description |
|---|---|---|
event | string | call.created, call.updated, recording.added or ping. |
deliveryId | string (UUID) | Unique for every HTTP request, including retries. Same value as the X-CallBix-Delivery header. |
sentAt | ISO 8601 | When this request was sent. Covered by the signature, so you can reject old replays. |
call.id | string | CallBix's id for the call. Stable across every event for the same call — use it as your key. |
call.phoneNumber | string | The other party's number as the phone recorded it. |
call.normalizedNumber | string | The same number in E.164 (+919876543210) when it can be parsed, otherwise digits only. |
call.contactName | string | null | The name from the phone's contacts or the matched CRM record. |
call.direction | string | INCOMING, OUTGOING, MISSED, REJECTED, BLOCKED or UNKNOWN. |
call.startedAt | ISO 8601 | When the call started (UTC). |
call.durationSec | number | Talk time in seconds. 0 for calls that weren't answered. |
call.disposition | string | null | The outcome the agent picked. |
call.notes | string | null | The agent's notes. |
call.followUpAt | ISO 8601 | null | When the agent plans to follow up. |
call.tags | string[] | Tags on the call. |
call.recordingUrl | string | null | Signed link to the recording. It redirects to the audio file and stays valid for a year by default. Null until a recording is uploaded. |
call.fields | object | Your custom fields, keyed by the key you chose. See Custom fields. |
call.agent | object | The team member whose phone logged the call: id, name, email, phone. |
contact | object | null | Always null for webhooks: CallBix has no CRM to look the caller up in. |
Custom fields
To collect more than the outcome and notes, open Webhook → Fields & status and add fields. For each one you choose a label (what agents see after a call), a key, and a type. Agents fill them in on the phone or the web, and they arrive in call.fields under your keys.
| Type | Value in call.fields |
|---|---|
| Text, long text, email, phone, link | string |
| Number | number |
| Yes / no | true or false |
| Date | "YYYY-MM-DD" |
| Date & time | ISO 8601 string, UTC |
| Pick one | the chosen value, as a string |
| Pick many | array of strings |
| Left empty | null, or the key is missing |
Headers
| Header | Value |
|---|---|
Content-Type | application/json |
User-Agent | CallBix-Webhook/1.0 |
X-CallBix-Event | The event name, the same as event in the body. |
X-CallBix-Delivery | Unique id of this request, the same as deliveryId. |
X-CallBix-Event-Id | The call id. The same for every event and retry about one call. |
X-CallBix-Signature | Hex HMAC-SHA256 of the raw request body, keyed with your signing secret. Only sent when a secret is set. |
Verifying signatures
Compute HMAC-SHA256 over the raw request body, exactly as received, using your signing secret. Hex-encode the result and compare it to X-CallBix-Signature with a constant-time comparison. Parsing and re-serialising the JSON first changes the bytes and breaks the check.
sentAt is inside the signed body, so checking it's recent (within five minutes, say) stops someone from replaying an old request.
import crypto from "node:crypto";
import express from "express";
const app = express();
const SECRET = process.env.CALLBIX_WEBHOOK_SECRET;
// Verify against the raw bytes — not re-serialised JSON
app.post("/callbix", express.raw({ type: "application/json" }), (req, res) => {
const expected = crypto.createHmac("sha256", SECRET).update(req.body).digest("hex");
const received = req.get("X-CallBix-Signature") ?? "";
const ok =
received.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));
if (!ok) return res.status(401).send("bad signature");
const event = JSON.parse(req.body.toString("utf8"));
const ageMs = Date.now() - Date.parse(event.sentAt);
if (ageMs > 5 * 60_000) return res.status(400).send("too old");
// Answer fast; do the real work in a queue
queue.add(event);
res.sendStatus(204);
});<?php
$secret = getenv('CALLBIX_WEBHOOK_SECRET');
$raw = file_get_contents('php://input');
$expected = hash_hmac('sha256', $raw, $secret);
$received = $_SERVER['HTTP_X_CALLBIX_SIGNATURE'] ?? '';
if (!hash_equals($expected, $received)) {
http_response_code(401);
exit('bad signature');
}
$event = json_decode($raw, true);
if (time() - strtotime($event['sentAt']) > 300) {
http_response_code(400);
exit('too old');
}
// ... store or queue $event ...
http_response_code(204);import hashlib, hmac, json, os, time
from datetime import datetime
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ["CALLBIX_WEBHOOK_SECRET"].encode()
@app.post("/callbix")
def callbix():
raw = request.get_data() # raw bytes, before any JSON parsing
expected = hmac.new(SECRET, raw, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, request.headers.get("X-CallBix-Signature", "")):
abort(401)
event = json.loads(raw)
sent = datetime.fromisoformat(event["sentAt"].replace("Z", "+00:00")).timestamp()
if time.time() - sent > 300:
abort(400)
# ... store or queue the event ...
return "", 204Delivery & retries
- Any
2xxanswer within 15 seconds counts as delivered. The response body is ignored. - Timeouts, network errors,
5xx,401,408and429are retried with growing gaps: about 2 minutes, then 4, 8, 16 and so on, capped at 6 hours, for up to 8 attempts. - Any other
4xx(such as400,404or422) is treated as permanent and isn't retried. - Calls that couldn't be delivered show as Failed on the call and on the Webhook card. After fixing your endpoint, press Retry failed to send them again.
Handling events well
- Upsert by call.id. The same event can arrive more than once (after a retry, for example), and every event holds the full call, so insert-or-update on
call.idrather than appending. - Ignore stale updates. Events are usually in order, but a retry can land after a newer event. Keep the
sentAtyou last applied for each call and skip anything older. - Answer quickly. Store the event and reply
2xx; do slow work (CRM calls, emails) in the background. - Copy the recording if you need it for good.
recordingUrlexpires, and recordings are removed after your plan's retention period.
Testing
Send test event on the Webhook card posts a signed ping with a sample call, including your custom field keys, and shows the status code your endpoint returned. While you build, a request inspector such as webhook.site or a local tunnel (ngrok, Cloudflare Tunnel) lets you see exactly what arrives.