Webhooks
Subscribe an installed app to Hekana events: the topics, the thin payload, verifying the HMAC signature, retry timing and when a subscription is switched off.
On this page
Hekana sends events about a store to the apps installed on it. An app subscribes with its installation token, using the same Bearer token as the rest of the Apps API. The token decides the installation, so an app only ever sees and changes its own subscriptions.
Manage subscriptions
All under https://api.hekana.com/api/v1/apps-api/webhooks. No scope is needed to manage subscriptions; each topic needs the scope shown below.
| Call | What it does |
|---|---|
GET /webhooks/topics | The topics, the scope each needs (requiredScope) and whether this installation may use it (available). |
POST /webhooks | Subscribe. Body: topic, url, optional secret. |
GET /webhooks | This installation's subscriptions, without secrets. |
DELETE /webhooks/:id | Unsubscribe (204). |
GET /webhooks/:id/deliveries | The latest deliveries (?limit=, 1 to 100, default 50): status, attempts, last status code, last error. |
POST /webhooks/:id/test | Sends a webhook/test event now. Replies { status, delivered }. |
{ "topic": "orders/paid", "url": "https://example.com/hekana/webhooks" }The reply includes id, topic, url, active and secret. The signing secret is shown once. You may instead supply your own secret (24 to 128 characters) so one secret serves every topic. Subscribing again to a topic and URL that Hekana switched off turns it back on with a new secret. An installation can hold at most 50 subscriptions. Limits: 20 subscribes and 10 tests a minute.
Use an https URL. Addresses that resolve to private or internal networks are refused when you subscribe and again on every delivery, and redirects are not followed.
Topics
| Topic | Needs scope | Sent when |
|---|---|---|
orders/created | read_orders | An order is created: checkout, manual order or POS. |
orders/paid | read_orders | An order is paid, on any payment path. |
orders/updated | read_orders | An order's status changes. |
products/created | read_products | A product is created. |
products/updated | read_products | A product, its variants or media change. |
customers/created | read_customers | A customer is created. |
app/uninstalled | none | The store uninstalls your app. |
What arrives
Payloads are thin on purpose: ids only. Fetch the details from the Apps API with your token, so a webhook can never carry more than your scopes allow.
{
"id": "<event id>",
"topic": "orders/created",
"merchant_id": "<store id>",
"occurred_at": "2026-10-19T10:00:00.000Z",
"data": { "order_id": "<order id>", "order_number": 1001 }
}| Topic | data |
|---|---|
orders/created, orders/paid | order_id, order_number |
orders/updated | order_id, order_number, status |
products/created, products/updated | product_id |
customers/created | customer_id (never name, phone or email) |
app/uninstalled | app_id, installation_id |
Headers on every delivery: X-Hekana-Topic, X-Hekana-Webhook-Id (the subscription), X-Hekana-Event-Id (the same on every retry), X-Hekana-Delivery-Attempt and X-Hekana-Hmac-SHA256.
Verify the signature
X-Hekana-Hmac-SHA256 is the base64 HMAC-SHA256 of the raw request body with the subscription's secret. Compute it over the bytes you received, before any JSON parsing, and compare in constant time.
import { createHmac, timingSafeEqual } from "node:crypto";
export function isFromHekana(rawBody: string | Buffer, header: string | undefined, secret: string): boolean {
if (!header) return false;
const expected = createHmac("sha256", secret).update(rawBody).digest();
const given = Buffer.from(header, "base64");
return given.length === expected.length && timingSafeEqual(given, expected);
}verifyWebhook() in @hekana/apps-sdk does the same. Then:
- Deduplicate on
id: a delivery can arrive more than once. - Reject an
occurred_atthat is too old. The body is signed, so the timestamp is too. - Answer
2xxquickly and do the work afterwards. The timeout is 10 seconds.
Delivery and retries
| Your answer | Result |
|---|---|
2xx | Delivered. |
408, 429, 5xx, timeout, no connection | Retried after 1 minute, 5 minutes, 15 minutes, 1 hour, 3 hours, 6 hours, 12 hours and 24 hours: 9 attempts over about 48 hours, then abandoned. |
410 | Abandoned, and the subscription is switched off. |
Any other 4xx | Abandoned for that event; the subscription stays on. |
25 failed deliveries in a row switch a subscription off (too_many_failures); subscribe again to turn it on. A URL that resolves to a refused address switches it off at once (url_blocked). Deliveries are kept for 14 days. Events are queued separately from the action that caused them, so a slow or failing receiver never slows an order or a payment.
Uninstall
When a store uninstalls the app, Hekana sends app/uninstalled once to each distinct endpoint of the installation, drops events still waiting and switches every subscription off. An app with no subscription gets no notice. See Install and uninstall.
Not available
- A signing secret per app that you can rotate: each subscription has its own secret today.
- Delivery metrics, longer retention than 14 days, and alerts when a subscription is switched off.
- Separate store-data erasure requests.