Subscribe to real-time webhooks

Webhooks let you react to sale.created, purchase_order.received, stock.low, connection.approved, and connection.revoked events instead of polling. They're set up from the developer dashboard, not called with an x-api-key, and delivery runs on a ~5-minute cron sweep rather than instantly.

AskBiz webhooks push five event types to a URL you control: sale.created, purchase_order.received, stock.low, connection.approved, and connection.revoked — no others exist today. Unlike every other page in this API reference, webhooks are not something a third-party server calls with an x-api-key. They’re an account-settings action: you register the endpoint URL and event types from the developer.askbiz.co dashboard’s Webhooks page, the same place you’d manage API keys. Delivery is asynchronous and runs on a cron sweep roughly every 5 minutes, not instantly.

  1. 1

    Create a webhook from the dashboard

    Webhooks aren’t created with an x-api-key REST call — they’re managed from the developer.askbiz.co dashboard’s Webhooks page, the same way you’d manage account settings. Add your endpoint URL (must be https://) and pick which event types to subscribe to: sale.created, purchase_order.received, stock.low, connection.approved, connection.revoked. You can subscribe to as few or as many as you like, and you can hold up to 10 webhooks per account.

  2. 2

    Save the whsec_ secret — it’s shown once

    Creating a webhook generates a signing secret in the form whsec_… and shows it to you exactly once, at creation time. Store it in your own environment (e.g. ASKBIZ_WEBHOOK_SECRET) immediately — there’s no way to retrieve it again later from the dashboard, only to delete the webhook and create a new one.

  3. 3

    Verify the x-askbiz-signature header on your receiving endpoint

    Every delivery is signed: the request body is HMAC-SHA256’d with your webhook secret, and the resulting hex digest is sent in the x-askbiz-signature header. Compute the same HMAC over the raw request body on your end and compare it to the header using a constant-time comparison — never a plain === or ==, which leaks timing information. Reject the request if it doesn’t match. Each delivery’s JSON body is { "event": "…", "data": { … } }, where event is one of your subscribed event types.

    const crypto = require('crypto')
    
    function isValidSignature(rawBody, signatureHeader, secret) {
      const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex')
      // Constant-time comparison — never use === on secrets/signatures
      return crypto.timingSafeEqual(Buffer.from(signatureHeader), Buffer.from(expected))
    }
    
    // Example Express handler — use the raw request body, not a re-serialized object
    app.post('/webhooks/askbiz', express.raw({ type: 'application/json' }), (req, res) => {
      const signature = req.headers['x-askbiz-signature']
      if (!isValidSignature(req.body, signature, process.env.ASKBIZ_WEBHOOK_SECRET)) {
        return res.status(401).send('Invalid signature')
      }
    
      const { event, data } = JSON.parse(req.body)
      // event is one of: sale.created | purchase_order.received | stock.low | connection.approved | connection.revoked
      console.log(event, data)
      res.status(200).send('ok')
    })
  4. 4

    Send a test event and check the delivery log before going live

    From the webhook’s row on the dashboard, use "Send test event" to enqueue a real synthetic delivery through the exact same signing and delivery path as a live event — it exercises your actual signature-verification code, not a mock. Then use "View deliveries" to see recent delivery attempts and their status. Do this before wiring the webhook into anything that matters, rather than waiting for a real sale, purchase order, or low-stock event to find out your endpoint or secret is wrong.

  5. 5

    Plan around ~5-minute delivery latency, not instant delivery

    Webhook delivery isn’t real-time — events are captured immediately when the underlying action happens, but delivery runs on a cron sweep that fires roughly every 5 minutes. Don’t build a flow that assumes sub-second or even sub-minute delivery; if you need a synchronous result (e.g. confirming a scan succeeded before continuing), use the relevant REST endpoint’s direct response instead of waiting on a webhook.

Event types and payload shape

Every delivery body has the shape { "event": "…", "data": { … } }, where event is the event type name and data holds the event-specific fields below.

sale.created

Fires when a POS sale completes.

{
  "event": "sale.created",
  "data": {
    "transaction_id": "b4a1...",
    "total": 1450,
    "subtotal": 1400,
    "tax_amount": 50,
    "payment_type": "mpesa",
    "created_at": "2026-07-17T09:12:00.000Z"
  }
}

purchase_order.received

Fires when a purchase order’s status transitions to received.

{
  "event": "purchase_order.received",
  "data": {
    "purchase_order_id": "9f2e...",
    "supplier_id": "1c7d...",
    "total_cost": 32000,
    "received_at": "2026-07-17T09:12:00.000Z"
  }
}

stock.low

Fires once, at the moment an item’s stock quantity crosses at or below its low-stock threshold — not repeatedly on every update while it stays low.

{
  "event": "stock.low",
  "data": {
    "inventory_id": "e83a...",
    "name": "Coca-Cola 500ml",
    "stock_qty": 4,
    "low_stock_threshold": 10
  }
}

connection.approved

Fires when a merchant approves a Connection request — or, on a test key, the instant a sandbox fixture connection is created (POST /api/v1/connections with a test key never reaches a real merchant, but still fires this event so you can verify your receiver before going live). test_mode tells the two apart.

{
  "event": "connection.approved",
  "data": {
    "connection_id": "c1a2b3c4-...",
    "app_id": "d4e5f6a7-...",
    "merchant_email": "owner@example-shop.com",
    "scopes": ["read_inventory"],
    "test_mode": false,
    "approved_at": "2026-07-21T09:12:00.000Z"
  }
}

connection.revoked

Fires when a merchant revokes a Connection — from the original /connect/{token} confirmation page, or from their own AskBiz account’s Connected Apps settings. This is the only way to find out about a merchant-initiated revoke without polling GET /api/v1/connections.

{
  "event": "connection.revoked",
  "data": {
    "connection_id": "c1a2b3c4-...",
    "app_id": "d4e5f6a7-...",
    "merchant_email": "owner@example-shop.com",
    "revoked_at": "2026-08-02T14:03:00.000Z"
  }
}

What’s next

For the request-response endpoints that make up the rest of the API, see API Reference. If you haven’t built anything against AskBiz yet, start with the quickstart.

Webhooks FAQ

Can I create or manage webhooks with an API call instead of the dashboard?+

No. Webhook management (create, list, update, delete) is session-authenticated and only available from the developer.askbiz.co dashboard’s Webhooks page — there’s no x-api-key REST endpoint a third-party server calls to register a webhook. It’s treated as an account setting, not a per-request API action.

How fast are webhook deliveries?+

Not instant. Events are captured the moment the underlying action happens, but delivery to your endpoint runs on a cron sweep that fires roughly every 5 minutes, so plan for delivery latency bounded by about 5 minutes rather than real time.

What happens if I lose my webhook secret?+

The whsec_… secret is shown exactly once, at the moment you create the webhook. There’s no way to view it again afterward — if you lose it, delete the webhook and create a new one to get a fresh secret.

Why did signature verification fail even though the secret is correct?+

The most common cause is computing the HMAC over a re-serialized version of the JSON body instead of the exact raw bytes received — re-serializing can change key order or whitespace and produce a different digest. Compute the signature over the raw request body before any JSON parsing.

Can I test a webhook without waiting for a real sale or stock event?+

Yes — use "Send test event" on the webhook’s row in the dashboard. It enqueues a real delivery through the same signing and cron-delivery path as a live event, and "View deliveries" shows you the resulting attempt and status, so you can confirm your endpoint and signature check work before relying on a real event.