ICD - Partner Systems - EPC - POS Redemption

Interface Control Document (ICD): Partner Systems (Point of Sale) - EPC

Definitions & List of Abbreviations

Abbreviation Description
EPC Emirates Platinum Card — the Platinum programme and its API
Partner A merchant organisation contracted to offer benefits to Platinum members
Partner System A partner-operated system that calls this API: a cash register (POS) at a counter, or the partner's POS server or middleware
POS Point of Sale — a cash register or till at a shop, restaurant or counter
Store number The partner's own code for a store, branch or venue. Mapped once in the Platinum backoffice to the partner location it represents
API key A static secret issued to a partner and sent in the X-API-Key header on every call. One key serves all of the partner's stores and registers
QR Value The exact string a partner system reads from the member's Platinum QR code, or that a cashier types from the member's card. It is opaque to the partner system
Offer A benefit a partner publishes to members: a percentage discount, a special price, or a special deal
posId The partner-facing code of an offer, configured per offer in the Platinum backoffice so that the POS can key the promotion it applies
Token A single-use value returned by POS Login that identifies the verified member for the Redeem Offer call that follows. Bound to the member, the store and the partner; expires
Transaction id The partner's own identifier of the sale (check, receipt or order) on its POS. Identifies the sale in Redeem Offer and Void Transaction
Redemption The record that a member used an offer in a specific sale at a store
Void Reversal of a redemption after a voided sale or refunded order
Fils The minor unit of AED. All amounts in this interface are integers in fils (1 AED = 100 fils)
UAT User Acceptance Testing environment
API Application Programming Interface

1. Interface Description

Platinum partners today record member benefits through the Partner Portal: a member of staff signs in, scans the member's QR code and enters the transaction. This interface lets a partner's own cash register do the same job directly, in three steps: it scans the member's QR code and receives the member id together with the offers the member can use at this store, it reports the sale on which the offer was redeemed, and it voids the redemption if the sale is later reversed. The eligibility, availability and discount rules remain on the Platinum side; the partner system does not implement them, and no member personal data other than the member id is returned to it.

Business Value

Objective

Scope

In scope Out of scope
Over-the-counter offers: discount, special_price, special_deal Leisure venue check-in (lifestyle_hub offers). Household entitlements and per-person pricing remain in the Partner Portal
Point of sale: a cash register at a shop, restaurant or counter Offline operation. POS Login is a live call; the partner system must be online
Void of a redemption after a voided sale or refunded order Partial refunds. A redemption stands unless the whole sale is reversed
One Platinum offer per transaction Several Platinum offers on one transaction
Sales on which a Platinum offer was applied Sales without a Platinum offer. Platinum is not an order-capture system
Membership confirmation, the member id and the member's eligible offers Other member data. No name, photo, tier or contact detail is returned
Retry-safe recording through the partner's transaction id Read endpoints for reconciliation or lookup

2. Assumptions

3. Non-Functional Requirements

3.1 Security Requirements

Sr No. Non-Functional Requirement Value
1 PHI or Credit Card data? No credit card data and no patient health information. Card payment details are never sent to Platinum; unknown fields are dropped before anything is stored
2 Authentication Pattern Static API key per partner, sent in the X-API-Key header on every call. The key is shown once at creation, stored hashed, and revocable at any time. Two keys may be active during rotation
3 Authorisation The key identifies the partner. storeNumber must be one of the partner's mapped stores. The status of the key, the partner and the store is checked on every request, so deactivation in the backoffice takes effect on the next call
4 Secure Channel / Encryption in Transit TLS 1.2 or higher (HTTPS only)
5 Input validation Yes. All fields validated against the JSON schema in the accompanying OpenAPI specification
6 Internet exposed? IP whitelisting? Yes, exposed via CloudFront. Because one static key serves the whole partner, an IP allow-list (WAF) is strongly recommended and is required for production keys unless waived at onboarding
7 Rate limiting Per key, and additionally per key, store and terminal for POS Login (see 3.2). Exceeding a limit returns 429 with code 1006
8 Logging and audit Every call is logged with the partner, store, terminal and request id. Every redemption and void is written to the Platinum audit trail
9 Data exposure POS Login returns the member id and the offers the member can use. No name, photo, tier, contact details, employment data or membership history are returned
10 Secret handling The API key is displayed once at creation or rotation and stored hashed. It is never recoverable; a lost key is replaced by rotation
11 Duplicate protection A sale is recorded once per partner and transaction id. A repeated Redeem Offer for the same sale is answered with the original result and never recorded twice

3.2 Availability and Limits

Item Value
Rate limit: POS Login 600 per minute per key, and 30 per minute per key, store and terminal
Rate limit: Redeem Offer and Void Transaction 600 per minute per key
Sizing Limits are indicative defaults and are sized per partner at onboarding against expected transaction volumes
Recommended client timeout 10 seconds per call
Request body size 16 KB maximum
Token lifetime Returned as tokenExpiresAt on every POS Login response. A per-partner setting; default 4 hours, so a member scanned when the check is opened can still be recorded when the check is closed
Void window Returned as voidableUntil on every redemption. Indicative: 30 days after redeemedAt

4. Solution View

4.1 Logical

Component Value
Source Partner System (POS terminal or POS server)
Source Protocol HTTPS, JSON. Three POST methods
Source Authentication Static API key in the X-API-Key header
Target EPC (Emirates Platinum Card) API
Target Protocol HTTPS
Target Data Format JSON, UTF-8
Direction Inbound to EPC. Every call is initiated by the partner system; EPC does not call the partner
Base path /partners/pos/v1

4.2 Architecture

4.2.1 Interface Architecture

┌────────────────────┐                                         ┌────────────────────┐
│                    │  1. POST /posLogin                      │                    │
│   Partner System   │ ── { qrValue, storeNumber,            ▶ │                    │
│  (cash register)   │      terminalId }                       │                    │
│                    │ ◀── token, memberId, eligible offers ── │      EPC API       │
│  member presents   │                                         │                    │
│  QR ──▶ scanner    │  2. POST /redeemOffer                   │  eligibility rules │
│                    │ ── { token, offerId, transactionId,   ▶ │  availability      │
│  cashier applies   │      totalAmount, discountAmount }      │  discount check    │
│  the offer         │ ◀── redemption record ───────────────── │  redemption record │
│                    │                                         │  audit trail       │
│  sale voided or    │  3. POST /voidTransaction               │  member notice     │
│  refunded          │ ── { transactionId, reason }          ▶ │                    │
│                    │ ◀── voided record ───────────────────── │                    │
└────────────────────┘                                         └────────────────────┘
                    X-API-Key header on every call

Flow:

4.2.2 Security Mechanisms

4.2.3 Exception/Error Management Guidelines

API Monitoring

Exception Handling

HTTP Status Scenario Partner Action
200 / 201 Success Continue. A 200 with duplicate: true means this sale, or this void, was already recorded; treat as success
400 Validation error, unsupported QR value, invalid amounts, offer not active or not at this store Do not retry as-is. Correct the request. For an offer that is no longer usable, remove it and complete the sale at full price
401 API key missing, unknown or revoked Stop. Check the key configuration; contact the programme team
403 Partner inactive, or member's membership not active Partner inactive: stop and contact the programme team. Member not active: inform the member; proceed at full price
404 Member not found, store number not mapped, offer not found, token not found, redemption not found Member: inform the member and proceed at full price. Store: configuration issue, contact the programme team. Token: scan again
409 Availability exhausted; sale already recorded with different details; token already used or issued for another store; void window expired; void from another store Availability: fall back to full price. Sale already recorded: check details.redemptionId. Token: scan again. Others: do not retry
410 Token expired Scan the member again
422 Member not eligible for the offer at confirmation Offer one of the offers in details.offers, or proceed at full price
429 Rate limit exceeded Back off and retry after the Retry-After header
500 Server error Retry the identical request with back-off (1 s, 2 s, 4 s)
503 Service unavailable Retry the identical request later

Retry rules: retry only on 429, 500, 503 and network timeouts, always by re-sending the identical request (same transactionId, same details), at most three times with exponential back-off. Never retry other 4xx responses without changing the request. A Redeem Offer re-sent after a restart is still safe: EPC checks the transactionId first and answers with the existing record when the sale was already recorded (see 4.3.4).

4.3 Interface Touchpoints / Use Cases

4.3.1 POS Login

Actors: Cashier, Member, Partner System

Trigger: Member presents their Platinum QR code (or reads out the code printed on their card)

Flow:

An empty offers list means the member was identified but has nothing to use at this store today. Only offers returned by POS Login can be redeemed; an offer that was not returned is rejected on Redeem Offer.

4.3.2 Redeem Offer

Actors: Cashier, Partner System

Trigger: Sale is tendered with the offer applied

Flow:

Availability running out mid-transaction: the offer list shown at POS Login may be minutes old. If the last unit was taken in between, step 6 fails and EPC returns 409 with code 25024. The partner system must remove the discount, inform the customer, and complete the sale at full price. Nothing is recorded on the Platinum side and the token remains usable.

Duplicate submission: if the same sale is sent again with the same details, for example after a timeout, EPC returns 200 with the original redemption and duplicate: true. Nothing is recorded twice and availability is not consumed twice. Two identical requests that arrive at the same time are resolved the same way: one is recorded, the other returns it.

One offer per transaction: a transactionId carries one completed redemption, and the token is consumed by it. Recording a second Platinum offer on the same sale is not supported in this version.

Failed calls: a Redeem Offer rejected with a 4xx other than the token errors leaves the token usable, so the cashier can correct the request and resend.

4.3.3 Void Transaction

Actors: Cashier, Partner System

Trigger: The sale is voided at the till, or refunded in full

Flow:

A void is a status change on the redemption record. No money moves on the Platinum side. A partial refund is not a void; the redemption stands. No token is needed: the member has left, and the sale is identified by its transaction id. After a void, the same transactionId may be recorded again through a fresh POS Login and Redeem Offer, for example when the wrong offer was applied.

4.3.4 Recover After a Timeout

Actors: Partner System

Trigger: A call timed out or returned 5xx and the outcome is unknown

Flow:

4.4 Fields Definition

Data Conventions

POS Login — Request Fields

POST /partners/pos/v1/posLogin

Field Type Required Description Example
qrValue String Yes The value read from the member's QR code, or typed from their card, exactly as read "2155581665"
storeNumber String Yes The partner's own store code, as mapped in the Platinum backoffice "DXB-MALL-01"
terminalId String Yes Register or terminal identifier, up to 64 characters "2"
posUser String Optional Cashier or operator code, up to 64 characters. Do not send personal names "9999"
transactionId String Optional The partner's own identifier of the open check. Echoed for correlation only "5142"

POS Login — Response Fields

Field Type Description
token String Single-use value to send on Redeem Offer. Bound to this member, this store and this partner
tokenExpiresAt String (date-time) When the token can no longer be used
generatedAt String (date-time) Server time of this response
memberId String The member's Platinum id. The only member field returned
store Object { storeNumber, name } — the store as mapped in the backoffice
transactionId String As sent, when sent
offers Array of Object Offers this member can use at this store now. Empty when none
offers[].offerId String Identifier to send on Redeem Offer
offers[].posId String Partner-facing offer code configured in the backoffice, for keying the promotion in the POS. null when none is configured
offers[].name String Offer name
offers[].shortDescription String Short description, up to 500 characters
offers[].offerType String "discount", "special_price" or "special_deal" (see 6.1)
offers[].percentage Number Discount percentage. discount offers only
offers[].price Integer Special price in fils. special_price offers only
offers[].label String Short label of the benefit, for example "Free dessert". special_deal offers, when configured
offers[].startDate String (date-time) Start of the offer's redemption period
offers[].endDate String (date-time) End of the redemption period. Absent when open-ended
offers[].status String Always "active" in this list
offers[].categories Array of String Category names the offer is published under
offers[].subcategories Array of String Subcategory names, when configured

Redeem Offer — Request Fields

POST /partners/pos/v1/redeemOffer

Field Type Required Description Example
token String Yes From the POS Login response "tk_01J7ZTOK4M5N6P7Q8R9S0T1V2W"
memberId String Optional Cross-check: must match the member the token was issued for "2155581665"
offerId String Yes The offer applied. Must be one returned by POS Login for this member and store "01J7ZOFR5N6P7Q8R9S0T1V2W3X"
transactionId String Yes The partner's own identifier of this sale "5142"
storeNumber String Yes The partner's own store code, as mapped in the Platinum backoffice. Must be the store the token was issued for "DXB-MALL-01"
terminalId String Yes Register or terminal identifier, up to 64 characters "2"
posUser String Optional Cashier or operator code, up to 64 characters. Do not send personal names "9999"
totalAmount Integer Yes Total of the sale before the Platinum discount, in fils 12500
discountAmount Integer Yes Discount given for this Platinum offer, in fils. 0 for a special deal with no monetary discount 1875
transactionTime String (date-time) Yes Time of sale on the partner system, with UTC offset "2026-09-09T14:05:33+04:00"
receiptId String Optional Receipt number when it differs from transactionId, up to 64 characters "R-000123"
notes String Optional Free text, up to 500 characters "Table 12"

Redemption — Response Fields

Returned by Redeem Offer (201, or 200 with duplicate: true) and by Void Transaction (200).

Field Type Description
redemptionId String Platinum's identifier for the redemption
status String "completed" or "voided"
duplicate Boolean Present and true when the call repeated an earlier recording of the same sale, or an earlier void, and nothing new was recorded
transactionId String As submitted
memberId String The member's Platinum id
store Object { storeNumber, name }
offer Object { offerId, posId, name, offerType } as at the time of redemption
totalAmount Integer As submitted, in fils
discountAmount Integer As submitted, in fils — the discount the member received for this offer
transactionTime String (date-time) As submitted
redeemedAt String (date-time) When Platinum recorded the redemption
voidableUntil String (date-time) Last moment a void is accepted
voidedAt String (date-time) Present when status is "voided"
terminalId String As submitted
posUser String As submitted
receiptId String As submitted
notes String As submitted

Void Transaction — Request Fields

POST /partners/pos/v1/voidTransaction

Field Type Required Description Example
transactionId String One of two The partner's transaction id of the sale to void "5142"
redemptionId String One of two Alternatively, Platinum's redemption id. Exactly one of transactionId and redemptionId must be sent "01J7ZRED3L4M5N6P7Q8R9S0T1V"
storeNumber String Yes The partner's own store code. Must be the store the redemption was recorded at "DXB-MALL-01"
terminalId String Yes Register or terminal identifier, up to 64 characters "2"
posUser String Optional Cashier or operator code, up to 64 characters. Do not send personal names "9999"
reason String Yes "cancellation" (sale voided at the till), "refund" (sale refunded), "error" (recorded by mistake), "other" "refund"
note String Optional Free text, up to 500 characters "Customer returned goods"
voidReference String Optional Partner's refund or void reference, up to 64 characters "RF-000045"

4.5 Request/Response Samples

Security / Headers

X-API-Key: <partner api key>
Content-Type: application/json
Accept-Language: en-us
X-Request-Id: <optional client-generated id, echoed back>

4.5.1 POS Login

Request:

POST /partners/pos/v1/posLogin HTTP/1.1
Host: uat.api.platinum.retter.io
X-API-Key: pk_uat_8f3a…redacted…
Content-Type: application/json

{
  "qrValue": "2155581665",
  "storeNumber": "DXB-MALL-01",
  "terminalId": "2",
  "posUser": "9999",
  "transactionId": "5142"
}

Response (200 OK) — member identified, two offers usable:

{
  "token": "tk_01J7ZTOK4M5N6P7Q8R9S0T1V2W",
  "tokenExpiresAt": "2026-09-09T18:05:33+04:00",
  "generatedAt": "2026-09-09T14:05:33+04:00",
  "memberId": "2155581665",
  "store": { "storeNumber": "DXB-MALL-01", "name": "Dubai Mall — Ground Floor" },
  "transactionId": "5142",
  "offers": [
    {
      "offerId": "01J7ZOFR5N6P7Q8R9S0T1V2W3X",
      "posId": "PLT-DINE15",
      "name": "15% off dine-in",
      "shortDescription": "15% off the food bill for Platinum members, dine-in only.",
      "offerType": "discount",
      "percentage": 15,
      "startDate": "2026-09-01T00:00:00+04:00",
      "endDate": "2026-12-31T23:59:59+04:00",
      "status": "active",
      "categories": ["Dining"],
      "subcategories": ["Restaurants"]
    },
    {
      "offerId": "01J7ZOFR9X8W7V6U5T4S3R2Q1P",
      "posId": null,
      "name": "Set lunch at AED 49",
      "shortDescription": "Two-course set lunch for AED 49, weekdays 12:00 to 15:00.",
      "offerType": "special_price",
      "price": 4900,
      "startDate": "2026-09-01T00:00:00+04:00",
      "status": "active",
      "categories": ["Dining"]
    }
  ]
}

Response (200 OK) — member identified, nothing to use here today:

{
  "token": "tk_01J7ZTOK7P8Q9R0S1T2V3W4X5Y",
  "tokenExpiresAt": "2026-09-09T18:06:10+04:00",
  "generatedAt": "2026-09-09T14:06:10+04:00",
  "memberId": "2155581672",
  "store": { "storeNumber": "DXB-MALL-01", "name": "Dubai Mall — Ground Floor" },
  "offers": []
}

Response (404 Not Found) — unknown QR value:

{
  "code": 25010,
  "moduleId": "PartnerPos",
  "message": "Member not found"
}

Response (403 Forbidden) — membership not active:

{
  "code": 25013,
  "moduleId": "PartnerPos",
  "message": "Membership is not active"
}

4.5.2 Redeem Offer

Request:

POST /partners/pos/v1/redeemOffer HTTP/1.1
Host: uat.api.platinum.retter.io
X-API-Key: pk_uat_8f3a…redacted…
Content-Type: application/json

{
  "token": "tk_01J7ZTOK4M5N6P7Q8R9S0T1V2W",
  "memberId": "2155581665",
  "offerId": "01J7ZOFR5N6P7Q8R9S0T1V2W3X",
  "transactionId": "5142",
  "storeNumber": "DXB-MALL-01",
  "terminalId": "2",
  "posUser": "9999",
  "totalAmount": 12500,
  "discountAmount": 1875,
  "transactionTime": "2026-09-09T14:35:12+04:00",
  "receiptId": "R-000123"
}

Response (201 Created):

{
  "redemptionId": "01J7ZRED3L4M5N6P7Q8R9S0T1V",
  "status": "completed",
  "transactionId": "5142",
  "memberId": "2155581665",
  "store": { "storeNumber": "DXB-MALL-01", "name": "Dubai Mall — Ground Floor" },
  "offer": { "offerId": "01J7ZOFR5N6P7Q8R9S0T1V2W3X", "posId": "PLT-DINE15", "name": "15% off dine-in", "offerType": "discount" },
  "totalAmount": 12500,
  "discountAmount": 1875,
  "transactionTime": "2026-09-09T14:35:12+04:00",
  "redeemedAt": "2026-09-09T14:35:13+04:00",
  "voidableUntil": "2026-10-09T14:35:13+04:00",
  "terminalId": "2",
  "posUser": "9999",
  "receiptId": "R-000123"
}

Response (200 OK) — the same sale was sent again after a timeout:

{
  "redemptionId": "01J7ZRED3L4M5N6P7Q8R9S0T1V",
  "status": "completed",
  "duplicate": true,
  "transactionId": "5142",
  "…": "… (remaining fields identical to the original 201 response)"
}

Response (409 Conflict) — the sale was already recorded with different details:

{
  "code": 25044,
  "moduleId": "PartnerPos",
  "message": "This transaction has already been recorded with different details",
  "details": {
    "redemptionId": "01J7ZRED3L4M5N6P7Q8R9S0T1V",
    "status": "completed",
    "redeemedAt": "2026-09-09T14:35:13+04:00"
  }
}

Response (409 Conflict) — availability exhausted between POS Login and Redeem Offer:

{
  "code": 25024,
  "moduleId": "PartnerPos",
  "message": "Offer availability has been exhausted"
}

Response (400 Bad Request) — amounts inconsistent:

{
  "code": 25041,
  "moduleId": "PartnerPos",
  "message": "Transaction amounts are inconsistent",
  "details": {
    "reason": "discount_exceeds_total"
  }
}

Response (422 Unprocessable Entity) — member no longer eligible at confirmation:

{
  "code": 25025,
  "moduleId": "PartnerPos",
  "message": "Member is not eligible for this offer",
  "details": {
    "offers": [
      { "offerId": "01J7ZOFR9X8W7V6U5T4S3R2Q1P", "posId": null, "name": "Set lunch at AED 49", "offerType": "special_price", "price": 4900 }
    ]
  }
}

4.5.3 Void Transaction

Request:

POST /partners/pos/v1/voidTransaction HTTP/1.1
Host: uat.api.platinum.retter.io
X-API-Key: pk_uat_8f3a…redacted…
Content-Type: application/json

{
  "transactionId": "5142",
  "storeNumber": "DXB-MALL-01",
  "terminalId": "2",
  "posUser": "9999",
  "reason": "refund",
  "note": "Customer returned goods",
  "voidReference": "RF-000045"
}

Response (200 OK):

{
  "redemptionId": "01J7ZRED3L4M5N6P7Q8R9S0T1V",
  "status": "voided",
  "transactionId": "5142",
  "memberId": "2155581665",
  "store": { "storeNumber": "DXB-MALL-01", "name": "Dubai Mall — Ground Floor" },
  "offer": { "offerId": "01J7ZOFR5N6P7Q8R9S0T1V2W3X", "posId": "PLT-DINE15", "name": "15% off dine-in", "offerType": "discount" },
  "totalAmount": 12500,
  "discountAmount": 1875,
  "transactionTime": "2026-09-09T14:35:12+04:00",
  "redeemedAt": "2026-09-09T14:35:13+04:00",
  "voidableUntil": "2026-10-09T14:35:13+04:00",
  "voidedAt": "2026-09-11T10:12:07+04:00",
  "terminalId": "2",
  "posUser": "9999",
  "receiptId": "R-000123"
}

Response (200 OK) — void sent again: the same record with duplicate: true.

4.6 Error Responses

All errors share one envelope. code is a stable numeric identifier to branch on; message is human-readable and may change; details is present only where documented. The shape is the same as the loyalty POS integration the partner team already knows, with moduleId in place of classId.

{
  "code": 25024,
  "moduleId": "PartnerPos",
  "message": "Offer availability has been exhausted",
  "details": { }
}

Validation and rate-limit errors are returned with moduleId "Common".

Error Catalogue

Code Name HTTP When Partner Action
1004 VALIDATION_ERROR 400 Request body fails schema validation, including neither or both of transactionId and redemptionId on a void. details.errors lists fields Fix the request
1006 TOO_MANY_REQUESTS 429 Rate limit exceeded for this key, or for this key, store and terminal Wait for Retry-After, then re-send the same request
25000 DEFAULT 500 Unexpected server error Re-send the same request with back-off
25001 INVALID_API_KEY 401 X-API-Key missing or unknown Check configuration; contact programme team after repeated failures
25002 API_KEY_REVOKED 401 Key has been revoked Stop. Switch to the replacement key
25005 PARTNER_INACTIVE 403 Partner has been deactivated Stop. Contact programme team
25007 STORE_NOT_FOUND 404 storeNumber is not mapped for this partner Check configuration; contact programme team
25008 STORE_INACTIVE 400 Store has been deactivated Contact programme team
25010 MEMBER_NOT_FOUND 404 QR value does not resolve to a member Inform the customer; proceed at full price
25011 QR_VALUE_UNSUPPORTED 400 QR value is in a format this interface does not accept (for example a non-Platinum code) Ask the member to present their Platinum QR code
25012 QR_VALUE_INVALID 400 QR value is recognised but no longer valid Ask the member to refresh their QR code and scan again
25013 MEMBER_NOT_ACTIVE 403 Member exists but the membership is not active Inform the member; proceed at full price
25020 OFFER_NOT_FOUND 404 offerId does not exist or does not belong to this partner Use an offer returned by POS Login
25021 OFFER_NOT_ACTIVE 400 Offer is inactive, expired or not yet started. details.reason is inactive, expired or not_started Remove the discount; proceed at full price
25022 OFFER_NOT_AT_STORE 400 Offer is not available at this store Use an offer returned by POS Login for this store
25023 OFFER_TYPE_NOT_REDEEMABLE 400 Offer is a leisure venue check-in offer, which this interface does not handle Use the Partner Portal
25024 OFFER_QUOTA_EXCEEDED 409 Availability exhausted at confirmation Remove the discount; complete the sale at full price
25025 MEMBER_NOT_ELIGIBLE 422 Member does not meet the offer's eligibility rules, or the offer was not in this member's POS Login list. details.offers lists offers the member can use Offer one of those or proceed at full price
25030 TOKEN_NOT_FOUND 404 token unknown, or issued to another partner Scan the member again
25031 TOKEN_EXPIRED 410 Token is past tokenExpiresAt Scan the member again
25032 TOKEN_ALREADY_USED 409 Token was already consumed by a Redeem Offer Scan the member again for a new sale
25033 TOKEN_STORE_MISMATCH 409 storeNumber differs from the store the token was issued for Scan the member again at this store
25034 TOKEN_MEMBER_MISMATCH 409 memberId sent does not match the token's member Check the POS state; scan again
25041 DISCOUNT_INVALID 400 Amounts are inconsistent or impossible for the offer. details.reason explains (see 6.1) Correct the amounts
25043 TRANSACTION_TIME_OUT_OF_RANGE 400 transactionTime is more than 24 hours in the past or more than 5 minutes in the future Check the partner system clock
25044 TRANSACTION_ALREADY_REDEEMED 409 A completed redemption exists for this transactionId with different details. details.redemptionId, details.status, details.redeemedAt identify it Investigate; void it first if the recording was wrong
25050 REDEMPTION_NOT_FOUND 404 No completed or voided redemption for this transactionId or redemptionId at this partner Nothing to void
25051 VOID_WINDOW_EXPIRED 409 voidableUntil has passed Contact programme team for a manual reversal
25052 VOID_STORE_MISMATCH 409 storeNumber differs from the store the redemption was recorded at Void from the store that made the sale

Codes by Method

Method Codes
POS Login 1004, 1006, 25000, 25001, 25002, 25005, 25007, 25008, 25010, 25011, 25012, 25013
Redeem Offer 1004, 1006, 25000, 25001, 25002, 25005, 25007, 25008, 25020, 25021, 25022, 25023, 25024, 25025, 25030, 25031, 25032, 25033, 25034, 25041, 25043, 25044
Void Transaction 1004, 1006, 25000, 25001, 25002, 25005, 25007, 25008, 25050, 25051, 25052

400 - Validation Error

{
  "code": 1004,
  "moduleId": "Common",
  "message": "Validation failed",
  "details": {
    "errors": {
      "totalAmount": ["Expected integer, received number"]
    }
  }
}

401 - Invalid API Key

{
  "code": 25001,
  "moduleId": "PartnerPos",
  "message": "API key is missing or invalid"
}

400 - Discount Invalid

{
  "code": 25041,
  "moduleId": "PartnerPos",
  "message": "Transaction amounts are inconsistent",
  "details": {
    "reason": "discount_implausible"
  }
}

details.reason is one of negative_amount, discount_exceeds_total, discount_implausible.

410 - Token Expired

{
  "code": 25031,
  "moduleId": "PartnerPos",
  "message": "Token has expired"
}

429 - Too Many Requests

HTTP/1.1 429 Too Many Requests
Retry-After: 30
Content-Type: application/json

{
  "code": 1006,
  "moduleId": "Common",
  "message": "Too many requests. Please try again later."
}

5. API Endpoints

Environment Base URL
Staging https://staging.api.platinum.retter.io/partners/pos/v1
UAT https://uat.api.platinum.retter.io/partners/pos/v1
Production Issued at go-live
Method Path Purpose
POST /posLogin Identify the member from the QR value; return the member id, a single-use token and the offers usable at this store
POST /redeemOffer Record the redemption of an offer on a sale (retry-safe by transaction id)
POST /voidTransaction Void the redemption of a sale

All three methods require the X-API-Key header. The machine-readable contract is the OpenAPI specification openapi-partner-pos.draft.yaml that accompanies this document. Where this document and the specification differ, the specification prevails for field names and types, and this document prevails for behaviour.

6. Appendix

6.1 Offer Types and Discount Validation

Offer type Meaning Partner sends as `discountAmount` Platinum's reference discount (fils)
discount Percentage off The discount actually applied for this offer round(totalAmount × percentage ÷ 100). Used to reject implausible figures only; an offer applied to part of the sale may legitimately come in below it
special_price A fixed member price for a defined item or set The discount actually applied for this offer Not computed
special_deal A non-monetary benefit (for example a complimentary item) 0 unless a monetary value was given Not computed

The partner-reported discountAmount is recorded as the discount the member received. Platinum computes the reference discount for its own reporting and rejects only impossible figures.

Validation of amounts at Redeem Offer (performed before availability is reserved)

Rule Outcome
Any amount negative or non-integer 400 code 25041, reason: negative_amount
discountAmount greater than totalAmount 400 code 25041, reason: discount_exceeds_total
discount offer with discountAmount far above the reference (indicative: more than 1.5 × reference + 500 fils) 400 code 25041, reason: discount_implausible

The implausibility threshold is configuration on the Platinum side and is confirmed with the partner team.

6.2 Redemption Status Lifecycle

   POST /posLogin ──▶ token (single use, valid until tokenExpiresAt)
          │
          │  POST /redeemOffer: token consumed, availability reserved
          ▼
   ┌─────────────┐   POST /voidTransaction                  ┌───────────┐
   │  completed  │ ───────────────────────────────────────▶ │  voided   │
   └─────────────┘   within voidableUntil, same store,      └───────────┘
          │          availability released                        │
          │                                                       │
   repeat redeemOffer                                     repeat voidTransaction
   (same transactionId, same details)                     → 200, duplicate: true
   → 200, duplicate: true                                         │
                                                                  ▼
                                                     new posLogin + redeemOffer with
                                                     the same transactionId
                                                     → new completed record