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).
tenantIdis resolved from your auth context — you never send it.transactionIdidentifies the whole transaction. It is optional on/v2/evaluateand/v2/simulate: if you omit it, the server generates a UUID and returns it inmeta.header.transactionId. It is required on/v2/confirm.transactionCounteris the iteration number, 1-indexed per(tenantId, transactionId). It is server-assigned — do not set it on evaluate/simulate. The server returns it onmeta.header.transactionCounter, and it is required on/v2/confirmto 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.
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
confirmreturns:- 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 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
- POS endpoints — the full endpoint catalogue.
- POS Service wire contract — field tables for evaluate, confirm, and the side-effects poll.
- Coupon lifecycle — how coupon redemption fits into confirm.