Skip to main content
Sign in with your store to see only the help that applies to you.

Webhooks

Admin

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​

  1. You create a webhook subscription via the API, specifying an HTTPS URL and which events to listen for.
  2. When a subscribed event occurs, Brother POS sends an HTTP POST to your URL with the event data.
  3. Your server processes the event and returns a 2xx status 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" }
}
Save the signing secret now

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.
  • events must 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​

EventFires When
sale.completedA sale is completed — once per sale (see below)
sale.voidedA sale's status changes to voided
product.createdA product is created
product.updatedA product record is updated — including when a deleted product is restored
product.deletedA product is deleted. data is { "id": ... }
customer.createdA customer is created (from any source, including the register)
customer.updatedA customer record is updated — including when it is deleted (see below)
inventory.adjustedAn entry is written to the stock adjustment ledger — from any source, such as receiving, counts, transfers or manual stock edits
purchase_order.receivedA 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.

Deleting a customer sends customer.updated

There 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:

FieldNotes
idThe inventory transaction ID
product_id, product_skuThe product that moved
transaction_type, sourceWhat kind of movement it was
old_stock, new_stock, stock_changeNumbers
old_price, new_priceTwo-decimal strings, omitted when empty
created_atISO 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_at is when the payload was built, not when the change happened.
  • If the record can no longer be found, data is { "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.

OutcomeRetried?
Connection problem — timeout, refused or reset connection, a hostname that does not resolve, TLS errorYes
5xx response, or 429 Too Many RequestsYes
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 addressNo

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).

Delivery is at-least-once

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?​