# Guide to Belco Webhooks: Features and Best Practices

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

| Feature | Details |
| --- | --- |
| Configuration | REST API: `POST https://api.belco.io/v1/webhooks` with `shopId`, `url` and `events` |
| Management | List, retrieve, update, activate, deactivate and delete through the API |
| Signature | hex HMAC-SHA256 |
| Signature header | `x-hook-signature` |
| Signed content | The raw request body only |
| Secret | One per webhook, the `secret` property of the Webhook object |
| Body | JSON |
| Event routing | Body field `eventName` |
| Response window | A `2xx` within 5 seconds |
| Retries | Up to 9 retries, the interval growing by 30 seconds each time |
| Auto-disable | 10 consecutive failures within 5 minutes |
| Source IPs | Not published |

## Events and payloads

| Event | Triggered When | Common Use Cases |
| --- | --- | --- |
| `call.created` | A call is ringing | Screen pop, log the call |
| `call.queued` | A call was queued | Queue monitoring |
| `call.answered` | A call was answered | Track handling time |
| `call.closed` | A call ended | Call reporting |
| `conversation.created` | A new conversation started | Create a ticket in another system |
| `conversation.replied` | A reply was added | Sync messages |
| `conversation.note.added` | An internal note was added | Sync notes |
| `conversation.assigned` | A conversation was assigned | Notify the assignee |
| `conversation.closed` | A conversation was closed | Close the linked record, send a survey |
| `conversation.reopened` | A conversation was reopened | Reopen 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](https://github.com/hookdeck/webhook-samples/blob/main/providers/belco/v1/conversation.assigned.json). It is one of the sample payloads on Belco's developer site.

```json
{
  "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:

```bash
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](/docs/cli) and use the forwarding URL as `url`:

```bash
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](https://developers.belco.io/reference/webhooks-verifying-requests).

```javascript
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`.

```javascript
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](/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](/webhooks/guides/implement-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](/webhooks/guides/why-implement-asynchronous-processing-webhooks) 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](https://hookdeck.com) takes Belco's deliveries off your critical path, verifies them and delivers them at a pace your services can handle.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and handle Belco webhooks reliably in minutes.