Skip to content

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.

CallWhat it does
GET /webhooks/topicsThe topics, the scope each needs (requiredScope) and whether this installation may use it (available).
POST /webhooksSubscribe. Body: topic, url, optional secret.
GET /webhooksThis installation's subscriptions, without secrets.
DELETE /webhooks/:idUnsubscribe (204).
GET /webhooks/:id/deliveriesThe latest deliveries (?limit=, 1 to 100, default 50): status, attempts, last status code, last error.
POST /webhooks/:id/testSends a webhook/test event now. Replies { status, delivered }.
POST /webhooks (request body)JSON
{ "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

TopicNeeds scopeSent when
orders/createdread_ordersAn order is created: checkout, manual order or POS.
orders/paidread_ordersAn order is paid, on any payment path.
orders/updatedread_ordersAn order's status changes.
products/createdread_productsA product is created.
products/updatedread_productsA product, its variants or media change.
customers/createdread_customersA customer is created.
app/uninstallednoneThe 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.

orders/createdJSON
{
  "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 }
}
Topicdata
orders/created, orders/paidorder_id, order_number
orders/updatedorder_id, order_number, status
products/created, products/updatedproduct_id
customers/createdcustomer_id (never name, phone or email)
app/uninstalledapp_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.

verify.tsTypeScript
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_at that is too old. The body is signed, so the timestamp is too.
  • Answer 2xx quickly and do the work afterwards. The timeout is 10 seconds.

Delivery and retries

Your answerResult
2xxDelivered.
408, 429, 5xx, timeout, no connectionRetried 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.
410Abandoned, and the subscription is switched off.
Any other 4xxAbandoned 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.

Next steps