Skip to main content

Terminal API Payment Flow

How to make an in-person payment with Adyen's Terminal API, from the SaleToPOIRequest your POS app sends to the response and confirming webhook.

This page describes how to make an in-person payment with Adyen's Terminal API: you send a payment request, the terminal interacts with the shopper, and you receive the result. For how your POS app connects, see Architecture Overview.

At a glance

Your POS app sends a Terminal API payment request, the request reaches the terminal, the shopper taps, inserts, or swipes their card and completes any prompt such as PIN, and you receive the result. The authoritative final outcome is also confirmed by a webhook. See Webhooks.

The request

A Terminal API payment is a SaleToPOIRequest that wraps a message header and a PaymentRequest.

The message header (SaleToPOIRequest.MessageHeader) identifies the transaction and the terminal:

  • ProtocolVersion: 3.0

  • MessageClass: Service

  • MessageCategory: Payment

  • MessageType: Request

  • ServiceID: a unique ID for this request from your POS app

  • SaleID: your identifier for the POS system component sending the request

  • POIID: the unique ID of the target terminal, in the format [device model]-[serial number]

The PaymentRequest carries the transaction reference and amount:

  • SaleData.SaleTransactionID: an object with a TransactionID (your reference for the payment, recommended unique per payment, shown as the merchant reference in your Customer Area) and a TimeStamp in UTC format

  • PaymentTransaction.AmountsReq: an object with Currency (three-letter currency code) and RequestedAmount (the amount in number format with a decimal point, for example 10.99)

POST {terminal-api-endpoint}/sync  // cloud, synchronous result

{
  "SaleToPOIRequest": {
    "MessageHeader": {
      "ProtocolVersion": "3.0",
      "MessageClass": "Service",
      "MessageCategory": "Payment",
      "MessageType": "Request",
      "ServiceID": "0421",  // unique per request
      "SaleID": "POS-Register-1",
      "POIID": "MODEL-123456789"  // [device model]-[serial number]
    },
    "PaymentRequest": {
      "SaleData": {
        "SaleTransactionID": {
          "TransactionID": "ORDER-12345",  // unique per payment
          "TimeStamp": "2026-06-12T10:00:00.000Z"  // UTC
        }
      },
      "PaymentTransaction": {
        "AmountsReq": { "Currency": "USD", "RequestedAmount": 10.99 }
      }
    }
  }
}

The exact structure above is illustrative. Confirm field-level details against Adyen's Terminal API documentation before building. See Adyen's Make a payment with the Terminal API guide.

The response

You receive a SaleToPOIResponse containing a PaymentResponse. Check:

  • POIData.POITransactionID.TransactionID: the unique transaction identifier for the payment, in the format tenderReference.pspReference. Save it: you need it later for lookups, reconciliation, refunds, or manual capture.

  • Response.Result: Success for an approved payment, Failure for a declined payment, or Partial for a partial authorization.

  • Response.ErrorCondition: present when the result is Failure, indicating why the payment failed.

// PaymentResponse (abbreviated)
{
  "POIData": { "POITransactionID": { "TransactionID": "x5Hp...gQ.881..." } },
  "Response": { "Result": "Success" }
}

Cloud and local endpoints

  • Local: send the request directly to the terminal, for example https://{terminal-ip}:8443/nexo. You receive the result synchronously.

  • Cloud: send the request to the Adyen platform. Use /sync to receive the result synchronously or /async to receive it as an event notification. Test endpoints are https://terminal-api-test.adyen.com/sync and /async. Live uses a region-specific host, for example https://terminal-api-live-us.adyen.com/sync.

Enable Terminal API in your Customer Area before you start.

Confirm with a webhook

If you do not receive a response, verify the transaction status rather than retrying blindly, to avoid duplicate payments. Update your records based on the webhook Adyen sends, not on the synchronous response alone.

Did this answer your question?