POS Itemised Payment Contract#
Updated: 2026-10-26
This is the mobile/POS contract for submitting an itemised cart through the five supported
Transaction APIs. It complements Device Integration and
Product Catalog.
Endpoints#
| Flow | Endpoint | Initial financial status | Inventory finality |
|---|---|---|---|
| New card or cash sale | POST /api/v1/transactions/sale |
Card: processor result; cash/check: settled |
Approved card or settled cash queues settlement |
| Card authorization | POST /api/v1/transactions/auth |
Authorization result | No deduction until successful capture |
| Saved-card payment | POST /api/v1/transactions/token-sale |
Processor result | SALE deducts after approval; AUTH waits for capture |
| ACH debit | POST /api/v1/transactions/ach-sale |
Usually pending |
No deduction until ACH settlement |
| Device SDK payment | POST /api/v1/transactions/external-payment |
pending when device notification arrives first |
Confirmed external SALE queues settlement; AUTH waits for capture |
The inventory-finality column assumes POS inventory settlement is enabled in that environment.
The client cannot read or control that server-side rollout setting, and payment success by itself
never proves that stock was deducted.
Do not use POST /api/v1/public/transaction for a POS cart. Its compatibility whitelist does not
forward lineItems.
Authentication and headers#
Mobile must send:
Authorization: Bearer <user-jwt>
X-Device-Token: <device-token>
X-Tenant-ID: <tenant-id>
Content-Type: application/jsonThe device token supplies the current server-authoritative merchant, device, and physical branch.
Do not send a cashier-selected stock location or calculate Available on the device.
The full catalog, location, and stock preflight in this document applies because these requests
carry verified device context. Non-device API integrations retain the quantity-limit-only contract
documented in Transactions, section "Itemised sale (lineItems)".
For /sale, /auth, /token-sale, and /ach-sale, also send:
Idempotency-Key: <stable-uuid-for-this-payment-attempt>The key must be 16–255 characters. A network retry must reuse the same key and identical body. A
new payment attempt must use a new key. Reusing a key with a different body returns422 IDEMPOTENCY_KEY_REUSED; a concurrent duplicate may return 409 until the first attempt
finishes. /external-payment is instead idempotent by (tenantId, gatewayTransactionId); always
reuse the provider's original transaction id.
For the first four endpoints, merchantId must be the merchant selected by the authenticated
mobile session and must match the device context. /external-payment derives the merchant fromX-Device-Token and does not need merchantId in its body.
The client must preserve that match; the current runtime does not yet reject every mismatched body
merchant/device combination on the first four routes. Refresh the device session after login or a
merchant switch and never reuse a cart/payment body from the previous merchant.
Cart contract#
All five endpoints accept the same optional lineItems array:
| Field | Type | Required | Notes |
|---|---|---|---|
type |
product | misc |
Recommended | Defaults to product when productId is present; otherwise misc |
productId |
UUID | Product only | id returned by GET /api/v1/device/products; forbidden on misc |
name |
string | Yes | Receipt snapshot; trimmed; max 150 characters |
quantity |
number | Yes | > 0, maximum 1,000,000, maximum 3 decimal places |
unit |
string | No | Catalog unit code or display text; max 50 characters |
unitPrice |
number | Yes | >= 0, maximum 2 decimal places |
lineTotal |
number | No | Must equal rounded quantity × unitPrice within $0.01; server computes it when omitted |
lineItems[].type is a transaction-line classification, not the catalog item'sGET /device/products type. Map mobile cart rows as follows:
| Mobile cart source | Catalog type |
Send lineItems[].type |
productId |
Inventory behavior |
|---|---|---|---|---|
| Catalog physical product | product |
product |
Required | May be tracked and deducted |
| Catalog service | service |
product |
Required | Validated against catalog; never tracked |
| Manually entered/custom “Miscellaneous” row at the till | Not applicable | misc |
Omit | Never affects catalog or inventory |
Do not send lineItems[].type: "service"; the Transaction API accepts only product or misc
and returns 400 INVALID_LINE_ITEMS for any other value. In this contract, product means
“catalog-backed line”, so it intentionally includes both catalog products and catalog services.
Additional rules:
- Maximum 200 lines.
[],null, or omitted means no cart. - Sum of normalized
lineTotalmust equalbaseAmountwithin $0.01. - If
baseAmountis omitted, the server derives it asamount - tipAmount - taxAmount - convenienceFee. - Duplicate product lines are allowed. The server aggregates their quantities to three decimals
formaxQuantityand inventory validation, while preserving the individual receipt lines. - Product lines are revalidated against the live product, current device branch,
maxQuantity,
tracking policy,allowZeroStockSale, and server-calculated Available. misclines never create catalog products and never affect inventory or most-sold counts.- The saved cart is immutable receipt history; later catalog edits do not rewrite it.
Example cart and amount breakdown:
{
"amount": 19.98,
"baseAmount": 18.48,
"taxAmount": 1.5,
"tipAmount": 0,
"convenienceFee": 0,
"lineItems": [
{
"type": "product",
"productId": "3f2b8c1e-4d5a-4e6f-8a7b-9c0d1e2f3a4b",
"name": "Diesel",
"quantity": 2.5,
"unit": "gallon",
"unitPrice": 4.19
},
{
"type": "misc",
"name": "Car wash",
"quantity": 1,
"unitPrice": 8.0
}
]
}The normalized product line total is 10.48; the misc line total is 8.00; thereforebaseAmount is 18.48.
1. Sale#
Cash/check — easiest safe Postman test#
{
"merchantId": "{{merchantId}}",
"amount": 19.98,
"baseAmount": 18.48,
"taxAmount": 1.5,
"tipAmount": 0,
"currency": "USD",
"paymentMethod": "Cash",
"lineItems": [
{
"type": "product",
"productId": "{{productId}}",
"name": "Diesel",
"quantity": 2.5,
"unit": "gallon",
"unitPrice": 4.19
},
{
"type": "misc",
"name": "Car wash",
"quantity": 1,
"unitPrice": 8
}
]
}Cash and check skip the card gateway and are immediately stored as settled. AUTH does not support
cash/check.
Card sale#
Use the same amount/cart fields plus a PCI token:
{
"merchantId": "{{merchantId}}",
"amount": 19.98,
"baseAmount": 18.48,
"taxAmount": 1.5,
"currency": "USD",
"paymentMethod": "Card",
"pciToken": "{{pciToken}}",
"lineItems": [
{
"type": "product",
"productId": "{{productId}}",
"name": "Diesel",
"quantity": 2.5,
"unit": "gallon",
"unitPrice": 4.19
},
{
"type": "misc",
"name": "Car wash",
"quantity": 1,
"unitPrice": 8
}
]
}Never send PAN or CVV to this endpoint. Obtain pciToken from the hosted PCI fields/SDK. Sandbox
testers may use the documented secret-key tokenization endpoint, but raw card data must not enter
mobile, Postman collections shared with the team, logs, or Production.
Success: 201 { "success": true, "data": <transaction> }.
2. Authorization#
Body: the card-sale body above with a fresh Idempotency-Key. A successful AUTH stores the cart but
does not deduct inventory. Capture it later with:
Only the successful capture queues inventory settlement. Capture requires the ordinary capture
permission and its own idempotency key.
3. Saved-card sale or authorization#
{
"merchantId": "{{merchantId}}",
"amount": 19.98,
"baseAmount": 18.48,
"taxAmount": 1.5,
"currency": "USD",
"gatewayToken": "{{gatewayToken}}",
"paymentMethodId": "{{paymentMethodId}}",
"transactionType": "sale",
"lineItems": [
{
"type": "product",
"productId": "{{productId}}",
"name": "Diesel",
"quantity": 2.5,
"unit": "gallon",
"unitPrice": 4.19
},
{
"type": "misc",
"name": "Car wash",
"quantity": 1,
"unitPrice": 8
}
]
}Required payment identity: gatewayToken or gatewayCustomerId. paymentMethodId selects a
specific saved method when applicable. transactionType defaults to sale; use auth to hold
funds and wait for capture. Success: 201.
4. ACH sale#
Prefer an ACH vault token:
{
"merchantId": "{{merchantId}}",
"amount": 19.98,
"baseAmount": 18.48,
"taxAmount": 1.5,
"currency": "USD",
"pciAchToken": "{{pciAchToken}}",
"accountType": "checking",
"accountHolderName": "Test Customer",
"secCode": "PPD",
"lineItems": [
{
"type": "product",
"productId": "{{productId}}",
"name": "Diesel",
"quantity": 2.5,
"unit": "gallon",
"unitPrice": 4.19
},
{
"type": "misc",
"name": "Car wash",
"quantity": 1,
"unitPrice": 8
}
]
}The API also accepts accountNumber plus a nine-digit routingNumber, but mobile should usepciAchToken so bank details are not placed in app logs or Postman exports. accountType defaults
to checking. ACH submission commonly returns pending; no inventory is deducted until the ACH
transaction reaches settlement. Success: 201.
5. External IPOS/NearPay payment#
Call this only after the device SDK has returned its provider transaction id:
{
"gatewayTransactionId": "{{providerTransactionId}}",
"paymentMode": "NEARPAY",
"type": "sale",
"amount": 19.98,
"baseAmount": 18.48,
"taxAmount": 1.5,
"tipAmount": 0,
"convenienceFee": 0,
"currency": "USD",
"cardLast4": "4242",
"cardBrand": "Visa",
"orderId": "MOBILE-ORDER-1001",
"lineItems": [
{
"type": "product",
"productId": "{{productId}}",
"name": "Diesel",
"quantity": 2.5,
"unit": "gallon",
"unitPrice": 4.19
},
{
"type": "misc",
"name": "Car wash",
"quantity": 1,
"unitPrice": 8
}
]
}paymentMode is case-insensitive IPOS or NEARPAY and must match the authenticated device
gateway. type defaults to sale; it may be auth. The server derives merchant, device, branch,
MID, and terminal id from the device token.
The client must send the authenticated device's actual mode. Runtime currently performs the
explicit mode-mismatch rejection only when an enhancedData candidate is also present, so clients
must not treat server acceptance by itself as proof that the provider mode was correct.
201: the device notification arrived first; apendingrecord was created.200: the provider webhook arrived first; the existing record was enriched with device fee/cart
facts without replacing webhook-owned status/card/auth facts.- Retry with the same
gatewayTransactionId; never generate a replacement id. - Because the provider may already have collected money, product validation failure does not reject
or roll back this financial record. When a final external SALE is confirmed, the server queues
inventory settlement if the environment rollout is enabled. After Invoice accepts the settlement,
unresolved stock or location outcomes become durable Pending Review. A terminal delivery failure
remains operational outbox work rather than becoming a mobile payment failure.
Postman alone cannot prove this flow's final settlement. A sandbox provider callback/webhook is
needed to move the transaction from pending to the provider's final status.
Responses and cart errors#
The first four endpoints return 201 on success. /external-payment returns 201 or 200 as
described above. Success bodies use:
{
"success": true,
"data": {
"id": "<transaction-row-id>",
"transactionId": "<display/reference-id>",
"status": "approved|settled|pending|...",
"lineItems": [
{
"type": "product",
"productId": "<uuid>",
"name": "Diesel",
"quantity": 2.5,
"unit": "gallon",
"unitPrice": 4.19,
"lineTotal": 10.48
}
]
}
}Important errors:
| HTTP | Code | Meaning |
|---|---|---|
| 400 | INVALID_LINE_ITEMS |
Invalid line shape/precision/count or subtotal mismatch |
| 400 | QUANTITY_LIMIT_EXCEEDED |
Aggregated quantity exceeds product maxQuantity |
| 404 | PRODUCT_NOT_FOUND |
Product is not in this merchant's live catalog |
| 422 | PRODUCT_NOT_ACTIVE |
Product is inactive |
| 422 | DEVICE_LOCATION_REQUIRED |
Tracked product but device has no current branch |
| 422 | DEVICE_LOCATION_INACTIVE |
Current branch is not a usable active physical location |
| 409 | INSUFFICIENT_STOCK |
Short-selling is off and requested quantity exceeds Available |
| 503 | PRODUCT_CART_VALIDATION_UNAVAILABLE |
Product validation could not complete; pre-payment paths fail closed |
| 401 | INVALID_DEVICE_TOKEN / UNAUTHORIZED |
User/device token is malformed, expired, revoked, or unknown |
| 403 | DEVICE_SUSPENDED / TX_TYPE_DISABLED |
Device or transaction type is disabled |
| 422 | LIMIT_EXCEEDED |
Device or merchant transaction limit rejected the payment |
error.details.violations identifies every affected product. INVALID_LINE_ITEMS supplies the
line index when the failure belongs to one line. A subtotal mismatch supplies lineItemsTotal andbaseAmount.
Inventory behavior mobile must assume#
allowZeroStockSale=false: the first four endpoints reject insufficient stock before provider
work. No payment and no deduction occur.allowZeroStockSale=true: payment may complete. Under the balance lock, Available 2 / sell 5
records deducted 2, not deducted 3, and leaves Available at 0. Later Quantity In does not consume
the historical shortfall.- AUTH and pending ACH do not deduct. Capture and ACH settlement deduct once.
- Duplicate requests, callbacks, reconciliation, and worker retries do not deduct twice.
- Settlement delivery retries are server-managed and bounded. A terminal delivery failure is
retained for operational review; it does not create a second payment or change payment success. - Refund, void, reversal, dispute, ACH return, and customer return do not automatically restock.
Physical returns require an explicit Quantity In / Customer Return movement. - Payment success is never rolled back because post-payment inventory work failed.
- Refresh
GET /api/v1/device/productsafter final sale/capture/settlement; the previous Available
value is advisory and may already be stale.
Postman verification workflow#
For local testing, import the ready-made collection and environment described in
"POS Inventory Local Postman". The steps below remain useful when recreating
the requests manually or adapting them for UAT.
Create environment variables:
baseUrl = https://uat.auxcore.net
tenantId
userJwt
deviceToken
merchantId
productId
pciToken # card sale/auth only
gatewayToken # saved-card flow only
paymentMethodId # when required by the saved method
pciAchToken # ACH only
providerTransactionId # external-payment only
idempotencyKey # stable for one attempt; replace for a new attemptUse {{baseUrl}}/api/v1/... in each request. Before payment:
- Confirm the paired device session/test-connection is usable.
- Call
GET {{baseUrl}}/api/v1/device/products?limit=200with both tokens. - Select a real
data[].id, copy its name/unit/price, and notetrackInventory,allowZeroStockSale,available, andeligible. - Build
lineItemsand make its sum equalbaseAmount. - Generate a UUID for
idempotencyKey; keep it unchanged only when retrying the exact request.
For the first four requests, this optional Postman pre-request script creates a key only when the
environment does not already have one:
if (!pm.environment.get('idempotencyKey')) {
pm.environment.set('idempotencyKey', pm.variables.replaceIn('{{$guid}}'));
}Set the header to Idempotency-Key: {{idempotencyKey}}. Delete or replace the environment value
before starting a genuinely new payment; leave it unchanged for a transport retry. Add this test
script to save the returned transaction identifier and catch malformed success responses:
pm.test('payment request succeeded', () => {
pm.expect([200, 201]).to.include(pm.response.code);
});
const response = pm.response.json();
pm.expect(response.success).to.eql(true);
pm.expect(response.data.id).to.be.a('string');
pm.environment.set('transactionRowId', response.data.id);
pm.environment.set('transactionId', response.data.transactionId);Recommended cases:
| Case | Expected result |
|---|---|
| Cash sale with valid product cart | 201, settled, normalized lineItems; stock later reflects one settlement |
| Replay same body + same key | Same response; Idempotent-Replayed: true; no second deduction |
| Same key + changed body | 422 IDEMPOTENCY_KEY_REUSED |
Wrong baseAmount |
400 INVALID_LINE_ITEMS; no payment |
| Four-decimal quantity | 400 INVALID_LINE_ITEMS; no payment |
| Tracked product, no branch | 422 DEVICE_LOCATION_REQUIRED; no payment |
| Shortage with short-selling off | 409 INSUFFICIENT_STOCK; no payment |
| Available 2 / sell 5 with short-selling on | Payment succeeds; deducted 2 / not deducted 3 |
| AUTH | Authorization succeeds; stock unchanged until capture |
| ACH accepted/pending | Stock unchanged until settlement callback/reconciliation |
| External notification before webhook | 201 pending; repeat gives existing record; webhook needed for finality |
After a successful request:
- Save
data.id. - Call
GET {{baseUrl}}/api/v1/transactions/{{transactionRowId}}; the path accepts either the
UUIDdata.idor the human-readabledata.transactionId. The detail response includes
normalizedlineItems. - Do not use
GET /transactionsto validate the cart; list rows intentionally omitlineItems. - Re-fetch
GET /device/productsafter financial finality to inspect server-confirmed Available.
Inventory settlement is asynchronous, so an unchanged value on the first refresh is not by
itself a failure. Retry briefly; if it still does not converge, do not resubmit the payment.
Operational staff must inspect the Transaction settlement outbox and any Invoice Pending Review
application before concluding that no deduction occurred.
What can and cannot be tested in the current web UI#
The web portal is useful for setup and observation:
/invoices/products: create/edit a product, enable inventory tracking and short-sale policy;
use the Inventory tab for Quantity In and Inventory Activity./branches: create an active physical location and assign/reassign the device./transactions: find the transaction; open/transactions/<id>to verify the normalized cart,
amounts, payment status, branch, and receipt data.
The current Tenant Dashboard Virtual Terminal does not send this POS lineItems contract, so it
does not prove the five itemised endpoints end to end. Use Postman or the mobile build to submit the
cart. Also, the existing Inventory Pending Review component is not currently rendered in the
Inventory tab; verify accepted business-review work through the recovery API, and terminal
Transaction delivery failures through operational logs, metrics, or outbox tooling until that UI
is enabled.
Mobile implementation checklist#
- Gate POS cart UI on
GET /api/v1/device/parameters→posLite.enabled. - Read products only from
GET /api/v1/device/products; never use portalGET /products. - Send product id/name/unit/price as a sale snapshot; do not send Available or branch selection.
- Recalculate display totals locally for UX, but treat server subtotal/product validation as final.
- Persist one idempotency key per payment attempt and reuse it across transport retries.
- Treat AUTH and pending ACH/external payments as non-final; do not optimistically reduce stock.
- On
409/422product violations, refresh the device catalog before the cashier retries. - After final payment, show payment success even if inventory is delayed or under review; never
create a replacement payment to recover inventory.