Webhooks
Webhooks push event notifications to your server when things happen in Brother POS — a sale completes, a product is updated, inventory moves, a delivery is received. This is more efficient than polling the API repeatedly.
How Webhooks Work
- You create a webhook subscription via the API, specifying an HTTPS URL and which events to listen for.
- When a subscribed event occurs, Brother POS sends an HTTP
POSTto your URL with the event data. - Your server processes the event and returns a
2xxstatus code.
Creating a Subscription
Your API key needs the webhooks:manage scope.
curl -X POST \
-H "X-API-Key: bpos_abc..." \
-H "Content-Type: application/json" \
-d '{
"webhook_subscription": {
"url": "https://your-server.com/webhooks/brotherpos",
"events": ["product.updated", "inventory.adjusted"]
}
}' \
https://yourstore.brotherpos.ca/api/v1/webhook_subscriptions
Response (201 Created):
{
"data": {
"id": 1,
"url": "https://your-server.com/webhooks/brotherpos",
"events": ["product.updated", "inventory.adjusted"],
"active": true,
"failure_count": 0,
"created_at": "2026-03-22T14:30:00Z",
"updated_at": "2026-03-22T14:30:00Z",
"secret": "3f9a1c..."
},
"meta": { "request_id": "...", "timestamp": "2026-03-22T14:30:00Z" }
}
secret is the key you use to verify signatures. This create response is the only API response that includes it — listing, reading and updating the subscription never return it. If you lose it, an admin can reveal it on the subscription's page in the admin panel (see Monitoring in the Admin Panel).
Rules:
- The URL must use HTTPS, and its hostname must resolve when you save it. URLs that resolve to private, loopback, link-local or other reserved IP addresses are rejected. The address is checked again on every delivery.
eventsmust be a non-empty list of the event names below.- A store can have at most 25 webhook subscriptions.
- A subscription belongs to the API key that created it. Listing, reading, updating, deleting and testing only see that key's own subscriptions.
Available Events
| Event | Fires When |
|---|---|
sale.completed | A sale is completed — once per sale (see below) |
sale.voided | A sale's status changes to voided |
product.created | A product is created |
product.updated | A product record is updated — including when a deleted product is restored |
product.deleted | A product is deleted. data is { "id": ... } |
customer.created | A customer is created (from any source, including the register) |
customer.updated | A customer record is updated — including when it is deleted (see below) |
inventory.adjusted | An entry is written to the stock adjustment ledger — from any source, such as receiving, counts, transfers or manual stock edits |
purchase_order.received | A purchase order moves into the received status |
About sale.completed
The event fires once for each sale, at the moment it becomes completed:
- A sale rung up at the register is created already completed, so the event fires when it is created.
- A sale created as pending (for example an online order) fires when its status later changes to
completed.
Voiding a sale sends sale.voided, not another sale.completed.
customer.updatedThere is no customer.deleted event. Customers are archived rather than removed, and archiving sends customer.updated; because the archived record can no longer be loaded, data contains only { "id": ... }. Treat a customer.updated event whose data has nothing but an id as a deletion, and confirm with a 404 from GET /api/v1/customers/:id.
About purchase_order.received
This event fires on the transition into received — and only then.
Editing a received order afterwards, correcting a quantity, attaching an invoice, or saving the order again does not re-fire it. Nor do the earlier steps of the order's life: pending, confirmed, shipped and partially_received are all silent. An order that goes partially_received and then later received fires once, at that final step.
The data object is the full purchase order, in the same shape as GET /api/v1/purchase_orders/:id — header fields plus every line item with its ordered / invoiced / received figures and variance. See Purchasing & Receiving.
About inventory.adjusted
resource_type is InventoryTransaction, and resource_id is the ID of that transaction record — not the ID of the row returned by GET /api/v1/stock_adjustments. The data object carries:
| Field | Notes |
|---|---|
id | The inventory transaction ID |
product_id, product_sku | The product that moved |
transaction_type, source | What kind of movement it was |
old_stock, new_stock, stock_change | Numbers |
old_price, new_price | Two-decimal strings, omitted when empty |
created_at | ISO 8601 |
Webhook Payload
{
"id": "a1b2c3d4-e5f6-...",
"event": "sale.voided",
"resource_type": "Sale",
"resource_id": 12345,
"created_at": "2026-03-22T14:30:00Z",
"data": {
"id": 12345,
"receipt_number": "R-001234",
"status": "voided",
"total": "47.50",
"line_items": [ ... ],
"customer": { ... },
"voided_at": "2026-03-22T14:29:58Z"
}
}
- The payload is built once per event, when it is first sent, from the record's state at that moment, using the same fields as the API: sales use the Sales shape, products the Products shape (never including cost fields), customers the Customers shape.
- Every subscription to that event receives the same payload, with the same
id. Retries resend it unchanged — they do not rebuild it from the record's newer state. created_atis when the payload was built, not when the change happened.- If the record can no longer be found,
datais{ "id": <resource_id> }.
Headers
Content-Type: application/json
User-Agent: BrotherPOS-Webhooks/1.0
X-BrotherPOS-Signature: sha256=a1b2c3d4e5f6...
X-BrotherPOS-Event: sale.voided
X-BrotherPOS-Delivery: a1b2c3d4-e5f6-...
X-BrotherPOS-Delivery equals the payload's id.
Verifying Signatures
Every delivery is signed with HMAC-SHA256 using the subscription's signing secret. The signature is the lowercase hex digest of the raw request body, prefixed with sha256=, sent in X-BrotherPOS-Signature.
A random secret is generated for each subscription. It is returned once, as secret in the 201 response to POST /api/v1/webhook_subscriptions. An admin can also reveal it at any time under Signing Secret on the subscription's page in the admin panel. Keep it private.
Verification Example (Node.js)
const crypto = require('crypto');
function verifyWebhook(rawBody, signature, secret) {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
const a = Buffer.from(signature || '');
const b = Buffer.from(expected);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
Verification Example (Python)
import hmac, hashlib
def verify_webhook(body: bytes, signature: str, secret: str) -> bool:
expected = 'sha256=' + hmac.new(
secret.encode(), body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)
Verify against the exact bytes you received, before parsing the JSON.
Failures and Retries
A delivery counts as successful only when your server answers with a 2xx status. Brother POS waits up to 5 seconds to connect and 10 seconds for the response. Every attempt, successful or not, is recorded in the subscription's delivery log.
What is retried
Retries are handled separately for each subscription, so one slow endpoint does not affect another.
| Outcome | Retried? |
|---|---|
| Connection problem — timeout, refused or reset connection, a hostname that does not resolve, TLS error | Yes |
5xx response, or 429 Too Many Requests | Yes |
Any other non-2xx response (for example 400, 401, 404, 410) | No |
| The URL is not HTTPS, or resolves to a private or otherwise blocked IP address | No |
A retried delivery is attempted 5 times in total. The waits between attempts are 1 minute, 5 minutes, 30 minutes and 2 hours. If the fifth attempt also fails, the delivery has finally failed and that event is dropped for your endpoint.
Every attempt sends identical bytes: the same body, the same id, the same X-BrotherPOS-Delivery header and the same X-BrotherPOS-Signature.
Auto-disable
The subscription's failure count goes up by one each time a delivery finally fails — either a non-retried failure, or a retried one whose fifth attempt failed. Attempts that are still going to be retried do not count. Any successful delivery resets the count to 0. When the count reaches 5, the subscription is disabled (active: false, disabled_at set) and receives nothing more, including retries already scheduled.
To resume a disabled subscription, open Settings → Developer API → Webhooks, choose the subscription, and click Reset Failures & Re-enable. You can also send PATCH with "active": true, but that does not reset the failure count, so the next failed delivery disables it again.
Revoked or expired API keys
A subscription belongs to the API key that created it. Once that key is revoked or has expired, the subscription stops receiving deliveries — including retries already scheduled — even though it still shows as active (active: true, and Active in the admin panel).
A retry cannot tell "your server never got it" apart from "your server got it but the response was lost", so an event you already processed can arrive again. Because every attempt carries the same delivery ID, deduplicate on X-BrotherPOS-Delivery (the same value as the payload's id): record the IDs you have processed and ignore repeats. The ID is shared by all subscriptions for the same event, so if one receiver handles several subscriptions, the same ID arriving at each is the same event.
Don't Rely on Webhooks Alone
Webhooks give you latency; they do not give you a guarantee. A 4xx answer (other than 429) drops the event, an endpoint that is down for longer than the retry window misses it, some changes do not emit the event you would expect (see Available Events), and after five failed deliveries the subscription switches itself off — from that point you receive nothing at all, with no error on your side to notice.
Pair every webhook integration with a scheduled reconciliation pull that walks the same data using incremental cursors and corrects any drift. The full pattern, including cursor handling, is in the Developer API Overview.
Managing Subscriptions
All of these require webhooks:manage and only act on subscriptions created by the calling key.
List Subscriptions
curl -H "X-API-Key: bpos_abc..." \
https://yourstore.brotherpos.ca/api/v1/webhook_subscriptions
Returns an array of subscriptions, newest first, each with id, url, events, active, failure_count, last_triggered_at, disabled_at, created_at and updated_at. Not paginated. The signing secret is not included here, in Get a Subscription, or in the update response.
Get a Subscription
curl -H "X-API-Key: bpos_abc..." \
https://yourstore.brotherpos.ca/api/v1/webhook_subscriptions/1
Returns data.webhook_subscription and data.recent_deliveries — the last 20 delivery attempts, each with id, event, response_status (0 when no response was received), duration_ms, success and created_at.
Update a Subscription
You can change url, events and active.
curl -X PATCH \
-H "X-API-Key: bpos_abc..." \
-H "Content-Type: application/json" \
-d '{"webhook_subscription": {"events": ["product.updated"]}}' \
https://yourstore.brotherpos.ca/api/v1/webhook_subscriptions/1
Delete a Subscription
curl -X DELETE \
-H "X-API-Key: bpos_abc..." \
https://yourstore.brotherpos.ca/api/v1/webhook_subscriptions/1
Send a Test Ping
curl -X POST \
-H "X-API-Key: bpos_abc..." \
https://yourstore.brotherpos.ca/api/v1/webhook_subscriptions/1/test
This sends a signed POST to the subscription URL straight away, with X-BrotherPOS-Event: webhook.test and this body:
{
"id": "…",
"event": "webhook.test",
"created_at": "2026-03-22T14:30:00Z",
"data": { "message": "This is a test webhook delivery from BrotherPOS" }
}
The test request has no X-BrotherPOS-Delivery header, is not recorded in the delivery log, and does not change the failure count. The API responds with success, status_code and message; if the delivery could not be made at all, the status is 422.
Monitoring in the Admin Panel
Go to Settings → Developer API and open the Webhooks tab to see every webhook subscription for the store, its status and failure count. Open a subscription to see its last 50 delivery attempts, reveal its Signing Secret, and click Reset Failures & Re-enable if it was auto-disabled. Subscriptions cannot be created, edited or deleted from the admin panel — use the API.
What's Next?
- Recommended Sync Architecture — Pair webhooks with a reconciliation pull
- Purchasing & Receiving — The data behind
purchase_order.received - Endpoints — Full endpoint reference
- Error Handling — Understanding error responses