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

  1. Receipt in your system: your software sends POST /api/v1/orders and gets back the order number and a claim_url.
  2. 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.
  3. Ready: when the order is done your software sends POST /api/v1/orders/{id}/ready and 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.

  1. Sign in to the dashboard as the shop owner. Staff accounts don’t see this tab.
  2. Open the “Integrations” tab (in Arabic: «الربط»).
  3. In the card “API keys for your POS” type a name you will recognise, e.g. “Main POS”.
  4. Press “Create key”.
  5. 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.
  6. 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.
Security: anyone with the key can create orders and change their status in your shop. Don’t send it over WhatsApp and never put it in browser or mobile-app code. Keep it on the POS server only.

Webhook URL (optional)

  1. In the same tab, the card “Webhook: notify your system”.
  2. Type “Receiving URL (https only)”, e.g. https://pos.example.com/dawri-webhook, then “Save URL”.
  3. Press “Show secret” to copy the whsec_… secret used to verify signatures. (“Rotate secret” replaces it and the old one stops working.)
  4. Press “Send test”: a ping event 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

ItemValue
Base URL BASE_URLhttps://your-domain (the same address you open Dawri at, no trailing slash)
Auth headerAuthorization: Bearer dwr_XXXXXXXXXXXXXXXXXXXXXXXX
Content typeContent-Type: application/json
Suggested variable for the keyDAWRI_API_KEY
Suggested variable for the addressDAWRI_BASE_URL
Suggested variable for the webhook secretDAWRI_WEBHOOK_SECRET (value whsec_…)
Create-order fieldsref · name · show_on_tablet
Response fieldsorder · claim_url · claim_expires_at · on_tablet · duplicate
Action names (end of the path)accept · preparing · ready · collected · cancel · reject
Webhook headersX-Dawri-Event · X-Dawri-Signature
Webhook eventsorder.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.

PathPurposeBody / response
GET /api/v1/meTest the key, see the shopResponse: {shop, name, mode}
POST /api/v1/ordersCreate an order with a new numberBody (all optional):
ref string up to 60 chars: your receipt number
name up to 30 chars: customer name
show_on_tablet false to keep it off the customer’s iPad
201 response: order · claim_url · claim_expires_at · on_tablet
GET /api/v1/ordersCurrent orders (recently collected included)Optional ?status=ready. Response: {orders:[{id, number, status, name, ref, source}]}
GET /api/v1/orders/{id}One orderResponse: {order}
POST /api/v1/orders/{id}/acceptAccept a pending order (becomes “preparing”)Response: {order}
POST /api/v1/orders/{id}/preparingMove a ready order back to “preparing”Response: {order}
POST /api/v1/orders/{id}/readyOrder is ready: the customer’s phone ringsResponse: {order}
POST /api/v1/orders/{id}/collectedThe customer took the orderResponse: {order}
POST /api/v1/orders/{id}/cancelCancel the orderOptional body: {"reason":"fake"}, "left" or "other"
POST /api/v1/orders/{id}/rejectReject a pending orderResponse: {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)

FieldTypeMeaning
idnumberOrder id inside Dawri. Use it in the paths.
numbernumber 1–999The number the customer sees and is called by. Starts at 1 each day (or continuous, per the shop’s setting).
statusstringpending · preparing · ready · collected · cancelled · rejected
namestring or nullCustomer name, if sent.
refstring or nullYour receipt number, if sent.
sourcestringapi, manual (from the cashier) or qr (customer scanned on their own).
cancel_reasonstring or nullfake · left · other · rejected
created_at · ready_at · collected_atnumber or nullTime of the event in milliseconds (Unix epoch).
phone_connectedtrue/falseWhether 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

FromTo (action)
pendingpreparing (accept) · rejected (reject) · cancelled (cancel)
preparingready (ready) · cancelled (cancel)
readycollected (collected) · preparing (preparing) · cancelled (cancel)
collectedready (ready: to call again)

Orders created through the API start directly as preparing. Any other transition returns 409.

8. Errors

CodeerrorCause and fix
401invalid_api_keyThe key is missing, wrong or revoked, or the header isn’t in the form Bearer dwr_….
403inactiveThe shop’s subscription has ended or is suspended. Renew it from the dashboard.
404not_found · unknown_actionThe order isn’t in your shop, or the action name is wrong.
409invalid_transitionA transition that isn’t allowed (e.g. “ready” on a cancelled order). The response carries the current status.
429rate_limitedMore 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 typeWhen
order.createdAn 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.acceptedThe order moved to “preparing”.
order.readyThe order is ready (the customer’s phone rang).
order.collectedThe order was handed over.
order.cancelledThe order was cancelled. Reason in cancel_reason.
order.rejectedA pending order was rejected.
order.reopenedA ready order was put back to “preparing”.
pingThe “Send test” button. data holds {"message":"Hello from Dawri"}.

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"])) < 300

PHP:

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:

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

Need help connecting? Tell us which POS you use and we’ll suggest the best way. Contact us on WhatsApp