Draft v0.1 — directional. This contract shows which information the two REKOM
endpoints exchange with OnlinePOS. Every field name, header, enum value and JSON
structure in it is directional: none has been aligned, OnlinePOS stated on 6 Oct 2026
that POS/mPOS define the basket object, and REKOM will adopt OnlinePOS’s default names
and structures rather than require any of these. Request fields keep the Version 1
names wherever they existed. If OnlinePOS already has a field or flow that does the
same job, REKOM wants to use that instead; see the banner on the
introduction page.
Endpoints
| Endpoint | Purpose | Called |
|---|---|---|
POST /v1/onlinepos/basket | Calculate the loyalty basket for a recognised member. Synchronous. | Once per basket calculation, inside the payment motion |
POST /v1/onlinepos/sale | Report the outcome of a calculation after the sale is finalised or abandoned. | Exactly once per calculation, outside the checkout path |
api-reference/openapi.yaml in this repository.
Hosts
| Environment | Base URL |
|---|---|
| Production | https://loyalty.xeniamoments.com (TBC) |
| Staging / test | https://loyalty-staging.xeniamoments.com (TBC) |
422 venue_not_configured on the other.
Headers (directional names)
Request
| Header | Required | Value |
|---|---|---|
Content-Type | yes | application/json; charset=utf-8 |
OnlinePOS-Signature | yes | t=<unix seconds>,v1=<hex HMAC-SHA256>; see Request signing |
Idempotency-Key | yes | The calculationId (UUID). Same key + same body replays the stored response; same key + different body is 409. |
X-Request-Id | recommended | Fresh UUID per HTTP attempt, for log correlation |
Accept | optional | application/json |
Response
| Header | Value |
|---|---|
X-Request-Id | Echo of the request header, or a server-generated UUID |
Server-Timing | app;dur=<ms> — server processing time, for joint latency measurement |
Retry-After | On 429 and 503: seconds to wait (relevant for the sale event; the POS never waits) |
Idempotent-Replayed | true when the response was served from the idempotency store |
Conventions
| Topic | Rule |
|---|---|
| Encoding | JSON, UTF-8. Unknown fields in requests are ignored; unknown fields in responses must be ignored by OnlinePOS. |
| Identifiers | Strings, even where OnlinePOS uses integers internally (venueId, productId, baxId). calculationId is a lower-case UUID. |
| Money | Integer minor units (øre for DKK) with an ISO 4217 currency per basket. Never floats. VAT is not sent or returned. Proposed — OnlinePOS to confirm; the REST API uses decimal strings. |
| Percentages | Number with at most two decimals, 0–100, e.g. 12.5. Amounts derived from percentages are computed by REKOM with round_half_up per line. |
| Timestamps | RFC 3339 with UTC offset, e.g. 2026-10-08T21:14:03+02:00. |
| Booleans | Always explicit; no null for flags. |
| Field naming | Directional. camelCase in this draft; final names follow OnlinePOS defaults. Context fields mirror the OnlinePOS REST transaction API in meaning: venueId ↔ venue, cashRegisterId ↔ cash_register_id, clerkNumber ↔ clerk_number, productId ↔ product_id, productMasterId ↔ product_master_id, ean ↔ product_ean. baxId (BAX number of the terminal setup) and terminalId (payment terminal TID) identify the venue and till for REKOM and are required alongside cashRegisterId. |
| Versioning | Path version (/v1/). Additive changes (new optional fields, new enum values on reason) are not breaking; OnlinePOS should tolerate them. Breaking changes get /v2/. |
Errors
Errors are JSON with a stablecode:
{
"error": {
"code": "arithmetic_mismatch",
"message": "totals.total (19000) does not equal the sum of orderLinePrice (18000).",
"details": { "field": "totals.total", "expected": 18000, "received": 19000 },
"requestId": "0b7a7a4e-0c1b-4e2a-9f3d-5c6d7e8f9a0b"
}
}
| HTTP | code | Meaning |
|---|---|---|
| 400 | invalid_request | Malformed JSON or schema violation (missing field, wrong type) |
| 401 | invalid_signature | Signature missing or does not verify with any active secret |
| 401 | stale_timestamp | Signature timestamp outside ±300 s |
| 409 | idempotency_conflict | Same Idempotency-Key, different body |
| 422 | arithmetic_mismatch | Request totals or line identities do not add up |
| 422 | unknown_currency | Currency not configured for the venue |
| 422 | venue_not_configured | Venue not mapped in this environment |
| 422 | too_many_lines | More than 200 lines |
| 429 | rate_limited | Too many requests; Retry-After set |
| 500 | internal_error | Unexpected error on REKOM’s side |
| 503 | unavailable | Maintenance or overload; Retry-After set |
How the POS treats each response
ForPOST /v1/onlinepos/basket:
| Response | POS action | Order loyalty status | Sale event failureReason |
|---|---|---|---|
200 status applied, validation passes | Replace basket | applied | — |
200 status unchanged | Keep basket | unchanged | — |
200 status member_not_found | Keep basket | member_not_found | — |
200 but validation fails | Keep basket | failed | invalid_response |
Any 4xx / 5xx | Keep basket; do not retry | failed | http_error |
| Timeout | Keep basket; do not retry | failed | timeout |
| Connection failure before the request was sent | Retry once with the same key, then keep basket | failed | connection_error |
POST /v1/onlinepos/sale:
| Response | OnlinePOS backend action |
|---|---|
200 | Done. result is recorded, duplicate or unknown_calculation; all three are final. |
429, 5xx, network error | Retry with exponential backoff up to 24 h, same Idempotency-Key |
Other 4xx | Stop; log for support. Indicates a contract mismatch, not a transient fault. |
Test data
On staging, REKOM provides a test member whose loyalty ID yields a deterministic 20 % line discount on every discountable line, plus a loyalty ID that yieldsunchanged and one that yields member_not_found. Values are shared with the
staging secret.