Niqati NiqatiAPI
Request access
Niqati Developer Platform

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.

Base URL https://api.niqati.com/v1
🛡️

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.

  1. In the Niqati app, open الإدارة → الربط البرمجي (API) and create a key. Copy the secret — it is shown once.
  2. Send the key as a Bearer token to GET /v1/me.
  3. 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);
200 · response
{
  "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.

ScopeGrants
customers:readLook up customers and balances.
customers:writeEnroll customers.
transactions:readRead transaction history.
transactions:writeAward points, redeem rewards, adjust balances.
cards:readRead loyalty cards and their reward catalog.
webhooks:manageCreate 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.

typeEffect
earnAward points, or add a stamp on a stamp card.
redeemRedeem a reward (deducts its point cost, or completes a stamp card).
bonusAdd promotional points on top of a sale.
adjustSigned 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.

retry → same result
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.

cursor pagination
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:

HeaderMeaning
X-RateLimit-LimitRequests allowed per minute for this key.
X-RateLimit-RemainingRequests left in the current window.
Retry-AfterSeconds 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

unauthorizedMissing or invalid key. 401
entitlement_requiredAPI access not enabled. 403
insufficient_scopeKey lacks the scope. 403
rate_limitedToo many requests. 429
invalid_requestMalformed body or params. 400
idempotency_conflictKey reused with a different body. 409
customer_not_foundNo such customer for you. 404
insufficient_balanceBalance too low to redeem. 409
stamps_fullStamp card is full; redeem first. 409
reward_not_foundNo active reward with that id. 404
error envelope
{
  "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

POST /v1/customers customers:write

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

FieldType
card_id requireduuidThe card to enroll on.
phone requiredstringSaudi phone, any common format.
display_namestringOptional 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 …
201 · created
{
  "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

GET /v1/customers customers:read

List customers, or find one by identifier. Combine filters as needed.

Query
phoneFind by phone number.
qr_codeFind by scanned QR payload.
customer_numberFind by short number.
card_idRestrict to one card.
limit, cursorPagination.

A single customer is available at GET /v1/customers/{id}, and their history at /v1/customers/{id}/transactions.

find by phone
curl "https://api.niqati.com/v1/customers?phone=0501234567" \
  -H "Authorization: Bearer nq_live_YOUR_KEY"
200 · response
{
  "ok": true,
  "data": [{
    "id": "7f0c…", "customer_number": 102444,
    "total_points": 120, "stamps_count": 0,
    "card_id": "abe7a8fd-…"
  }],
  "next_cursor": null
}

Create a transaction

POST /v1/transactions transactions:write

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 requiredearn · redeem · bonus · adjust
customer_id / customer_refExactly one. Ref = QR, number, or phone.
pointsAward amount; signed delta for adjust.
amount_spentInvoice total; per-SAR cards compute points for you.
reward_idRequired to redeem on non-stamp cards.
reference_idYour 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",
]);
201 · created
{
  "ok": true,
  "replayed": false,
  "transaction": {
    "id": "c31f…", "type": "earn",
    "points": 50, "points_after": 170,
    "reference_id": "order-9F2A"
  },
  "customer": { "total_points": 170 }
}

List transactions

GET /v1/transactions transactions:read

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

GET /v1/cards cards:read

Returns your loyalty cards with their points mode, rules, and reward catalog — everything you need to render a redemption menu at the register.

200 · response
{
  "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.createdAny transaction was recorded.
points.awardedPoints were added.
reward.redeemedA reward was redeemed.
customer.createdA 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)
event payload
{
  "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.