Apps API reference
The Hekana Apps API: endpoints, scopes, request bodies and responses, pagination, idempotency keys, API versions, rate limits and error codes.
On this page
Base URL: https://api.hekana.com/api/v1/apps-api. Everything is scoped to the store and installation the Bearer token names. Money is in piastres (EGP minor units, 100 piastres to the pound).
Conventions
Authentication
Authorization: Bearer <access token>. See Authentication and scopes.
Envelope
Successful replies are { "success": true, "data": ..., "requestId": "..." }. The shapes below describe data. Errors are { "statusCode", "message", "error" }, and where a stable code exists it is the message (for example insufficient_funds).
Version
Send Hekana-Api-Version: 2026-10-01 (optional). The version in use is echoed on every /apps-api reply except the webhook, data and checkout-extension routes. An unknown version answers 400 unsupported_api_version with the supported list. Without the header you get the current version.
Pagination
GET /orders and GET /products return { data, total, page, limit, nextCursor }. Pass nextCursor back as ?cursor= until it is null; do not build cursors yourself. ?limit= is 1 to 100 (default 20). GET /customers returns { data } only, takes ?q= and ?limit=, and has no cursor.
Idempotency
Send Idempotency-Key: <8 to 80 characters of A-Z a-z 0-9 : _ . -> on a POST to the routes in the table below. The first request runs and its answer is kept 24 hours. The same key with the same body returns that answer without running again; the same key with a different body is 422 idempotency_key_reused; a repeat while the first is running is 409 request_in_progress. A failed request is not kept. A malformed key is 400 invalid_idempotency_key. Keys are per installation.
Limits
Each endpoint has its own per-minute limit (in the table), and every installation has 5,000 requests an hour across the API. Past that: 429 hourly_request_limit with a Retry-After header. A global per-IP limit also applies.
Endpoints
| Method and path | Scope | Per minute |
|---|---|---|
GET /orders | read_orders | 60 |
POST /orders | write_orders | 30 |
GET /products, GET /products/:id | read_products | 60 |
GET /customers, GET /customers/:id | read_customers | 60 |
GET /analytics | read_analytics | 60 |
POST /discounts | write_discounts | 30 |
POST /messages | send_messages | 20 |
POST /charge | billing | 30 |
GET /earnings | billing | 60 |
GET /data, GET|PUT|DELETE /data/:key | app_data | PUT 60 |
GET /checkout-extensions, PUT|DELETE /checkout-extensions/:key | checkout_extensions | PUT 30 |
GET /webhooks/topics, GET|POST /webhooks, DELETE /webhooks/:id, GET /webhooks/:id/deliveries, POST /webhooks/:id/test | none | see Webhooks |
Idempotency-Key and the version header apply to the first nine rows (/orders through /earnings). Ids in paths are UUIDs.
Orders
/orders/ordersGET /orders returns, per order: id, orderNumber, fulfillmentState, paymentState, deliveryState, totalCents, customerName, createdAt.
POST /orders places a cash-on-delivery order for one of the store's customers, through the same path as a storefront checkout: the store prices each line from its catalogue, takes stock and honours its discounts and limits. You never send a price or a shipping fee.
{
"reference": "renewal:sub_1042:2026-11",
"customerId": "<customer uuid>",
"items": [{ "productId": "<product uuid>", "variantId": "<variant uuid, optional>", "quantity": 2 }],
"shippingAddressId": "<saved address uuid, optional>",
"discountCode": "<existing code, optional>"
}reference: 8 to 80 characters of letters, digits and: _ . -. Your own id for the order. Sending it again returns the first order instead of a second.items: 1 to 50 lines,quantity1 to 9999.shippingAddressIddefaults to the customer's default address; a customer with no saved address is a400.notesis accepted by validation but is not applied to the order.
The reply (201) is { id, orderNumber, status, totalCents }.
Products and customers
/products/products/:id/customers/customers/:idGET /products items: id, sku, slug, nameAr, nameEn, status, priceCents. GET /products/:id adds variants: id, sku, name, optionValues, priceCents (null when the variant sells at the product price), comparePriceCents, imageUrl, isActive. Cost data is never included.
GET /customers items: id, nameAr, nameEn, phone, email, totalOrders. GET /customers/:id adds addresses: id, label, recipientName, phone, governorate, city, district, street, building, isDefault.
GET /analytics returns { totalOrders, totalProducts }. That is all it returns today.
Discounts
/discountsPOST /discounts creates a code on the store, through the same rules as a code the owner makes by hand.
{
"code": "WINBACK10",
"type": "percentage",
"value": 10,
"productIds": ["<product uuid>"],
"perCustomerLimit": 1,
"totalUsageLimit": 100,
"expiresAt": "2026-12-31T00:00:00.000Z"
}type is percentage (value 1 to 100) or fixed_amount (value in piastres). productIds is required (1 to 500, all from this store), so a code can never cover the whole catalogue. Automatic, free-shipping, buy-one-get-one and tiered discounts cannot be made. Reply (201): { id, code, type, isActive }.
Messages
/messagesPOST /messages sends one customer a short email or WhatsApp message.
{ "customerId": "<customer uuid>", "channel": "email", "subject": "Your order", "text": "Your reorder is ready." }text is 1 to 500 characters; subject (email only) up to 100. Links are refused, in the text and the subject, including anything that looks like a domain (400 links_not_allowed). The message goes through the store's own sending rules and allowance, and carries a footer naming your app. Each installation may send 200 a day (429 daily_message_limit). Reply: { sent: true }, or { sent: false, reason } where reason is no_contact (the customer has no address for that channel) or not_sent.
Charges
POST /charge and GET /earnings are covered in Billing and pricing.
App data
A private key-value store for the installation, deleted when the store uninstalls.
- Keys: 1 to 128 characters of
A-Z a-z 0-9 _ . : -. Values: any JSON, up to 64 KB each. At most 500 keys and 1 MB in all per installation. PUT /data/:keywith body{ "value": ... }replies{ key, sizeBytes }.GET /data/:keyreplies{ key, value, updatedAt }.GET /data?prefix=lists{ key, sizeBytes, updatedAt }.DELETEreplies204.- Errors:
invalid_key,invalid_value,key_not_found(404),value_too_large,too_many_keys,store_full(413).
Checkout extensions
A checkbox on the store's checkout that offers one of the store's own products. Ticking it adds that product to the cart as a normal line, so the order path prices it; an extension never sends a price or changes a total.
{ "labelAr": "<label shown in Arabic>", "labelEn": "Add gift wrap", "productId": "<product uuid>", "enabled": true }- The key is 1 to 64 characters of
A-Z a-z 0-9 _ . -.labelAris required (up to 120 characters),labelEnoptional. - The product must be active, simple (no variants) and priced. Otherwise:
product_not_found,product_not_active,product_has_variants,product_has_no_price. - Three per store across all apps (
too_many_extensions). Extensions are removed when the app is uninstalled. GETlists the app's own extensions:{ key, labelAr, labelEn, productId, enabled }.
Error codes
| Status | message | Meaning |
|---|---|---|
| 400 | unsupported_api_version | The version header names a version that does not exist. |
| 400 | invalid_idempotency_key | The key is not 8 to 80 allowed characters. |
| 400 | links_not_allowed | A message contains a link. |
| 401 | missing_bearer_token, invalid_token | No token, or the token is expired, revoked or unusable. |
| 403 | insufficient_scope: <scope> | The installation was not granted that scope. |
| 404 | Customer not found. and similar | The id does not exist in this store. |
| 409 | request_in_progress | An identical request with this key is still running. |
| 422 | idempotency_key_reused | The key was used with a different body. |
| 429 | hourly_request_limit, daily_message_limit | A budget is spent; see Retry-After on the first. |
| 503 | message_cap_unavailable | The daily message counter is down; nothing was sent. |
Billing errors are in Billing and pricing.