Skip to content

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 pathScopePer minute
GET /ordersread_orders60
POST /orderswrite_orders30
GET /products, GET /products/:idread_products60
GET /customers, GET /customers/:idread_customers60
GET /analyticsread_analytics60
POST /discountswrite_discounts30
POST /messagessend_messages20
POST /chargebilling30
GET /earningsbilling60
GET /data, GET|PUT|DELETE /data/:keyapp_dataPUT 60
GET /checkout-extensions, PUT|DELETE /checkout-extensions/:keycheckout_extensionsPUT 30
GET /webhooks/topics, GET|POST /webhooks, DELETE /webhooks/:id, GET /webhooks/:id/deliveries, POST /webhooks/:id/testnonesee Webhooks

Idempotency-Key and the version header apply to the first nine rows (/orders through /earnings). Ids in paths are UUIDs.

Orders

GET/ordersScope read_orders · 60 a minute
POST/ordersScope write_orders · 30 a minute

GET /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.

POST /orders (request body)JSON
{
  "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, quantity 1 to 9999.
  • shippingAddressId defaults to the customer's default address; a customer with no saved address is a 400.
  • notes is accepted by validation but is not applied to the order.

The reply (201) is { id, orderNumber, status, totalCents }.

Products and customers

GET/productsScope read_products · 60 a minute
GET/products/:idScope read_products · 60 a minute
GET/customersScope read_customers · 60 a minute
GET/customers/:idScope read_customers · 60 a minute

GET /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

POST/discountsScope write_discounts · 30 a minute

POST /discounts creates a code on the store, through the same rules as a code the owner makes by hand.

POST /discounts (request body)JSON
{
  "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

POST/messagesScope send_messages · 20 a minute

POST /messages sends one customer a short email or WhatsApp message.

POST /messages (request body)JSON
{ "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/:key with body { "value": ... } replies { key, sizeBytes }. GET /data/:key replies { key, value, updatedAt }. GET /data?prefix= lists { key, sizeBytes, updatedAt }. DELETE replies 204.
  • 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.

PUT /checkout-extensions/gift-wrap (request body)JSON
{ "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 _ . -. labelAr is required (up to 120 characters), labelEn optional.
  • 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.
  • GET lists the app's own extensions: { key, labelAr, labelEn, productId, enabled }.

Error codes

StatusmessageMeaning
400unsupported_api_versionThe version header names a version that does not exist.
400invalid_idempotency_keyThe key is not 8 to 80 allowed characters.
400links_not_allowedA message contains a link.
401missing_bearer_token, invalid_tokenNo token, or the token is expired, revoked or unusable.
403insufficient_scope: <scope>The installation was not granted that scope.
404Customer not found. and similarThe id does not exist in this store.
409request_in_progressAn identical request with this key is still running.
422idempotency_key_reusedThe key was used with a different body.
429hourly_request_limit, daily_message_limitA budget is spent; see Retry-After on the first.
503message_cap_unavailableThe daily message counter is down; nothing was sent.

Billing errors are in Billing and pricing.

Next steps