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-sdkandcreate-hekana-appare not on npm, so there is nonpm installfor them andnpx create-hekana-appdoes not work. The examples use plainfetch(Node.js 18 or newer) andnode: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.
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
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:
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.
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"const res = await fetch("https://api.hekana.com/api/v1/apps-api/orders?limit=5", {
headers: {
Authorization: `Bearer ${process.env.HEKANA_ACCESS_TOKEN}`,
"Hekana-Api-Version": "2026-10-01",
},
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
const { data } = await res.json();
for (const order of data.data) {
console.log(order.orderNumber, order.paymentState, order.totalCents / 100, "EGP");
}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.
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);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.
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.
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.