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”?
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 URL | https://<your-tenant-host>/api/v2/external |
|---|---|
| Transport | HTTPS only (TLS 1.2+) |
| Body format | JSON (UTF-8) |
| Auth | API key + HMAC-SHA256 request signature |
| Rate limit | 60 requests/minute per key (default) |
Who is this for?
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.comStep 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: 4c8b2e9f1d3a7b6e5c9f2d8a1b4e7c3f6a9d2e5b8c1f4a7dSave the secretKey NOW
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?
2. Generating API Keys
Only the master shop owner can create keys. Two ways:
A. From the Shop Owner mobile app
- Open the app and go to the shop dashboard.
- Tap "Shop API Integration" (تكامل الـ API).
- Tap + New Key, give it a descriptive name (e.g. "POS System").
- Copy the
secretKeyshown 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
Anatomy of a key pair
publicKey— starts withwsl_. Sent in every request asx-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:
| Header | Value |
|---|---|
x-api-key | The public key, e.g. wsl_a3f9... |
x-timestamp | Unix epoch in seconds — current time |
x-signature | HMAC-SHA256 hex of the signing string |
Signing string
Concatenate with \n (single newline, no trailing newline):
`${timestamp}\n${method}\n${path}\n${bodyHash}`methodis uppercase (GET,POST,PATCH,DELETE).pathis the full path including query string, e.g./api/v2/external/orders?page=1.bodyHashissha256(rawBody)as hex. For GET / DELETE without a body, usesha256("").
Signature
signature = HMAC_SHA256(secretKey, signingString) // hex outputClock drift
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
/ordersCreate 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"
}
}/ordersList your shop's orders. Supports filters.
| Param | Notes |
|---|---|
status | pending, accepted, preparing, ready_for_pickup, picked_up, delivered, cancelled |
createdBy | user, shop, admin, api |
from / to | ISO timestamps (default: last 30 days) |
page / limit | default 1 / 20, max limit 100 |
/orders/:idFull order detail including driver info and timeline.
/orders/:id/cancelCancel before pickup. Body: { "reason": "customer changed mind" }.
/orders/:id/addressChange 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
| Event | Fired when |
|---|---|
order_driver_assigned | A driver accepts the delivery |
order_picked_up | Driver has picked up the package |
order_delivered | Driver has delivered the package |
order_cancelled | Order is cancelled by any party |
Webhook payload signature
Each webhook POST carries:
x-wsl-timestamp | Unix epoch seconds |
x-wsl-signature | HMAC-SHA256 of `${timestamp}.${rawBody}` using the webhook secret shown at registration |
Verify every webhook
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
| Code | Meaning |
|---|---|
| 400 | VALIDATION_ERROR — missing/invalid field (see details[]) |
| 401 | UNAUTHORIZED — bad API key, bad signature, expired timestamp |
| 403 | API_SUSPENDED or SHOP_INACTIVE |
| 404 | NOT_FOUND — order not owned by this shop |
| 409 | Conflict — e.g. too many active API keys |
| 422 | INSUFFICIENT_BALANCE, ORDER_NOT_CANCELLABLE |
| 429 | RATE_LIMITED — retry after 60s |
| 5xx | INTERNAL_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
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.