Accept online payments with Hosted Payment Elements

Build a secure checkout quickly with NCR Voyix Hosted Payment Elements (HPE). Embed individual hosted payment fields directly in your page, support card payments, and keep card data outside your systems.

HPE is a client-side SDK that renders each payment field in its own <iframe>. Your backend creates a session, your frontend collects card data through the hosted fields, and your backend finalizes the payment after the form is submitted.

How HPE works

  1. Load the SDK in your page and call ncr.elements() to instantiate the hosted payment form.
  2. Configure payment fields — each field (card number, expiry, CVV, etc.) renders in its own secured iframe hosted by NCR.
  3. Create session on your server with InitializeSession and receive the session details needed to submit.
  4. Submit the form by calling paymentForm.submit(submissionConfig) with the session details.
Why teams choose HPE

Full UI control with PCI-safe hosted fields, granular masking and styling per field, and a consistent payment flow managed by NCR Voyix.

Quickstart (first successful payment)

Follow this order to get to your first successful checkout with minimal rework.

  1. Generate RSA keys and keep private key material secure. See RSA key generation.
  2. Validate with Postman first so signing and headers are confirmed before app coding. See Postman end-to-end.
  3. Load the HPE SDK and call ncr.elements() to create the payment form with hosted fields.
  4. Implement the submit flow: InitializeSession → paymentForm.submit() → CompleteSession.
  5. Verify success criteria: receive statusCode = 100 and then call CompleteSession once.
First success criteria

Your setup is healthy when InitializeSession returns session details, HPE fields load and validate, submit resolves successfully, and CompleteSession succeeds exactly once.

Prerequisites

Complete these setup items before coding integration steps. This prevents most first-run failures.

  1. Generate RSA keys first and secure private key material. Follow RSA key generation (clean setup).
  2. Prepare Postman environment with privateEncryptedKeyPEM and password to validate signatures before app integration.
  3. Confirm NCR onboarding values: X-CompanyNumber, X-StoreNumber, and allowed origins for ReturnURL/FrameHostingURL.
  4. Set server environment variables and keep private key usage server-side only.
Recommended onboarding order

RSA setup → Postman end-to-end validation → iframe integration in your app → reversal and mobile extensions.

RSA key generation (clean setup)

Use the Dev Portal RSA Key Generation Studio output as your single source for Postman and backend signing inputs.

Download RSA Key Generator (.zip)

Step-by-step

  1. Open RsaKeyGenerationStudio.exe from the Dev Portal package.
  2. Enter key prefix (for example: KC250630-STORE190).
  3. Use key size 2048 or 4096 (recommended default: 2048).
  4. Set a key secret so private PEM is exported as encrypted PKCS#8.
  5. Generate and verify these files: {Prefix}.Private.pem, {Prefix}.Public.pem, {Prefix}.Private.xml, {Prefix}.Public.xml.

Where each key file is used

FileUsed byPurpose
{Prefix}.Private.pemPostman (privateEncryptedKeyPEM)RSA signing for InitializeSession / CompleteSession scripts
{Prefix}.Public.pemPostman optional verificationSignature verification during troubleshooting
{Prefix}.Public.xmlMobile registration bodyClientPublicKey for RegisterMobileDevice
{Prefix}.Private.xmlLegacy tools onlyNot required in standard HPP browser iframe flow
Security baseline

Never commit private keys, PEM passwords, or full Postman environments with secrets to source control. Keep key material in secure secret stores and rotate periodically.

Postman end-to-end sandbox

Let merchants validate the complete purchase flow without writing app code. Use this Postman-first path aligned with the Dev Portal collections to verify configuration, signing, and API responses before frontend integration.

Download Postman Collection Package (.zip)

Recommended collection flow

  1. Import WebEPS API New 2026 for Ziosk collection and matching environment.
  2. Set environment values: baseURL, API Version, Company Number, Store Number, Client Application Name.
  3. Set key material: privateEncryptedKeyPEM and password.
  4. Run InitializeSession request (pre-request script injects RequestExpirationUTC and X-Signature).
  5. Open returned RequestURL in browser and complete payment.
  6. Run CompleteSession using saved CompleteSession collection variable.
Swagger-style interactive option

Need a guided API explorer with grouped endpoints and multi-language snippets? Click below to open and explore.

Open API Reference (Swagger)

Architecture

The integration has three actors: your Application Server, your Browser / Client, and the NCR HPE API. The browser never calls NCR directly — all signed API calls go through your server. Payment fields are rendered as secure iframes hosted by NCR; card data never touches your application.

⚠️ Critical — never call NCR APIs from the browser

InitializeSession and CompleteSession must be called from your Application Server. Your NCR private key and company credentials must never be exposed client-side.

StepWhere it runsDescription
InitializeSessionServerSigns and POSTs to NCR; returns session details to browser
Load HPE SDKBrowserCalls ncr.elements() to render hosted payment fields
Payment field iframesBrowserEach field renders in its own NCR-hosted iframe
Submit formBrowserCalls paymentForm.submit(submissionConfig) with session details
CompleteSessionServerFinalises the transaction — call only after submit resolves successfully

Integration flow

Browser / Client Your frontend Your App Server Node / Express backend NCR HPE API NCR Voyix Host 1 INITIALIZE SESSION POST /api/payments/initialize InitializeSession (RSA-signed) SessionId + RequestURL { sessionId, requestURL } 2 LOAD IFRAME iframe src = RequestURL Load HPP payment form (iframe) postMessage: PAGE_LOAD_COMPLETE postMessage: IFRAME_RESIZE { height } 3 CUSTOMER PAYS Customer enters card inside iframe Process card / wallet token Authorisation result Redirect iframe → ReturnURL?statusCode=100 4 PAYMENT_RETURN ReturnURL page loaded postMessage: PAYMENT_RETURN { statusCode } statusCode === 100 → proceed to CompleteSession 5 COMPLETE SESSION POST /api/payments/complete { sessionId } CompleteSession (RSA-signed) Transaction finalised { success: true } ✓ Navigate to order confirmation Browser / Client Your App Server NCR HPE API

Step 1 — InitializeSession

Endpoint: POST /v1.4/PaymentTransactionManagement/InitializeSession  Server-side

Call this from your backend when the customer proceeds to checkout. Sign the request body with your RSA-SHA256 private key and pass back only sessionId and the session details to the browser for use in paymentForm.submit().

Required headers

HeaderRequiredDescription
Content-TypeYesapplication/json or text/xml
X-CompanyNumberYesMerchant company number
X-StoreNumberYesMerchant store number
X-ReferenceIdYesUnique correlation ID per request
X-Client-Application-NameYesClient integration identifier
X-Client-Application-VersionOptionalClient application version
X-SignatureYesRSA-SHA256 signature generated server-side
curl -X POST "{{baseURL}}/v1.4/PaymentTransactionManagement/InitializeSession" \
  -H "Content-Type: application/json" \
  -H "X-CompanyNumber: {{Company Number}}" \
  -H "X-StoreNumber: {{Store Number}}" \
  -H "X-ReferenceId: {{$guid}}" \
  -H "X-Client-Application-Name: {{Client Application Name}}" \
  -H "X-Client-Application-Version: {{Client Application Name Version}}" \
  -H "X-Signature: {{GeneratedSignature}}" \
  -d '{
    "Amount": 59.99,
    "ReturnMethod": "Redirect",
    "ReturnURL": "https://merchant.example.com/payment-return",
    "FrameHostingURL": "https://merchant.example.com",
    "RequestExpirationUTC": "2026-06-30T12:35:00.000Z",
    "TokenRetrievalRequired": true,
    "AccountAddressCollectionMode": "Full",
    "ValidateAccountSecurityCode": true,
    "ValidateAccountAddress": true
  }'
http POST {{baseURL}}/v1.4/PaymentTransactionManagement/InitializeSession \
  Content-Type:application/json \
  X-CompanyNumber:{{Company Number}} \
  X-StoreNumber:{{Store Number}} \
  X-ReferenceId:{{$guid}} \
  X-Client-Application-Name:{{Client Application Name}} \
  X-Client-Application-Version:{{Client Application Name Version}} \
  X-Signature:{{GeneratedSignature}} \
  Amount:=59.99 \
  ReturnMethod=Redirect \
  ReturnURL=https://merchant.example.com/payment-return \
  FrameHostingURL=https://merchant.example.com \
  RequestExpirationUTC=2026-06-30T12:35:00.000Z \
  TokenRetrievalRequired:=true \
  AccountAddressCollectionMode=Full \
  ValidateAccountSecurityCode:=true \
  ValidateAccountAddress:=true
import requests

url = "{{baseURL}}/v1.4/PaymentTransactionManagement/InitializeSession"
headers = {
  "Content-Type": "application/json",
  "X-CompanyNumber": "{{Company Number}}",
  "X-StoreNumber": "{{Store Number}}",
  "X-ReferenceId": "{{$guid}}",
  "X-Client-Application-Name": "{{Client Application Name}}",
  "X-Client-Application-Version": "{{Client Application Name Version}}",
  "X-Signature": "{{GeneratedSignature}}"
}
payload = {
  "Amount": 59.99,
  "ReturnMethod": "Redirect",
  "ReturnURL": "https://merchant.example.com/payment-return",
  "FrameHostingURL": "https://merchant.example.com",
  "RequestExpirationUTC": "2026-06-30T12:35:00.000Z",
  "TokenRetrievalRequired": True,
  "AccountAddressCollectionMode": "Full",
  "ValidateAccountSecurityCode": True,
  "ValidateAccountAddress": True
}

response = requests.post(url, headers=headers, json=payload, timeout=30)
print(response.status_code)
print(response.text)
using var client = new HttpClient();
using var req = new HttpRequestMessage(HttpMethod.Post,
    "{{baseURL}}/v1.4/PaymentTransactionManagement/InitializeSession");

req.Headers.TryAddWithoutValidation("X-CompanyNumber", "{{Company Number}}");
req.Headers.TryAddWithoutValidation("X-StoreNumber", "{{Store Number}}");
req.Headers.TryAddWithoutValidation("X-ReferenceId", "{{$guid}}");
req.Headers.TryAddWithoutValidation("X-Client-Application-Name", "{{Client Application Name}}");
req.Headers.TryAddWithoutValidation("X-Client-Application-Version", "{{Client Application Name Version}}");
req.Headers.TryAddWithoutValidation("X-Signature", "{{GeneratedSignature}}");

req.Content = new StringContent("{\"Amount\":59.99,\"ReturnMethod\":\"Redirect\",\"ReturnURL\":\"https://merchant.example.com/payment-return\",\"FrameHostingURL\":\"https://merchant.example.com\",\"RequestExpirationUTC\":\"2026-06-30T12:35:00.000Z\",\"TokenRetrievalRequired\":true,\"AccountAddressCollectionMode\":\"Full\",\"ValidateAccountSecurityCode\":true,\"ValidateAccountAddress\":true}", Encoding.UTF8, "application/json");

var res = await client.SendAsync(req);
Console.WriteLine((int)res.StatusCode);
Console.WriteLine(await res.Content.ReadAsStringAsync());
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
  .uri(URI.create("{{baseURL}}/v1.4/PaymentTransactionManagement/InitializeSession"))
  .header("Content-Type", "application/json")
  .header("X-CompanyNumber", "{{Company Number}}")
  .header("X-StoreNumber", "{{Store Number}}")
  .header("X-ReferenceId", "{{$guid}}")
  .header("X-Client-Application-Name", "{{Client Application Name}}")
  .header("X-Client-Application-Version", "{{Client Application Name Version}}")
  .header("X-Signature", "{{GeneratedSignature}}")
  .POST(HttpRequest.BodyPublishers.ofString("{\"Amount\":59.99,\"ReturnMethod\":\"Redirect\",\"ReturnURL\":\"https://merchant.example.com/payment-return\",\"FrameHostingURL\":\"https://merchant.example.com\",\"RequestExpirationUTC\":\"2026-06-30T12:35:00.000Z\",\"TokenRetrievalRequired\":true,\"AccountAddressCollectionMode\":\"Full\",\"ValidateAccountSecurityCode\":true,\"ValidateAccountAddress\":true}"))
  .build();

HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode());
System.out.println(response.body());
{
  "Amount": 59.99,
  "ReturnMethod": "Redirect",
  "ReturnURL": "https://merchant.example.com/payment-return",
  "FrameHostingURL": "https://merchant.example.com",
  "RequestExpirationUTC": "2026-06-30T12:35:00.000Z",
  "TokenRetrievalRequired": true,
  "AccountAddressCollectionMode": "Full",
  "ValidateAccountSecurityCode": true,
  "ValidateAccountAddress": true
}
<InitializeSessionRequest>
  <Amount>59.99</Amount>
  <ReturnMethod>Redirect</ReturnMethod>
  <ReturnURL>https://merchant.example.com/payment-return</ReturnURL>
  <FrameHostingURL>https://merchant.example.com</FrameHostingURL>
  <RequestExpirationUTC>2026-06-30T12:35:00.000Z</RequestExpirationUTC>
  <TokenRetrievalRequired>true</TokenRetrievalRequired>
  <AccountAddressCollectionMode>Full</AccountAddressCollectionMode>
  <ValidateAccountSecurityCode>true</ValidateAccountSecurityCode>
  <ValidateAccountAddress>true</ValidateAccountAddress>
</InitializeSessionRequest>
{
  "200": {
    "SessionId": "45016f4cce57e5ab4e308a1822b9b9fc3336",
    "RequestURL": "https://.../WebEPS/HPP?session=45016f4cce57e5ab4e308a1822b9b9fc3336"
  },
  "400": {
    "ErrorCode": "INVALID_REQUEST",
    "Message": "Request body or headers are invalid.",
    "Details": "Verify required fields and signature payload."
  },
  "401": {
    "ErrorCode": "UNAUTHORIZED",
    "Message": "Authentication failed.",
    "Details": "Invalid company/store credentials, signature, or allowed origin setup."
  }
}
<Responses>
  <Status code="200">
    <InitializeSessionResponse>
      <SessionId>45016f4cce57e5ab4e308a1822b9b9fc3336</SessionId>
      <RequestURL>https://.../WebEPS/HPP?session=45016f4cce57e5ab4e308a1822b9b9fc3336</RequestURL>
    </InitializeSessionResponse>
  </Status>
  <Status code="400">
    <Error>
      <Code>INVALID_REQUEST</Code>
      <Message>Request body or headers are invalid.</Message>
      <Details>Verify required fields and signature payload.</Details>
    </Error>
  </Status>
  <Status code="401">
    <Error>
      <Code>UNAUTHORIZED</Code>
      <Message>Authentication failed.</Message>
      <Details>Invalid company/store credentials, signature, or allowed origin setup.</Details>
    </Error>
  </Status>
</Responses>

Use uniqueTransactionIdentifier and requestUrl from the response in your paymentForm.submit() call. Save SessionId — you'll need it for CompleteSession.

Step 2 — Elements SDK Setup

Load the HPE SDK script in your page, then call ncr.elements() to instantiate the hosted payment form. Each payment field renders in its own NCR-hosted iframe — card data never passes through your application.

Step 2a — Load the SDK

Include the Elements SDK script in your HTML <head> or before the closing </body> tag. Replace <environment-domain> and {version} with the values provided by NCR.

HTML
<script src="https://<environment-domain>/securepay/elements/{version}/sdk.js"></script>

Step 2b — Configure payment fields

Call ncr.elements() with a configuration object. The sample below shows a complete working integration; the table that follows describes every configuration field in detail.

Complete integration walkthrough

This sample covers loading the SDK, configuring each payment field, applying custom styles, handling events, and submitting the form to process a payment.

HTML containers

Add a placeholder <div> for each payment field. The SDK injects an iframe into each container at runtime.

HTML
<!-- Payment field containers -->
<div>
  <label for="field-nameOnCard">Name on Card</label>
  <div id="field-nameOnCard" class="field"></div>
</div>
<div>
  <label for="field-cardNumber">Card Number</label>
  <div id="field-cardNumber" class="field"></div>
</div>
<div>
  <label for="field-expiration">Expiration</label>
  <div id="field-expiration" class="field"></div>
</div>
<div>
  <label for="field-securityCode">Security Code</label>
  <div id="field-securityCode" class="field"></div>
</div>
<div>
  <label for="field-postalCode">Zip Code</label>
  <div id="field-postalCode" class="field"></div>
</div>
<button id="submit-btn" disabled>Pay</button>

Complete JavaScript sample

Initialize the payment form with ncr.elements(), configure fields and styles, set up event hooks, and wire up the submit button.

JavaScript
const btn = document.getElementById("submit-btn");
let paymentFormObj = "";

const promisingObject = ncr.elements({
  data: {
    paymentType: "CHECKOUT", // Options: CHECKOUT or WALLET
    method: "CARD",
    domain: ["https://your-app.com"], // Whitelisted domains
    brands: ["visa", "master", "amex", "discover", "MAESTRO", "JCB", "UNIONPAY", "DINERS-CLUB"],
    fields: {
      nameOnCard: {
        parentElement: "field-nameOnCard",
        placeholder: "Enter name on card",
        value: "John"
      },
      cardNumber: {
        parentElement: "field-cardNumber",
        enableFormatting: true,
        masking: {
          character: "*",
          mode: "ALWAYS_MASK_ALL", // NO_MASKING | ALWAYS_MASK_EXCEPT_LAST_4 | ALWAYS_MASK_ALL
                                   // BLUR_MASK_EXCEPT_LAST_4 | BLUR_MASK_All
                                   // BLUR_MASK_EXCEPT_LAST_4_SHRINK | BLUR_MASK_ALL_SHRINK
          shrinkLength: "8"        // Only applies to *_SHRINK modes
        }
      },
      expiration: {
        parentElement: "field-expiration",
        placeholder: "MM / YY",
        format: "MM_YY"            // MM_YY or MM_YYYY
      },
      securityCode: {
        parentElement: "field-securityCode",
        masking: {
          // NO_MASKING | ALWAYS_MASK_ALL | BLUR_MASK_All
          mode: "ALWAYS_MASK_ALL"
        },
        brand: "<brand-type>"     // Drives CVV length: visa -> 3, amex -> 4 (default: 4)
      },
      zipCode: {
        parentElement: "field-postalCode"
      },
    },
    cssClassNames: {
      invalid: "invalid",
      valid: "valid"
    },
    css: {
      input: {
        "font-family": "BeautifulPeople",
        "font-size": "16px",
        color: "#00a9e0",
        "outline-style": "dotted"
      },
      ".valid": { color: "#43B02A" },
      ".invalid": { color: "#C01324" },
      "input::placeholder": { color: "#aaa" }
    },
    font: {
      data: "<base64_encoded_font_data>",
      family: "BeautifulPeople",
      // Supported: font/otf | font/ttf | font/woff | font/woff2
      format: "font/ttf",
      integrity: "l3g1AMGSOJ4lOAHqnEIEtk9qip5LK2OuLFJl35A7F9I="
    },
  },
  events: {
    onCardBrandChange: (cardBrand, formState, form) => {
      document.getElementById("status-cardBrand").innerText = cardBrand ?? "";
    },
    onFormValid: (formState, form) => {
      document.getElementById("status-formValid").innerText = "TRUE";
      btn.removeAttribute("disabled");
    },
    onFormInValid: (formState, form) => {
      document.getElementById("status-formValid").innerText = "FALSE";
      btn.setAttribute("disabled", "true");
    },
    onFocus: (field, fieldState) => {
      switch (field) {
        case "expiration":
          document.getElementById("field-expiration").classList.add("focus");
          break;
        case "securityCode":
          document.getElementById("field-securityCode").classList.add("focus");
          break;
      }
    },
    onLostFocus: () => {},
    onFieldValidityChange: () => {},
  },
});

promisingObject.then((paymentForm) => {
  paymentFormObj = paymentForm;
  btn.addEventListener("click", (e) => {
    document.getElementById("api-response").innerText = "Loading...";
    const submissionConfig = {
      sessionDetails: {
        uniqueTransactionIdentifier: "<from InitializeSession>",
        requestUrl: "https://<environment-domain>/ecommerce/SecurePay/Checkout/?companyNumber=<COMPANY_NUMBER>"
      },
      tokens: [
        {
          tokenType: "201",
          tokenValue: "<saved_token_value>"
        }
      ],
      params: {
        billingDetails: {
          fullName: "<cardholder-name>",
          address1: "3456 Dublin Rd",
          address2: "APT 430",
          city: "Cupertino",
          state: "CA",
          country: "USA",
          postalCode: "95234",
          isBillingSameAsShipping: true,
        },
        profileDetails: {
          isDefaultCard: true,
          nickName: "Mike",
        },
      },
    };
    paymentForm.submit(submissionConfig).then((response) => {
      document.getElementById("api-response").innerText = JSON.stringify(response);
    });
  });
});
FieldR / ODescription
paymentTypeRequiredTransaction type. Supported value: "CHECKOUT"
methodRequiredPayment method. Supported value: "CARD"
brandsOptionalList of supported card brands. If empty, all major brands are accepted.
fieldsRequiredField-level configuration including placeholders, formatting, and masking.
parentElementRequiredID of the container element where each hosted field iframe is embedded.
placeholderOptionalPlaceholder text displayed inside the input field.
enableFormattingOptionalIf true, automatically formats field input (e.g. card number groups).
maskingOptionalDefines how field values are masked as the user types.
characterOptionalThe masking character (e.g. "*").
modeOptionalMasking mode: NO_MASKING, ALWAYS_MASK_ALL, ALWAYS_MASK_EXCEPT_LAST_4, BLUR_MASK_ALL, BLUR_MASK_EXCEPT_LAST_4, BLUR_MASK_ALL_SHRINK, BLUR_MASK_EXCEPT_LAST_4_SHRINK.
shrinkLengthOptionalNumber of digits to shrink when using a *_SHRINK masking mode.
cssClassNamesOptionalCSS class names applied for valid / invalid validation states.
cssOptionalCustom CSS styling rules injected into each hosted field iframe.
fontOptionalCustom font settings: data (base64), family, format (font/otf, font/ttf, font/woff), and integrity.
promiseOptionalThe promise resolves to an instance of the payment form on success, or an error on failure.
Supported card brands

Visa, Mastercard, American Express, Discover, Maestro, JCB, UnionPay, Diners Club

Step 2c — Submit the form

After ncr.elements() resolves, call paymentForm.submit(submissionConfig) with the session details from InitializeSession. The promise resolves with the payment response.

JavaScript
promisingObject.then((paymentForm) => {
  paymentFormObj = paymentForm;
  btn.addEventListener("click", (e) => {
    const submissionConfig = {
      sessionDetails: {
        uniqueTransactionIdentifier: "********",
        requestUrl: "https://<environment-domain>/ecommerce/SecurePay/Checkout/?companyNumber=<COMPANY_NUMBER>"
      },
      tokens: [
        {
          tokenType: "201",
          tokenValue: "<saved_token_value>"
        }
      ],
      params: {
        billingDetails: {
          fullName: "<cardholder-name>",
          address1: "3456 Dublin Rd",
          address2: "APT 430",
          city: "Cupertino",
          state: "CA",
          country: "USA",
          postalCode: "95234",
          isBillingSameAsShipping: true,
        },
        profileDetails: {
          isDefaultCard: true,
          nickName: 'Mike',
        },
      },
    };
    const publicKey = "merchant-public-key";
    paymentForm.submit(submissionConfig).then((response) => {
      // Response status
    });
  });
});
FieldR / ODescription
uniqueTransactionIdentifierRequiredUnique ID for the transaction session, returned by InitializeSession.
requestUrlRequiredPayment endpoint URL returned by InitializeSession.
companyNumberRequiredMerchant company number, appended to the requestUrl.
fullNameOptionalCardholder full name for billing.
address1OptionalPrimary billing address line.
address2OptionalSecondary address line (e.g., apartment number).
cityOptionalCity of the billing address.
stateOptionalState or province of the billing address.
countryOptionalCountry of the billing address.
postalCodeOptionalPostal / ZIP code used for AVS validation.
isBillingSameAsShippingOptionalSet to true if the billing address matches the shipping address.
isDefaultCardOptionalFlag the card as the default saved card for this profile.
nickNameOptionalA friendly nickname to identify the saved card.
tokensOptionalArray of saved token objects to use for this transaction.
tokenTypeConditionalRequired when tokens are provided. Token type (e.g. "201").
tokenValueConditionalRequired when tokens are provided. The saved token value.

Step 3 — CompleteSession

Endpoint: POST /v1.4/PaymentTransactionManagement/CompleteSession  Server-side

Call this from your server only after paymentForm.submit() resolves with a success status. Like InitializeSession, it must be RSA-SHA256 signed server-side.

Guard against double-calls (React StrictMode)

Use a useRef flag (completingRef.current) to ensure CompleteSession is invoked exactly once per session, even in React StrictMode.

Required headers

HeaderRequiredDescription
Content-TypeYesapplication/json or text/xml
X-CompanyNumberYesMerchant company number
X-StoreNumberYesMerchant store number
X-ReferenceIdYesUnique correlation ID per request
X-SignatureYesRSA-SHA256 signature generated server-side
curl -X POST "{{baseURL}}/v1.4/PaymentTransactionManagement/CompleteSession" \
  -H "Content-Type: application/json" \
  -H "X-CompanyNumber: {{Company Number}}" \
  -H "X-StoreNumber: {{Store Number}}" \
  -H "X-ReferenceId: {{$guid}}" \
  -H "X-Signature: {{GeneratedSignature}}" \
  -d '{
    "SessionId": "{{CompleteSession}}",
    "RequestExpirationUTC": "2026-06-30T12:35:00.000Z"
  }'
http POST {{baseURL}}/v1.4/PaymentTransactionManagement/CompleteSession \
  Content-Type:application/json \
  X-CompanyNumber:{{Company Number}} \
  X-StoreNumber:{{Store Number}} \
  X-ReferenceId:{{$guid}} \
  X-Signature:{{GeneratedSignature}} \
  SessionId={{CompleteSession}} \
  RequestExpirationUTC=2026-06-30T12:35:00.000Z
import requests

url = "{{baseURL}}/v1.4/PaymentTransactionManagement/CompleteSession"
headers = {
  "Content-Type": "application/json",
  "X-CompanyNumber": "{{Company Number}}",
  "X-StoreNumber": "{{Store Number}}",
  "X-ReferenceId": "{{$guid}}",
  "X-Signature": "{{GeneratedSignature}}"
}
payload = {
  "SessionId": "{{CompleteSession}}",
  "RequestExpirationUTC": "2026-06-30T12:35:00.000Z"
}

response = requests.post(url, headers=headers, json=payload, timeout=30)
print(response.status_code)
print(response.text)
using var client = new HttpClient();
using var req = new HttpRequestMessage(HttpMethod.Post,
    "{{baseURL}}/v1.4/PaymentTransactionManagement/CompleteSession");

req.Headers.TryAddWithoutValidation("X-CompanyNumber", "{{Company Number}}");
req.Headers.TryAddWithoutValidation("X-StoreNumber", "{{Store Number}}");
req.Headers.TryAddWithoutValidation("X-ReferenceId", "{{$guid}}");
req.Headers.TryAddWithoutValidation("X-Signature", "{{GeneratedSignature}}");

req.Content = new StringContent("{\"SessionId\":\"{{CompleteSession}}\",\"RequestExpirationUTC\":\"2026-06-30T12:35:00.000Z\"}", Encoding.UTF8, "application/json");

var res = await client.SendAsync(req);
Console.WriteLine((int)res.StatusCode);
Console.WriteLine(await res.Content.ReadAsStringAsync());
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
  .uri(URI.create("{{baseURL}}/v1.4/PaymentTransactionManagement/CompleteSession"))
  .header("Content-Type", "application/json")
  .header("X-CompanyNumber", "{{Company Number}}")
  .header("X-StoreNumber", "{{Store Number}}")
  .header("X-ReferenceId", "{{$guid}}")
  .header("X-Signature", "{{GeneratedSignature}}")
  .POST(HttpRequest.BodyPublishers.ofString("{\"SessionId\":\"{{CompleteSession}}\",\"RequestExpirationUTC\":\"2026-06-30T12:35:00.000Z\"}"))
  .build();

HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode());
System.out.println(response.body());
{
  "SessionId": "{{CompleteSession}}",
  "RequestExpirationUTC": "2026-06-30T12:35:00.000Z"
}
<CompleteSessionRequest>
  <SessionId>{{CompleteSession}}</SessionId>
  <RequestExpirationUTC>2026-06-30T12:35:00.000Z</RequestExpirationUTC>
</CompleteSessionRequest>
{
  "200": {
    "TransactionStatus": "Approved",
    "AuthorizationCode": "A12345",
    "ReferenceNumber": "{{ReferenceNumber}}"
  },
  "400": {
    "ErrorCode": "INVALID_REQUEST",
    "Message": "SessionId is missing or invalid.",
    "Details": "Verify request payload and expiration timestamp."
  },
  "401": {
    "ErrorCode": "UNAUTHORIZED",
    "Message": "Authentication failed.",
    "Details": "Invalid signature or merchant credentials."
  }
}
<Responses>
  <Status code="200">
    <CompleteSessionResponse>
      <TransactionStatus>Approved</TransactionStatus>
      <AuthorizationCode>A12345</AuthorizationCode>
      <ReferenceNumber>{{ReferenceNumber}}</ReferenceNumber>
    </CompleteSessionResponse>
  </Status>
  <Status code="400">
    <Error>
      <Code>INVALID_REQUEST</Code>
      <Message>SessionId is missing or invalid.</Message>
      <Details>Verify request payload and expiration timestamp.</Details>
    </Error>
  </Status>
  <Status code="401">
    <Error>
      <Code>UNAUTHORIZED</Code>
      <Message>Authentication failed.</Message>
      <Details>Invalid signature or merchant credentials.</Details>
    </Error>
  </Status>
</Responses>

Wallet Integration — Apple Pay & Google Pay

SDK v1.3.0+ required

Wallet payment fields (applePay, googlePay) are available from Elements SDK version 1.3.0 and above. Ensure your SDK script URL points to /securepay/elements/1.3.0/sdk.js or later.

The Elements SDK renders native Apple Pay and Google Pay buttons directly inside your page. There is no InitializeSession or CompleteSession step for wallet payments — the wallet handles the payment sheet lifecycle and your code receives an encrypted token through event callbacks. Wallet buttons can appear standalone or combined with card fields in the same ncr.elements() call.

Apple Pay

End-to-end flow

The Apple Pay flow spans three phases across four actors. No card data is entered in your page — Apple manages tokenisation and your backend handles all NCR API calls.

Browser / Client Your frontend Your Backend Node / .NET / Python Apple Pay apple.com servers NCR Ecommerce /Capture + CompleteSession 1 — MERCHANT VALIDATION (server-side mTLS required) User taps Apple Pay button POST /your-backend/validate-apple-pay-merchant { validationURL } POST validationURL using Merchant Identity Cert (mTLS) { merchantIdentifier, domainName, displayName } merchantSession (opaque JSON — do NOT modify) merchantSession forwarded to browser completeFn(merchantSession) → Apple Pay sheet opens 2 — SHOPPER AUTHORISATION Face ID / Touch ID onApplePayPaymentAuthorized fires payment { token.paymentData (encrypted), billingContact } 3 — PAYMENT PROCESSING & COMPLETE SESSION Mirror of HPP CompleteSession — must be called after capture succeeds POST /your-backend/process-apple-pay-payment { payment } — full object incl. billingContact Decrypt token (ECDH + AES-256-GCM) POST /Ecommerce/SecurePay/Payment/Capture { SourceType: APPLEPAY, EncryptionBlock: payment } { success: true, statusCode: 100 } payment captured POST Cart/CompleteSession { sessionId } Finalises the NCR session — same as HPP CompleteSession Session closed { success: true } completeFn({ status: 'success' }) → ✓ Apple Pay sheet closes Browser / Client Your Backend Apple Pay NCR Ecommerce

One-time setup

Complete these steps once in the Apple Developer portal before adding the button to your page.

Register a Merchant ID

Go to Certificates, Identifiers & Profiles → Merchant IDs → + and create an identifier such as merchant.com.your-company.shop. You will use this in your backend validation call.

Create a Merchant Identity Certificate

Used for mutual TLS (mTLS) when your server calls Apple's merchant validation URL. Under your Merchant ID → Apple Pay Merchant Identity Certificate → Create Certificate. Download the .cer and convert to .pem + key for use on your server.

bash
openssl x509 -inform DER -in merchant_id.cer -out merchant_id.pem
openssl pkcs12 -export -in merchant_id.pem -inkey merchant_id.key -out merchant_id.p12

Create a Payment Processing Certificate

Used to decrypt the Apple Pay payment token on your backend. Under your Merchant ID → Apple Pay Payment Processing Certificate → Create Certificate. Store securely on your payment processing server.

Register and verify your domain

Apple Pay only works on HTTPS domains Apple has verified. Download the Domain Association File from your Merchant ID settings and serve it — with no redirects — at:

text
https://your-domain.com/.well-known/apple-developer-merchantid-domain-association

Then go to Merchant Domains → Register Domain → Verify. Repeat for every domain (including subdomains) where Apple Pay will appear.

HTML container

html
<div id="apple-pay-container" style="width:100%;"></div>

SDK configuration

js
ncr.elements({
  data: {
    fields: {
      applePay: {
        parentElement: 'apple-pay-container',
        buttonType:    'buy',           // 'buy' | 'plain' | 'pay' | 'book' | 'donate'
        buttonStyle:   'black',         // 'black' | 'white' | 'white-outline'
        height:        48,              // button height in px
        borderRadius:  4,
        merchantInfo: {
          locale:               'en-US',
          merchantIdentifier:   'YOUR_APPLE_PAY_MERCHANT_IDENTIFIER',
          merchantName:         'Your Store Name',
          merchantCountryCode:  'US',
          currencyCode:         'USD',
          supportedNetworks:    ['visa', 'masterCard', 'amex', 'discover'],
          merchantCapabilities: ['supports3DS'],
          totalLabel:           'Your Store Name',
          totalAmount:          '19.99'
        }
      }
    }
  },
  events: {
    onReady:                     function() { /* SDK ready — hide your spinner */ },
    onError:                     function(error) { /* SDK error — show fallback */ },
    onWalletReady:               function(provider) { /* Apple Pay button is available */ },
    onWalletUnavailable:         function(provider, reason) { /* hide button, show alternative */ },
    onApplePayValidateMerchant:  async function(validationURL, completeFn, abortFn) { /* Step 1 */ },
    onApplePayPaymentAuthorized: async function(token, payment, completeFn) { /* Step 2 */ },
    onApplePayCancel:            function() { /* Step 3 */ }
  }
});

Event — onApplePayValidateMerchant (Step 1: Merchant validation)

Fires as soon as the Apple Pay sheet opens. Apple verifies your server is a legitimate merchant. You must call completeFn within a few seconds or the sheet closes. This call must be server-side — Apple blocks cross-origin browser requests and requires mTLS using your Merchant Identity Certificate.

Your backend receives the validationURL, POSTs to it with your merchant certificate, and returns Apple's opaque merchantSession JSON unmodified.

onApplePayValidateMerchant: async function(validationURL, completeFn, abortFn) {
  try {
    const response = await fetch('/your-backend/validate-apple-pay-merchant', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ validationURL })
    });
    if (!response.ok) throw new Error('Validation failed: ' + response.status);
    // Pass Apple's opaque session object directly — do NOT modify it
    completeFn(await response.json());
  } catch (error) {
    abortFn(error.message || 'Merchant validation failed');
  }
}
// Node.js / Express — server-side
const https = require('https');
const fs    = require('fs');
const CERT  = fs.readFileSync('/certs/merchant_id.pem');
const KEY   = fs.readFileSync('/certs/merchant_id.key');

app.post('/your-backend/validate-apple-pay-merchant', (req, res) => {
  const { validationURL } = req.body;
  const payload = JSON.stringify({
    merchantIdentifier: 'merchant.com.your-company.shop',
    domainName:         req.hostname,    // must match the domain the shopper is on
    displayName:        'Your Store Name'
  });
  const url      = new URL(validationURL);
  const appleReq = https.request({
    hostname: url.hostname, path: url.pathname + url.search, method: 'POST',
    cert: CERT, key: KEY,
    headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) }
  }, r => { let b = ''; r.on('data', c => b += c); r.on('end', () => res.json(JSON.parse(b))); });
  appleReq.on('error', e => res.status(500).json({ error: e.message }));
  appleReq.end(payload);
});
# Python / Flask — server-side
import requests
from flask import request, jsonify

CERT = '/certs/merchant_id.pem'
KEY  = '/certs/merchant_id.key'

@app.route('/your-backend/validate-apple-pay-merchant', methods=['POST'])
def validate_merchant():
    validation_url = request.json['validationURL']
    payload = {
        'merchantIdentifier': 'merchant.com.your-company.shop',
        'domainName':         request.host,   # must match the shopper's domain
        'displayName':        'Your Store Name'
    }
    r = requests.post(validation_url, json=payload, cert=(CERT, KEY))  # mTLS
    r.raise_for_status()
    return jsonify(r.json())   # return Apple's session object unmodified

Event — onApplePayPaymentAuthorized (Step 2: Payment processing)

Fires after the shopper authenticates with Face ID / Touch ID. You have ~30 seconds to process the payment and call completeFn. Send the full payment object to your backend — it contains the encrypted token (payment.token.paymentData) and billing contact. Your backend decrypts it using the Payment Processing Certificate and submits to your acquirer.

completeFn outcomes

completeFn({ status: 'success' }) — shows a green checkmark and closes the sheet.
completeFn({ status: 'failure', errors: [{ message: '...' }] }) — shows an error; sheet stays open for retry.

onApplePayPaymentAuthorized: async function(token, payment, completeFn) {
  try {
    // Send the full payment object — backend needs billingContact AND the encrypted token
    const response = await fetch('/your-backend/process-apple-pay-payment', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ payment })
    });
    if (!response.ok) throw new Error('Payment request failed: ' + response.status);
    const result    = await response.json();
    const isSuccess = result.success === true || result.status === 'success';
    completeFn(isSuccess
      ? { status: 'success' }
      : { status: 'failure', errors: result.errors || [{ message: result.message || 'Payment declined' }] }
    );
  } catch (error) {
    completeFn({ status: 'failure', errors: [{ message: error.message || 'Payment failed' }] });
  }
}
# Python / Flask — server-side
@app.route('/your-backend/process-apple-pay-payment', methods=['POST'])
def process_apple_pay():
    payment = request.json['payment']

    # POST to NCR Payment Capture API with session headers
    headers = {
        'Content-Type': 'application/json',
        'X-SessionId': session_id,                   # from InitializeExternalSession
        'X-Company': company_id,
        'X-UniqueTransactionIdentifier': unique_tx_id
    }
    payload = {
        'paymentType': { 'sourceType': 'APPLEPAY' },
        'sources': [{
            'sourceType': 'APPLEPAY',
            'encryptionData': {
                'encryptionType': 'RSA',
                'encryptionTarget': 'WEB',
                'encryptionBlock': json.dumps(payment)  # full payment object as string
            }
        }]
    }
    resp = requests.post(
        f'{NCR_BASE_URL}/Ecommerce/SecurePay/Payment/Capture',
        json=payload, headers=headers
    )
    resp.raise_for_status()
    result = resp.json()

    if result.get('success') and result.get('data', {}).get('statusCode') == 100:
        # Call CompleteSession to finalise the transaction
        complete_session(session_id)
        return jsonify({ 'success': True })
    return jsonify({ 'success': False, 'message': 'Capture failed' })

Event — onApplePayCancel (Step 3: Cancellation)

Fires when the shopper dismisses the sheet without authorising. Use it to reset any pending order state.

js
onApplePayCancel: function() {
  console.log('[Apple Pay] User cancelled');
  // Reset any pending order state
}

Apple Pay events summary

EventParametersWhen it firesYour action
onReady none SDK has initialised and the Apple Pay button is rendered in the DOM. Hide any loading skeleton; make the payment section visible.
onError (error) SDK or runtime initialisation error. Show a fallback payment method; log error for support diagnostics.
onWalletReady (provider) Apple Pay confirmed as available — device is capable and has saved cards. Optionally reveal additional Apple Pay UI. provider is "applePay".
onWalletUnavailable (provider, reason) Apple Pay unavailable (non-Safari browser, no saved cards, or device not supported). Hide the Apple Pay button; show an alternative payment option. Log reason if needed.
onApplePayValidateMerchant (validationURL, completeFn, abortFn) Apple Pay sheet is opening — Apple requests server-side merchant validation. POST validationURL to your backend → receive merchant session → call completeFn(session). Must complete within a few seconds. Call abortFn(msg) on error.
onApplePayPaymentAuthorized (token, payment, completeFn) Shopper authenticated with Face ID / Touch ID. Payment token is ready to process. POST payment object to your backend for decryption and acquirer submission. Call completeFn({ status: 'success' }) or completeFn({ status: 'failure', errors: [...] }). Must complete within ~30 s.
onApplePayCancel none Shopper dismissed the Apple Pay sheet without authorising. Reset any pending order state your UI created before the sheet opened.

Apple Pay testing

Apple Pay only works in Safari on macOS or iOS — it is unavailable in Chrome or Firefox. Your domain must be registered with Apple even for testing. Use a device enrolled in Apple's Sandbox environment with test cards added to Wallet. See Apple Sandbox Testing for setup.

Google Pay

End-to-end flow

Google Pay has no merchant validation step — the shopper authenticates entirely inside Google's sheet and the SDK fires a callback with the encrypted token, which your backend submits to the NCR gateway.

Browser / Client Your frontend Your Backend Node / .NET / Python Google Pay / NCR Gateway google.com · ncrpaymentsolutions gateway 1 — SHOPPER AUTHORISATION User taps Google Pay button Google Pay sheet opens (SDK-managed, no mTLS needed) Shopper selects card & confirms onGooglePayPaymentAuthorized fires paymentData { paymentMethodData.tokenizationData.token (encrypted) } 2 — PAYMENT PROCESSING POST /your-backend/process-google-pay-payment { paymentData } Submit tokenizationData.token to NCR gateway gateway (ncrpaymentsolutions) decrypts and authorises { success: true } authorisation approved { success: true } completeFn({ transactionState: 'SUCCESS' }) → ✓ Google Pay sheet closes Browser / Client Your Backend Google Pay / NCR Gateway

One-time setup

Google Pay merchant account

Register at pay.google.com/business/console. You receive a Merchant ID once approved for production. A Merchant ID is not required in TEST environment.

Gateway configuration

Your NCR account team provides the gateway identifier (e.g. ncrpaymentsolutions) and gatewayMerchantId for your account. These go in tokenizationParameters inside the SDK data config.

HTML container

html
<div id="google-pay-container" style="width:100%;"></div>

SDK configuration

js
ncr.elements({
  data: {
    fields: {
      googlePay: {
        parentElement:    'google-pay-container',
        environment:      'TEST',           // 'TEST' or 'PRODUCTION'
        merchantId:       'YOUR_GOOGLE_MERCHANT_ID',  // omit in TEST
        merchantName:     'Your Store Name',
        currencyCode:     'USD',
        countryCode:      'US',
        totalPriceStatus: 'FINAL',
        totalPrice:       '19.99',
        buttonType:       'buy',            // 'buy' | 'pay' | 'plain' | 'book' | 'donate'
        buttonColor:      'default',        // 'default' | 'black' | 'white'
        tokenizationParameters: {
          gateway:           'ncrpaymentsolutions',
          gatewayMerchantId: 'YOUR_GATEWAY_MERCHANT_ID'
        }
      }
    }
  },
  events: {
    onReady:                      function() { /* SDK ready */ },
    onError:                      function(error) { /* SDK error */ },
    onWalletReady:                function(provider) { /* Google Pay button available */ },
    onWalletUnavailable:          function(provider, reason) { /* hide button */ },
    onGooglePayPaymentAuthorized: async function(paymentData, completeFn) { /* Step 1 */ },
    onGooglePayCancel:            function() { /* Step 2 */ }
  }
});

Event — onGooglePayPaymentAuthorized (Step 1: Payment processing)

Fires after the shopper selects a card and confirms in the Google Pay sheet. The key field is paymentData.paymentMethodData.tokenizationData.token — send this to your gateway for decryption and authorisation.

completeFn outcomes

completeFn({ transactionState: 'SUCCESS' }) — Google confirms and closes the sheet.
completeFn({ transactionState: 'ERROR' }) — Google shows an error to the shopper.

onGooglePayPaymentAuthorized: async function(paymentData, completeFn) {
  try {
    const response = await fetch('/your-backend/process-google-pay-payment', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ paymentData })
    });
    if (!response.ok) throw new Error('Payment request failed: ' + response.status);
    const result    = await response.json();
    const isSuccess = result.success === true || result.status === 'success';
    if (typeof completeFn === 'function') completeFn({ transactionState: isSuccess ? 'SUCCESS' : 'ERROR' });
    return { transactionState: isSuccess ? 'SUCCESS' : 'ERROR' };
  } catch (error) {
    const out = { transactionState: 'ERROR' };
    if (typeof completeFn === 'function') completeFn(out);
    return out;
  }
}
# Python / Flask — server-side
@app.route('/your-backend/process-google-pay-payment', methods=['POST'])
def process_google_pay():
    payment_data = request.json['paymentData']

    # Extract the encrypted token from the Google Pay payload
    token = payment_data['paymentMethodData']['tokenizationData']['token']

    # POST to NCR Payment Capture API with session headers
    headers = {
        'Content-Type': 'application/json',
        'X-SessionId': session_id,                   # from InitializeExternalSession
        'X-Company': company_id,
        'X-UniqueTransactionIdentifier': unique_tx_id
    }
    payload = {
        'paymentType': { 'sourceType': 'GOOGLEPAY' },
        'sources': [{
            'sourceType': 'GOOGLEPAY',
            'encryptionData': {
                'encryptionType': 'RSA',
                'encryptionTarget': 'WEB',
                'encryptionBlock': json.dumps(token)  # tokenizationData.token as string
            }
        }]
    }
    resp = requests.post(
        f'{NCR_BASE_URL}/Ecommerce/SecurePay/Payment/Capture',
        json=payload, headers=headers
    )
    resp.raise_for_status()
    result = resp.json()

    if result.get('success') and result.get('data', {}).get('statusCode') == 100:
        # Call CompleteSession to finalise the transaction
        complete_session(session_id)
        return jsonify({ 'success': True })
    return jsonify({ 'success': False, 'message': 'Capture failed' })

Event — onGooglePayCancel (Step 2: Cancellation)

js
onGooglePayCancel: function() {
  console.log('[Google Pay] User cancelled');
}

Google Pay events summary

EventParametersWhen it firesYour action
onReady none SDK initialised and the Google Pay button is rendered in the DOM. Hide any loading skeleton; make the payment section visible.
onError (error) SDK or runtime initialisation error. Show a fallback payment method; log error for support diagnostics.
onWalletReady (provider) Google Pay confirmed as available on this device. provider is "googlePay". Optionally reveal additional Google Pay UI cues.
onWalletUnavailable (provider, reason) Google Pay unavailable on this browser or device. Hide the Google Pay button; show an alternative. In TEST mode this never fires.
onGooglePayPaymentAuthorized (paymentData, completeFn) Shopper selected a card and confirmed. Encrypted token is ready to process. POST paymentData to your backend; gateway decrypts paymentData.paymentMethodData.tokenizationData.token. Call completeFn({ transactionState: 'SUCCESS' }) or completeFn({ transactionState: 'ERROR' }).
onGooglePayCancel none Shopper closed the Google Pay sheet without completing payment. Reset any pending order state.

Google Pay testing

Set environment: 'TEST' in the SDK config. TEST mode shows a mock card that produces a simulated encrypted token — no real card required. Switch to 'PRODUCTION' with your approved merchantId before going live. Review the Google Pay integration checklist before launching.

Payment Capture API — Apple Pay & Google Pay

After the shopper authorises payment, your backend calls the NCR Payment Capture endpoint to submit the encrypted wallet token. This single endpoint handles both Apple Pay and Google Pay — only the sourceType and encryptionBlock content differ.

Required request headers

All three headers are received from your session initialisation step and must be included in the Capture request:

HeaderDescription
X-SessionIdSession identifier returned by InitializeExternalSession
X-CompanyMerchant company identifier
X-UniqueTransactionIdentifierUnique transaction reference for this payment

Endpoint

text
POST /Ecommerce/SecurePay/Payment/Capture

Request payload

{
  "paymentType": {
    "sourceType": "APPLEPAY"
  },
  "sources": [{
    "sourceType": "APPLEPAY",
    "encryptionData": {
      "encryptionType": "RSA",
      "encryptionTarget": "WEB",
      "encryptionBlock": "<stringified Apple Pay payment object>"
    }
  }]
}

// encryptionBlock: JSON.stringify(payment)
// where 'payment' is the full object from onApplePayPaymentAuthorized —
// includes payment.token.paymentData AND payment.billingContact
{
  "paymentType": {
    "sourceType": "GOOGLEPAY"
  },
  "sources": [{
    "sourceType": "GOOGLEPAY",
    "encryptionData": {
      "encryptionType": "RSA",
      "encryptionTarget": "WEB",
      "encryptionBlock": "<stringified Google Pay token>"
    }
  }]
}

// encryptionBlock: JSON.stringify(paymentData.paymentMethodData.tokenizationData.token)
// where 'paymentData' is the object from onGooglePayPaymentAuthorized

Response

{
  "success": true,
  "data": {
    "sessionId": "<session-id>",
    "statusCode": 100
  }
}

// statusCode 100 = payment authorised and captured successfully
400 Invalid encryption data   — wallet token is missing or malformed
401 Session expired           — session is no longer valid; re-initialise
403 Merchant not authorised   — wallet not enabled or configured for this account
404 Session ID not found      — session or endpoint is invalid
500 Internal server error     — general error; contact NCR support

Complete the session

After a successful capture response (statusCode: 100), finalise the transaction by calling CompleteSession. This is identical to the standard card payment flow:

json
POST /v1.4/PaymentTransactionManagement/CompleteSession

{
  "SessionId": "<session-id>",
  "RequestExpirationUtc": "2026-08-28T12:00:00.000Z"
}

API Reference — Request headers

HeaderExample valueDescription
Content-Typeapplication/jsonAlways required
X-CompanyNumberYOUR_COMPANY_NUMBERYour NCR company number
X-StoreNumberYOUR_STORE_NUMBERYour NCR store number
X-ReferenceId32-char hexUnique per request — crypto.randomUUID().replace(/-/g,"")
X-Client-Application-NameYourAppNameIdentifier for your integration
X-Client-Application-Version1.0.0Your application version string
X-SignatureUppercase hexRSA-SHA256 signature of the full serialised request body — server-side only

API Reference — Request body fields

FieldTypeRequiredDescription
AmountnumberRequiredTotal charge in dollars (e.g. 59.99)
ReturnURLstringRequiredYour page that receives statusCode after redirect
FrameHostingURLstringRequiredOrigin of the page hosting the iframe (for postMessage trust)
ReturnMethodstringRequired"Redirect" — iframe navigates to ReturnURL on completion
RequestExpirationUTCstring (ISO)RequiredSession expiry — typically 5 min from now
PaymentMethodTypestring[]Optional["CreditCard"] — controls which payment buttons appear
AuthorizationModestringOptional"Sale" (purchase flow)
TokenRetrievalRequiredbooleanOptionalReturn a payment token for future use
AccountAddressCollectionModestringOptional"Full" | "PostalCodeOnly" | "None"
ValidateAccountSecurityCodebooleanOptionalRequire CVV entry (set false for wallet payments)
ValidateAccountAddressbooleanOptionalRequire billing address in the iframe
CardTypeobjectOptional{ Type: "Credit"|"Debit", Name: "Visa"|"MasterCard"… }
TimeoutInMinutesnumber | nullOptionalSession timeout override (default 5 min)
AllowCardNamingbooleanOptionalAllow customer to name/save their card

Status code reference

CodeLabelDescriptionRecommended action
100✅ SuccessPayment completed successfullyCall CompleteSession → navigate to confirmation
201⚠️ CancelledCustomer cancelled the paymentShow retry option
202❌ Network ErrorConnectivity issue during transactionAsk user to retry
203❌ Server RejectTransaction rejected by NCR serverLog & contact support
204❌ Technical ErrorInternal technical issueRetry or contact support
205⚠️ Session ExpiredPayment session has expired (> 5 min)Re-initialise session
206❌ External ErrorError in an external systemRetry later
207❌ Invalid RequestMalformed or invalid request bodyCheck request fields
208❌ Auth FailedAuthentication / 3DS failedAsk for a different card
209⚠️ TimeoutPayment request timed outRetry

Reversal flow (TOR / Void)

The Dev Portal happy-path package also includes a reversal path for timeout and void scenarios. Use this when payment was authorized but your client flow did not complete cleanly.

Endpoint: POST /v1.4/ReversalAdministration/ReverseTransaction  Server-side

Call reversal from your server only

Use the same signed-header model as InitializeSession and CompleteSession. Keep private keys and merchant credentials out of browser code.

Required headers

HeaderRequiredDescription
Content-TypeYesapplication/json or text/xml
X-CompanyNumberYesMerchant company number
X-StoreNumberYesMerchant store number
X-ReferenceIdYesUnique correlation ID per request
X-SignatureYesRSA-SHA256 signature generated server-side
curl -X POST "{{baseURL}}/v1.4/ReversalAdministration/ReverseTransaction" \
  -H "Content-Type: application/json" \
  -H "X-CompanyNumber: {{Company Number}}" \
  -H "X-StoreNumber: {{Store Number}}" \
  -H "X-ReferenceId: {{$guid}}" \
  -H "X-Signature: {{GeneratedSignature}}" \
  -d '{
    "OriginalReferenceId": "{{OriginalReferenceId}}",
    "ReversalType": "timeout",
    "RequestExpirationUTC": "2026-06-30T12:35:00.000Z"
  }'
http POST {{baseURL}}/v1.4/ReversalAdministration/ReverseTransaction \
  Content-Type:application/json \
  X-CompanyNumber:{{Company Number}} \
  X-StoreNumber:{{Store Number}} \
  X-ReferenceId:{{$guid}} \
  X-Signature:{{GeneratedSignature}} \
  OriginalReferenceId={{OriginalReferenceId}} \
  ReversalType=timeout \
  RequestExpirationUTC=2026-06-30T12:35:00.000Z
import requests

url = "{{baseURL}}/v1.4/ReversalAdministration/ReverseTransaction"
headers = {
  "Content-Type": "application/json",
  "X-CompanyNumber": "{{Company Number}}",
  "X-StoreNumber": "{{Store Number}}",
  "X-ReferenceId": "{{$guid}}",
  "X-Signature": "{{GeneratedSignature}}"
}
payload = {
  "OriginalReferenceId": "{{OriginalReferenceId}}",
  "ReversalType": "timeout",
  "RequestExpirationUTC": "2026-06-30T12:35:00.000Z"
}

response = requests.post(url, headers=headers, json=payload, timeout=30)
print(response.status_code)
print(response.text)
using var client = new HttpClient();
using var req = new HttpRequestMessage(HttpMethod.Post,
    "{{baseURL}}/v1.4/ReversalAdministration/ReverseTransaction");

req.Headers.TryAddWithoutValidation("X-CompanyNumber", "{{Company Number}}");
req.Headers.TryAddWithoutValidation("X-StoreNumber", "{{Store Number}}");
req.Headers.TryAddWithoutValidation("X-ReferenceId", "{{$guid}}");
req.Headers.TryAddWithoutValidation("X-Signature", "{{GeneratedSignature}}");

req.Content = new StringContent("{\"OriginalReferenceId\":\"{{OriginalReferenceId}}\",\"ReversalType\":\"timeout\",\"RequestExpirationUTC\":\"2026-06-30T12:35:00.000Z\"}", Encoding.UTF8, "application/json");

var res = await client.SendAsync(req);
Console.WriteLine((int)res.StatusCode);
Console.WriteLine(await res.Content.ReadAsStringAsync());
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
  .uri(URI.create("{{baseURL}}/v1.4/ReversalAdministration/ReverseTransaction"))
  .header("Content-Type", "application/json")
  .header("X-CompanyNumber", "{{Company Number}}")
  .header("X-StoreNumber", "{{Store Number}}")
  .header("X-ReferenceId", "{{$guid}}")
  .header("X-Signature", "{{GeneratedSignature}}")
  .POST(HttpRequest.BodyPublishers.ofString("{\"OriginalReferenceId\":\"{{OriginalReferenceId}}\",\"ReversalType\":\"timeout\",\"RequestExpirationUTC\":\"2026-06-30T12:35:00.000Z\"}"))
  .build();

HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode());
System.out.println(response.body());
{
  "OriginalReferenceId": "{{OriginalReferenceId}}",
  "ReversalType": "timeout",
  "RequestExpirationUTC": "2026-06-30T12:35:00.000Z"
}
<ReverseTransactionRequest>
  <OriginalReferenceId>{{OriginalReferenceId}}</OriginalReferenceId>
  <ReversalType>timeout</ReversalType>
  <RequestExpirationUTC>2026-06-30T12:35:00.000Z</RequestExpirationUTC>
</ReverseTransactionRequest>
{
  "200": {
    "ReversalStatus": "Completed",
    "OriginalReferenceId": "{{OriginalReferenceId}}",
    "ReferenceNumber": "RV-1001"
  },
  "400": {
    "ErrorCode": "INVALID_REQUEST",
    "Message": "OriginalReferenceId is missing or invalid.",
    "Details": "Verify request payload and reversal type."
  },
  "401": {
    "ErrorCode": "UNAUTHORIZED",
    "Message": "Authentication failed.",
    "Details": "Invalid signature or merchant credentials."
  }
}
<Responses>
  <Status code="200">
    <ReverseTransactionResponse>
      <ReversalStatus>Completed</ReversalStatus>
      <OriginalReferenceId>{{OriginalReferenceId}}</OriginalReferenceId>
      <ReferenceNumber>RV-1001</ReferenceNumber>
    </ReverseTransactionResponse>
  </Status>
  <Status code="400">
    <Error>
      <Code>INVALID_REQUEST</Code>
      <Message>OriginalReferenceId is missing or invalid.</Message>
      <Details>Verify request payload and reversal type.</Details>
    </Error>
  </Status>
  <Status code="401">
    <Error>
      <Code>UNAUTHORIZED</Code>
      <Message>Authentication failed.</Message>
      <Details>Invalid signature or merchant credentials.</Details>
    </Error>
  </Status>
</Responses>

Environment variables

.env (server)
HPP_API_BASE_URL=https://{ncr-hpp-api-host}
NCR_COMPANY_NUMBER=YOUR_COMPANY_NUMBER
NCR_STORE_NUMBER=YOUR_STORE_NUMBER
NCR_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----"
APP_BASE_URL=https://your-app.com