Build loyalty into your checkout.
A clean, predictable REST API to award and redeem points, enroll and look up customers, and receive real-time events — from Foodics, Odoo, Shopify, WooCommerce, or your own POS. Every request is scoped to your business.
One base URL
https://api.niqati.com/v1 — HTTPS only, JSON in and out.
API keys
Issue scoped keys from the app. Rotate and revoke anytime.
Idempotent writes
Retries never double-award. Safe for flaky POS networks.
Webhooks
Signed, retried events for every loyalty action.
Introduction
The Niqati API is organized around REST. It has predictable, resource-oriented URLs, accepts JSON request bodies, returns JSON responses, and uses standard HTTP verbs, response codes, and authentication.
The API is available to Enterprise-tier merchants. Your account team enables API access; once enabled, you manage keys yourself from the الإدارة (Management) tab in the Niqati trader app.
Every key resolves to exactly one merchant. A key can never read or write another merchant's data — scoping is enforced server-side on every query.
Quickstart
Make your first authenticated call in under a minute.
- In the Niqati app, open الإدارة → الربط البرمجي (API) and create a key. Copy the secret — it is shown once.
- Send the key as a Bearer token to
GET /v1/me. - You'll get back your business profile and the key's scopes.
That's it — you're ready to enroll a customer and award points.
curl https://api.niqati.com/v1/me \
-H "Authorization: Bearer nq_live_YOUR_KEY"const res = await fetch("https://api.niqati.com/v1/me", {
headers: { Authorization: "Bearer nq_live_YOUR_KEY" }
});
const { data } = await res.json();import requests
r = requests.get(
"https://api.niqati.com/v1/me",
headers={"Authorization": "Bearer nq_live_YOUR_KEY"},
)
print(r.json()["data"])$ch = curl_init("https://api.niqati.com/v1/me");
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Authorization: Bearer nq_live_YOUR_KEY"]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$data = json_decode(curl_exec($ch), true);{
"ok": true,
"data": {
"trader": { "trader_id": "0c8f…", "business_name": "شاي الزعيم" },
"auth_method": "api_key",
"key": { "scopes": ["transactions:write"], "rate_limit_per_min": 120 }
}
}Authentication
The Niqati API authenticates with API keys. A key looks like:
nq_live_<key_id>.<secret>Send it in the Authorization header as a Bearer token on every request. All API requests must be made over HTTPS; calls over plain HTTP are redirected. Requests without a valid key return 401.
Your secret key carries privileges — keep it server-side. Never embed it in a mobile app, browser, or public repository. If a key leaks, revoke it instantly from the app.
Rotating a key
Rotating issues a new key and keeps the old one valid for a 24-hour grace window, so you can roll credentials across a fleet of terminals without downtime.
# Every request carries the Bearer token
curl https://api.niqati.com/v1/cards \
-H "Authorization: Bearer nq_live_YOUR_KEY"const niqati = (path, init={}) => fetch(
`https://api.niqati.com/v1${path}`,
{ ...init, headers: {
Authorization: `Bearer ${process.env.NIQATI_KEY}`,
...init.headers } }
);import os, requests
s = requests.Session()
s.headers["Authorization"] = f"Bearer {os.environ['NIQATI_KEY']}"
r = s.get("https://api.niqati.com/v1/cards")$key = getenv("NIQATI_KEY");
$headers = ["Authorization: Bearer $key"];Scopes
Every key is issued with a set of scopes. A request that needs a scope the key doesn't hold returns 403 insufficient_scope. Grant the least you need.
| Scope | Grants |
|---|---|
customers:read | Look up customers and balances. |
customers:write | Enroll customers. |
transactions:read | Read transaction history. |
transactions:write | Award points, redeem rewards, adjust balances. |
cards:read | Read loyalty cards and their reward catalog. |
webhooks:manage | Create and manage webhook endpoints. |
The transaction engine
Every loyalty action in Niqati — a sale, a redemption, a bonus, a manual correction — is represented as a transaction. You never manipulate a balance directly; you create a transaction, and the engine applies it atomically: it validates the reward, enforces stamp-card rules, updates the balance, and pushes the customer's Apple/Google Wallet pass through the same pipeline the Niqati app uses.
| type | Effect |
|---|---|
earn | Award points, or add a stamp on a stamp card. |
redeem | Redeem a reward (deducts its point cost, or completes a stamp card). |
bonus | Add promotional points on top of a sale. |
adjust | Signed manual correction (positive or negative). |
Idempotency
All write requests require an Idempotency-Key header — a unique string you generate per logical operation (a UUID works well). A POS will retry on a flaky network, and idempotency guarantees a retry never awards points twice.
- An identical retry returns the original result, marked
"replayed": true. - Reusing a key with a different body returns 409 idempotency_conflict.
- A failed request never consumes its key — it's safe to retry.
Keys are remembered for 48 hours.
curl -X POST https://api.niqati.com/v1/transactions \
-H "Authorization: Bearer nq_live_YOUR_KEY" \
-H "Idempotency-Key: 8f2a-b3c1-checkout-991" \
-H "Content-Type: application/json" \
-d '{"type":"earn","customer_ref":"0501234567","points":50}'Pagination
List endpoints return up to limit items (default 25, max 100) plus a next_cursor. To fetch the next page, pass it back as ?cursor=. When next_cursor is null, you've reached the end.
curl "https://api.niqati.com/v1/customers?limit=50" -H "Authorization: Bearer …"
# → { "ok": true, "data": [ … ], "next_cursor": "eyJ0Ijoi…" }
curl "https://api.niqati.com/v1/customers?limit=50&cursor=eyJ0Ijoi…" -H "Authorization: Bearer …"Rate limits
Each key has a per-minute rate limit (set when you create it). Responses include the current budget:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Requests allowed per minute for this key. |
X-RateLimit-Remaining | Requests left in the current window. |
Retry-After | Seconds to wait (sent with 429). |
Exceeding the limit returns 429 rate_limited. Back off for Retry-After seconds and retry.
Errors
Niqati uses conventional HTTP status codes and returns a consistent error envelope. The machine-readable code is stable; the message is human-readable; doc_url links here. We never leak stack traces, internal paths, or another merchant's data.
Common codes
| unauthorized | Missing or invalid key. 401 |
| entitlement_required | API access not enabled. 403 |
| insufficient_scope | Key lacks the scope. 403 |
| rate_limited | Too many requests. 429 |
| invalid_request | Malformed body or params. 400 |
| idempotency_conflict | Key reused with a different body. 409 |
| customer_not_found | No such customer for you. 404 |
| insufficient_balance | Balance too low to redeem. 409 |
| stamps_full | Stamp card is full; redeem first. 409 |
| reward_not_found | No active reward with that id. 404 |
{
"ok": false,
"error": {
"code": "insufficient_balance",
"message": "The customer balance is lower than required for this redemption.",
"doc_url": "https://niqati.com/api/#errors"
}
}Every response also carries an X-Request-Id — include it when contacting support.
Enroll a customer
Find-or-create a customer on a card by phone. If the phone already exists on that card, the existing customer is returned (created: false). Returns the wallet card_url the customer opens to add their pass.
Body
| Field | Type | |
|---|---|---|
card_id required | uuid | The card to enroll on. |
phone required | string | Saudi phone, any common format. |
display_name | string | Optional name. |
Requires an Idempotency-Key header.
curl -X POST https://api.niqati.com/v1/customers \
-H "Authorization: Bearer nq_live_YOUR_KEY" \
-H "Idempotency-Key: enroll-0501234567" \
-H "Content-Type: application/json" \
-d '{
"card_id": "abe7a8fd-…",
"phone": "0501234567",
"display_name": "Sara"
}'await fetch("https://api.niqati.com/v1/customers", {
method: "POST",
headers: {
Authorization: `Bearer ${key}`,
"Idempotency-Key": "enroll-0501234567",
"Content-Type": "application/json"
},
body: JSON.stringify({
card_id: "abe7a8fd-…", phone: "0501234567", display_name: "Sara"
})
});s.post("https://api.niqati.com/v1/customers",
headers={"Idempotency-Key": "enroll-0501234567"},
json={"card_id": "abe7a8fd-…",
"phone": "0501234567",
"display_name": "Sara"})$body = json_encode([
"card_id" => "abe7a8fd-…",
"phone" => "0501234567",
"display_name" => "Sara",
]);
// POST with Idempotency-Key header …{
"ok": true,
"created": true,
"customer": {
"id": "7f0c…",
"customer_number": 102444,
"qr_code": "70291ed0-…",
"total_points": 0
},
"card_url": "https://add.niqati.com/card/jnral"
}List & find customers
List customers, or find one by identifier. Combine filters as needed.
| Query | |
|---|---|
phone | Find by phone number. |
qr_code | Find by scanned QR payload. |
customer_number | Find by short number. |
card_id | Restrict to one card. |
limit, cursor | Pagination. |
A single customer is available at GET /v1/customers/{id}, and their history at /v1/customers/{id}/transactions.
curl "https://api.niqati.com/v1/customers?phone=0501234567" \
-H "Authorization: Bearer nq_live_YOUR_KEY"{
"ok": true,
"data": [{
"id": "7f0c…", "customer_number": 102444,
"total_points": 120, "stamps_count": 0,
"card_id": "abe7a8fd-…"
}],
"next_cursor": null
}Create a transaction
Award points, redeem a reward, or adjust a balance. Identify the customer with exactly one of customer_id or customer_ref (a QR payload, customer number, or phone).
| Field | |
|---|---|
type required | earn · redeem · bonus · adjust |
customer_id / customer_ref | Exactly one. Ref = QR, number, or phone. |
points | Award amount; signed delta for adjust. |
amount_spent | Invoice total; per-SAR cards compute points for you. |
reward_id | Required to redeem on non-stamp cards. |
reference_id | Your order/receipt id, stored on the transaction. |
The customer's Apple & Google Wallet pass updates automatically within seconds — no extra call.
# Award 50 points for a SAR 100 sale
curl -X POST https://api.niqati.com/v1/transactions \
-H "Authorization: Bearer nq_live_YOUR_KEY" \
-H "Idempotency-Key: order-9F2A" \
-H "Content-Type: application/json" \
-d '{
"type": "earn",
"customer_ref": "0501234567",
"points": 50,
"reference_id": "order-9F2A"
}'await niqati("/transactions", {
method: "POST",
headers: {
"Idempotency-Key": "order-9F2A",
"Content-Type": "application/json"
},
body: JSON.stringify({
type: "earn", customer_ref: "0501234567",
points: 50, reference_id: "order-9F2A"
})
});s.post("https://api.niqati.com/v1/transactions",
headers={"Idempotency-Key": "order-9F2A"},
json={
"type": "earn",
"customer_ref": "0501234567",
"points": 50,
"reference_id": "order-9F2A",
})$body = json_encode([
"type" => "earn",
"customer_ref" => "0501234567",
"points" => 50,
"reference_id" => "order-9F2A",
]);{
"ok": true,
"replayed": false,
"transaction": {
"id": "c31f…", "type": "earn",
"points": 50, "points_after": 170,
"reference_id": "order-9F2A"
},
"customer": { "total_points": 170 }
}List transactions
Filter by customer_id, card_id, type, and a from/to date range. Cursor-paginated. A single transaction is at /v1/transactions/{id}.
List cards
Returns your loyalty cards with their points mode, rules, and reward catalog — everything you need to render a redemption menu at the register.
{
"ok": true,
"data": [{
"id": "abe7a8fd-…",
"card_name": "Free Green Tea",
"points_mode": "per_sar",
"points_per_sar": 1,
"rewards": [{ "id": "free-tea", "points_required": 50 }]
}]
}Webhooks
Rather than poll, subscribe to events. Niqati sends a signed POST to your HTTPS endpoint whenever something happens. Create endpoints with a webhooks:manage key; the signing secret is shown once.
Deliveries retry with backoff (1m → 5m → 30m → 2h → 12h) and dead-letter after 6 attempts. An endpoint that fails persistently is auto-disabled — you'll see it, and every attempt, in the app.
Verifying signatures
Each request carries X-Niqati-Signature: t=<timestamp>,v1=<hmac>. Compute HMAC-SHA256(secret, "{t}.{rawBody}") and compare in constant time. Reject if it doesn't match, or if t is more than 5 minutes old (replay protection).
Event types
transaction.created | Any transaction was recorded. |
points.awarded | Points were added. |
reward.redeemed | A reward was redeemed. |
customer.created | A new customer enrolled. |
import crypto from "node:crypto";
function verify(secret, header, rawBody) {
const [t, v1] = header.split(",").map(p => p.split("=")[1]);
const expected = crypto.createHmac("sha256", secret)
.update(`${t}.${rawBody}`).digest("hex");
return crypto.timingSafeEqual(
Buffer.from(v1), Buffer.from(expected));
}import hmac, hashlib
def verify(secret, header, raw_body):
parts = dict(p.split("=") for p in header.split(","))
expected = hmac.new(secret.encode(),
f"{parts['t']}.{raw_body}".encode(),
hashlib.sha256).hexdigest()
return hmac.compare_digest(parts["v1"], expected){
"api_version": "v1",
"type": "points.awarded",
"created": "2026-07-12T21:00:00Z",
"data": { "transaction": { "…" }, "customer": { "…" } }
}SDK examples
There's no SDK to install — the API is plain HTTP, so any HTTP client works. A tiny wrapper is all you need. Switch languages with the tabs anywhere on this page; here's a complete "award points at checkout" example:
import { randomUUID } from "node:crypto";
const BASE = "https://api.niqati.com/v1";
const KEY = process.env.NIQATI_KEY;
export async function awardPoints(phone, points, orderId) {
const res = await fetch(`${BASE}/transactions`, {
method: "POST",
headers: {
Authorization: `Bearer ${KEY}`,
"Idempotency-Key": orderId ?? randomUUID(),
"Content-Type": "application/json"
},
body: JSON.stringify({ type: "earn", customer_ref: phone, points, reference_id: orderId })
});
const body = await res.json();
if (!body.ok) throw new Error(body.error.code);
return body.transaction;
}import os, uuid, requests
BASE = "https://api.niqati.com/v1"
S = requests.Session()
S.headers["Authorization"] = f"Bearer {os.environ['NIQATI_KEY']}"
def award_points(phone, points, order_id=None):
order_id = order_id or str(uuid.uuid4())
r = S.post(f"{BASE}/transactions",
headers={"Idempotency-Key": order_id},
json={"type": "earn", "customer_ref": phone,
"points": points, "reference_id": order_id})
body = r.json()
if not body["ok"]:
raise RuntimeError(body["error"]["code"])
return body["transaction"]function award_points($phone, $points, $orderId) {
$ch = curl_init("https://api.niqati.com/v1/transactions");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . getenv("NIQATI_KEY"),
"Idempotency-Key: " . $orderId,
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode([
"type" => "earn", "customer_ref" => $phone,
"points" => $points, "reference_id" => $orderId,
]),
]);
return json_decode(curl_exec($ch), true);
}Downloads
Import the collection into Postman or Insomnia, or generate a client from the OpenAPI spec.
Questions, higher rate limits, or an OAuth integration (e.g. Foodics)? Talk to our team — we're happy to help you ship.