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

# Post-sale and reconciliation

> The single post-sale event, what is stored on the transaction, the REST sync, receipts, offline sales and how cancellations differ from refunds.

<Note>
  **Directional.** Field names, enum values, headers and JSON structures on this page
  show *which* information is exchanged, not the final names. None of them has been
  aligned with OnlinePOS yet. REKOM will adopt whatever OnlinePOS has by default for
  the order object and its webhooks, and does not require any of these specific names.
</Note>

## One post-sale event

<Info>
  **Proposed — for OnlinePOS confirmation.** Version 1 had three REKOM endpoints after
  the sale: `loyaltySaleFinalization`, `loyaltySaleCancellation` and `loyaltyOfflineSale`.
  Version 2 merges them into **one** event, `POST /v1/onlinepos/sale`, with a `status`.
  This removes the "three versus four webhooks" ambiguity in the 4 Sep text and gives
  REKOM one record per calculation.
</Info>

| Rule | Detail |
| - | - |
| When | After the transaction is finalised in the OnlinePOS backend, or when a payment is abandoned after a calculation. |
| How many | **Exactly one per `calculationId`.** Superseded calculations get none. |
| Blocking | Never. The event is sent from the OnlinePOS backend, outside the checkout path. |
| Retries | On network error, `429` or `5xx`: retry with exponential backoff (e.g. 10 s, 1 min, 10 min, 1 h, up to 24 h) with the same `Idempotency-Key`. On `4xx` other than `429`: stop and log. |
| Idempotency | `Idempotency-Key` = `calculationId`. A repeat is acknowledged, not re-processed. |
| Missing event | Tolerated. REKOM reconciles from the REST sync; a missing event is a support signal, not a data loss. |

### Statuses

| `status` | Meaning | `transactionId` |
| - | - | - |
| `applied` | Returned basket used; transaction finalised. | required |
| `unchanged` | No-change response; transaction finalised with original basket. | required |
| `member_not_found` | Lookup or REKOM backend found no member; transaction finalised with original basket. | required |
| `failed` | Call failed or response rejected; transaction finalised with original basket. `failureReason` required. | required |
| `cancelled` | Payment aborted after a calculation; nothing charged; no transaction. | absent |

### Failure reasons

| `failureReason` | Set when |
| - | - |
| `timeout` | No response within the per-attempt timeout or the hard cap |
| `connection_error` | DNS, TCP or TLS failure (after the single connect-retry) |
| `http_error` | Non-200 HTTP status from the REKOM backend |
| `invalid_response` | 200 but schema or arithmetic validation failed |
| `offline` | POS was offline; no call was attempted |
| `pos_error` | POS-side error while applying the returned basket |

### Payload

The event carries the GUID, the status, the OnlinePOS `transactionId` and
`receiptNumber`, the same context block as the basket request (venue, cash
register, BAX, clerk, channel), the loyalty ID if one was resolved, the amount
actually paid and the loyalty discount total that ended up on the transaction.
See the [API reference](/api-reference/introduction) for the schema and examples.

## Data on the transaction

<Check>**Agreed 7 Oct 2026, mechanism agreed after 7 Oct 2026:** the calculation GUID, the common loyalty/order ID, is stored on the order and exposed through the REST API as a **meta attribute**.</Check>

| Item | Status |
| - | - |
| Calculation GUID (`calculationId`) | Agreed (7 Oct 2026); exposed as a meta attribute on the order in the REST API (after 7 Oct 2026) |
| Loyalty ID as an order tag (Nightpay pattern) | Agreed for Phase 1 (17 Aug 2026); whether the tag is readable via REST is open |
| Loyalty status per order | Open. Listed in V1 §15, but 6 Oct: "POS has no structures to store or transfer REKOM-specific values" |
| Per-line discount data (amount, text, discount code) | OnlinePOS stores what it needs to reproduce the receipt (3 Sep 2026). Exposure via REST: open |
| Filtering the transaction list by loyalty ID | Open; REKOM does not depend on it |

<Info>
  **Proposed — for OnlinePOS confirmation.** Since meta attributes are now the agreed
  carrier for the GUID, REKOM proposes storing the loyalty ID and the order loyalty
  status as two further meta attributes on the same order, so that Phase 1 attribution
  and support lookups need no separate tag or structure ([open point O3](/reference/open-points)).
</Info>

REKOM's design does **not** depend on anything beyond the GUID: the REKOM backend
already holds the full request and response for every calculation, keyed by GUID.
The REST sync adds the OnlinePOS transaction ID, receipt number, final amounts and
the discount code OnlinePOS recorded.

## REST sync and reconciliation

The REKOM backend pulls transactions through the existing OnlinePOS REST API, as it
does today for other purposes ("we sync orders anyway", 3 Sep 2026; agreed 7 Oct 2026).

```mermaid theme={null}
flowchart LR
  A["Basket request + response<br/>(REKOM, keyed by calculationId)"] --> J
  B["Sale event<br/>(status, transactionId)"] --> J
  C["REST transactions<br/>(calculationId meta attribute,<br/>lines, totals)"] --> J
  J{"Join on calculationId<br/>then transactionId"} --> D["Loyalty ledger<br/>reporting, member history"]
  J --> X["Exceptions queue<br/>(missing event, amount mismatch,<br/>transaction without calculation)"]
```

| Check | Outcome |
| - | - |
| Calculation `applied`, event `applied`, REST transaction with same GUID and matching discount total | Settled |
| Calculation `applied`, no event within 24 h, REST transaction present | Settled from REST; event logged as missing |
| Calculation present, no event, no REST transaction after expiry | Abandoned; reservation released |
| Event `applied`, but REST total differs from the calculated total | Exception; investigated with OnlinePOS using the GUID |
| REST transaction carries a GUID REKOM never saw | Exception; indicates an environment mismatch |

OnlinePOS's audit trail and end-of-day figures are unaffected by any of this
(*agreed 7 Oct 2026*).

## Receipts

OnlinePOS stores the discount data needed to reprint or reproduce the receipt
(*3 Sep 2026*): note-line text, discount amount, discount code and the footer
text. Nothing is fetched from the REKOM backend to print a receipt.

## Offline sales

When the POS is offline, no lookup and no live pricing happen; the sale completes
with the original basket (V1 §14). When the connection returns and the order is
replayed to the OnlinePOS backend, the backend sends one sale event with
`status: failed`, `failureReason: offline`, the loyalty ID if the terminal still
returned one, and the transaction ID. The POS generates a `calculationId` for the
attempt even though no call was made, so the event has a key.

Offline-replayed sales are **not** priced afterwards. The REKOM backend may credit
the member for the spend (points, visit) from the REST sync, but no discount is
applied retroactively. Finalised transactions are immutable (*17 Aug 2026*).

## Cancellation versus refund

| Case | Loyalty event? | What REKOM does |
| - | - | - |
| Payment aborted after a calculation (card declined, staff cancels) | `cancelled` | Releases any reservation for the calculation |
| Basket changed before payment | None for the old GUID; new calculation with `supersedesCalculationId` | Releases the old, records the new |
| Refund or return of a finalised transaction | **No event.** Refunds are not loyalty events. | Sees the refund through the REST sync and adjusts member history according to its own rules |
| Transaction voided the same day | No event | Same as refund |


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