> ## Documentation Index
> Fetch the complete documentation index at: https://docs.loyalty.xeniamoments.com/llms.txt
> Use this file to discover all available pages before exploring further.

# API introduction

> Conventions for the REKOM Loyalty Partner API (for OnlinePOS): hosts, headers, money, errors and how the POS treats each response.

<Warning>
  **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](/).
</Warning>

## 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 |

The full schema, with request and response examples, is on the two endpoint pages
that follow. The source is `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) |

Both hosts serve TLS 1.2+ only. Secrets differ per environment; a venue mapped to
one environment is rejected with `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](/architecture/configuration-and-security#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 stable `code`:

```json theme={null}
{
  "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

For `POST /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` |

For `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 yields
`unchanged` and one that yields `member_not_found`. Values are shared with the
staging secret.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.