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
requestIdis unique per event: use it to deduplicate retries.eventNamerepeats the event name. No header carries it.dateis when the event happened, as an ISO 8601 string.payload.shopIdidentifies the shop. Conversation events carryconversation(status, channel, customer and assignee) andconversation_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
200after storing the event and do the real work in a background job. - Monitor the webhook's
activeflag 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.