Developers

Dawwerni API & integrations

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.

  • REST JSON API (v1)
  • Scoped API keys
  • HMAC-SHA256 signed webhooks
  • Premium & Enterprise plans
Base URLhttps://app.dawwerni.com/api/ext/v1

What you can build

Six integrations that work today, with no custom work on the Dawwerni side.

Quick start

Three steps to your first call. About five minutes.

  1. 1
    Get integrations

    Integrations (API, webhooks, POS) come with the Premium and Enterprise plans. The account owner manages the keys.

  2. 2
    Create an API key

    Name it, pick its permissions and optionally a single business. The full key (dw_live_…) is shown only once: store it on your server.

  3. 3
    Make your first call

    GET /me returns the key’s name, its permissions and the businesses it can reach.

In the appMenuIntegrationsAPI keysCreate a key

First call · cURL
curl https://app.dawwerni.com/api/ext/v1/me \
  -H "Authorization: Bearer $DAWWERNI_API_KEY"
200 OK ResponseReal API response (test data)
{
  "apiVersion": "v1",
  "key": {
    "id": 7,
    "name": "POS bridge - Tunis",
    "prefix": "dw_live_a1b2c3d4",
    "scopes": [
      "customers:read",
      "customers:write",
      "points:write",
      "redemptions:write",
      "rewards:read",
      "webhooks:manage",
      "winnings:verify"
    ],
    "businessScope": null
  },
  "businesses": [
    {
      "id": 186,
      "name": "Café Amira"
    }
  ],
  "rateLimitPerMinute": 120
}

Authentication

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)

PermissionAllows
customers:readLook up customers and their points
customers:writeCreate or update customers
points:writeRecord purchases and award points
rewards:readList loyalty rewards
redemptions:writeRedeem rewards with points
winnings:verifyVerify and hand over gifts
webhooks:manageManage 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.
List format
{
  "data": [ … ],
  "page": { "number": 0, "size": 20, "totalElements": 42, "totalPages": 3, "hasNext": true }
}
Rate-limit headers
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 119
# when exceeded: HTTP 429 + Retry-After: <seconds>

Endpoints

Every response below was produced by calling the API for a test café in Tunis.

Key information

GET/api/ext/v1/me

Permission: any valid key

Name, prefix and permissions of the key, the businesses it can reach and the rate limit. Handy to test your setup.

curl "https://app.dawwerni.com/api/ext/v1/me" \
  -H "Authorization: Bearer $DAWWERNI_API_KEY"
200 OK ResponseReal API response (test data)
{
  "apiVersion": "v1",
  "key": {
    "id": 7,
    "name": "POS bridge - Tunis",
    "prefix": "dw_live_a1b2c3d4",
    "scopes": [
      "customers:read",
      "customers:write",
      "points:write",
      "redemptions:write",
      "rewards:read",
      "webhooks:manage",
      "winnings:verify"
    ],
    "businessScope": null
  },
  "businesses": [
    {
      "id": 186,
      "name": "Café Amira"
    }
  ],
  "rateLimitPerMinute": 120
}

Find customers

GET/api/ext/v1/customers

Permission: customers:read

Paginated list of loyalty accounts. Filter by phone or email; without a filter, every customer of the key’s businesses.

Parameters
NameInTypeDescription
phonequerystringCustomer phone (encode the + as %2B) (optional)
emailquerystringCustomer email (optional)
businessIdqueryintegerLimit to one business (optional)
pagequeryinteger ≥ 0Page, from 0 (optional)
sizequeryinteger 1–100 (20)Page size (optional)
curl "https://app.dawwerni.com/api/ext/v1/customers?phone=%2B21698123456" \
  -H "Authorization: Bearer $DAWWERNI_API_KEY"
200 OK ResponseReal API response (test data)
{
  "data": [
    {
      "id": 313,
      "businessId": 186,
      "name": "Amira B.",
      "email": "[email protected]",
      "phone": "+21698123456",
      "points": {
        "spendable": 0,
        "locked": 0,
        "lifetimeEarned": 0
      },
      "createdAt": "2026-10-13T21:36:40.087382",
      "updatedAt": "2026-10-13T21:36:40.121926"
    }
  ],
  "page": {
    "number": 0,
    "size": 20,
    "totalElements": 1,
    "totalPages": 1,
    "hasNext": false
  }
}

Get a customer

GET/api/ext/v1/customers/313

Permission: customers:read

One loyalty account by its Dawwerni id.

Parameters
NameInTypeDescription
id*pathintegerDawwerni customer id (required)
curl "https://app.dawwerni.com/api/ext/v1/customers/313" \
  -H "Authorization: Bearer $DAWWERNI_API_KEY"
200 OK ResponseReal API response (test data)
{
  "id": 313,
  "businessId": 186,
  "name": "Amira B.",
  "email": "[email protected]",
  "phone": "+21698123456",
  "points": {
    "spendable": 0,
    "locked": 0,
    "lifetimeEarned": 0
  },
  "createdAt": "2026-10-13T21:36:40.087382",
  "updatedAt": "2026-10-13T21:36:40.121926"
}
404 Not Found ResponseReal API response (test data)
{
  "error": {
    "code": "CUSTOMER_NOT_FOUND",
    "message": "Customer not found"
  }
}

Create or update a customer

POST/api/ext/v1/customers

Permission: customers:write

Matches the customer by email, then phone; creates it when missing (201) or completes its contact details (200).

Parameters
NameInTypeDescription
phonebodystring (E.164)Phone; phone or email required (optional)
emailbodystringEmail; phone or email required (optional)
namebodystring ≤ 120Display name (optional)
businessIdbodyintegerRequired when the key covers several businesses (optional)
Idempotency-Keyheaderstring ≤ 255Safe replay for 24 h (optional)
curl -X POST "https://app.dawwerni.com/api/ext/v1/customers" \
  -H "Authorization: Bearer $DAWWERNI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Amira Ben Salah", "phone": "+21698123456", "email": "[email protected]" }'
201 Created ResponseReal API response (test data)
{
  "id": 313,
  "businessId": 186,
  "name": "Amira Ben Salah",
  "email": "[email protected]",
  "phone": "+21698123456",
  "points": {
    "spendable": 0,
    "locked": 0,
    "lifetimeEarned": 0
  },
  "createdAt": "2026-10-13T21:36:40.0873817",
  "updatedAt": "2026-10-13T21:36:40.0873817"
}
200 OK ResponseReal API response (test data)
{
  "id": 313,
  "businessId": 186,
  "name": "Amira B.",
  "email": "[email protected]",
  "phone": "+21698123456",
  "points": {
    "spendable": 0,
    "locked": 0,
    "lifetimeEarned": 0
  },
  "createdAt": "2026-10-13T21:36:40.087382",
  "updatedAt": "2026-10-13T21:36:40.1219263"
}
400 Bad Request ResponseReal API response (test data)
{
  "error": {
    "code": "CUSTOMER_IDENTIFIER_REQUIRED",
    "message": "Provide a phone or an email"
  }
}

Points balance

GET/api/ext/v1/customers/313/points

Permission: customers:read

Spendable, locked and lifetime points, split per program (games / loyalty) and stamp card.

Parameters
NameInTypeDescription
id*pathintegerDawwerni customer id (required)
curl "https://app.dawwerni.com/api/ext/v1/customers/313/points" \
  -H "Authorization: Bearer $DAWWERNI_API_KEY"
200 OK ResponseReal API response (test data)
{
  "customerId": 313,
  "businessId": 186,
  "total": 192,
  "spendable": 192,
  "locked": 0,
  "lifetimeEarned": 192,
  "byProgram": {
    "GAMES": 0,
    "LOYALTY": 192
  },
  "balanceMode": "COMBINED",
  "loyaltyMode": "POINTS",
  "stampCount": 0,
  "stampsRequired": 10,
  "stampRewardsAvailable": 0
}

Record a purchase (points)

POST/api/ext/v1/purchases

Permission: points:write

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
NameInTypeDescription
externalId*bodystring ≤ 120Unique receipt id in your POS (required)
amount*bodynumber ≥ 0Amount paid (required)
customer*body{ id | phone | email, name }Customer: Dawwerni id, or phone / email (+ name) (required)
currencybodyISO 4217 (TND, EGP, SAR…)ISO currency (TND, EGP, SAR, USD…) (optional)
pointsbodyinteger 0–1 000 000Fixed points (otherwise computed by your rule) (optional)
itemsbody[{ sku, name, quantity, unitPrice }]Receipt lines (only their count is stored) (optional)
businessIdbodyintegerRequired when the key covers several businesses (optional)
Idempotency-Keyheaderstring ≤ 255Safe replay for 24 h (optional)
curl -X POST "https://app.dawwerni.com/api/ext/v1/purchases" \
  -H "Authorization: Bearer $DAWWERNI_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: f3b0c442-98fc-4c1c-9a3a-6b0e5a1d2c01" \
  -d '{ "customer": { "phone": "+21698123456", "name": "Amira Ben Salah" }, "amount": 42.5, "currency": "TND", "externalId": "R-2026-000123", "items": [ { "sku": "LATTE", "name": "Latte", "quantity": 2, "unitPrice": 7.5 } ] }'
201 Created ResponseReal API response (test data)
{
  "purchaseId": 9,
  "externalId": "R-2026-000123",
  "duplicate": false,
  "businessId": 186,
  "amount": 42.5,
  "currency": "TND",
  "itemsCount": 1,
  "pointsRequested": null,
  "pointsAwarded": 42,
  "customer": {
    "id": 313,
    "businessId": 186,
    "name": "Amira Ben Salah",
    "email": "[email protected]",
    "phone": "+21698123456",
    "points": {
      "spendable": 42,
      "locked": 0,
      "lifetimeEarned": 42
    },
    "createdAt": "2026-10-13T21:36:40.087382",
    "updatedAt": "2026-10-13T21:36:40.2972964"
  },
  "balance": {
    "customerId": 313,
    "businessId": 186,
    "total": 42,
    "spendable": 42,
    "locked": 0,
    "lifetimeEarned": 42,
    "byProgram": {
      "GAMES": 0,
      "LOYALTY": 42
    },
    "balanceMode": "COMBINED",
    "loyaltyMode": "POINTS",
    "stampCount": 0,
    "stampsRequired": 10,
    "stampRewardsAvailable": 0,
    "program": "LOYALTY",
    "outcome": "OK"
  },
  "source": "api"
}
200 OK ResponseReal API response (test data)
{
  "purchaseId": 9,
  "externalId": "R-2026-000123",
  "duplicate": true,
  "businessId": 186,
  "amount": 42.5,
  "currency": "TND",
  "itemsCount": 1,
  "pointsRequested": null,
  "pointsAwarded": 0,
  "customer": {
    "id": 313,
    "businessId": 186,
    "name": "Amira Ben Salah",
    "email": "[email protected]",
    "phone": "+21698123456",
    "points": {
      "spendable": 42,
      "locked": 0,
      "lifetimeEarned": 42
    },
    "createdAt": "2026-10-13T21:36:40.087382",
    "updatedAt": "2026-10-13T21:36:40.2972964"
  },
  "balance": {
    "customerId": 313,
    "businessId": 186,
    "total": 42,
    "spendable": 42,
    "locked": 0,
    "lifetimeEarned": 42,
    "byProgram": {
      "GAMES": 0,
      "LOYALTY": 42
    },
    "balanceMode": "COMBINED",
    "loyaltyMode": "POINTS",
    "stampCount": 0,
    "stampsRequired": 10,
    "stampRewardsAvailable": 0,
    "program": "LOYALTY",
    "outcome": "OK"
  },
  "source": "api"
}
400 Bad Request ResponseReal API response (test data)
{
  "error": {
    "code": "EXTERNAL_ID_REQUIRED",
    "message": "externalId (POS receipt id, max 120 chars) is required"
  }
}

List rewards

GET/api/ext/v1/rewards

Permission: rewards:read

Active rewards of your businesses: points cost, stock and the program they can be paid with.

Parameters
NameInTypeDescription
businessIdqueryintegerLimit to one business (optional)
pagequeryinteger ≥ 0Page, from 0 (optional)
sizequeryinteger 1–100 (20)Page size (optional)
curl "https://app.dawwerni.com/api/ext/v1/rewards" \
  -H "Authorization: Bearer $DAWWERNI_API_KEY"
200 OK ResponseReal API response (test data)
{
  "data": [
    {
      "id": 34,
      "businessId": 186,
      "name": "Free cappuccino",
      "description": "One cappuccino, any size",
      "pointsCost": 100,
      "stock": 50,
      "program": "BOTH"
    }
  ],
  "page": {
    "number": 0,
    "size": 20,
    "totalElements": 1,
    "totalPages": 1,
    "hasNext": false
  }
}

Redeem a reward

POST/api/ext/v1/redemptions

Permission: redemptions:write

Deducts the points, decrements the stock and creates a PENDING redemption shown in your loyalty pages.

  • The customer needs at least one game participation (otherwise 422 NO_PARTICIPATION_FOR_CUSTOMER).
  • With externalId, sending the same redemption twice does not duplicate it ("duplicate": true).
Parameters
NameInTypeDescription
customerId*bodyintegerDawwerni customer id (required)
rewardId*bodyintegerReward id (GET /rewards) (required)
externalIdbodystring ≤ 120Your reference, prevents duplicates (optional)
notesbodystring ≤ 250Internal note (optional)
Idempotency-Keyheaderstring ≤ 255Safe replay for 24 h (optional)
curl -X POST "https://app.dawwerni.com/api/ext/v1/redemptions" \
  -H "Authorization: Bearer $DAWWERNI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "customerId": 313, "rewardId": 33, "externalId": "RD-1001", "notes": "Counter 1" }'
201 Created ResponseExample built from the API source (needs a real game participation)
{
  "redemptionId": 57,
  "businessId": 186,
  "rewardId": 33,
  "rewardName": "Free cappuccino",
  "pointsUsed": 100,
  "status": "PENDING",
  "requestedAt": "2026-10-13T21:40:12.418302",
  "program": "LOYALTY",
  "customerId": 313,
  "externalId": "RD-1001",
  "duplicate": false
}
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
NameInTypeDescription
code*bodystringQR token, verify URL or 8-character reference (required)
curl -X POST "https://app.dawwerni.com/api/ext/v1/winnings/verify" \
  -H "Authorization: Bearer $DAWWERNI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "code": "a1b2c3d4" }'
200 OK ResponseExample built from the API source (needs a real game participation)
{
  "verified": true,
  "alreadyClaimed": false,
  "message": "Winning verified successfully",
  "winning": {
    "id": 9120,
    "ref": "a1b2c3d4",
    "businessId": 186,
    "gameId": 412,
    "prize": "Free croissant",
    "fullName": "Amira Ben Salah",
    "email": "[email protected]",
    "phone": "+21698123456",
    "status": "taked",
    "createdAt": "2026-10-12T17:05:44.120931"
  }
}
404 Not Found ResponseReal API response (test data)
{
  "error": {
    "code": "WINNING_NOT_FOUND",
    "message": "No gift matches this code"
  }
}
400 Bad Request ResponseReal API response (test data)
{
  "error": {
    "code": "INVALID_CODE",
    "message": "The code is not a valid Dawwerni gift code"
  }
}

List webhooks

GET/api/ext/v1/webhooks

Permission: webhooks:manage

Your endpoints, their events and delivery health.

Parameters
NameInTypeDescription
pagequeryinteger ≥ 0Page, from 0 (optional)
sizequeryinteger 1–100 (20)Page size (optional)
curl "https://app.dawwerni.com/api/ext/v1/webhooks" \
  -H "Authorization: Bearer $DAWWERNI_API_KEY"
200 OK ResponseReal API response (test data)
{
  "data": [
    {
      "id": 2,
      "url": "https://example.com/webhooks/dawwerni",
      "description": "CRM sync",
      "events": [
        "winning.created",
        "points.earned"
      ],
      "active": false,
      "failureCount": 0,
      "lastSuccessAt": null,
      "lastFailureAt": null,
      "disabledAt": null,
      "disabledReason": null,
      "secretHint": "whsec_...9fQe",
      "createdAt": "2026-10-13T20:36:40.854516Z"
    }
  ],
  "page": {
    "number": 0,
    "size": 20,
    "totalElements": 1,
    "totalPages": 1,
    "hasNext": false
  }
}

Create a webhook

POST/api/ext/v1/webhooks

Permission: webhooks:manage

Registers a public HTTPS URL and the events to receive. The response contains the signing secret (also visible later in the app).

  • HTTPS only; private or local addresses are refused. Up to 10 endpoints per account.
Parameters
NameInTypeDescription
url*bodyhttps URLPublic HTTPS URL of your server (required)
events*bodystring[]Events to receive (see Webhooks) (required)
descriptionbodystring ≤ 255Free label (optional)
activebodyboolean (true)false to create it paused (optional)
curl -X POST "https://app.dawwerni.com/api/ext/v1/webhooks" \
  -H "Authorization: Bearer $DAWWERNI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://crm.example.com/webhooks/dawwerni", "events": [ "points.earned", "winning.created" ], "description": "CRM sync" }'
201 Created ResponseReal API response (test data)
{
  "id": 2,
  "url": "https://example.com/webhooks/dawwerni",
  "description": "CRM sync",
  "events": [
    "winning.created",
    "points.earned"
  ],
  "active": false,
  "failureCount": 0,
  "lastSuccessAt": null,
  "lastFailureAt": null,
  "disabledAt": null,
  "disabledReason": null,
  "secretHint": "whsec_...9fQe",
  "createdAt": "2026-10-13T20:36:40.854516100Z",
  "secret": "whsec_<shown once: store it in your server config>"
}
400 Bad Request ResponseReal API response (test data)
{
  "error": {
    "code": "INVALID_WEBHOOK_URL",
    "message": "Only https:// URLs are allowed"
  }
}
400 Bad Request ResponseReal API response (test data)
{
  "error": {
    "code": "UNKNOWN_EVENT",
    "message": "Unknown event: order.paid"
  }
}

Delete a webhook

DELETE/api/ext/v1/webhooks/2

Permission: webhooks:manage

Deletes the endpoint and cancels its pending deliveries.

Parameters
NameInTypeDescription
id*pathintegerEndpoint id (required)
curl -X DELETE "https://app.dawwerni.com/api/ext/v1/webhooks/2" \
  -H "Authorization: Bearer $DAWWERNI_API_KEY"
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"
  }
}
HTTPCodeMeaning
400VALIDATION_FAILEDMissing or malformed parameter
400INVALID_JSONThe body is not valid JSON
400CUSTOMER_IDENTIFIER_REQUIREDPhone or email required
400INVALID_PHONEInvalid phone (international format recommended)
400INVALID_EMAILInvalid email
400EXTERNAL_ID_REQUIREDexternalId missing (max 120 characters)
400INVALID_AMOUNTAmount missing or negative
400INVALID_CURRENCYCurrency is not an ISO 4217 code
400INVALID_POINTSpoints outside 0 to 1,000,000
400BUSINESS_REQUIREDThe key covers several businesses: send businessId
400INVALID_IDEMPOTENCY_KEYIdempotency-Key: 1 to 255 ASCII characters
400INVALID_EXTERNAL_IDexternalId too long
400CODE_REQUIREDcode missing
400INVALID_CODENot a Dawwerni gift code
400INVALID_WEBHOOK_URLURL refused (public HTTPS only)
400EVENTS_REQUIREDPick at least one event
400UNKNOWN_EVENTUnknown event
401UNAUTHENTICATEDNo API key sent
401INVALID_API_KEYInvalid API key
401API_KEY_REVOKEDAPI key revoked
403ACCOUNT_INACTIVEDawwerni account inactive
403FEATURE_NOT_ENABLEDIntegrations not included in the plan
403INSUFFICIENT_SCOPEThe key lacks a permission
403BUSINESS_FORBIDDENBusiness outside the key’s reach
403BUSINESS_UNAVAILABLEThe key’s business was deleted
404CUSTOMER_NOT_FOUNDCustomer not found
404REWARD_NOT_FOUNDReward not found
404WINNING_NOT_FOUNDNo gift for this code
404WEBHOOK_NOT_FOUNDWebhook endpoint not found
409WINNING_NOT_CLAIMABLEParticipation without a gift (lost or cancelled)
409IDEMPOTENCY_CONFLICTSame Idempotency-Key being processed: retry
409DUPLICATE_EXTERNAL_IDSame externalId being processed: retry
415UNSUPPORTED_MEDIA_TYPESend Content-Type: application/json
422IDEMPOTENCY_KEY_REUSEDIdempotency-Key already used for another request
422INSUFFICIENT_POINTSNot enough points
422REWARD_INACTIVEReward disabled
422REWARD_OUT_OF_STOCKReward out of stock
422NO_PARTICIPATION_FOR_CUSTOMERThe customer has not played any game yet
422REDEMPTION_REJECTEDRefused by the loyalty rules
422WEBHOOK_LIMIT_REACHEDUp to 10 endpoints
422NO_BUSINESSThe account has no active business
429RATE_LIMITEDToo many requests: wait Retry-After seconds
500INTERNAL_ERRORUnexpected 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 appMenuIntegrationsWebhooksAdd an endpoint

Events

EventSent when
participant.createdA player leaves contact details in a game (winner or not)
winning.createdA player wins a gift
winning.claimedA gift is handed over (QR scan, status change or /winnings/verify)
points.earnedPoints are credited by a purchase received through the API or a connected POS
reward.redeemedA reward is redeemed through the API

Payload example

{
  "id": "evt_8f2k4m9q1x7w3z5r0t6y2u4p",
  "type": "points.earned",
  "createdAt": "2026-10-13T20:36:40.512Z",
  "apiVersion": "v1",
  "data": {
    "customerId": 313,
    "businessId": 186,
    "points": 42,
    "reason": "purchase",
    "externalId": "R-2026-000123",
    "amount": 42.5,
    "currency": "TND",
    "source": "api",
    "balance": {
      "customerId": 313,
      "businessId": 186,
      "total": 42,
      "spendable": 42,
      "locked": 0,
      "lifetimeEarned": 42,
      "byProgram": {
        "GAMES": 0,
        "LOYALTY": 42
      },
      "program": "LOYALTY",
      "outcome": "OK"
    }
  }
}

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 appMenuIntegrationsPOS connectionsConnect

  • 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:

externalIdUnique receipt id (required)
amountTotal paid (required)
currencyISO currency, optional
type"sale" (default); "refund" is ignored
customerphone, email, name: phone or email needed to earn points
itemsReceipt lines, optional
cURL
curl -X POST "$DAWWERNI_POS_URL" \
  -H "Content-Type: application/json" \
  -d '{ "externalId": "R-2026-000200", "amount": 31.5, "currency": "TND", "type": "sale", "customer": { "phone": "+21698123456", "name": "Amira" }, "items": [ { "sku": "CROISSANT", "name": "Croissant", "quantity": 3, "unitPrice": 2.5 } ] }'
200 OK ResponseReal API response (test data)
{
  "received": 1,
  "processed": 1,
  "duplicates": 0,
  "ignored": 0,
  "failed": 0
}

Several receipts at once

Send {"purchases": [...]} (up to 100 receipts). The response counts processed, duplicate, ignored and failed receipts; details are in “Last events”.

Request
{
  "purchases": [
    {
      "externalId": "R-2026-000200",
      "amount": 31.5,
      "currency": "TND",
      "type": "sale",
      "customer": {
        "phone": "+21698123456",
        "name": "Amira"
      },
      "items": [
        {
          "sku": "CROISSANT",
          "name": "Croissant",
          "quantity": 3,
          "unitPrice": 2.5
        }
      ]
    },
    {
      "externalId": "R-2026-000201",
      "amount": 12,
      "currency": "TND",
      "customer": {
        "email": "[email protected]"
      }
    },
    {
      "externalId": "R-2026-000202",
      "amount": 9,
      "type": "refund"
    }
  ]
}
200 OKReal API response (test data)
{
  "received": 3,
  "processed": 1,
  "duplicates": 1,
  "ignored": 1,
  "failed": 0
}

Optional signature

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

400INVALID_JSONThe body is not valid JSON
400PAYLOAD_TOO_LARGEBody limited to 512 KB
401INVALID_CONNECTION_TOKENUnknown connection or invalid token
401INVALID_SIGNATURESignature missing, stale or wrong
403CONNECTION_PAUSEDConnection paused in Dawwerni
403FEATURE_NOT_ENABLEDIntegrations not included in the plan
404UNKNOWN_CONNECTORUnknown POS type in the URL
422UNSUPPORTED_PAYLOADUnreadable payload
429RATE_LIMITED120 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 appMenuCampaignsWebsite & online storeNew 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 appMenuLeadsLead formsShareEmbed 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>

Need help integrating?

A POS that is not listed, an ERP, a custom integration (Enterprise)? Tell us which tool you use and we’ll reply with the best option.