Developer dashboard

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:

http
Authorization: Bearer <user-jwt>
X-Device-Token: <device-token>
X-Tenant-ID: <tenant-id>
Content-Type: application/json

The 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:

http
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 returns
422 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 from
X-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's
GET /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 lineTotal must equal baseAmount within $0.01.
  • If baseAmount is omitted, the server derives it as
    amount - tipAmount - taxAmount - convenienceFee.
  • Duplicate product lines are allowed. The server aggregates their quantities to three decimals
    for maxQuantity and 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.
  • misc lines 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:

json
{
  "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; therefore
baseAmount is 18.48.

1. Sale#

Cash/check — easiest safe Postman test#

POST/api/v1/transactions/sale
Cash/check — easiest safe Postman testjson
{
  "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:

json
{
  "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#

POST/api/v1/transactions/auth

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:

POST/api/v1/transactions/<data.id>/capture

Only the successful capture queues inventory settlement. Capture requires the ordinary capture
permission and its own idempotency key.

3. Saved-card sale or authorization#

POST/api/v1/transactions/token-sale
3. Saved-card sale or authorizationjson
{
  "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#

POST/api/v1/transactions/ach-sale

Prefer an ACH vault token:

4. ACH salejson
{
  "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 use
pciAchToken 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:

POST/api/v1/transactions/external-payment
5. External IPOS/NearPay paymentjson
{
  "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; a pending record 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:

json
{
  "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 and
baseAmount.

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/products after 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:

text
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 attempt

Use {{baseUrl}}/api/v1/... in each request. Before payment:

  1. Confirm the paired device session/test-connection is usable.
  2. Call GET {{baseUrl}}/api/v1/device/products?limit=200 with both tokens.
  3. Select a real data[].id, copy its name/unit/price, and note trackInventory,
    allowZeroStockSale, available, and eligible.
  4. Build lineItems and make its sum equal baseAmount.
  5. 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:

js
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:

js
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:

  1. Save data.id.
  2. Call GET {{baseUrl}}/api/v1/transactions/{{transactionRowId}}; the path accepts either the
    UUID data.id or the human-readable data.transactionId. The detail response includes
    normalized lineItems.
  3. Do not use GET /transactions to validate the cart; list rows intentionally omit lineItems.
  4. Re-fetch GET /device/products after 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 portal GET /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/422 product 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.