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

  1. 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.
  2. Enter your endpoint. It must be https:// and reachable from the internet.
  3. Set a signing secret of at least 16 characters, then store the same value on your server. CallBix never shows it again.
  4. Pick the events you want, then save.
  5. 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

EventSent when
call.createdA call from an agent's phone is logged in CallBix. Sent once per call, as the first delivery for it.
call.updatedSomeone saves the call's outcome, notes, follow-up or custom fields, in the app or on the web.
recording.addedThe call's recording finishes uploading (or is replaced). call.recordingUrl is set.
pingYou 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.

POST https://your-server.example/callbix
{
  "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

FieldTypeDescription
eventstringcall.created, call.updated, recording.added or ping.
deliveryIdstring (UUID)Unique for every HTTP request, including retries. Same value as the X-CallBix-Delivery header.
sentAtISO 8601When this request was sent. Covered by the signature, so you can reject old replays.
call.idstringCallBix's id for the call. Stable across every event for the same call — use it as your key.
call.phoneNumberstringThe other party's number as the phone recorded it.
call.normalizedNumberstringThe same number in E.164 (+919876543210) when it can be parsed, otherwise digits only.
call.contactNamestring | nullThe name from the phone's contacts or the matched CRM record.
call.directionstringINCOMING, OUTGOING, MISSED, REJECTED, BLOCKED or UNKNOWN.
call.startedAtISO 8601When the call started (UTC).
call.durationSecnumberTalk time in seconds. 0 for calls that weren't answered.
call.dispositionstring | nullThe outcome the agent picked.
call.notesstring | nullThe agent's notes.
call.followUpAtISO 8601 | nullWhen the agent plans to follow up.
call.tagsstring[]Tags on the call.
call.recordingUrlstring | nullSigned 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.fieldsobjectYour custom fields, keyed by the key you chose. See Custom fields.
call.agentobjectThe team member whose phone logged the call: id, name, email, phone.
contactobject | nullAlways 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.

TypeValue in call.fields
Text, long text, email, phone, linkstring
Numbernumber
Yes / notrue or false
Date"YYYY-MM-DD"
Date & timeISO 8601 string, UTC
Pick onethe chosen value, as a string
Pick manyarray of strings
Left emptynull, or the key is missing

Headers

HeaderValue
Content-Typeapplication/json
User-AgentCallBix-Webhook/1.0
X-CallBix-EventThe event name, the same as event in the body.
X-CallBix-DeliveryUnique id of this request, the same as deliveryId.
X-CallBix-Event-IdThe call id. The same for every event and retry about one call.
X-CallBix-SignatureHex 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.

Node.js (Express)
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
<?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);
Python (Flask)
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 "", 204

Delivery & retries

  • Any 2xx answer within 15 seconds counts as delivered. The response body is ignored.
  • Timeouts, network errors, 5xx, 401, 408 and 429 are 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 as 400, 404 or 422) 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.id rather than appending.
  • Ignore stale updates. Events are usually in order, but a retry can land after a newer event. Keep the sentAt you 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. recordingUrl expires, 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.