Gareth Wilson Gareth Wilson

Guide to Belco Webhooks: Features and Best Practices

Published


Belco is a shared inbox for online shops that brings chat, email and phone support into one place. Its webhooks report what happens in that inbox: a call rings, joins the queue or is answered, a customer starts or replies to a conversation, an agent adds a note, assigns or closes it. Shops use them to mirror support activity into a CRM, an order system or a data warehouse, or to trigger a satisfaction survey when a conversation closes.

This guide covers how to create Belco webhooks through the API, the call and conversation events, how to verify x-hook-signature, and the tight timing that can switch a Belco webhook off.

What are Belco webhooks?

Belco webhooks are created per shop through the REST API, not in a settings screen. Each webhook has a URL, a list of events, an active flag and its own secret. When a subscribed event happens in that shop, Belco sends an HTTPS POST with a JSON body and an x-hook-signature header, which is a hex HMAC-SHA256 of the raw body keyed with that webhook's secret.

Every delivery has a requestId, and Belco's implementation checklist tells you to store it so a retried delivery isn't processed twice.

Belco webhook features

FeatureDetails
ConfigurationREST API: POST https://api.belco.io/v1/webhooks with shopId, url and events
ManagementList, retrieve, update, activate, deactivate and delete through the API
Signaturehex HMAC-SHA256
Signature headerx-hook-signature
Signed contentThe raw request body only
SecretOne per webhook, the secret property of the Webhook object
BodyJSON
Event routingBody field eventName
Response windowA 2xx within 5 seconds
RetriesUp to 9 retries, the interval growing by 30 seconds each time
Auto-disable10 consecutive failures within 5 minutes
Source IPsNot published

Events and payloads

EventTriggered WhenCommon Use Cases
call.createdA call is ringingScreen pop, log the call
call.queuedA call was queuedQueue monitoring
call.answeredA call was answeredTrack handling time
call.closedA call endedCall reporting
conversation.createdA new conversation startedCreate a ticket in another system
conversation.repliedA reply was addedSync messages
conversation.note.addedAn internal note was addedSync notes
conversation.assignedA conversation was assignedNotify the assignee
conversation.closedA conversation was closedClose the linked record, send a survey
conversation.reopenedA conversation was reopenedReopen the linked record

Event Payload Structure

  • requestId is unique per event: use it to deduplicate retries.
  • eventName repeats the event name. No header carries it.
  • date is when the event happened, as an ISO 8601 string.
  • payload.shopId identifies the shop. Conversation events carry conversation (status, channel, customer and assignee) and conversation_item, the message, note or assignment that triggered the event.

Belco publishes samples for the conversation events only. The four call.* events have no published payload, so log a real one before you write code that depends on its fields.

Published payload example

This is the conversation.assigned sample from the webhook samples library. It is one of the sample payloads on Belco's developer site.

{
  "requestId": "4q3Rem4G6pRrWt5zs",
  "eventName": "conversation.assigned",
  "date": "2025-06-11T09:17:20.936Z",
  "payload": {
    "shopId": "G4ag3eGyiKibrGcM7",
    "conversation": {
      "_id": "sSzxq7tMBFmCY28o8",
      "organizationId": "nM3kTrdCv5JznJHFE",
      "customerId": "YX6HM6Sfbt4GvbvZb",
      "channel": "chat",
      "type": "inbound-message",
      "status": "open",
      "assignedTo": "GKa97c5NNLzcWcHhP",
      "firstItemId": "rxxtPXZng45rALFjE",
      "lastItemId": "vEHvMczSiiKv2MpCk",
      "createdAt": "2025-06-11T09:15:05.075Z"
    },
    "conversation_item": {
      "_id": "sJ2KnQqJZsEJ8pci5",
      "type": "assignment",
      "reference": {
        "type": "user",
        "_id": "GKa97c5NNLzcWcHhP"
      },
      "meta": {
        "assignedTo": "hDPhQuae27KmsYREq",
        "previousUserId": "GKa97c5NNLzcWcHhP"
      },
      "shopId": "G4ag3eGyiKibrGcM7",
      "conversationId": "sSzxq7tMBFmCY28o8",
      "createdAt": "2025-06-11T09:17:20.768Z",
      "viaApi": false,
      "updatedAt": "2025-06-11T09:17:20.768Z"
    }
  }
}

In an assignment, conversation_item.meta holds both the new assignee and previousUserId, so you can notify the person who handed the conversation over as well as the one who picked it up.

Setting up Belco webhooks

You need the shop's shopId and a Belco API access token, which goes in the Authorization header. Then create the webhook:

curl -X POST https://api.belco.io/v1/webhooks \
  -H "Authorization: $BELCO_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "shopId": "YOUR_SHOP_ID",
    "url": "https://example.com/webhooks/belco",
    "events": ["conversation.created", "conversation.closed", "call.answered"]
  }'

The response is the Webhook object, including secret. Store it server-side as WEBHOOK_SECRET. Belco documents no format or rotation for the secret, so treat it as an opaque string and use it as-is. If you run several webhooks for one shop, each has its own secret.

The same API lists, retrieves and updates webhooks, and PUT /webhooks/{webhookId}/activate turns one back on after Belco has disabled it.

To watch deliveries on your machine before you deploy, forward them with the Hookdeck CLI and use the forwarding URL as url:

npx hookdeck-cli listen 3000 belco --path /webhooks/belco

Securing Belco webhooks

Compute an HMAC-SHA256 of the raw request body with the webhook's secret, encode it as hex and compare it with x-hook-signature in constant time. Belco's newer sample uses crypto.timingSafeEqual, and its older one uses !==, which leaks timing. See the verification documentation.

const crypto = require('node:crypto');

function verifySignature(rawBody, headers, secret) {
  if (!Buffer.isBuffer(rawBody) || typeof secret !== 'string' || !secret) return false;
  const signature = headers['x-hook-signature'];
  if (typeof signature !== 'string') return false;
  const key = Buffer.from(secret, 'utf8');
  const message = rawBody;
  const candidates = [signature];
  const expected = crypto.createHmac('sha256', key).update(message).digest();
  return candidates.some((candidate) => {
    if (!/^[0-9a-fA-F]{64}$/.test(candidate)) return false;
    const received = Buffer.from(candidate, 'hex');
    return received.length === expected.length && crypto.timingSafeEqual(received, expected);
  });
}

With only 5 seconds to answer, the handler does as little as possible: verify, skip a requestId it has already stored, save the event and return 200.

const express = require('express');

function createBelcoApp(inbox) {
  const app = express();
  app.post('/webhooks/belco', express.raw({ type: '*/*' }), async (req, res) => {
    const secret = process.env.WEBHOOK_SECRET;
    if (!secret) return res.sendStatus(500);
    if (!verifySignature(req.body, req.headers, secret)) return res.sendStatus(401);

    let parsed;
    try {
      parsed = JSON.parse(req.body.toString('utf8'));
    } catch {
      return res.sendStatus(400);
    }
    if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return res.sendStatus(400);
    const { requestId, eventName, payload } = parsed;
    if (!requestId || typeof eventName !== 'string') return res.sendStatus(400);
    try {
      // Returns false if this requestId was already stored.
      await inbox.saveOnce(requestId, { eventName, shopId: payload?.shopId, payload });
    } catch {
      return res.sendStatus(503);
    }
    return res.sendStatus(200);
  });
  return app;
}

Belco signs only the body and sends no timestamp header, so a replayed request verifies. Deduplicating on requestId covers that and Belco's own retries.

Hookdeck's Belco source type verifies x-hook-signature as a hex HMAC-SHA256 of the raw body with your webhook's secret. Hookdeck always answers Belco with 200 and does so straight away, records a mismatch as failed verification and does not forward that request. Nothing time-based is checked.

Make Belco webhooks production-ready. Hookdeck Event Gateway answers Belco well inside its 5-second window, verifies x-hook-signature, and holds each call and conversation event until your service has processed it.

Belco webhook limitations and pain points

Five seconds, then a fast path to being disabled

The Problem: A slow afternoon on your server and Belco stops sending events altogether.

Why It Happens: Belco expects a 2xx within 5 seconds. Ten consecutive failures within 5 minutes disable the webhook, and it stays off until someone re-enables it. In a busy support inbox, a handler that waits on a CRM API can fail ten times in well under five minutes.

Workarounds:

  • Return 200 after storing the event and do the real work in a background job.
  • Monitor the webhook's active flag through the API and alert when it changes.
  • Re-enable with PUT /webhooks/{webhookId}/activate, then reconcile the gap from the Belco API.

How Hookdeck Can Help: Hookdeck answers Belco immediately, so your own response time and outages never count towards Belco's ten-failure limit. Your service receives events from Hookdeck at the pace you set.

No dashboard, no secret management

The Problem: You can't see or rotate a webhook's secret anywhere except the API.

Why It Happens: Belco documents webhooks only as API objects. It documents no dashboard location, secret format or rotation procedure.

Workarounds:

  • Store the secret from the create response straight away.
  • To rotate, create a new webhook, switch your receiver to accept its secret, then delete the old one.

How Hookdeck Can Help: Keep one Hookdeck source per Belco webhook, each holding its own secret, and route them to the same destination.

Call events are undocumented in shape

The Problem: Code written for call.answered breaks on the first real call.

Why It Happens: Belco publishes sample payloads for the conversation events but not for the four call events.

Workarounds:

  • Capture a real delivery of each call event before writing handlers.
  • Access call fields defensively and log unknown shapes.

How Hookdeck Can Help: Hookdeck keeps every request it receives, so you can inspect real call payloads and replay them against your handler while you build it.

Best practices

Verify the raw bytes

Belco's own sample passes req.body as "the raw JSON string". In Express that means express.raw({ type: '*/*' }), not express.json(). Validate the header's length (64) and alphabet before decoding, because Node's hex decoder silently drops invalid characters.

Deduplicate on requestId

Belco retries up to nine times, so the same event can arrive more than once. Store requestId with the change it causes. See implementing webhook idempotency.

Subscribe per need

Each webhook carries its own event list. A reporting pipeline may want every event, while a survey trigger only needs conversation.closed. Separate webhooks keep each consumer's failures from disabling the other.

Process asynchronously

Five seconds leaves no room for downstream calls. Verify, store and answer, then process. The case for asynchronous processing applies doubly here.

Conclusion

Belco's signing is simple, and its retry policy is generous on paper, but the 5-second window and the ten-in-five-minutes disable rule make slow handlers costly. Keep the handler minimal, deduplicate on requestId, and watch the active flag.

Hookdeck Event Gateway takes Belco's deliveries off your critical path, verifies them and delivers them at a pace your services can handle.

Get started with Hookdeck for free and handle Belco webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.