Developer docs

Shop API

Push delivery orders from your POS, website, or custom backend. Signed HMAC requests, idempotent POSTs, real-time webhooks. Works the same for every Waslni tenant.

1. Overview

The Shop API lets your shop create delivery orders from your own systems — a POS terminal, a website checkout, a WhatsApp bot, anything that can make an HTTPS call. The platform does the rest: assigns a driver, sends push notifications, tracks the delivery, and calls your webhook when the status changes.

What is a “tenant host”?

Each Waslni deployment (one per country/operator) has its own API address — that's the tenant host. It's the same URL the Shop Owner mobile app uses to talk to its backend.

How to find yours: open the Shop Owner app → Shop API Integration → Docs. The docs page will open with your tenant host already filled into every example. If you don't have the app, ask your platform administrator for the URL directly — we don't publish the list here.

Every request in these docs assumes you're hitting https://<your-tenant-host>/api/v2/external/....

At a glance

Base URLhttps://<your-tenant-host>/api/v2/external
TransportHTTPS only (TLS 1.2+)
Body formatJSON (UTF-8)
AuthAPI key + HMAC-SHA256 request signature
Rate limit60 requests/minute per key (default)

Who is this for?

Shop owners who want to push delivery orders programmatically — for example from a POS system, an e-commerce website, or a Zapier/Make integration. Keys are generated per shop and are secret— never embed them in a mobile app or frontend code.

2. Quick Start (5 min)

The fastest way to see it working. Copy-paste, replace 3 values, run.

Step 1 — Get your tenant host

Open the Shop Owner mobile app → tap تكامل الـ API (Shop API Integration). At the top you'll see the URL to use. For example:

https://api.example.com

Step 2 — Create a key

In the same screen tap + New Key. Give it a name (e.g. "My Website"). You'll get two values:

publicKey: wsl_a3f91b2c7d4e8f2091de45af
secretKey: 4c8b2e9f1d3a7b6e5c9f2d8a1b4e7c3f6a9d2e5b8c1f4a7d

Save the secretKey NOW

It is shown only once. If you lose it, you must revoke that key and create a new one.

Step 3 — Create your first delivery order

Here is a complete working curl command. Replace TENANT_HOST, PUBLIC_KEY, and SECRET_KEY with your values.

# ──────────────── 1. Set your values ────────────────
TENANT_HOST="api.example.com"
PUBLIC_KEY="wsl_a3f91b2c7d4e8f2091de45af"
SECRET_KEY="4c8b2e9f1d3a7b6e5c9f2d8a1b4e7c3f6a9d2e5b8c1f4a7d"

# ──────────────── 2. Build the request ──────────────
BODY='{
  "customerName": "Ahmed Ali",
  "customerPhone": "0599123456",
  "deliveryAddress": {
    "lat": 31.9030,
    "lng": 35.2050,
    "address": "Main St 12, Ramallah"
  },
  "packageNote": "2 pizzas"
}'

PATH_="/api/v2/external/orders"
TIMESTAMP=$(date +%s)
BODY_HASH=$(printf '%s' "$BODY" | openssl dgst -sha256 | awk '{print $2}')
STRING_TO_SIGN=$(printf '%s\n%s\n%s\n%s' "$TIMESTAMP" "POST" "$PATH_" "$BODY_HASH")
SIGNATURE=$(printf '%s' "$STRING_TO_SIGN" | openssl dgst -sha256 -hmac "$SECRET_KEY" | awk '{print $2}')

# ──────────────── 3. Send it ────────────────────────
curl -X POST "https://${TENANT_HOST}${PATH_}" \
  -H "x-api-key: $PUBLIC_KEY" \
  -H "x-timestamp: $TIMESTAMP" \
  -H "x-signature: $SIGNATURE" \
  -H "content-type: application/json" \
  -d "$BODY"

Step 4 — Expected response

HTTP/1.1 201 Created

{
  "ok": true,
  "order": {
    "id": 4521,
    "trackingRef": "TRK-20260418-4521",
    "status": "ready_for_pickup",
    "deliveryFee": 12.50,
    "estimatedDeliveryTime": 18,
    "totalPrice": 12.50,
    "createdAt": "2026-04-18T13:02:11.000Z"
  }
}

That's it — you just created a real order. It will appear in the shop dashboard and drivers will be notified. Keep reading to understand why each header matters, how to handle failures, and how to get real-time webhook updates instead of polling.

Prefer Node.js or PHP?

Jump to §8 Code Examples for ready-to-run Node and PHP snippets that do the signing for you.

2. Generating API Keys

Only the master shop owner can create keys. Two ways:

A. From the Shop Owner mobile app

  1. Open the app and go to the shop dashboard.
  2. Tap "Shop API Integration" (تكامل الـ API).
  3. Tap + New Key, give it a descriptive name (e.g. "POS System").
  4. Copy the secretKey shown on the confirmation screen. It is displayed once and never again.

B. From the Admin Panel

Staff with the shopApi.manage permission can create keys on behalf of a shop from Operations → Shop API → [shop] → Keys.

Important

Maximum 5 active keys per shop. Rotate regularly: create a new key, migrate your integration, then revoke the old one.

Anatomy of a key pair

  • publicKey — starts with wsl_. Sent in every request as x-api-key. Safe to log.
  • secretKey — 48-hex-char secret. Store encrypted. Only used to compute the HMAC signature.

3. Authentication & Request Signing

Every request must carry three headers:

HeaderValue
x-api-keyThe public key, e.g. wsl_a3f9...
x-timestampUnix epoch in seconds — current time
x-signatureHMAC-SHA256 hex of the signing string

Signing string

Concatenate with \n (single newline, no trailing newline):

`${timestamp}\n${method}\n${path}\n${bodyHash}`
  • method is uppercase (GET, POST, PATCH, DELETE).
  • path is the full path including query string, e.g. /api/v2/external/orders?page=1.
  • bodyHash is sha256(rawBody) as hex. For GET / DELETE without a body, use sha256("").

Signature

signature = HMAC_SHA256(secretKey, signingString)  // hex output

Clock drift

The server rejects timestamps more than 5 minutes off from its own clock. Keep your servers synced to NTP.

Idempotency (optional but recommended)

On any POST that creates a resource, set:

x-idempotency-key: <unique uuid, stable for 24h>

Retrying the same request within 24 hours with the same key returns the original response with header x-idempotent-replayed: true — no duplicate order is created.

4. Endpoints

POST/orders

Create a delivery order. Driver assignment is automatic.

Request body

{
  "customerName": "Ahmed Ali",
  "customerPhone": "0599123456",
  "deliveryAddress": {
    "lat": 31.9030,
    "lng": 35.2050,
    "address": "Main St 12, Building 4, Ramallah"
  },
  "packageNote": "2 pizzas, 1 soda",
  "prepaidAmount": 0,
  "deliveryNotes": "Call on arrival",
  "referenceId": "YOUR-ORDER-4521"
}

Response (201)

{
  "ok": true,
  "order": {
    "id": 4521,
    "trackingRef": "TRK-20260418-4521",
    "status": "ready_for_pickup",
    "deliveryFee": 12.50,
    "estimatedDeliveryTime": 18,
    "totalPrice": 12.50,
    "createdAt": "2026-04-18T13:02:11.000Z"
  }
}
GET/orders

List your shop's orders. Supports filters.

ParamNotes
statuspending, accepted, preparing, ready_for_pickup, picked_up, delivered, cancelled
createdByuser, shop, admin, api
from / toISO timestamps (default: last 30 days)
page / limitdefault 1 / 20, max limit 100
GET/orders/:id

Full order detail including driver info and timeline.

POST/orders/:id/cancel

Cancel before pickup. Body: { "reason": "customer changed mind" }.

PATCH/orders/:id/address

Change delivery address before pickup. Delivery fee is recalculated.

5. Webhooks

Instead of polling GET /orders, subscribe to events. Register an HTTPS URL in the Shop Owner app under Shop API Integration → Webhooks, pick which events to subscribe to, and we will POST JSON to your URL when they happen.

Events

EventFired when
order_driver_assignedA driver accepts the delivery
order_picked_upDriver has picked up the package
order_deliveredDriver has delivered the package
order_cancelledOrder is cancelled by any party

Webhook payload signature

Each webhook POST carries:

x-wsl-timestampUnix epoch seconds
x-wsl-signatureHMAC-SHA256 of `${timestamp}.${rawBody}` using the webhook secret shown at registration

Verify every webhook

Reject requests where the signature does not match or the timestamp is older than 5 minutes.

Retries

If your endpoint returns non-2xx we retry with exponential backoff up to 3 attempts. Consistent failures move the webhook to failing state; after extended failures it is paused.

6. Errors & Rate Limits

HTTP error codes

CodeMeaning
400VALIDATION_ERROR — missing/invalid field (see details[])
401UNAUTHORIZED — bad API key, bad signature, expired timestamp
403API_SUSPENDED or SHOP_INACTIVE
404NOT_FOUND — order not owned by this shop
409Conflict — e.g. too many active API keys
422INSUFFICIENT_BALANCE, ORDER_NOT_CANCELLABLE
429RATE_LIMITED — retry after 60s
5xxINTERNAL_ERROR — retry with backoff

Rate limits

Default 60 req/min per key. Responses include the standard RateLimit / RateLimit-Remaining / RateLimit-Reset headers (draft-7). Request a higher limit from your tenant's platform staff if you need it.

7. Code Examples

Node.js

const crypto = require('crypto');
const fetch = require('node-fetch');

const API_BASE   = 'https://api.example.com/api/v2/external';
const PUBLIC_KEY = 'wsl_xxxxxxxxxxxx';
const SECRET_KEY = '<secret>';

function sign(method, path, body) {
  const ts       = Math.floor(Date.now() / 1000).toString();
  const raw      = body ? JSON.stringify(body) : '';
  const bodyHash = crypto.createHash('sha256').update(raw).digest('hex');
  const str      = `${ts}\n${method.toUpperCase()}\n${path}\n${bodyHash}`;
  const sig      = crypto.createHmac('sha256', SECRET_KEY).update(str).digest('hex');
  return { ts, sig, raw };
}

async function createOrder(body) {
  const path = '/api/v2/external/orders';
  const { ts, sig, raw } = sign('POST', path, body);
  const res = await fetch(API_BASE + '/orders', {
    method: 'POST',
    headers: {
      'x-api-key':        PUBLIC_KEY,
      'x-timestamp':      ts,
      'x-signature':      sig,
      'x-idempotency-key': crypto.randomUUID(),
      'content-type':     'application/json',
    },
    body: raw,
  });
  return res.json();
}

PHP

<?php
$apiBase   = 'https://api.example.com/api/v2/external';
$publicKey = 'wsl_xxxxxxxxxxxx';
$secretKey = '<secret>';

function callApi($method, $path, $body = null) {
  global $apiBase, $publicKey, $secretKey;
  $ts       = (string) time();
  $raw      = $body === null ? '' : json_encode($body);
  $bodyHash = hash('sha256', $raw);
  $str      = $ts . "\n" . strtoupper($method) . "\n" . $path . "\n" . $bodyHash;
  $sig      = hash_hmac('sha256', $str, $secretKey);

  $ch = curl_init($apiBase . substr($path, strlen('/api/v2/external')));
  curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => $method,
    CURLOPT_HTTPHEADER     => [
      'x-api-key: ' . $publicKey,
      'x-timestamp: ' . $ts,
      'x-signature: ' . $sig,
      'content-type: application/json',
    ],
    CURLOPT_POSTFIELDS     => $raw,
    CURLOPT_RETURNTRANSFER => true,
  ]);
  return json_decode(curl_exec($ch), true);
}

8. Security Best Practices

  • Never embed the secret key in mobile or frontend code. It belongs on a server you control.
  • Use HTTPS for your own webhook endpoints.
  • Rotate keys every 6–12 months, or immediately if a laptop/server is lost.
  • Scope per integration — create one key per integration (POS, website, Zapier) so you can revoke just the affected one.
  • Verify webhook signatures — do not trust payloads without checking x-wsl-signature.
  • Use idempotency keys on all create/cancel operations to survive network retries safely.

If a secret leaks

Revoke the key immediately from the app or admin panel. Any in-flight request signed with the revoked key will be rejected once the cache expires (within ~60 seconds).
What's next

Let's
ride
together.

Start a sandbox with your brand on it — free for seven days. Click through the whole platform before you commit. Or talk to our team first if you\'d rather have a guided walkthrough.

WASLNI / PLATFORM
→ Palestine · Egypt · you next