Agent skill

Bland AI Webhooks Skill

Receive and verify Bland AI webhooks (AI phone calls). Use when setting up a Bland post-call webhook endpoint, creating the webhook signing secret, debugging X-Webhook-Signature (HMAC-SHA256) verification failures, or handling post-call payloads with call_id, status, transcripts, citations and post_transfer_transcript.

Install this skill

npx skills add hookdeck/webhook-skills --skill bland-ai-webhooks


Bland runs AI phone agents for inbound and outbound calls. When a call ends, Bland POSTs the call record (call_id, status, transcripts, summary, variables, ...) to the webhook URL you set on the call or on an inbound number. If you have created an account signing secret, each delivery carries an X-Webhook-Signature header so you can verify that Bland sent it.

When to Use This Skill

  • How do I receive Bland AI post-call webhooks?
  • How do I verify a Bland webhook signature (X-Webhook-Signature)?
  • Where do I get the Bland webhook signing secret? (Dev Portal → Account Settings → Keys → Replace Secret; shown once.)
  • How do I handle the post-call payload, citations, or the post-transfer transcript?
  • Why is my Bland webhook signature verification failing?

Verification (core)

HeaderValue
X-Webhook-SignatureHex-encoded HMAC-SHA256 of the request body, keyed with your account webhook signing secret

There is no timestamp and no message id in the signature. Bland's docs sign JSON.stringify(req.body); use the raw body bytes instead. They are identical for the compact JSON Bland sends, and raw bytes don't depend on re-serialisation.

const crypto = require('crypto');

function verifyBlandSignature(rawBody, signature, secret) { // rawBody: exact bytes, NOT re-serialised JSON
  if (!signature || !secret) return false;
  const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
  try {
    return crypto.timingSafeEqual(Buffer.from(signature.trim().toLowerCase()), Buffer.from(expected));
  } catch {
    return false; // different lengths = invalid
  }
}

// Express: mount the route with express.raw({ type: '*/*' }) so req.body is the raw bytes.

Python: hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest() compared with hmac.compare_digest. Bland has no webhook-verification SDK, so all three examples verify manually.

For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.

Enterprise accounts can switch to JWT-signed webhooks (RS256, verified against Bland's JWKS). That is a different scheme and is not covered by this skill.

Payload Shape

Bland's standard post-call webhook has no event-type field. It is the call record, sent once when the call ends:

{
  "call_id": "12345678-1234-1234-1234-123456789012",
  "status": "completed",
  "completed": true,
  "call_length": 0.18,
  "to": "+15551234567",
  "from": "+15559876543",
  "error_message": null,
  "summary": "...",
  "concatenated_transcript": "user: Hello? \n assistant: ...",
  "transcripts": [{ "id": 1, "user": "user", "text": "Hello?", "created_at": "..." }],
  "variables": {},
  "metadata": {}
}

Failed calls (busy, number not found, ...) still arrive as a 200 delivery with error_message set.

Common Payload Types

PayloadHow to recognise itSent when
Post-callcall_id + status (e.g. completed), no event_typeThe call ends (the webhook set on the call or inbound number)
citationsevent_type: "citations"Separate webhook after the call, if citations is in webhook_events. With the dashboard "Delay post call webhook" toggle, citations and corrected_transcript are merged into the post-call payload instead
post_transfer_transcriptevent_type: "post_transfer_transcript"Call was transferred to a human and post_transfer_transcript is in webhook_events (limited rollout)
evalsevent_type: "evals"Eval scores from an attached workbench; separate webhook when the run completes
multipleevent_type: "multiple" + contents arrayOne payload carries more than one enrichment
SMSconversation_id instead of call_idEach SMS/RCS/WhatsApp message and conversation status change

See references/overview.md for the details.

Delivery Semantics

  • Return any 2xx to acknowledge. 4xx, 429 and 5xx count as delivery failures and Bland retries automatically (the retry schedule isn't documented).
  • A call's call_id can appear in several webhooks (post-call, citations, evals), so dedupe on call_id plus event_type, not call_id alone.
  • No timestamp is signed, so a captured delivery can be replayed; idempotent handling is your protection.

Environment Variables

BLAND_WEBHOOK_SECRET=...   # required; the account-wide signing secret. Handlers fail closed without it

Local Development

# Express / Next.js (use 8000 for FastAPI). No install or account required.
npx hookdeck-cli listen 3000 bland-ai --path /webhooks/bland-ai

Use the printed https:// URL as the webhook parameter when sending a call, then place a call and let it end.

Reference Materials


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 3, 2026

View on GitHub →