For developers
Connect your POS to Dawri
Instead of the cashier tapping “Ready” in the Dawri screen, your own software calls the API: it creates the order number when the receipt is issued and sends “ready” when the food is done, and the customer’s phone rings. If you also want your system to know everything that happens in Dawri, it can receive signed events on a URL you choose (webhook).
1. The idea in three steps
- Receipt in your system: your software sends
POST /api/v1/ordersand gets back the ordernumberand aclaim_url. - The customer scans the code: your system (or the iPad screen) shows a QR for that link; the customer scans it and their phone gets tied to that number.
- Ready: when the order is done your software sends
POST /api/v1/orders/{id}/readyand the customer’s phone rings.
No app is needed on the customer’s phone. Nothing else changes in the shop: the cashier can still work from the Dawri screen if the integration goes down.
2. Creating the API key in the dashboard
The key is the “password” your POS uses to prove it may speak for your shop. You create it yourself, no developer needed.
- Sign in to the dashboard as the shop owner. Staff accounts don’t see this tab.
- Open the “Integrations” tab (in Arabic: «الربط»).
- In the card “API keys for your POS” type a name you will recognise, e.g. “Main POS”.
- Press “Create key”.
- A green box shows the key, starting with
dwr_. Press “Copy” and put it in your POS settings. It is shown only once. Dawri keeps a one-way hash and cannot recover it. If it is lost, revoke it and create another. - You can have up to 5 keys (e.g. one per branch or system). Each shows its name, its first characters and its last use; “Revoke” stops it immediately.
Webhook URL (optional)
- In the same tab, the card “Webhook: notify your system”.
- Type “Receiving URL (https only)”, e.g.
https://pos.example.com/dawri-webhook, then “Save URL”. - Press “Show secret” to copy the
whsec_…secret used to verify signatures. (“Rotate secret” replaces it and the old one stops working.) - Press “Send test”: a
pingevent reaches your system and the result appears under “Recent deliveries”.
For the manual receipt number without any API: tab «الكاشير والشاشة» (Cashier & screen) → “Cashier setup” → “POS receipt number”.
3. Names to copy as they are
| Item | Value |
|---|---|
Base URL BASE_URL | https://your-domain (the same address you open Dawri at, no trailing slash) |
| Auth header | Authorization: Bearer dwr_XXXXXXXXXXXXXXXXXXXXXXXX |
| Content type | Content-Type: application/json |
| Suggested variable for the key | DAWRI_API_KEY |
| Suggested variable for the address | DAWRI_BASE_URL |
| Suggested variable for the webhook secret | DAWRI_WEBHOOK_SECRET (value whsec_…) |
| Create-order fields | ref · name · show_on_tablet |
| Response fields | order · claim_url · claim_expires_at · on_tablet · duplicate |
| Action names (end of the path) | accept · preparing · ready · collected · cancel · reject |
| Webhook headers | X-Dawri-Event · X-Dawri-Signature |
| Webhook events | order.created · order.accepted · order.ready · order.collected · order.cancelled · order.rejected · order.reopened · ping |
4. The full flow (copy and run)
A. Check that the key works:
curl https://YOUR-DOMAIN/api/v1/me \
-H "Authorization: Bearer dwr_XXXX"
# → { "shop": "my-cafe", "name": "Morning Coffee", "mode": "cafe" }B. Create the order when the receipt is issued:
curl -X POST https://YOUR-DOMAIN/api/v1/orders \
-H "Authorization: Bearer dwr_XXXX" \
-H "Content-Type: application/json" \
-d '{"ref":"POS-1001","name":"Saad"}'
# 201 →
{
"order": { "id": 57, "number": 24, "status": "preparing", "name": "Saad", "ref": "POS-1001",
"source": "api", "cancel_reason": null, "created_at": 1789000000000,
"ready_at": null, "collected_at": null, "phone_connected": false },
"on_tablet": true,
"claim_url": "https://YOUR-DOMAIN/c/my-cafe?claim=Kq3xT9...",
"claim_expires_at": 1789007200000
}Keep order.id (57 here): it is what you use in the other calls. Show order.number to the customer on the receipt. claim_url is what the customer scans (see section 11).
C. When the food is done:
curl -X POST https://YOUR-DOMAIN/api/v1/orders/57/ready \
-H "Authorization: Bearer dwr_XXXX"
# 200 → { "order": { "id": 57, "number": 24, "status": "ready", ... } }D. When the customer collects (optional): POST /api/v1/orders/57/collected.
5. Endpoints
All paths are under /api/v1 and need the Authorization header. Requests and responses are JSON.
| Path | Purpose | Body / response |
|---|---|---|
GET /api/v1/me | Test the key, see the shop | Response: {shop, name, mode} |
POST /api/v1/orders | Create an order with a new number | Body (all optional):ref string up to 60 chars: your receipt numbername up to 30 chars: customer nameshow_on_tablet false to keep it off the customer’s iPad201 response: order · claim_url · claim_expires_at · on_tablet |
GET /api/v1/orders | Current orders (recently collected included) | Optional ?status=ready. Response: {orders:[{id, number, status, name, ref, source}]} |
GET /api/v1/orders/{id} | One order | Response: {order} |
POST /api/v1/orders/{id}/accept | Accept a pending order (becomes “preparing”) | Response: {order} |
POST /api/v1/orders/{id}/preparing | Move a ready order back to “preparing” | Response: {order} |
POST /api/v1/orders/{id}/ready | Order is ready: the customer’s phone rings | Response: {order} |
POST /api/v1/orders/{id}/collected | The customer took the order | Response: {order} |
POST /api/v1/orders/{id}/cancel | Cancel the order | Optional body: {"reason":"fake"}, "left" or "other" |
POST /api/v1/orders/{id}/reject | Reject a pending order | Response: {order} |
Retrying is safe: if your system sends the same ref twice on the same day while that order is still active, Dawri returns the same order with "duplicate": true and does not issue a new number. Always put your receipt number in ref so retries after a network drop don’t create duplicates.
6. The order object (order)
| Field | Type | Meaning |
|---|---|---|
id | number | Order id inside Dawri. Use it in the paths. |
number | number 1–999 | The number the customer sees and is called by. Starts at 1 each day (or continuous, per the shop’s setting). |
status | string | pending · preparing · ready · collected · cancelled · rejected |
name | string or null | Customer name, if sent. |
ref | string or null | Your receipt number, if sent. |
source | string | api, manual (from the cashier) or qr (customer scanned on their own). |
cancel_reason | string or null | fake · left · other · rejected |
created_at · ready_at · collected_at | number or null | Time of the event in milliseconds (Unix epoch). |
phone_connected | true/false | Whether the customer’s page is open right now or notifications were allowed. It becomes true after the customer scans and keeps the page open; scanning alone does not set it. |
7. Statuses and allowed transitions
| From | To (action) |
|---|---|
pending | preparing (accept) · rejected (reject) · cancelled (cancel) |
preparing | ready (ready) · cancelled (cancel) |
ready | collected (collected) · preparing (preparing) · cancelled (cancel) |
collected | ready (ready: to call again) |
Orders created through the API start directly as preparing. Any other transition returns 409.
8. Errors
| Code | error | Cause and fix |
|---|---|---|
| 401 | invalid_api_key | The key is missing, wrong or revoked, or the header isn’t in the form Bearer dwr_…. |
| 403 | inactive | The shop’s subscription has ended or is suspended. Renew it from the dashboard. |
| 404 | not_found · unknown_action | The order isn’t in your shop, or the action name is wrong. |
| 409 | invalid_transition | A transition that isn’t allowed (e.g. “ready” on a cancelled order). The response carries the current status. |
| 429 | rate_limited | More than 120 requests a minute on this key. Wait a minute. |
Error shape: {"error":"invalid_transition","status":"collected"}. Some errors add extra fields such as status.
9. Webhook: Dawri tells your system
If you saved a receiving URL, Dawri sends a JSON POST to it on every event. Useful when the cashier taps “Ready” in the Dawri screen and your system should know, or to log orders that customers created themselves.
POST https://pos.example.com/dawri-webhook
Content-Type: application/json
X-Dawri-Event: order.ready
X-Dawri-Signature: t=1789000300,v1=9f2c...e7a1
{
"id": "evt_3fa91c0b7d2e4a11",
"type": "order.ready",
"created": 1789000300,
"shop": "my-cafe",
"data": { "id": 57, "number": 24, "status": "ready", "name": "Saad", "ref": "POS-1001",
"source": "api", "cancel_reason": null, "created_at": 1789000000000,
"ready_at": 1789000300000, "collected_at": null, "phone_connected": true }
}Event type | When |
|---|---|
order.created | An order was created from the cashier screen or by a customer scan. (Orders your system created through the API do not send this event, since you already know them.) |
order.accepted | The order moved to “preparing”. |
order.ready | The order is ready (the customer’s phone rang). |
order.collected | The order was handed over. |
order.cancelled | The order was cancelled. Reason in cancel_reason. |
order.rejected | A pending order was rejected. |
order.reopened | A ready order was put back to “preparing”. |
ping | The “Send test” button. data holds {"message":"Hello from Dawri"}. |
- Reply with any
2xxwithin 8 seconds. Otherwise Dawri retries up to 3 times (after 1 s, then 4 s). - The same event can arrive more than once. Use
id(likeevt_…) to ignore duplicates. - The URL must be
httpson a public host. Internal addresses (such as192.168.x.xorlocalhost) are refused to protect you.
10. Verifying the signature
Always check that a request really comes from Dawri. The X-Dawri-Signature header looks like t=<unix>,v1=<hex>, and the signature is HMAC-SHA256 with your whsec_… secret over the text: t + a dot + the raw body exactly as received (before any JSON parsing).
Node.js:
const crypto = require('node:crypto');
function verify(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
const expected = crypto.createHmac('sha256', secret)
.update(parts.t + '.' + rawBody).digest('hex');
const ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300; // reject anything older than 5 minutes
return ok && fresh;
}
// Express: use express.raw({ type: 'application/json' }) to get the raw body
app.post('/dawri-webhook', express.raw({ type: 'application/json' }), (req, res) => {
if (!verify(req.body.toString('utf8'), req.get('X-Dawri-Signature') || '', process.env.DAWRI_WEBHOOK_SECRET))
return res.sendStatus(401);
const evt = JSON.parse(req.body);
// evt.type, evt.data.number, evt.data.ref ...
res.sendStatus(200);
});Python:
import hmac, hashlib, time
def verify(raw_body: bytes, header: str, secret: str) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
signed = parts["t"].encode() + b"." + raw_body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts["v1"]) and abs(time.time() - int(parts["t"])) < 300PHP:
function verify(string $raw, string $header, string $secret): bool {
parse_str(str_replace(',', '&', $header), $p); // t=...&v1=...
$expected = hash_hmac('sha256', $p['t'] . '.' . $raw, $secret);
return hash_equals($expected, $p['v1']) && abs(time() - (int)$p['t']) < 300;
}
$raw = file_get_contents('php://input');
if (!verify($raw, $_SERVER['HTTP_X_DAWRI_SIGNATURE'] ?? '', getenv('DAWRI_WEBHOOK_SECRET'))) { http_response_code(401); exit; }11. How the customer gets the QR
You have three options, all using the claim_url returned when the order is created:
- On the customer’s iPad (no extra code): if the shop uses the customer screen in its default mode, the new order’s code appears on it automatically and
on_tabletistrue. Send"show_on_tablet": falseif you don’t want that. - A ready-made image from Dawri: put this URL in an
<img>on your screen or print it on the receipt (no key needed):https://YOUR-DOMAIN/api/qr.svg?d=ENCODED_CLAIM_URL // JavaScript const img = BASE_URL + '/api/qr.svg?d=' + encodeURIComponent(claim_url); - A QR library in your software: generate the code from the
claim_urltext itself.
The link is valid for two hours (claim_expires_at) and for one phone only. Someone who photographs it and sends it on won’t get it to work, and it can’t be reused.
12. Limits and notes
- Rate: 120 requests per minute per key, and up to 5 keys per shop.
- Subscription: once it ends, every call returns
403 inactiveuntil renewed. - Isolation: a key can only see and change its own shop’s orders.
- Time: times in the order JSON are in milliseconds, while
createdin the webhook is in seconds. - Testing: the API, the webhook and the signature are covered by automated tests. They have not been tried with a specific commercial POS, since each has its own integration method. Try it on a test shop first.