Connect Dawwerni to your POS, website, CRM or automation tools: loyalty points on every sale, customers kept in sync, gifts verified at the counter and events pushed to your server in real time.
Every request carries your API key in the Authorization header (Bearer) or in X-Api-Key. Everything is HTTPS and JSON.
HTTP headers
Authorization: Bearer dw_live_a1b2c3d4••••••••••••••••••••••••••••••••••••••••
# or
X-Api-Key: dw_live_a1b2c3d4••••••••••••••••••••••••••••••••••••••••
Use the key on your servers only: the API does not accept browser calls (CORS), and a key shipped in a website or mobile app would be exposed.
A key only works within its permissions and businesses; otherwise the API answers 403.
If a key covers several businesses, send businessId in the request (otherwise 400 BUSINESS_REQUIRED).
A revoked key stops working immediately (401 API_KEY_REVOKED). Up to 20 active keys per account.
Permissions (scopes)
Permission
Allows
customers:read
Look up customers and their points
customers:write
Create or update customers
points:write
Record purchases and award points
rewards:read
List loyalty rewards
redemptions:write
Redeem rewards with points
winnings:verify
Verify and hand over gifts
webhooks:manage
Manage webhook endpoints
Conventions
Base URL
https://app.dawwerni.com/api/ext/v1 — JSON only (Content-Type: application/json on POST).
Dates
ISO 8601. Customer-account fields (createdAt, updatedAt) are local time without an offset; the others (keys, webhooks, logs) are UTC with a “Z”.
Phone numbers
International format recommended (+21698123456). “00216…” and spaces are accepted; a customer is matched by email first, then by phone.
Pagination
Lists accept page (from 0) and size (default 20, max 100) and return data + page.
Idempotency
Add an Idempotency-Key header (a UUID) to every POST. Replaying the same call within 24 h returns the original response with Idempotent-Replayed: true and does not run the operation again.
Rate limit
120 requests per minute per key. Above that: HTTP 429 with a Retry-After header.
Versioning
v1 only receives additive changes (new fields, new endpoints). Ignore unknown fields in responses.
Records a POS receipt and credits points with your loyalty program’s rules. The customer is created when missing.
externalId identifies the receipt: sending the same externalId again returns the first result with "duplicate": true and awards nothing.
Without "points", the amount is converted by your loyalty card rule (points per unit spent). Turn the loyalty card on in Loyalty; otherwise pointsAwarded is 0.
Refunds do not remove points: only send sales.
Parameters
Name
In
Type
Description
externalId*
body
string ≤ 120
Unique receipt id in your POS (required)
amount*
body
number ≥ 0
Amount paid (required)
customer*
body
{ id | phone | email, name }
Customer: Dawwerni id, or phone / email (+ name) (required)
currency
body
ISO 4217 (TND, EGP, SAR…)
ISO currency (TND, EGP, SAR, USD…) (optional)
points
body
integer 0–1 000 000
Fixed points (otherwise computed by your rule) (optional)
items
body
[{ sku, name, quantity, unitPrice }]
Receipt lines (only their count is stored) (optional)
businessId
body
integer
Required when the key covers several businesses (optional)
422 Unprocessable Entity ResponseReal API response (test data)
{
"error": {
"code": "NO_PARTICIPATION_FOR_CUSTOMER",
"message": "This customer has no participation yet; redemptions through the API need at least one"
}
}
Verify a gift
POST/api/ext/v1/winnings/verify
Permission: winnings:verify
Same as scanning the QR in the app: checks the code, marks the gift as handed over and returns the winner.
code accepts the QR token, the full /verify-winning/… URL or the 8-character reference from the winner email.
A gift already handed over returns 200 with "verified": false and "alreadyClaimed": true.
Parameters
Name
In
Type
Description
code*
body
string
QR token, verify URL or 8-character reference (required)
204 No Content ResponseReal API response (test data)
Error codes
Every error has the same shape. Rely on the code (stable); the English message may change.
403 ForbiddenReal API response (test data)
{
"error": {
"code": "INSUFFICIENT_SCOPE",
"message": "This API key is missing the scope points:write"
}
}
HTTP
Code
Meaning
400
VALIDATION_FAILED
Missing or malformed parameter
400
INVALID_JSON
The body is not valid JSON
400
CUSTOMER_IDENTIFIER_REQUIRED
Phone or email required
400
INVALID_PHONE
Invalid phone (international format recommended)
400
INVALID_EMAIL
Invalid email
400
EXTERNAL_ID_REQUIRED
externalId missing (max 120 characters)
400
INVALID_AMOUNT
Amount missing or negative
400
INVALID_CURRENCY
Currency is not an ISO 4217 code
400
INVALID_POINTS
points outside 0 to 1,000,000
400
BUSINESS_REQUIRED
The key covers several businesses: send businessId
400
INVALID_IDEMPOTENCY_KEY
Idempotency-Key: 1 to 255 ASCII characters
400
INVALID_EXTERNAL_ID
externalId too long
400
CODE_REQUIRED
code missing
400
INVALID_CODE
Not a Dawwerni gift code
400
INVALID_WEBHOOK_URL
URL refused (public HTTPS only)
400
EVENTS_REQUIRED
Pick at least one event
400
UNKNOWN_EVENT
Unknown event
401
UNAUTHENTICATED
No API key sent
401
INVALID_API_KEY
Invalid API key
401
API_KEY_REVOKED
API key revoked
403
ACCOUNT_INACTIVE
Dawwerni account inactive
403
FEATURE_NOT_ENABLED
Integrations not included in the plan
403
INSUFFICIENT_SCOPE
The key lacks a permission
403
BUSINESS_FORBIDDEN
Business outside the key’s reach
403
BUSINESS_UNAVAILABLE
The key’s business was deleted
404
CUSTOMER_NOT_FOUND
Customer not found
404
REWARD_NOT_FOUND
Reward not found
404
WINNING_NOT_FOUND
No gift for this code
404
WEBHOOK_NOT_FOUND
Webhook endpoint not found
409
WINNING_NOT_CLAIMABLE
Participation without a gift (lost or cancelled)
409
IDEMPOTENCY_CONFLICT
Same Idempotency-Key being processed: retry
409
DUPLICATE_EXTERNAL_ID
Same externalId being processed: retry
415
UNSUPPORTED_MEDIA_TYPE
Send Content-Type: application/json
422
IDEMPOTENCY_KEY_REUSED
Idempotency-Key already used for another request
422
INSUFFICIENT_POINTS
Not enough points
422
REWARD_INACTIVE
Reward disabled
422
REWARD_OUT_OF_STOCK
Reward out of stock
422
NO_PARTICIPATION_FOR_CUSTOMER
The customer has not played any game yet
422
REDEMPTION_REJECTED
Refused by the loyalty rules
422
WEBHOOK_LIMIT_REACHED
Up to 10 endpoints
422
NO_BUSINESS
The account has no active business
429
RATE_LIMITED
Too many requests: wait Retry-After seconds
500
INTERNAL_ERROR
Unexpected error: retry later
Webhooks
Dawwerni sends a signed HTTPS POST to your server for each event you pick. Answer with any 2xx status within 5 seconds. Works with any tool that receives webhooks (your server, Make, n8n, Zapier…).
In the appMenu→Integrations→Webhooks→Add an endpoint
Events
Event
Sent when
participant.created
A player leaves contact details in a game (winner or not)
winning.created
A player wins a gift
winning.claimed
A gift is handed over (QR scan, status change or /winnings/verify)
points.earned
Points are credited by a purchase received through the API or a connected POS
Example built from the API source (needs a real game participation)
Headers and signature
HTTP
POST https://crm.example.com/webhooks/dawwerni
Content-Type: application/json; charset=utf-8
User-Agent: Dawwerni-Webhooks/1.0
X-Dawwerni-Event: points.earned
X-Dawwerni-Delivery: dlv_4k9x2m7q1w8z5r3t6y0u2p4s (same value on every retry)
X-Dawwerni-Timestamp: 1791927400 (unix seconds)
X-Dawwerni-Signature: sha256=<hex HMAC-SHA256(secret, timestamp + "." + raw body)>
Verify the signature
Compute HMAC-SHA256(secret, timestamp + "." + raw body) as hex, prefixed with “sha256=”, and compare it in constant time with X-Dawwerni-Signature. Reject timestamps older than 5 minutes. The app’s “Send test event” button sends a ping event.
// Node.js (Express): verify on the RAW body, not on re-serialised JSON
const crypto = require('crypto');
const express = require('express');
const app = express();
app.post('/webhooks/dawwerni', express.raw({ type: 'application/json' }), (req, res) => {
const ts = req.header('X-Dawwerni-Timestamp');
const sig = req.header('X-Dawwerni-Signature') || '';
if (!ts || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return res.sendStatus(400);
const expected = 'sha256=' + crypto
.createHmac('sha256', process.env.DAWWERNI_WEBHOOK_SECRET)
.update(ts + '.' + req.body.toString('utf8'))
.digest('hex');
const ok = sig.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
if (!ok) return res.sendStatus(401);
const event = JSON.parse(req.body.toString('utf8'));
// de-duplicate on req.header('X-Dawwerni-Delivery'), then handle event.type
res.sendStatus(200); // answer within 5 seconds, do slow work afterwards
});
Retries
Failure = non-2xx status, timeout or network error. Retries after 1 min, 5 min, 30 min, 2 h and 12 h.
After 20 failures in a row the endpoint is disabled and you are notified in the app. Resume it once your server is fixed.
An event can arrive twice or out of order: de-duplicate on X-Dawwerni-Delivery.
Delivery log (30 days) and secret rotation are available in the app.
POS connections
No code to write: each connection has a secret link you paste into your POS. Every sale with the customer’s phone or email credits their points; duplicate receipts and refunds are ignored.
In the appMenu→Integrations→POS connections→Connect
FoodicsFOODICSRestaurants and cafés in Saudi Arabia and the Gulf: closed orders with a customer.
LoyverseLOYVERSEFree POS for cafés and shops: sales receipts with a customer (“Receipts” webhook).
Other POS (JSON)GENERIC_JSONAny POS, online store or automation tool that can send a JSON request after a sale.
The connection link
The link contains a secret token: anyone who has it can send sales to your account. Replace it at any time from the app (the old one stops working right away).
URL
https://app.dawwerni.com/api/ext/v1/pos/{CONNECTOR}/{connectionId}?token=pos_<secret token>
# CONNECTOR = GENERIC_JSON | LOYVERSE | FOODICS
# the token can also be sent as the header X-Dawwerni-Token: pos_<secret token>
Generic JSON format
For “Other POS”, send this JSON after each sale. Recognised fields:
externalId
Unique receipt id (required)
amount
Total paid (required)
currency
ISO currency, optional
type
"sale" (default); "refund" is ignored
customer
phone, email, name: phone or email needed to earn points
If you set a signing secret on the connection, every request must carry X-Dawwerni-Timestamp and X-Dawwerni-Signature (same scheme as outgoing webhooks, 5-minute tolerance).
Node.js
// Optional: sign the request when a signing secret is set on the connection
const crypto = require('crypto');
const body = JSON.stringify(sale);
const ts = Math.floor(Date.now() / 1000).toString();
const signature = 'sha256=' + crypto.createHmac('sha256', process.env.DAWWERNI_POS_SIGNING_SECRET)
.update(ts + '.' + body).digest('hex');
await fetch(process.env.DAWWERNI_POS_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-Dawwerni-Timestamp': ts, 'X-Dawwerni-Signature': signature },
body
});
POS link errors
400
INVALID_JSON
The body is not valid JSON
400
PAYLOAD_TOO_LARGE
Body limited to 512 KB
401
INVALID_CONNECTION_TOKEN
Unknown connection or invalid token
401
INVALID_SIGNATURE
Signature missing, stale or wrong
403
CONNECTION_PAUSED
Connection paused in Dawwerni
403
FEATURE_NOT_ENABLED
Integrations not included in the plan
404
UNKNOWN_CONNECTOR
Unknown POS type in the URL
422
UNSUPPORTED_PAYLOAD
Unreadable payload
429
RATE_LIMITED
120 events per minute per connection
Widget & web embeds
Show your games and forms on your own website or online store, with no API and no developer.
Game widget (widget.js)
Pick a game, a mode (floating button, pop-up or inside a page) and the allowed websites; the app gives you the code to paste.
In the appMenu→Campaigns→Website & online store→New widget
HTML
<!-- Floating button or pop-up: paste before </body> -->
<script src="https://app.dawwerni.com/widget.js" data-dawwerni-key="dw_YOUR_WIDGET_KEY" async></script>
The game opens in an isolated frame (Shadow DOM + iframe): your site’s styles can’t break it, and the other way round.
Pop-up shown immediately, after a delay, on scroll or on exit intent, with a display frequency.
Allowed websites: the widget only shows on your domains (and their subdomains).
Game language: data-lang attribute (ar, fr, en), otherwise your page’s language.
Impressions and opens are counted in the app. The widget is enabled on request: contact us.
“Inside a page” mode
HTML
<!-- Inline: the game appears inside this block -->
<div data-dawwerni-game="dw_YOUR_WIDGET_KEY"></div>
<script src="https://app.dawwerni.com/widget.js" data-dawwerni-key="dw_YOUR_WIDGET_KEY" data-lang="ar" async></script>
Embedded contact form
A published Dawwerni form can be embedded in an iframe; it reports its height so it fits without a scrollbar.
In the appMenu→Leads→Lead forms→Share→Embed on your website
HTML
<iframe id="dw-lead-your-form" src="https://app.dawwerni.com/f/your-form?embed=1&src=website"
title="Contact form" style="width:100%;max-width:600px;min-height:640px;border:0" loading="lazy"></iframe>
<script>
// optional: the form reports its height so the frame never shows a scrollbar
window.addEventListener('message', function (e) {
if (e.origin !== 'https://app.dawwerni.com') return;
var d = e.data || {};
if (d.type === 'dawwerni-lead-form:height' && d.slug === 'your-form') {
document.getElementById('dw-lead-your-form').style.height = (d.height + 8) + 'px';
}
});
</script>
Public links & QR codes
Each module publishes a public page. The app gives you the exact link (and a printable QR code for the menu, bio link and forms); here are the formats.