Zum Hauptinhalt springen

Evaluate → Confirm lifecycle

A DRE transaction has two phases. Evaluate (repeatable, read-mostly) prices the current basket; confirm (once, at checkout) commits one priced iteration and triggers its side effects. This page explains how the two fit together — the part most integrations get wrong is the transaction-identity tuple, so start there.

The iteration-identity tuple

Every transaction is identified by (tenantId, transactionId, transactionCounter).

  • tenantId is resolved from your auth context — you never send it.
  • transactionId identifies the whole transaction. It is optional on /v2/evaluate and /v2/simulate: if you omit it, the server generates a UUID and returns it in meta.header.transactionId. It is required on /v2/confirm.
  • transactionCounter is the iteration number, 1-indexed per (tenantId, transactionId). It is server-assigned — do not set it on evaluate/simulate. The server returns it on meta.header.transactionCounter, and it is required on /v2/confirm to say which iteration you are committing.

Each evaluate against the same transactionId is a new iteration (the counter increments) — a fresh priced snapshot of the basket as it changed. This gives you a clean audit chain: every price the customer saw is a numbered iteration, and confirm records exactly the one that was paid.

Carry the header forward

After the first evaluate, read meta.header.transactionId and echo it on every subsequent evaluate for the same basket. Echo the transactionCounter from the iteration you decide to commit into the confirm call.

Phase 1 — Evaluate (and simulate)

POST /pos/v2/evaluate returns EvaluateResponseV2: a basket mirror. Every input line is echoed back in lineItems[] (strictly 1:1, correlated by lineReference), each carrying the stack of discounts that touched it, plus basket totals, a savings summary, any injected free items, coupon results, and recommendations. Re-call it on every basket change.

POST /pos/v2/simulate runs identical logic but is read-only — no coupon redemption, no budget consumption, no audit log — and stamps meta.isSimulation: true. Use it for cart previews and "what would I save" widgets. A simulation is never confirmed; when the customer commits, run a real evaluate and confirm that iteration.

See the wire contract for the full field tables, and Promotion scenarios & actions for what each promotion type produces in the response.

Phase 2 — Confirm

At checkout, commit the chosen iteration with POST /pos/v2/confirm. You pass the full identity tuple and the promotions you are finalising. The appliedPromotions[] entries mirror the evaluate response — each accepts a discountAmount (Money), so you can echo what evaluate returned verbatim.

{
"request": {
"header": {
"transactionId": "TXN-1",
"transactionCounter": 2
},
"transactionId": "TXN-1",
"posGroupCode": "STORE-001",
"appliedPromotions": [
{
"promotionId": "10000000-0000-4000-8000-000000000001",
"couponCode": null,
"discountAmount": { "value": 18.00, "currency": "EUR" }
}
],
"customerId": "CUST-4711",
"timestamp": "2026-06-07T14:30:45Z"
}
}
{
"transactionId": "TXN-1",
"confirmed": true,
"message": "Transaction confirmed"
}

Side effects — synchronous vs asynchronous

confirm triggers several side effects. On BTP they are split:

  • Primary, synchronous — completed before confirm returns:
    • Budget consumption (so a budget can't be over-spent by a race).
    • Calculation audit log.
  • Secondary, asynchronous — queued and processed by a background worker:
    • Coupon redemption, loyalty points, receipt effects, post-purchase coupon generation, and webhooks.

This keeps the checkout response fast while guaranteeing the money-critical writes are durable before you tell the customer "done".

Polling the async results

Poll GET /pos/v2/transactions/{transactionId}/{transactionCounter}/side-effects to observe the secondary effects. The response reports job status, timing, and the concrete outcomes:

{
"minorVersion": 0,
"transactionId": "TXN-1",
"transactionCounter": 2,
"status": "COMPLETED",
"enqueuedAt": "2026-06-07T14:30:45.500Z",
"startedAt": "2026-06-07T14:30:45.620Z",
"completedAt": "2026-06-07T14:30:45.910Z",
"attempts": 1,
"couponsRedeemed": 1,
"budgetsConsumed": 1,
"loyaltyPointsEarned": 0,
"postPurchaseCoupons": [
{
"code": "XMAS-7F3K9A",
"couponTypeId": "...",
"customerId": "CUST-4711",
"issuedAt": "2026-06-07T14:30:45.880Z"
}
],
"reason": null
}

The poll result is cached ~1 second server-side, so a tight loop is safe. Stop when status is COMPLETED or FAILED.

Confirm is not a safe blind-retry

confirm commits one specific (transactionId, transactionCounter) iteration. If a confirm call times out, do not simply replay it — poll the side-effects endpoint for that tuple to learn whether it committed, then act on the result.

Local-store note

On a local-store instance the same flow applies, but when the store is offline the transaction is queued locally and the side effects reconcile on the next sync to BTP. meta.source (btp | local-store) and GET /pos/heartbeat tell you which mode you are in and whether transactions are pending.

See also