Agent skill

Belco Webhooks Skill

Receive and verify Belco webhooks. Use when setting up Belco (belco.io) webhook handlers, debugging x-hook-signature verification, or handling call and conversation events like conversation.created, conversation.closed and call.answered.

Install this skill

npx skills add hookdeck/webhook-skills --skill belco-webhooks


When to Use This Skill

  • How do I receive Belco webhooks?
  • How do I verify the x-hook-signature header on a Belco webhook?
  • How do I handle conversation.created, conversation.closed or call.answered events?
  • Why is my Belco webhook signature verification failing?
  • How do I deduplicate Belco webhook deliveries?

Verification (core)

Belco sends the header x-hook-signature: <hex> with no prefix. The value is the lowercase hex HMAC-SHA256 of the raw request body, keyed with the webhook's secret string (its UTF-8 bytes, used as-is). Nothing else is signed: no timestamp, no id. It is not the Standard Webhooks spec. Validate the header's shape (exactly 64 hex characters), then compare in constant time. Verify over the raw bytes before parsing.

Node (compares validated ASCII hex, so Buffer.from(x, 'hex') is never called on untrusted input):

const crypto = require('crypto');

function verifyBelcoSignature(rawBody, signatureHeader, secret) {
  if (typeof signatureHeader !== 'string' || !/^[0-9a-f]{64}$/i.test(signatureHeader)) return false;
  const expected = Buffer.from(crypto.createHmac('sha256', secret).update(rawBody).digest('hex'));
  const received = Buffer.from(signatureHeader.toLowerCase());
  return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}

Python (compare bytes so a non-ASCII header yields 401, not a 500):

import hashlib, hmac, re

def verify_belco_signature(raw_body: bytes, signature_header: str | None, secret: str) -> bool:
    if not signature_header or not re.fullmatch(r"[0-9a-fA-F]{64}", signature_header, re.ASCII):
        return False
    expected = hmac.new(secret.encode("utf-8"), raw_body, hashlib.sha256).hexdigest().encode("ascii")
    return hmac.compare_digest(signature_header.lower().encode("ascii"), expected)

Missing, empty, malformed or mismatched signature: respond 401. Express handlers must read the body with express.raw({ type: '*/*' }) so any content type verifies the exact bytes.

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

Events

Dispatch on the body's top-level eventName field (<object>.<action>); no header names the event. Every delivery is an HTTPS POST with a JSON body shaped { "requestId", "eventName", "date", "payload": { "shopId", ... } }.

EventMeaning
call.createdA call is ringing
call.queuedA call was queued
call.answeredA call was answered
call.closedA call ended
conversation.createdA new conversation started
conversation.repliedA reply was added to a conversation
conversation.note.addedAn internal note was added
conversation.assignedA conversation was assigned
conversation.closedA conversation was closed
conversation.reopenedA conversation was reopened

Delivery and Idempotency

Respond 200 within 5 seconds. Non-2xx responses are retried up to 9 times, with the interval growing by 30 seconds each retry. After 10 consecutive failures within 5 minutes the webhook is disabled until you re-enable it in the Belco dashboard. Dedupe on requestId, and acknowledge quickly and process asynchronously.

Environment Variables

BELCO_WEBHOOK_SECRET=your_webhook_secret   # the `secret` property of the Webhook object returned by the API

Setup with Hookdeck

Create the Hookdeck source with the Belco type and paste the webhook's secret into the source's webhook verification settings. Hookdeck verifies x-hook-signature (HMAC-SHA256, hex) and Belco's POST delivery.

Local Development

# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 belco --path /webhooks/belco

Reference Materials


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 11, 2026

View on GitHub →