# 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.

Source: https://hekana.dev/apps/examples

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](https://hekana.dev/apps/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](https://hekana.dev/apps/quickstart) 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.

```js title="authorize.mjs"
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

```js title="callback.mjs"
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:

```bash title="Refresh a token"
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.

```bash title="List orders (curl)"
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"
```

```js title="orders.mjs"
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.

```js title="products.mjs"
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);
```

```bash title="One product, with its variants"
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.

```bash title="Subscribe (curl)"
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.

```js title="webhook-server.mjs"
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](https://hekana.dev/apps/webhooks) for the topics and retry rules.
