Webhooks
Get a signed HTTPS request when a sale, order, stock level, product or customer changes, and verify it in Node or PHP.
A webhook is a request Bakasir sends to your server when something changes, so you do not need to poll the API. Add endpoints in Settings → Developer → Webhooks. A business can have up to 20 endpoints, and each endpoint chooses the events it wants.
Requirements for the endpoint
- The address must start with
https://and point to a public server. Addresses on private networks,localhostand cloud metadata addresses are refused. - Bakasir does not follow redirects. Answer on the exact address you saved.
- Reply with any
2xxstatus within 10 seconds. Anything else counts as a failure.
Events
| Event | Sent when |
|---|---|
sale.created | A sale is recorded in any channel |
sale.updated | A sale changes, for example it is completed, voided or refunded |
order.created | An online order is placed |
order.updated | An online order changes status or payment |
stock.updated | The stock of a product in an outlet changes |
product.created, product.updated, product.deleted | A product is created, edited or removed |
customer.created, customer.updated, customer.deleted | A customer is created, edited or removed |
You can also subscribe to a whole group such as product.*, or to everything with *. Changes made in the Demo outlet never send events. Several changes to the same record inside one operation are sent as one event.
The request
Bakasir sends a POST with a JSON body:
{
"id": "evt_5f2b0c7e91d34a0c8d1e6b27",
"type": "product.updated",
"created_at": "2026-11-04T08:30:00Z",
"data": {
"id": "01K9ZT6Q2X3M8N4V7B5C1D0EFG",
"name": "Es Kopi Susu",
"price": 24000
}
}data has the same shape as the matching API object (a product, a sale with its items, an online order with its lines, a customer). For stock.updated it is the stock row with store_id, product_id, variant_id, qty and avg_cost. For a deleted record it holds only the id. The data is read when the first delivery is attempted and then kept, so retries send the same body.
Headers:
| Header | Value |
|---|---|
Bakasir-Event | The event type |
Bakasir-Event-Id | The event id, the same as id in the body. Use it to ignore duplicates |
Bakasir-Delivery | The delivery id |
Bakasir-Attempt | 1 for the first try, then 2, 3 and so on |
Bakasir-Signature | t=<unix time>,v1=<hex signature> |
Verify the signature
Every request is signed with the secret of the endpoint, shown once when you create it or rotate it. Compute an HMAC SHA-256 of the text <t>.<raw body> with the secret and compare it with v1 in constant time. Use the raw body exactly as received, before any JSON parsing, and reject requests whose t is more than five minutes old.
Node.js:
import crypto from "node:crypto";
export function verifyBakasir(rawBody, header, secret, toleranceSeconds = 300) {
const parts = Object.fromEntries(header.split(",").map((pair) => pair.split("=")));
if (!parts.t || !parts.v1) return false;
if (Math.abs(Date.now() / 1000 - Number(parts.t)) > toleranceSeconds) return false;
const expected = crypto.createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest();
const given = Buffer.from(parts.v1, "hex");
return given.length === expected.length && crypto.timingSafeEqual(given, expected);
}With Express, read the raw body with express.raw({ type: "application/json" }) and pass req.body.toString("utf8").
PHP:
<?php
function verifyBakasir(string $rawBody, string $header, string $secret, int $tolerance = 300): bool
{
$parts = [];
foreach (explode(",", $header) as $pair) {
[$key, $value] = array_pad(explode("=", $pair, 2), 2, "");
$parts[$key] = $value;
}
if (!isset($parts["t"], $parts["v1"]) || abs(time() - (int) $parts["t"]) > $tolerance) {
return false;
}
$expected = hash_hmac("sha256", $parts["t"] . "." . $rawBody, $secret);
return hash_equals($expected, $parts["v1"]);
}
$raw = file_get_contents("php://input");
$signature = $_SERVER["HTTP_BAKASIR_SIGNATURE"] ?? "";
if (!verifyBakasir($raw, $signature, getenv("BAKASIR_WEBHOOK_SECRET"))) {
http_response_code(401);
exit;
}
http_response_code(204);Retries and the delivery log
If your endpoint fails or times out, Bakasir tries again 6 times, so up to 7 tries in total over about 21 hours: after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 12 hours. Order is not guaranteed and an event can arrive more than once, so use Bakasir-Event-Id to ignore repeats and use created_at to ignore an older state.
After the last try fails, the delivery is marked failed and the owner receives a notification. You can retry a failed delivery by hand.
Settings → Developer → Deliveries lists every delivery for 30 days with its status, number of tries, the answer of your server and the last error. Use Send test on an endpoint to send a signed webhook.test event right now.
Rotate or stop
- Rotate secret issues a new secret at once. Update your server first or briefly accept both.
- Turn an endpoint off to pause it, or delete it to stop all events and remove its log.
- If you reset your business to factory settings, all keys and endpoints are removed.
Last updated on

