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, localhost and cloud metadata addresses are refused.
  • Bakasir does not follow redirects. Answer on the exact address you saved.
  • Reply with any 2xx status within 10 seconds. Anything else counts as a failure.

Events

EventSent when
sale.createdA sale is recorded in any channel
sale.updatedA sale changes, for example it is completed, voided or refunded
order.createdAn online order is placed
order.updatedAn online order changes status or payment
stock.updatedThe stock of a product in an outlet changes
product.created, product.updated, product.deletedA product is created, edited or removed
customer.created, customer.updated, customer.deletedA 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:

HeaderValue
Bakasir-EventThe event type
Bakasir-Event-IdThe event id, the same as id in the body. Use it to ignore duplicates
Bakasir-DeliveryThe delivery id
Bakasir-Attempt1 for the first try, then 2, 3 and so on
Bakasir-Signaturet=<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.
How is this guide?

Last updated on