Zum Hauptinhalt springen

POS Service

The POS Service (/pos) provides endpoints consumed by POS terminals and mobile apps for real-time promotion evaluation, simulation, coupon management, article import, and transaction confirmation.

Overview

PropertyValue
Base Path/pos
ProtocolDual-protocol — @protocol: ['odata-v4', 'rest']
Evaluation wire contractv2 (EvaluateResponseV2, body minorVersion: 8)
MethodPOST (evaluate / simulate / confirm / coupon ops / article import); GET (side-effects poll, heartbeat)
AuthAdmin or POSReader role
Dual-protocol service

POSService is mounted with @protocol: ['odata-v4', 'rest'], so every action is reachable both as a plain REST call (the form documented here, e.g. POST /pos/v2/evaluate) and as an OData V4 unbound action (POST /pos/evaluate with the OData action-invocation envelope). The REST paths below are the canonical, documented integration surface. The documented /v2/... URLs are served via URL aliasing in the service bootstrap — CAP 8.x does not honour @rest.path directly, so the annotation is retained for documentation purposes.

v2 cutover — this page documents the live contract

The evaluate/simulate response is line-item-centric (v2). The legacy v1 shape (appliedPromotions[], discountedItems[], a flat totals.totalDiscount, top-level isSimulation/potentialSavings) has been removed — it no longer exists on the wire. If your integration still reads those fields, migrate to the v2 fields described below.

Promotion Evaluation

POST /pos/v2/evaluate

Evaluates a shopping basket against all active promotions and returns the line-item-centric EvaluateResponseV2: each promotion's effect lives inline on the line it touched (lineItems[].discounts[]), plus basket totals, a savings summary, granted free items, coupon results, and recommendations.

The entire payload is wrapped in a top-level request object.

Request — EvaluateRequest

FieldTypeRequiredNotes
headerRequestHeaderoptionalIteration identity (see below).
posGroupIdUUID stringconditionalPOS group identifier. At least one of posGroupId / posGroupCode is required.
posGroupCodestring(20)conditionalHuman-readable POS group code; the handler resolves it to posGroupId.
itemsBasketItem[]yesScanned basket lines (see below). Must be non-empty.
customerCustomerInfooptionalCustomer / loyalty context.
couponsCouponInput[]optionalPresented coupon codes — each entry is { "code": "…" }.
timestampISO-8601optionalTransaction time; defaults to server time.
channelstring(50)optionalChannel for CHANNEL conditions (e.g. IN_STORE, ONLINE, MOBILE); basket-level, case-insensitive.
paymentInfoPaymentInfooptional{ paymentMeans, cardType, cardNumber } for payment conditions.
userDefinedFieldsKeyValuePair[]optionalBasket-level key-value pairs for USER_DEFINED conditions.
manualDiscountManualDiscountInfooptional{ enabled, reasonCode } for MANUAL_DISCOUNT conditions.
includeInactivebooleanoptionalSimulation only — also evaluate INACTIVE promotions. Default false.
includeMissedPromotionsbooleanoptionalSimulator-only diagnostic — adds a missedPromotions[] array enumerating filtered-out promotions per pipeline stage. Default false; the field is omitted from the response entirely when not set.

RequestHeader — iteration identity, echoed back on meta.header so the POS can correlate request → response → audit across the (tenantId, transactionId, transactionCounter) tuple:

FieldTypeNotes
transactionIdstring(50)Optional on /v2/evaluate and /v2/simulate — if omitted, the server generates a UUID and returns it in meta.header.transactionId; the POS must echo it on follow-up calls. Required on /v2/confirm.
transactionCounterintegerServer-assigned, 1-indexed per (tenantId, transactionId). The POS must not set it on evaluate/simulate; it is returned on meta.header. Required on /v2/confirm.
receiptIdstring(50)Optional customer-facing receipt number; echoed in meta.header.
headerReferencestring(100)Optional opaque basket-level correlation ID; echoed verbatim.

BasketItem — one scanned article:

FieldTypeRequiredNotes
articleNumberstring(50)yesSKU / article identifier.
quantityDecimal(15,3)yes> 0 = sale line, < 0 = return/refund line, = 0 is rejected with HTTP 400.
unitPriceDecimal(15,2)yesPrice per unit before discounts. Echoed unverified — refund-amount correctness and returnability are the POS/ERP's responsibility.
eanstring(18)optionalBarcode.
articleGroupstring(100)optionalFree-text product group (legacy).
articleGroupIdstring(20)optionalBusiness identifier of the article group (matches ArticleGroups.articleGroupId).
manufacturerIdstring(255)optionalFor MANUFACTURER conditions.
lineReferencestring(50)optional, recommendedStable per-line reference. Auto-assigned if absent; used to correlate every response line (and affectedItems) back to the scanned item.
additionalFieldsKeyValuePair[]optionalPer-item key-value pairs for ADDITIONAL_FIELD conditions.

CustomerInfo: customerId, customerGroup, loyaltyCardNo, loyalty ({ tier, points }). CouponInput: { code }.

Response — EvaluateResponseV2

Top-level structure:

FieldTypeNotes
minorVersionintegerBody-level minor version. Currently 8. Additive shape changes bump this; the major version lives in the URL (/v2/).
metaMetaV2Response metadata + header echo.
lineItemsLineItemV2[]Strictly 1:1 with input items — every line echoed with its inline discount stack.
grantedItemsGrantedItemV2[]Free items injected by FREE_ITEM promotions (articles not already in the basket). Empty otherwise.
totalsTotalsV2Basket totals + always-present savings summary.
recommendationsRecommendationV2[]Near-miss / hint recommendations.
appliedCouponsAppliedCoupon[]{ code, couponTypeName, promotionIds[] }.
invalidCouponsInvalidCoupon[]{ code, reason }.
budgetLimitedPromotionsBudgetLimitedPromotion[]{ promotionId, promotionName, reason } — promotions dropped because a linked budget is exhausted.
missedPromotionsMissedPromotion[]Present only when includeMissedPromotions: true (simulator diagnostic).
nudgesNudgeV2[]Present only when tenant flag enableProductionNudges is ON; otherwise empty.
thresholdGapsThresholdGapV2[]Present only when tenant flag enableProductionNudges is ON; otherwise empty.

MetaV2:

FieldTypeNotes
headerRequestHeaderEcho, including the server-assigned transactionId + transactionCounter.
evaluatedAtTimestampWhen evaluation completed (ISO-8601).
sourcestringbtp | local-store.
instanceIdstringInstance identifier (e.g. BTP, STORE-001).
isSimulationbooleantrue when produced by /v2/simulate.
tenantIdstringResolved tenant identifier.
dataAgeTimestampFreshness of the underlying promotion data (last sync for local-store, last write for BTP).

LineItemV2 (every monetary field is Money = { value, currency }):

FieldTypeNotes
lineReferencestring(50)Echoed from input (auto-assigned when omitted).
articleNumber, ean, articleGroupId, manufacturerIdstringEchoed from input.
quantityQuantity{ value, unit }; unit defaults to "PCE". Negative for echoed return lines.
unitPriceMoneyPer-unit price before discounts.
lineTotalMoneyunitPrice × quantity (before discounts).
lineDiscountMoneySum of all discounts[].discountAmount (positive).
lineNetMoneylineTotal − lineDiscount, floored at 0.
discountsLineDiscountV2[]One entry per promotion that touched this line. Empty when none applied.
isFreeItembooleantrue when an already-in-basket line is granted free by a FREE_ITEM promotion (gift branch).
freeItemPromotionIdUUIDSet only when isFreeItem is true.

LineDiscountV2:

FieldTypeNotes
promotionIdUUIDPromotion that produced this discount.
promotionNamestring(255)Display name.
promotionTypestring(20)ARTICLE, RECEIPT, BUNDLE, …
discountTypestring(20)PERCENTAGE | ABSOLUTE | UNIT_PRICE.
discountValueDecimal(15,2)Configured value (percent or amount).
discountAmountMoneyActual monetary discount applied to this line.
totalDiscountMoneyCanonical alias of discountAmount (identical value). New integrations should read totalDiscount; discountAmount is retained as a deprecated alias.
couponCodestring(50)Coupon that triggered this promotion, if any.
triggeredByCouponbooleanWhether a coupon unlocked this promotion.

TotalsV2:

FieldTypeNotes
subtotalMoneySum of every line lineTotal (before discounts).
discountMoneySum of every line lineDiscount (positive).
grandTotalMoneysubtotal − discount.
saleSubtotalMoneyPresent only when the basket contains return lines.
returnSubtotalMoneyPresent only when the basket contains return lines (negative).
savingsSummarySavingsSummaryV2Always present.

SavingsSummaryV2: totalSavings (Money), savingsPercent (Decimal(5,2), ≤ 100), originalTotal (Money), finalTotal (Money), promotionBreakdown[] ({ promotionId, promotionName, totalDiscount: Money, affectedItems: string[] }affectedItems are lineReference strings), itemSavings[] ({ articleNumber, originalPrice, finalPrice, savings } — all Money), loyaltyPointsEarned (integer).

GrantedItemV2 (FREE_ITEM inject branch only): grantReference (deterministic, GRANT-<promoShort>-<code>-<n>), articleNumber, ean, quantity (integer), referencePrice (Money), priceSource (BASKET_PRICE | REFERENCE_PRICE | MASTER_DATA | UNKNOWN_ZERO), giveAwayValue (Money), promotionId, promotionName, triggeredByCoupon.

RecommendationV2: kind (NEAR_MISS | HINT), code (stable failure-category code — see the Recommendation Hint Catalog), params (KeyValuePair[] for POS-side localisation), defaultMessage (English fallback), promotionId, promotionName. matchPercent is emitted dynamically and only on NEAR_MISS entries; HINT entries omit it entirely.

ThresholdGapV2: promotionId, promotionName, type (RECEIPT_AMOUNT | ITEM_QUANTITY | SCALED_RECEIPT), currentValue, threshold, gap (all Decimal(15,2)), potentialSaving (Money).

NudgeV2: message, suggestedAction, potentialSaving (Money), promotionId, promotionName, gapType.

Example

Request — basket with two lines; the customer is GOLD-tier:

POST /pos/v2/evaluate
{
"request": {
"header": { "transactionId": "TXN-2026-001" },
"posGroupId": "60000000-0000-4000-8000-000000000001",
"items": [
{ "lineReference": "L1", "articleNumber": "ART-1001", "ean": "4007817327098", "articleGroupId": "ELECTRONICS", "quantity": 2, "unitPrice": 89.99 },
{ "lineReference": "L2", "articleNumber": "CIG-1001", "quantity": 4, "unitPrice": 25.00 }
],
"customer": { "customerId": "CUST-4711", "loyaltyCardNo": "LC-98765", "loyalty": { "tier": "GOLD", "points": 1250.00 } },
"timestamp": "2026-06-07T14:30:00Z"
}
}

Response — a 10% PERCENTAGE ARTICLE promotion fires on ART-1001; nothing matches CIG-1001:

{
"minorVersion": 8,
"meta": { "header": { "transactionId": "TXN-2026-001", "transactionCounter": 1 }, "evaluatedAt": "2026-06-07T14:30:01.234Z", "source": "btp", "instanceId": "BTP", "isSimulation": false, "tenantId": "tenant-abc" },
"lineItems": [
{ "lineReference": "L1", "articleNumber": "ART-1001", "ean": "4007817327098", "articleGroupId": "ELECTRONICS", "manufacturerId": null,
"quantity": { "value": 2.000, "unit": "PCE" },
"unitPrice": { "value": 89.99, "currency": "EUR" }, "lineTotal": { "value": 179.98, "currency": "EUR" },
"lineDiscount": { "value": 18.00, "currency": "EUR" }, "lineNet": { "value": 161.98, "currency": "EUR" },
"discounts": [ { "promotionId": "10000000-0000-4000-8000-000000000001", "promotionName": "Electronics 10% Off", "promotionType": "ARTICLE", "discountType": "PERCENTAGE", "discountValue": 10, "discountAmount": { "value": 18.00, "currency": "EUR" }, "totalDiscount": { "value": 18.00, "currency": "EUR" }, "couponCode": null, "triggeredByCoupon": false } ],
"isFreeItem": false, "freeItemPromotionId": null },
{ "lineReference": "L2", "articleNumber": "CIG-1001", "ean": null, "articleGroupId": null, "manufacturerId": null,
"quantity": { "value": 4.000, "unit": "PCE" },
"unitPrice": { "value": 25.00, "currency": "EUR" }, "lineTotal": { "value": 100.00, "currency": "EUR" },
"lineDiscount": { "value": 0.00, "currency": "EUR" }, "lineNet": { "value": 100.00, "currency": "EUR" },
"discounts": [], "isFreeItem": false, "freeItemPromotionId": null }
],
"grantedItems": [],
"totals": {
"subtotal": { "value": 279.98, "currency": "EUR" }, "discount": { "value": 18.00, "currency": "EUR" }, "grandTotal": { "value": 261.98, "currency": "EUR" },
"savingsSummary": { "totalSavings": { "value": 18.00, "currency": "EUR" }, "savingsPercent": 6.43, "originalTotal": { "value": 279.98, "currency": "EUR" }, "finalTotal": { "value": 261.98, "currency": "EUR" },
"promotionBreakdown": [ { "promotionId": "10000000-0000-4000-8000-000000000001", "promotionName": "Electronics 10% Off", "totalDiscount": { "value": 18.00, "currency": "EUR" }, "affectedItems": ["L1"] } ],
"itemSavings": [ { "articleNumber": "ART-1001", "originalPrice": { "value": 179.98, "currency": "EUR" }, "finalPrice": { "value": 161.98, "currency": "EUR" }, "savings": { "value": 18.00, "currency": "EUR" } } ],
"loyaltyPointsEarned": 0 }
},
"recommendations": [], "appliedCoupons": [], "invalidCoupons": [], "budgetLimitedPromotions": [], "nudges": [], "thresholdGaps": []
}
Coupons wire-format

The coupons field is an array of objects ([{ "code": "…" }]), not bare strings. See Coupons on the basket (wire-format) for the full shape and multi-coupon behaviour.

For the full catalogue of promotion scenarios and how each action type surfaces in this response (line discounts vs. grantedItems[] vs. loyalty points vs. receipt-only totalDiscount), see the Promotion Scenarios & Action Types guide.

POST /pos/v2/simulate

Identical evaluation logic and identical EvaluateResponseV2 shape as /v2/evaluate, but read-only: it does not redeem coupons, consume budgets, or write audit logs. The response carries meta.isSimulation: true. The simulator-only flags includeInactive and includeMissedPromotions are honoured here.

Transaction Confirmation

POST /pos/v2/confirm

Confirms a POS transaction. Triggers the side effects: coupon redemption, budget consumption, calculation audit logging, and post-purchase coupon generation.

On BTP, primary side effects (budget consumption, calculation log) run synchronously; secondary side effects (coupon redemption, loyalty, receipt, post-purchase coupons, webhooks) run asynchronously via the side-effects worker — poll their status with the side-effects endpoint below.

Request — ConfirmRequest

FieldTypeRequiredNotes
headerRequestHeaderyesheader.transactionId and header.transactionCounter are required to identify which iteration to commit.
transactionIdstring(50)yesTransaction identifier.
posGroupId / posGroupCodestringoptionalPOS group (UUID or code).
appliedPromotionsConfirmPromotion[]yesPromotions to finalise.
customerIdstring(50)optionalFor post-purchase coupon assignment.
timestampISO-8601optionalTransaction time.
itemsConfirmBasketItem[]optionalStructured basket lines with final pricing, for audit logging.

ConfirmPromotion: promotionId (required), couponCode, totalDiscount (Decimal — scalar, backwards-compatible), discountAmount (Money — additive, mirrors LineDiscountV2.discountAmount so an evaluate response can be echoed verbatim). When both are present, discountAmount.value takes precedence for budget consumption.

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

Response — ConfirmResponse: { transactionId, confirmed, message }.

GET /pos/v2/transactions/{transactionId}/{transactionCounter}/side-effects

Polls the async side-effects job for a confirmed transaction. Cached server-side for ~1 second to dampen tight polling loops. Returns { minorVersion, transactionId, transactionCounter, status, enqueuedAt, startedAt, completedAt, attempts, couponsRedeemed, budgetsConsumed, loyaltyPointsEarned, postPurchaseCoupons[], reason }, where each postPurchaseCoupons[] entry is { code, couponTypeId, customerId, issuedAt }.

Coupon Operations

All coupon endpoints accept a POST with a JSON body and return JSON. The coupon lifecycle is validate → reserve → redeem (with cancel to release a reservation); digital coupons add activate; CRM/marketing systems use issue.

Coupons on the basket (wire-format)

The coupons field on the POST /pos/v2/evaluate (and /pos/v2/simulate) request body is an array of objects, each with a code — not an array of bare strings. Sending a string array is rejected with 400 VALIDATION_FAILED (target: coupons).

Correct — object array:

{
"request": {
"header": { "transactionId": "TXN-2026-001" },
"posGroupId": "60000000-0000-4000-8000-000000000001",
"items": [
{ "lineReference": "L1", "articleNumber": "ART-1001", "quantity": 1, "unitPrice": 12.50 }
],
"coupons": [
{ "code": "WELCOME15" },
{ "code": "SUMMER25" }
]
}
}

Incorrect — string array (rejected):

{
"coupons": ["WELCOME15", "SUMMER25"]
}

CouponInput schema:

FieldTypeRequiredDescription
codestringyesThe coupon code to apply. Present on every coupon object.

Multiple coupons are evaluated in array order: each code resolves to its promotion(s), which stack by default unless an ExclusivityLevel or ExclusionGroups rule blocks the combination; the first-applied promotion wins ties.

At POST /pos/v2/confirm there is no top-level coupons field — the coupon that triggered each finalised promotion is carried per-promotion as appliedPromotions[].couponCode (see Transaction Confirmation). For the validate → reserve → redeem flow, see the coupon endpoints below and the coupon lifecycle guide.

POST /pos/coupons/validate

Validates a coupon code. Request { code }. Response { valid, code, message, promotionId, validUntil }.

POST /pos/coupons/reserve

Reserves a coupon for a transaction. Request { code, transactionId }. Response { reservationId, reserved, expiresAt, message }. Redeem before expiresAt.

POST /pos/coupons/redeem

Redeems a previously reserved coupon. Request { reservationId, transactionId }. Response { redeemed, message }.

POST /pos/coupons/cancel

Cancels a reservation, making the coupon available again. Request { reservationId }. Response { cancelled, message }.

POST /pos/coupons/activate

Activates a digital coupon (CREATEDACTIVE). Used by mobile apps when a customer activates a personalized coupon. Request { code, customerId }. Response { activated, code, message, validUntil }.

POST /pos/coupons/issue

Issues personalized coupons for customers (single or batch). Used by CRM / marketing automation. Request { couponTypeId | couponTypeName, customerId | customerIds[], reason, metadata }. Response { issuedCount, failedCount, issuedCoupons[], failures[], message }.

Rate limited to 1000 issuances per minute per tenant.

Article Import

POST /pos/articles/import

Synchronous article import with upsert semantics (create if new, update if exists based on articleNumber). Each article is processed independently — one failure does not abort the batch. The request body wraps the array in articles (the action's named parameter).

Request:

{
"articles": [
{
"articleNumber": "ART-10006",
"name": "Mineral Water 0.5L",
"ean": "4001954600106",
"manufacturer": "Spring Corp",
"category": "Beverages"
}
]
}

Response (ArticleImportResponse):

{
"imported": 1,
"updated": 0,
"failed": 0,
"results": [
{ "articleNumber": "ART-10006", "status": "created", "id": "uuid" }
],
"errors": []
}

Each results[] entry is { articleNumber, status (created | updated | failed), id, error }.

POST /pos/articles/import-batch

Asynchronous batch import with job tracking. Creates a persistent import job and processes articles in the background. Body: { name, articles[] }. Returns immediately with { jobId, status, message }.

Monitor job progress via GET /admin/ArticleImportJobs({jobId}) (Admin Service) or GET /api/v1/ArticleImportJobs({jobId}) (PublicAPI).

vorsicht

ArticleImportJobs tracks POS-originated batch imports only. For imports submitted via POST /api/v1/Articles:bulk-import (PublicAPI bulk import), use ImportJobs + ImportJobErrors instead. See Article Import API for the full comparison.

Health

GET /pos/heartbeat

Returns connectivity and health diagnostics: { status, version, mode, btpConnectivity, lastSync, pendingTransactions, promotionsLoaded, uptime }. On local-store instances it reflects circuit-breaker state and the pending transaction queue; on BTP it returns a simplified status.

Authorization

Requires the Admin or POSReader role.

User (Development)PasswordRole
adminadminAdmin
pos-readerposPOSReader

Integration Patterns

For detailed integration guidance including authentication, polling strategies, error handling, and data synchronization, see the Integrator Guide. For a per-scenario walkthrough of every promotion/action type and where it appears in the response, see Promotion Scenarios & Action Types.