Developer documentation

Nuqta POS API

Connect any point-of-sale system to a merchant's Nuqta loyalty programme: verify a customer's card, issue points on a sale, redeem rewards and void mistakes. JSON over HTTPS, OAuth 2.0 client credentials.

Base URL https://nuqtaloyalty.com/api/v1

Overview

Each merchant has its own credentials, so a POS only ever sees that merchant's customers. The flow is:

  1. The Nuqta team generates a client ID and client secret for the merchant in the admin panel. The secret is shown once; store it in your POS's secret store.
  2. Your POS exchanges them for an access token (valid 1 hour).
  3. Every API call sends Authorization: Bearer <access_token>. When the token expires, request a new one.

All amounts are decimal strings in the merchant's currency (JOD has 3 decimals: "18.750"). Points are integers. Times are ISO 8601.

Authentication

POST /pos/oauth/token — OAuth 2.0 client credentials grant. Send the credentials as form fields, JSON, or HTTP Basic auth.

curl -X POST https://nuqtaloyalty.com/api/v1/pos/oauth/token \
  -H "Accept: application/json" \
  -d grant_type=client_credentials \
  -d client_id=nq_xxxxxxxxxxxxxxxxxxxxxxxx \
  -d client_secret=nqs_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
HTTP/1.1 200 OK
{
  "access_token": "41|Qm9vZ…",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "pos:lookup pos:earn pos:redeem pos:void",
  "merchant": "demo-cafe"
}
ErrorHTTPMeaning
invalid_request400client_id or client_secret missing
unsupported_grant_type400grant_type must be client_credentials
invalid_client401Wrong or revoked credentials, or the merchant is paused
invalid_client429Too many failed attempts for this client (see Retry-After)

The scope lists what the credentials may do. A call outside that scope returns 403 FORBIDDEN. Revoking the credentials in the admin panel ends every token issued with them immediately.

Verify a card

POST /pos/lookup (scope pos:lookup). Send one of: barcode (what you scanned from the Wallet pass), membership_number, or phone_number in E.164 (+962791234567).

curl -X POST https://nuqtaloyalty.com/api/v1/pos/lookup \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"barcode": "WP10000001"}'
{
  "data": {
    "membership_number": "WP10000001",
    "full_name": "Rana Saleh",
    "points_balance": 1250,
    "monetary_value": { "minor": 12500, "amount": "12.500", "currency": "JOD", "formatted_en": "JOD 12.500" },
    "tier": { "code": "GOLD", "name_en": "Gold", "points_to_next_tier": 750 },
    "redemption": { "min_redeem_points": 100, "can_redeem": true },
    "wallet": { "apple_pass_url": "…", "google_save_url": "…" }
  }
}

Unknown cards return 404 CUSTOMER_NOT_FOUND. Lookups never change a balance.

Issue points

POST /pos/transactions/earn (scope pos:earn). Points are calculated by Nuqta from the merchant's earn rate and the customer's tier.

curl -X POST https://nuqtaloyalty.com/api/v1/pos/transactions/earn \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -H "Idempotency-Key: INV-2231" \
  -d '{
        "barcode": "WP10000001",
        "gross_amount": "18.750",
        "currency": "JOD",
        "receipt_number": "INV-2231",
        "occurred_at": "2026-09-28T10:38:15Z"
      }'
FieldRequiredNotes
barcode / membership_number / phone_numberone ofIdentifies the card
gross_amountyesDecimal string, at most the currency's decimals
idempotency_keyyesBody field or Idempotency-Key header
receipt_numbernoUp to 100 characters; shown in reports
occurred_atnoWithin the last 30 days
metadatanoUp to 25 keys of your own (e.g. cashier id)

Returns 201 with data.transaction (points, balance before and after, tier change) and the updated data.customer. The customer's Wallet card updates within seconds.

Redeem

POST /pos/transactions/redeem (scope pos:redeem). Spend points as a discount on the current sale.

curl -X POST https://nuqtaloyalty.com/api/v1/pos/transactions/redeem \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"idempotency_key": "burn-INV-2232", "membership_number": "WP10000001", "points": 500}'

The balance is checked under a lock, so two tills can never spend the same points. Not enough points returns 422 INSUFFICIENT_BALANCE with details.available_points; below the merchant's minimum returns 422 INVALID_POSTING. The response's gross_amount is the discount value to apply on the receipt.

Void

POST /pos/transactions/void (scope pos:void). Reverses an earn or a redemption exactly, for refunds and mistakes. A transaction can be voided once.

curl -X POST https://nuqtaloyalty.com/api/v1/pos/transactions/void \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"idempotency_key": "void-INV-2231", "original_transaction": "INV-2231", "reason": "Returned goods"}'

original_transaction is the transaction UUID from the earlier response, or the idempotency key you used for it.

Idempotency

Every earn, redeem and void needs a unique idempotency_key (your receipt number works well). If the network drops, retry with the same key: Nuqta returns the original result instead of posting twice. Reusing a key with a different payload returns 409 IDEMPOTENCY_KEY_REUSED.

Errors & limits

Every error from the POS endpoints has the same shape:

{ "error": { "code": "INSUFFICIENT_BALANCE", "message": "…", "details": { "available_points": 18 } } }
CodeHTTPMeaning
VALIDATION_FAILED422details lists field errors
UNAUTHENTICATED / FORBIDDEN401 / 403Missing or expired token, or outside the token's scope
TERMINAL_INACTIVE / TENANT_INACTIVE / CUSTOMER_INACTIVE423Credentials revoked, merchant paused, or card closed
CUSTOMER_NOT_FOUND / TRANSACTION_NOT_FOUND404Unknown card or transaction for this merchant
INSUFFICIENT_BALANCE422Not enough points (also when voiding an earn already spent)
INVALID_POSTING422Below the minimum redemption, or a purchase too small to earn
TRANSACTION_NOT_VOIDABLE422Already voided, or not an earn or redemption
IDEMPOTENCY_KEY_REUSED409Same key, different payload
RATE_LIMITED429Default 600 requests per minute per credential; the token endpoint allows 30 per minute per IP

Go-live checklist