Used Newie, our AI search tool in the docs? Take a 2-minute survey to rate your experience!

Integrating payments for self-checkout

Prev Next

NewStore self-checkout is headless for payments: the platform records the payment on the order but does not process it. Your self-checkout app takes payment through a payment terminal and payment service provider (PSP) of your choice, and then reports the completed payment to NewStore when placing the order. This article covers where payment sits in the transaction flow, the architecture it requires, terminal integration with Adyen and with NewStore Payments (Stripe), how to record the payment on the order correctly, and the payment-specific errors you may encounter.

Note

The payments contract on the Order Placement API is still being refined. The typed instrument fields described here may change before general availability. Validate against the latest integration guide before going to production.

How does payments work in the self-checkout flow

Payment happens between the tax quotation and order placement. The sequence is fixed:

  1. Get the final total — the totals.grand_total from the last tax quotation is the exact amount to charge. Never compute it client-side.

  2. Charge the terminal — your app sends the amount to the payment terminal via your PSP. The shopper taps or inserts their card.

  3. Place the order — only after the terminal reports success, call POST /orders with a CAPTURED or AUTHORIZED payment instrument carrying the PSP reference. For AUTHORIZED payments, they convert to CAPTURED with a delay. Reconcile such payments with your payment service provider and in Omnichannel Manager.

  4. Create the receipt — continue with the receipt flow as described in the integration guide.

Note

If the terminal declines or fails the payment transaction, no order is placed. The shopper retries payment or abandons the cart, but nothing is recorded on the platform. Pay first, place second is the only way to keep the payment logs and ensure payment does not silently fail on the platform.

Architecture requirements

The rest of the self-checkout integration can run entirely in the browser or on the device, but the payment step needs a thin server-side component that you own as the retailer. PSP API keys (Adyen, Stripe) are secrets and must never be shipped to or stored in the browser or in the app. Route all PSP calls through this thin server-side component.

  • The kiosk frontend calls your backend with the amount to charge.

  • Your backend holds the PSP API key and calls the PSP's cloud API, which pushes the payment to the physical terminal.

  • The terminal result (success/failure plus the PSP reference) flows back to the frontend, which then places the order.

Currency and amounts

  • One currency per store. NewStore supports a single currency per store location, set at the order level. All instrument amounts on an order use that currency.

  • NewStore uses integer minor units; some PSP APIs do not. A NewStore amount of 77519 (USD) corresponds to 775.19 in the Adyen Terminal API's RequestedAmount, which takes decimal major units. Convert exactly once, at the PSP boundary, and keep everything else in minor units.

  • Charge exactly totals.grand_total. The platform validates that instrument amounts sum to the order total; any mismatch rejects the order. The quotation already reflects the promotions calculation, so the amount you charge is the discounted total. See Promotions in the integration guide.

  • Dynamic currency conversion (DCC). If the retailer has DCC enabled on their Adyen account and the shopper chooses to pay in their home currency at the terminal, NewStore still records the payment in the store's currency. The DCC details are logged on the PSP side only.

Integrating an Adyen terminal

Use the Adyen cloud Terminal API (synchronous mode), which fits a web-based or app-based kiosk. The backend of the app sends a SaleToPOIRequest to Adyen's cloud endpoint, Adyen pushes it to the terminal, and the response returns when the shopper completes or cancels the payment.

Note

Terminal provisioning and boarding steps live in Adyen's documentation and the retailer's Adyen Customer Area, not in the self-checkout pack. For how NewStore tenants are set up with Adyen generally, see Integrating Adyen.

Prerequisites

Before the first payment can succeed on the device:

  • The terminal (for example a Verifone e285 or AMS1) is assigned to the store and boarded on the retailer's Adyen merchant account.

  • Terminal API is enabled in the Adyen Customer Area for that merchant account.

  • You have an Adyen API key with the Cloud Device API role, created under API credentials in the Customer Area. Use a test key against the Adyen test environment first.

  • The terminal is connected to Wi-Fi, and on the device, Settings > Diagnostics displays both PSP and Terminal API as Successful.

  • You know the terminal's POIID: <Model>-<Serial> (for example e285-XXXXXXXX), displayed on the device under Settings > Device info.

Configuration

Variable

Value

ADYEN_API_KEY

API key with the Cloud Device API role. Server-side only — never in the browser.

ADYEN_MERCHANT_ACCOUNT

The Adyen merchant account the terminal is boarded to (for example NewStorePOS).

ADYEN_TERMINAL_POIID

The terminal identifier, <Model>-<Serial>

ADYEN_ENVIRONMENT

test or live

If you use a company-level API key rather than a merchant-level one, name the merchant account on every payment request via SaleData.SaleToAcquirerData (merchantAccount=<name>).

Important

When you place the order, the card instrument must carry the Adyen merchant account in provider_account_id. Without it, the order is still accepted with 201, but asynchronous payment processing fails with missing_merchant_account and the order lands in the failed payment queue in Omnichannel Manager. Treat provider_account_id as required for every Adyen payment.

{"error_code": "missing_merchant_account", 
 "message": "Could not infer merchant account from request", 
 "request_accepted": false
}

provider_account_id is a typed field of the card instrument, but the order request does not enforce it, so nothing catches a missing value at request time. Ensure that this is a mandatory step for every Adyen payment. See the card payment model for the instrument fields and the metadata keys that the NewStore platform understands.

Integrating NewStore Payments via Stripe

With NewStore Payments, the payment infrastructure is powered by Stripe and the merchant account is managed by NewStore, which makes setup simpler than a direct PSP contract. Your backend uses the Stripe Terminal API to drive the reader (for example a Stripe S700).

Important

NewStore Payments uses Stripe connected accounts for each store for your business. Your app must create the PaymentIntent on the store's connected account, not on the platform account. If the intent is created on the platform account, the terminal payment appears to succeed and the order reaches NOM, but the platform cannot complete the financial instrument and the order fails with a failed authorization error.

Validating that you are connected to a physical reader

Stripe's SDKs default to a simulated reader in development. If your app displays failed to fetch or the physical terminal never lights up, check that the integration is registered against the real reader and not a simulated reader.

Recording the payment on the order

The full instrument contract is documented in the integration guide. Mentioned below are the payment-provider specifics that the integration needs.

{
  "payment": {
    "instruments": [{
      "id": "payment-1",
      "type": "CARD",
      "amount": 77519,
      "provider": "adyen",
      "status": "CAPTURED",
      "processed_at": "2026-08-13T09:39:15Z",
      "reference": "GDQVVVSCRVC5VP75",
      "origin": "terminal",
      "provider_account_id": "NewStorePOS",
      "brand": "mc",
      "last_digits": "9999"
    }]
  }
}
  • provider_account_id is required for Adyen payments (see above). For NewStore Payments the account linkage comes from the connected account used at payment time.

  • status is CAPTURED for self-checkout: the terminal has already taken the money before the order is placed. AUTHORIZED is also valid to use, but ensure that payment has been captured later by reconciling with your payment service provider and in Omnichannel Manager.

  • processed_at must be RFC 3339 with a timezone offset.

  • reference is the PSP's transaction reference — the tender reference returned by the Adyen terminal response, or the PaymentIntent id from Stripe. It is required for card payments and is what links the NewStore order to the PSP transaction for capture, refunds, and reconciliation.

  • brand and last_digits carry the card brand and last four digits; the receipt renders them.

Payment processing is asynchronous

A 201 from POST /orders means the order is accepted — not that payment processing has completed. The platform validates and processes the payment instrument asynchronously. A misconfigured instrument (missing merchant account, wrong connected account, missing reference) therefore surfaces after order placement, as an entry in the failed payment queue in NOM with an error such as:

Error communicating with EXTERNAL payment provider. This might indicate a payment provider CONFIGURATION or an INTEGRATION ERROR. Error: Could not infer merchant account from request

After your first end-to-end transaction against a real environment, always open the order in NOM (Omnichannel Manager) and confirm the payment shows as processed — a green kiosk screen is not proof that the payment side is correctly wired.

Cash and other tenders

Cash instruments (provider: "CASH" with given_amount, rounded_amount, change_amount) are supported by the order contract but a self-checkout kiosk may not have a  cash drawer. You can choose how to handle cash at the kiosk yourself. For example, you can have cash handled via handoff to an associate or use an associate-attended cash counting machine near the kiosk. Split payments and gift cards are not supported in the current self-checkout scope. See the limitations.

Local development

There is currently no payment terminal stub in the local stub server pack. Options for building before hardware is available:

  • Stub the terminal in your app — a fake payment port that returns Success or Failure lets you build and test the full order path against the local stub servers.

  • PSP test environments — Adyen's test environment and Stripe's test mode work with real boarded test terminals and test cards, without moving real money. This is the recommended step between local stubs and staging.

Troubleshooting

Symptom

Likely cause

Fix

Order accepted, but fails in NOM with missing_merchant_account

provider_account_id missing on the card instrument (Adyen)

Add the Adyen merchant account name as provider_account_id on the card instrument and place a new order.

Terminal payment succeeds, order fails with failed authorization (NewStore Payments)

PaymentIntent created on the platform account instead of the store's connected account

Create the PaymentIntent on the connected account.

Failed to fetch; physical terminal never reacts (Stripe)

App is talking to a simulated reader

Register and connect the physical reader; disable the simulator.

Adyen cloud request rejected or terminal unreachable

Terminal API not enabled, terminal not boarded to the store, or API key lacks the Cloud Device API role

Work through the prerequisites checklist above; check Settings → Diagnostics on the device.

400 on order placement

Instrument amounts do not sum to totals.grand_total, or a required instrument field is missing, such as the instrument id

Copy grand_total from the tax quotation directly; check the instrument id, reference, status, processed_at. The error body is an RFC 9457 problem details object with no error code; the reason is in detail.

Amount on the terminal is 100× too large or too small

Minor-unit / major-unit mismatch at the PSP boundary

Convert minor units to the PSP's expected format exactly once, in the backend.

Further reading