How to Set Up POS Terminal Payments with Adyen in Salesforce Commerce Cloud

Adyen’s Salesforce Commerce Cloud (SFCC) cartridge enables in-person payments through a service-driven flow that connects your storefront checkout to physical terminals via the Terminal API, requiring configuration of store IDs, regional endpoints, and service credentials before implementing the handle-authorize hook pattern.

The adyen/adyen-salesforce-commerce-cloud repository provides a complete Point of Sale (POS) integration that bridges web checkout flows with Adyen-managed physical terminals. This implementation allows merchants to process POS terminal payments with Adyen directly from their SFCC storefront, creating a unified commerce experience across online and in-person channels. The integration relies on three architectural layers: configuration management, front-end terminal discovery, and back-end payment authorization.

Understanding the POS Integration Architecture

The cartridge implements a layered architecture that separates concerns across configuration, API exposure, and payment processing. According to the source code in adyen/adyen-salesforce-commerce-cloud, the system organizes functionality into distinct components:

This architecture ensures that terminal selection occurs at checkout, while sensitive payment processing happens server-side through the AdyenPosPayment service.

Step 1: Enable and Configure the POS Payment Method

Activate AdyenPOS in Business Manager

The cartridge registers the payment method using the constant METHOD_ADYEN_POS defined in src/cartridges/int_adyen_SFRA/cartridge/adyen/config/constants.js. To enable POS terminal payments, activate the AdyenPOS payment method in Business Manager. The system checks availability via PaymentMgr.getPaymentMethod(constants.METHOD_ADYEN_POS).isActive() before exposing the flow during checkout.

Configure Store ID and Regional Endpoints

In the Business Manager interface (rendered via posSettings.isml), merchants must configure two critical parameters:

  • Store ID: Matches the store entity configured in your Adyen Customer Area for Terminal API access
  • Terminal API Region: Select from US, EU, AU, or APSE to determine the live endpoint target

These values persist through the AdyenConfigs helper and determine the service URL at runtime. The region selection maps to specific endpoints via constants like POS_ENVIRONMENT_EU or POS_ENVIRONMENT_US in constants.js.

Step 2: Retrieve Connected Terminals

When the checkout page loads, the client fetches available terminals through the getConnectedTerminals controller. Located at src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/pos/getConnectedTerminals.js, this script validates the active store ID and queries Adyen's Connected Terminals endpoint:

// src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/pos/getConnectedTerminals.js
function getConnectedTerminals(req, res, next) {
  const requestObject = {
    merchantAccount: AdyenConfigs.getAdyenMerchantAccount(),
    store: AdyenConfigs.getAdyenActiveStoreId()
  };
  
  const response = adyenTerminalApi.executeCall(
    constants.SERVICE.CONNECTEDTERMINALS,
    requestObject
  );
  
  res.json({ response: response.text });
}

The front-end populates a dropdown element (typically select id="terminalDropdown") with the returned terminal IDs and models. Only terminals linked to the configured Store ID appear in this list.

Step 3: Implement the Payment Flow

The payment execution follows SFCC's standard hook pattern with two middleware functions handling instrument creation and authorization.

Creating the Payment Instrument

The posHandle.js middleware prepares the basket by removing existing payment instruments and creating a fresh one bound to the AdyenPOS method:

// src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/hooks/payment/processor/middlewares/posHandle.js
function posHandle(basket) {
  Transaction.wrap(() => {
    collections.forEach(basket.getPaymentInstruments(), item => {
      basket.removePaymentInstrument(item);
    });
    
    const paymentInstrument = basket.createPaymentInstrument(
      constants.METHOD_ADYEN_POS,
      basket.totalGrossPrice
    );
    
    paymentInstrument.custom.adyenPaymentMethod = 'POS Terminal';
  });
  
  return { error: false };
}

This ensures a clean state before the authorization step and marks the instrument with the custom payment method identifier.

Authorizing the Terminal Payment

When the shopper confirms the order, posAuthorize.js extracts the selected terminal ID from the form data and initiates the Terminal API call:

// src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/hooks/payment/processor/middlewares/posAuthorize.js
function posAuthorize(order, paymentInstrument, paymentProcessor) {
  paymentInstrument.paymentProcessor = paymentProcessor;
  
  const form = server.forms.getForm('billing');
  const terminalId = form.adyenPaymentFields?.terminalId.value;
  
  if (!terminalId) {
    throw new AdyenError('No terminal selected');
  }
  
  return adyenTerminalApi.createTerminalPayment(
    order, 
    paymentInstrument, 
    terminalId
  );
}

Building the Terminal API Request

The createTerminalPayment function in adyenTerminalApi.js constructs a SaleToPOIRequest message conforming to the POS protocol specification:

// src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/pos/adyenTerminalApi.js
function createTerminalPayment(order, paymentInstrument, terminalId) {
  const service = adyenService.getService(constants.SERVICE.POSPAYMENT);
  
  // Dynamic URL rewriting for live regions
  if (AdyenConfigs.getAdyenEnvironment() === constants.MODE.LIVE) {
    const regionEndpoint = AdyenHelper.getTerminalApiEnvironment();
    const serviceUrl = service.getURL().replace('[ADYEN-REGION]', regionEndpoint);
    service.setURL(serviceUrl);
  }
  
  const request = buildTerminalRequest(order, terminalId);
  const result = service.call(JSON.stringify(request));
  
  if (result.error) {
    sendAbortRequest(terminalId, service);
    return { error: true };
  }
  
  return parsePaymentResponse(result.object, order, paymentInstrument);
}

The request structure includes:

  • MessageHeader: Protocol version, message class (SERVICE), category (PAYMENT), and unique ServiceID
  • PaymentRequest: SaleData containing SaleTransactionID (order number), SaleReferenceID, and SaleToAcquirerData
  • AmountsReq: Currency and amount extracted from the payment instrument

Step 4: Configure Service Credentials

The AdyenPosPayment service requires credential configuration in Business Manager. Defined in metadata/site_import/services.xml, the service uses placeholder URLs with the [ADYEN-REGION] token that gets replaced at runtime based on your region settings:

<service service-id="AdyenPosPayment">
  <credential-id>AdyenPosPayment</credential-id>
  <url>https://[ADYEN-REGION].terminal-api.adyen.com/.../sync</url>
  ...
</service>

Administrators must upload API keys and merchant account credentials for both test and live environments. The cartridge automatically switches between these based on the AdyenEnvironment configuration.

Summary

  • Enable the payment method: Activate AdyenPOS (METHOD_ADYEN_POS) in Business Manager to expose the terminal flow at checkout.
  • Configure store parameters: Set the Store ID and select the appropriate Terminal API region (US, EU, AU, APSE) in the Adyen Settings UI.
  • Implement terminal discovery: Use the getConnectedTerminals controller to populate the terminal selection dropdown with devices linked to your store.
  • Execute the payment flow: Implement posHandle.js to create payment instruments and posAuthorize.js to send SaleToPOIRequest messages via the AdyenPosPayment service.
  • Handle service credentials: Configure test and live credentials in services.xml, ensuring the [ADYEN-REGION] placeholder aligns with your selected region endpoint.
  • Manage failures: The system automatically sends abort requests to terminals when HTTP-level failures occur, preventing orphaned transactions.

Frequently Asked Questions

What is the Terminal API and how does it differ from online payments?

The Terminal API is Adyen's protocol for communicating with physical POS terminals using JSON-based SaleToPOIRequest messages. Unlike online payments that redirect to Adyen-hosted pages or use encrypted card data, Terminal API sends payment instructions directly to a specific terminal ID, and the terminal handles card presentment, PIN entry, and receipt printing locally. The SFCC integration wraps these messages in the adyenTerminalApi.js service layer while maintaining the same order management flow as e-commerce transactions.

How do I handle failed POS transactions or timeouts?

The cartridge implements automatic error handling in posAuthorize.js and adyenTerminalApi.js. When a service call fails or returns an error response, the sendAbortRequest function immediately cancels the pending terminal transaction to prevent double-charging. The order status is set to NotPaid, and detailed error logs are written for debugging. For timeouts, ensure your service timeout settings in Business Manager accommodate the typical duration of card-present interactions (usually 30-60 seconds).

Can I use multiple store IDs with different terminal fleets?

Yes, but the current implementation in posSettings.isml and AdyenConfigs supports a single active Store ID per site configuration. To support multiple stores, you would need to extend the getConnectedTerminals controller to accept dynamic store ID parameters from the front-end rather than relying solely on AdyenConfigs.getAdyenActiveStoreId(). Each store ID must be properly configured in your Adyen Customer Area with its associated terminal fleet.

What regions are supported for Adyen POS terminal payments?

The integration supports four regional endpoints defined in constants.js: US (United States), EU (Europe), AU (Australia), and APSE (Asia-Pacific South East). The AdyenHelper.getTerminalApiEnvironment() method maps these selections to live endpoints like live-eu.terminal-api.adyen.com or live-us.terminal-api.adyen.com. Always select the region closest to your terminal's physical location to minimize latency and ensure compliance with local processing requirements.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →