POS Integration
Transactions

Transactions

Verification gate (Phase 26): the org's preset decides which actions need a PIN. Standard never gates the POS API. Balanced gates value-deducting actions (/webhook/redeem, /webhook/coupons/redeem). Strict gates every action including /webhook/pos earn — an attacker with a stolen QR shouldn't even be able to stack stamps on the customer's account through a compromised cashier terminal. Each endpoint below documents its own PIN behaviour. Full retry pattern: Fraud Detection → PIN Validation API Flow.

Earn — POST /webhook/pos

Award stamps or points to a customer after a purchase.

POST /webhook/pos
X-Api-Key: {your_secret_key}
Content-Type: application/json

Request body

{
  "customer_code": 482193,
  "amount": 85.50,
  "card_type": "point",
  "external_id": "order_9f2a1c",
  "verification_pin": "4821",
  "manual_code": false
}
FieldRequiredDescription
customer_code6-digit customer identifier
amountPurchase total (positive number)
card_type"point" (default) or "stamp"
external_idYour unique order/transaction ID — used for idempotency
verification_pinconditionalThe PIN read from the customer's dashboard. Required only at Strict. Omit on the first call — the server returns 412 pin_required to start the challenge. Standard and Balanced never gate earn, so these calls succeed without a PIN.
manual_codeoptionaltrue if the cashier typed the customer code by hand instead of scanning a QR. Strict orgs reject manual entry with manual_code_disabled (422).

Point campaigns: points earned = round(amount × earn_ratio) (standard rounding). The earn ratio is configured in your merchant app settings.

Stamp campaigns: Pass the number of stamps to award as amount (e.g. "amount": 1 to give 1 stamp). Fractional values are rounded; the minimum awarded is 1 stamp. The value cannot exceed your configured maximum stamp count per card.

Response

{
  "data": {
    "stamps": 0,
    "points": 206
  }
}

The response reflects the customer's updated balance after the earn.

If a stamp goal is reached and a reward coupon is issued, the response includes the coupon:

{
  "data": {
    "stamps": 0,
    "points": 0,
    "coupon": {
      "id": "e3b0c442-...",
      "name": "Free Coffee",
      "qr_token": "abc123..."
    }
  }
}

Error cases (Strict orgs only)

ConditionHTTPError code
PIN required412pin_required — body includes challenge_id. Read the PIN from the customer's dashboard and retry with verification_pin set.
PIN incorrect422pin_invalid — body includes remaining_attempts. After 3 wrong attempts the challenge is consumed; retry without verification_pin to issue a fresh one.
PIN expired422pin_expired — challenge older than 90 s. Retry without verification_pin to issue a fresh one.
PIN attempts exceeded429pin_attempts_exceeded
Manual entry blocked422manual_code_disabled

End-to-end PIN-aware earn examples (Node.js / Python / PHP / cURL): POS Examples → Earn with PIN verification.


Redeem — POST /webhook/redeem

Deduct stamps or points from a customer's balance.

POST /webhook/redeem
X-Api-Key: {your_secret_key}
Content-Type: application/json

Request body

{
  "customer_code": 482193,
  "value": 50,
  "card_type": "point",
  "external_id": "redeem_a7c3d1",
  "verification_pin": "4821",
  "manual_code": false
}
FieldRequiredDescription
customer_code6-digit customer identifier
valueNumber of stamps or points to deduct (positive integer)
card_type"stamp" (default) or "point"
external_idYour unique transaction ID — used for idempotency
verification_pinconditionalThe PIN read from the customer's dashboard. Required at the Balanced and Strict presets. Omit on the first call — the server returns 412 pin_required to start the challenge.
manual_codeoptionaltrue if the cashier typed the customer code by hand instead of scanning a QR. Strict orgs reject manual entry with manual_code_disabled (422).

Response

{
  "data": {
    "stamps": 2,
    "points": 0
  }
}

Error cases

ConditionHTTPError code
Balance too low422insufficient_balance
Customer not found400customer not found
Subscription expired402subscription_required
PIN required (Balanced / Strict orgs)412pin_required — body includes challenge_id. Read the PIN from the customer's dashboard and retry with verification_pin set.
PIN incorrect422pin_invalid — body includes remaining_attempts. After 3 wrong attempts the challenge is consumed; retry without verification_pin to issue a fresh one.
PIN expired422pin_expired — challenge older than 90 s. Retry without verification_pin to issue a fresh one.
PIN attempts exceeded429pin_attempts_exceeded
Manual entry blocked (Strict orgs)422manual_code_disabled

Full retry pattern with code: Fraud Detection → PIN Validation API Flow. End-to-end examples for /webhook/redeem are in POS Examples → Redeem with PIN verification.


Idempotency

Both /webhook/pos and /webhook/redeem are fully idempotent via external_id.

If the same external_id is sent twice (e.g. due to a network retry), the second call returns HTTP 200 with:

{
  "data": {
    "message": "already processed"
  }
}

The transaction is not recorded twice. Use your order ID or a UUID generated at checkout time as the external_id.

Always set external_id. Requests without it are rejected with HTTP 400.