Skip to content

Code examples

Apps API examples in curl and JavaScript: OAuth with PKCE, listing orders and products, a webhook subscription and checking its HMAC signature in Node.js.

On this page

Working calls for the Apps API: the OAuth flow with PKCE, listing orders and products, subscribing to a webhook and checking its signature. Every endpoint, header and field below is the one the API implements; see the API reference for the rest.

Placeholders are written in capitals (YOUR_ACCESS_TOKEN, YOUR_CLIENT_ID, YOUR_WEBHOOK_SECRET). Replace them with your own values, keep real ones out of source control, and never put a client secret in a browser or a mobile app.

The SDK packages are not published yet. @hekana/apps-sdk, hekana-sdk and create-hekana-app are not on npm, so there is no npm install for them and npx create-hekana-app does not work. The examples use plain fetch (Node.js 18 or newer) and node:crypto, which need no package. See Getting started for what exists.

All requests go to https://api.hekana.com/api/v1. A successful reply is { "success": true, "data": ..., "requestId": "..." }, so read data.

1. Sign a store in with OAuth and PKCE

PKCE with S256 is required. Make a random code verifier, send its SHA-256 as the challenge, and keep the verifier on your server until the store owner comes back.

authorize.mjsJavaScript
import { createHash, randomBytes } from "node:crypto";

const codeVerifier = randomBytes(32).toString("base64url");
const codeChallenge = createHash("sha256").update(codeVerifier).digest("base64url");
const state = randomBytes(16).toString("base64url");
// Save codeVerifier and state in the visitor's session, then send them here:

const url = new URL("https://dashboard.hekana.com/oauth/authorize");
url.search = new URLSearchParams({
  client_id: "YOUR_CLIENT_ID",
  redirect_uri: "https://example.com/hekana/callback", // exactly as registered
  scope: "read_orders read_products",                  // space separated
  state,
  code_challenge: codeChallenge,
  code_challenge_method: "S256",
}).toString();

console.log(url.toString());

The store owner sees the app name and the scopes, and approves. The browser returns to your redirect_uri with code and state. Check that state matches before you use code. A code works once and lives 60 seconds.

2. Exchange the code for tokens

callback.mjsJavaScript
const res = await fetch("https://api.hekana.com/api/v1/oauth/token", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    grant_type: "authorization_code",
    code: "CODE_FROM_THE_REDIRECT",
    redirect_uri: "https://example.com/hekana/callback", // the same one
    client_id: "YOUR_CLIENT_ID",
    client_secret: process.env.HEKANA_CLIENT_SECRET,     // confidential apps only
    code_verifier: "THE_VERIFIER_YOU_SAVED",
  }),
});
if (!res.ok) throw new Error(`token exchange failed: ${res.status} ${await res.text()}`);
const { data } = await res.json();
// data: { access_token, token_type: "Bearer", expires_in: 3600, refresh_token, scope }

The access token lasts one hour. Refresh it with the same endpoint and store the new refresh token from every reply, because each one is single use:

Refresh a tokenShell
curl -X POST https://api.hekana.com/api/v1/oauth/token \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "refresh_token",
    "refresh_token": "YOUR_REFRESH_TOKEN",
    "client_id": "YOUR_CLIENT_ID",
    "client_secret": "YOUR_CLIENT_SECRET"
  }'

3. List orders

Needs the read_orders scope. The token names the store, so there is no store id in the request.

List orders (curl)Shell
curl "https://api.hekana.com/api/v1/apps-api/orders?limit=5" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Hekana-Api-Version: 2026-10-01"

data holds { data, total, page, limit, nextCursor }. Each order has id, orderNumber, fulfillmentState, paymentState, deliveryState, totalCents (piastres), customerName and createdAt. The version header is optional.

4. List every product, page by page

Needs read_products. Pass nextCursor back as cursor until it is null; do not build cursors yourself.

products.mjsJavaScript
const base = "https://api.hekana.com/api/v1/apps-api";
const headers = { Authorization: `Bearer ${process.env.HEKANA_ACCESS_TOKEN}` };

let cursor = null;
do {
  const url = new URL(`${base}/products`);
  url.searchParams.set("limit", "100");
  if (cursor) url.searchParams.set("cursor", cursor);

  const res = await fetch(url, { headers });
  if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
  const { data } = await res.json();

  for (const p of data.data) console.log(p.sku, p.nameEn ?? p.nameAr, p.priceCents / 100);
  cursor = data.nextCursor;
} while (cursor);
One product, with its variantsShell
curl "https://api.hekana.com/api/v1/apps-api/products/PRODUCT_ID" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

If a call answers 429, wait for the number of seconds in the Retry-After header. If it answers 401 invalid_token, refresh the token. If it answers 403 with insufficient_scope: ..., the store has not granted that scope.

5. Subscribe to a webhook

Needs no scope, but the topic needs one (orders/paid needs read_orders). The reply holds the signing secret, shown once.

Subscribe (curl)Shell
curl -X POST https://api.hekana.com/api/v1/apps-api/webhooks \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "topic": "orders/paid", "url": "https://example.com/hekana/webhooks" }'

You can send your own secret (24 to 128 characters) in the body instead, so one secret serves every topic. Then send a test event with POST /apps-api/webhooks/WEBHOOK_UUID/test.

6. Verify a webhook signature

X-Hekana-Hmac-SHA256 is the base64 HMAC-SHA256 of the raw request body, made with the subscription's secret. Compute it over the bytes you received, before parsing the JSON, and compare in constant time.

webhook-server.mjsJavaScript
import { createServer } from "node:http";
import { createHmac, timingSafeEqual } from "node:crypto";

const secret = process.env.HEKANA_WEBHOOK_SECRET; // YOUR_WEBHOOK_SECRET

function isFromHekana(rawBody, header) {
  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);
}

createServer((req, res) => {
  const chunks = [];
  req.on("data", (c) => chunks.push(c));
  req.on("end", () => {
    const raw = Buffer.concat(chunks);
    if (!isFromHekana(raw, req.headers["x-hekana-hmac-sha256"])) {
      res.writeHead(401).end();
      return;
    }
    res.writeHead(200).end(); // answer fast, work afterwards
    const event = JSON.parse(raw.toString("utf8"));
    // event: { id, topic, merchant_id, occurred_at, data: { order_id, order_number } }
    console.log(event.topic, event.data);
  });
}).listen(3000);

Payloads carry ids only. Fetch the order with GET /apps-api/orders and your token. A delivery can arrive more than once: deduplicate on the event id. See Webhooks for the topics and retry rules.

Next steps