Skip to content

Data webhooks ​

Webhooks let Bito tell you when a merchant's data changes, so you do not have to poll. A destination gets there in one of two ways:

  • the integration's webhook_uri — Bito subscribes it automatically on every install;
  • webhook/subscribe — over the Integration API, per destination, with the events you choose.

webhook_uri: automatic subscription ​

If the integration has a webhook_uri, every time the integration is installed Bito subscribes that destination to create, update and delete on these collections: currencies, customers, trades, users, productCategories, products, unitsOfMeasure, saleOrders, invoices, transactions, purchases, purchaseReturns, purchaseOrders.

status_change is never sent this way — subscribe to it separately through webhook/subscribe if you need it. Uninstalling removes this subscription; reinstalling re-creates it.

Discover the events ​

POST /integration/api/v2/webhook/events lists everything you can subscribe to. Event names are <collection>.<action>.

Response (abridged)

json
{
    "code": 0,
    "data": [
        {
            "collection_name": "saleOrders",
            "actions": ["create", "update", "delete", "status_change"],
            "events": [
                "saleOrders.create",
                "saleOrders.update",
                "saleOrders.delete",
                "saleOrders.status_change"
            ]
        },
        {
            "collection_name": "customers",
            "actions": ["create", "update", "delete"],
            "events": ["customers.create", "customers.update", "customers.delete"]
        }
    ],
    "time": "2025-01-15T10:30:00.000Z"
}
ActionFires when
createA record is created
updateA record is updated
deleteA record is deleted
status_changeA record's status changes. Only offered for collections that actually have a status — the events call tells you which.

Subscribe ​

POST /integration/api/v2/webhook/subscribe. The call is an upsert scoped to one destination: re-sending it replaces that destination's event list and leaves your other destinations untouched.

Request

http
POST /integration/api/v2/webhook/subscribe
api-key: mycompany:9ad41c0e7b3f28d5610ac94e7f1b26d8
Content-Type: application/json

{
    "destination": "https://your-app.example.com/bito/webhook",
    "events": ["saleOrders.create", "saleOrders.status_change", "customers.update"]
}

Response

json
{
    "code": 0,
    "data": {
        "destination": "https://your-app.example.com/bito/webhook",
        "events": ["saleOrders.create", "saleOrders.status_change", "customers.update"],
        "secret": "d7c1904ae5b3268f04ad91c7e63b58f2019ce74db28a1650f3c9e7a84b2d6015"
    },
    "time": "2025-01-15T10:30:00.000Z"
}
EndpointBodyWho can call it
POST webhook/events-Any authenticated integration user
POST webhook/get-all-Any authenticated integration user
POST webhook/subscribe{ destination, events[] }Account owner or additional owner
POST webhook/unsubscribe{ destination }Account owner or additional owner

subscribe and unsubscribe check who the request acts as: a call made as an ordinary employee through the user_id header is refused.

Limits: at most 100 distinct destinations per integration, at most 500 events in one subscribe call; subscribe and unsubscribe accept 1 request per second.

Receiving a delivery ​

What Bito sends you

http
POST https://your-app.example.com/bito/webhook
account-id: 507f1f77bcf86cd799439011
X-Bito-Signature: t=1736937000,v1=6b1f0a...c94e
Content-Type: application/json

{"collection_name":"saleOrders","action":"status_change","id":"6712f0a9c4d1e87b3a0f9911","organization_id":"6712f0a9c4d1e87b3a0f7788"}

The body carries the identity of what changed, not the change itself. Fetch the current record from the regular v2 read endpoints using id, scoped to the account in the account-id header.

HeaderWhen presentMeaning
account-idAlwaysThe account the event happened in
X-Bito-SignatureAlwaysThe signature — see below
api-keywebhook_uri subscriptions onlyA token Bito generated for that subscription. It is not your integration's api_key — do not use it to call the API. Subscriptions created with webhook/subscribe send no api-key header.

Bito waits 10 seconds for your response and does not follow redirects (3xx). A response that does not arrive within 10 seconds, or is not 2xx, counts as a failed delivery. Queue heavy work and return 2xx immediately.

Verifying the signature ​

X-Bito-Signature is t=<unix seconds>,v1=<hex>, where v1 is HMAC_SHA256(secret, "{t}.{raw_body}"). The secret is your client secret (client_secret); subscribe and get-all return exactly that value. It is one value for every account and every destination, it does not change on reinstall, and it changes only when you rotate the secret.

Example verification

js
import { createHmac, timingSafeEqual } from 'crypto';

// `raw` MUST be the untouched request body bytes. Do NOT JSON.parse and
// re-stringify before verifying -- key order and number formatting will differ
// and the signature will never match.
export function verify(raw: string, header: string, secret: string): boolean {
    const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
    const t = parts.t;
    const v1 = parts.v1;
    if (!t || !v1) return false;

    // Reject anything older than 5 minutes to blunt replay.
    if (Math.abs(Math.floor(Date.now() / 1000) - Number(t)) > 300) return false;

    const expected = createHmac('sha256', secret).update(`${t}.${raw}`).digest('hex');
    const a = Buffer.from(expected, 'hex');
    const b = Buffer.from(v1, 'hex');
    return a.length === b.length && timingSafeEqual(a, b);
}

WARNING

Hash the raw body bytes. This is the single most common mistake with Bito webhooks. If you JSON.parse the body and re-serialize it before computing the HMAC, the bytes you hash will differ from the bytes Bito hashed the moment key order, whitespace or number formatting differs — and the signature will never match. Verify first, over the untouched body. Parse second.

What webhooks do not guarantee ​

WARNING

  • One attempt, no retry. There is no retry, no backoff and no dead-letter queue. If your endpoint is down, that event is gone.
  • No ordering. Deliveries can arrive out of order.
  • Bulk operations emit nothing. Mass imports and bulk updates do not fire webhooks at all. Treat a webhook as a hint to re-fetch, never as the state itself, and reconcile periodically with a full read. An integration that assumes complete webhook coverage will silently drift.

Bito 2.0 Integration API