Skip to content
In this section

Apple Pay

Accept Apple Pay either server-to-server via /payments-api/v1/initialize-payment, or through the legacy hosted-checkout surface (/api/v1/checkout/apple-pay-*).

Apple Pay lets customers authorize payments with Face ID, Touch ID, or a paired Apple Watch without entering card details. The browser-side ApplePaySession API (or your own native app) returns a payment token whose paymentData is encrypted by the device Secure Element — you decrypt it server-side with your Apple Pay payment processing certificate, then send the decrypted fields to Fynex.

Fynex accepts Apple Pay two ways:

  • Server-to-server API — decrypt the Apple Pay token yourself, then submit the decrypted fields to POST /payments-api/v1/initialize-payment with paymentMethod: "apple_pay". Bearer-authenticated, the same endpoint used for card and Google Pay. Recommended for direct integrations.
  • Hosted-checkout (legacy) — the /api/v1/checkout/apple-pay-* surface, session-cookie authenticated, where a Fynex checkout page drives the ApplePaySession handshake. Documented under Hosted-checkout flow below.

Unlike Google Pay — where you may hand Fynex the still-encrypted token — Apple Pay decryption is always the integrator's responsibility: Fynex performs no server-side Apple Pay token decryption. You decrypt the device-encrypted paymentData with your Apple Pay payment processing certificate and pass the resulting decryptedData block, which Fynex relays straight to the upstream processor's single-use-token endpoint.

Server-to-server API

POST /payments-api/v1/initialize-payment, authenticated with your seller Authorization: Bearer token. Set paymentMethod to apple_pay and put the decrypted Apple Pay token under applePayData:

curl -sS -X POST "$FYNEX_API/payments-api/v1/initialize-payment" \
  -H "Authorization: Bearer $FYNEX_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "externalOrderRef": "ORDER-1042",
    "amount": 1.23,
    "paymentType": "card",
    "paymentMethod": "apple_pay",
    "currencyCode": "USD",
    "countryCode": "GB",
    "returnLinks": [
      { "rel": "default", "href": "https://merchant.example/return", "method": "GET" }
    ],
    "billingDetails": {
      "firstName": "John", "lastName": "Doe",
      "street": "1 Example St", "city": "London",
      "postalCode": "SW1A1AA", "countryCode": "GB"
    },
    "applePayData": {
      "label": "Pay with Apple",
      "requestBillingAddress": true,
      "paymentToken": {
        "transactionIdentifier": "<token.transactionIdentifier>",
        "paymentMethod": {
          "displayName": "MasterCard 1470",
          "network": "MasterCard",
          "type": "credit"
        },
        "decryptedData": {
          "applicationPrimaryAccountNumber": "<device PAN>",
          "applicationExpirationDate": "YYMMDD",
          "currencyCode": "840",
          "transactionAmount": "123",
          "onlinePaymentCryptogram": "<cryptogram>",
          "eciIndicator": "2"
        }
      }
    }
  }'

The applePayData.paymentToken.decryptedData fields map directly from Apple's PKPaymentToken.paymentData after decryption.

Apple Pay token

Every field marked required below returns 400 when it is absent. String fields are also rejected when blank; the object rows are presence checks only, so an empty object passes and fails instead on the required field inside it.

Field Required Notes
paymentToken.transactionIdentifier yes From the Apple Pay token.
paymentToken.paymentMethod yes Object presence check.
paymentToken.paymentMethod.network yes Visa, MasterCard, Amex, …
paymentToken.decryptedData yes Object presence check.
decryptedData.applicationPrimaryAccountNumber yes Device PAN (DPAN).
decryptedData.applicationExpirationDate yes YYMMDD.
decryptedData.onlinePaymentCryptogram yes The 3-D Secure cryptogram.
decryptedData.version no Apple's PKPaymentToken cryptogram version (EC_v1 / EC_v2 / RSA_v1). Forwarded to the upstream processor when set; not validated.
decryptedData.eciIndicator no Apple Pay populates the ECI only for some networks (typically Visa); Mastercard / Amex tokens routinely omit it. The validator deliberately does not require it.
billingDetails recommended Include a postalCode — if your upstream account runs an AVS check it rejects the payment when the postal code is absent. An absent block is dropped from the outbound request, not sent as empty strings.

Header + common body (shared with every paymentMethod)

These layers run before the Apple Pay-specific checks:

Field Format
Idempotency-Key (HTTP header) UUID, non-zero
externalOrderRef non-empty string
amount number > 0
paymentType "card"
paymentMethod "apple_pay"
currencyCode 3-letter ISO, must pass IsValidCurrencyCode
countryCode 2-letter ISO

paymentType and paymentMethod are additionally checked against each other — an incompatible pair returns 400.

accountId is not sent by the caller — Fynex resolves the upstream merchant account from the seller's terminal routing.

A fresh request returns 202 Accepted:

{
  "paymentId": "ORDER-1042",
  "status": "provider_pending",
  "lifecycleStatus": "pending",
  "legacyStatus": "provider_pending",
  "amount": 1.23,
  "currencyCode": "USD",
  "requiresAction": false
}

When requiresAction is true, redirect the customer to the returned action URL for the 3-D Secure step-up. Submitting the same Idempotency-Key returns the existing payment instead of creating a duplicate.

Upstream outbound mapping (for debugging processor rejections)

The Fynex DTO is flatter than the upstream processor's shape: decryptedData.onlinePaymentCryptogram and decryptedData.eciIndicator sit at the same level as applicationPrimaryAccountNumber in the inbound request, but Fynex re-wraps them into the processor's nested decryptedData.paymentData.{onlinePaymentCryptogram, eciIndicator} block on the way out. Use the mapping below when comparing a processor reject reason to your outbound request body.

Fynex DTO path Upstream outbound path
applePayData.paymentToken.transactionIdentifier applePay.applePayPaymentToken.token.transactionIdentifier
applePayData.paymentToken.paymentMethod.network applePay.applePayPaymentToken.token.paymentMethod.network
applePayData.paymentToken.decryptedData.version applePay.applePayPaymentToken.token.paymentData.decryptedData.version
applePayData.paymentToken.decryptedData.applicationPrimaryAccountNumber applePay.applePayPaymentToken.token.paymentData.decryptedData.applicationPrimaryAccountNumber
applePayData.paymentToken.decryptedData.applicationExpirationDate applePay.applePayPaymentToken.token.paymentData.decryptedData.applicationExpirationDate
applePayData.paymentToken.decryptedData.onlinePaymentCryptogram applePay.applePayPaymentToken.token.paymentData.decryptedData.paymentData.onlinePaymentCryptogram (one level deeper)
applePayData.paymentToken.decryptedData.eciIndicator applePay.applePayPaymentToken.token.paymentData.decryptedData.paymentData.eciIndicator (one level deeper)

Caveats

  1. The processor does not mark fields as required vs optional explicitly — there is only a sample. Fynex's validator picks the minimum set without which the processor rejects. Stricter upstream requirements may exist for specific networks or 3DS levels.
  2. paymentToken.paymentMethod is required as an object (Fynex validates network), but displayName and type are optional in the Fynex DTO. They are accepted upstream when present.
  3. Passthrough fields not validated by Fynex but present in the processor's sample: decryptedData.currencyCode (numeric ISO 4217, e.g. "840"), transactionAmount, cardholderName, deviceManufacturerIdentifier, paymentDataType. They round-trip when set on the inbound request.
  4. billingDetails is a top-level object, not part of the Apple Pay token block. When it is not set on the inbound request the field is dropped rather than sent as empty strings, so the processor never sees a populated-with-empty-strings billing block. Include a postalCode if your upstream account runs an AVS check.

Hosted-checkout flow (legacy surface)

The remainder of this guide covers the legacy /api/v1/checkout/apple-pay-* surface, where a Fynex-hosted checkout page drives the browser ApplePaySession handshake. It is session-cookie authenticated and remains available for hosted-checkout integrations.

Prerequisites

Before showing the Apple Pay button you need two things in place:

  1. The Apple Pay JS SDK — load https://applepay.cdn-apple.com/jsapi/1.latest/apple-pay-sdk.js before your checkout code runs.
  2. Browser support — check window.ApplePaySession before rendering the button. Apple Pay is only available in Safari on Apple devices (or macOS + Safari + an Apple Pay-enrolled card).
<!-- Load the Apple Pay SDK in your page head -->
<script
  src="https://applepay.cdn-apple.com/jsapi/1.latest/apple-pay-sdk.js"
  crossorigin="anonymous"
></script>

Flow overview

Your backendCustomer browserFynexavailablevalidationURLmerchantSessionapplePaymentTokenrequiresActionsuccess, statusCheck availabilitywindow.ApplePaySessionShow Apple Pay buttoncustomer clicks buttonValidate merchantcalls Apple from your serverno Fynex endpointApple Pay sheet openscompleteMerchantValidationCustomer authorizesonpaymentauthorized firesSubmit tokenPOST …/checkout/apple-pay-submit3DS if requiresActionredirect to actionUrlComplete paymentcompletePayment(STATUS_SUCCESS)

The submit call is POST /api/v1/checkout/apple-pay-submit; it responds { success, status, requiresAction?, actionUrl? }. In production, merchant validation calls Apple from your own server — there is no Fynex endpoint for it. If requiresAction is set (3DS), redirect the browser to actionUrl; once the customer authenticates, the hosted page finalizes through the shared checkout contract. Then call session.completePayment(ApplePaySession.STATUS_SUCCESS).

API calls in this flow

Step Method Path
1 POST Your own server-side merchant-validation endpoint (calls Apple). Fynex's /api/v1/checkout/apple-pay-validate-merchant exists only for the hosted checkout page — it is bound to a live checkout session and is not usable from your own Apple Pay button
2 POST /api/v1/checkout/apple-pay-submit — initializes the payment through the shared checkout pipeline; there is no separate completion call

Step-by-step

  1. Check availability and show the button

Gate the Apple Pay button on ApplePaySession being present in the browser window.

   function isApplePayAvailable() {
     return (
       typeof window !== 'undefined' &&
       'ApplePaySession' in window &&
       ApplePaySession.canMakePayments()
     );
   }

   if (isApplePayAvailable()) {
     document.getElementById('apple-pay-button').style.display = 'block';
   }
  1. Handle the button click — open an ApplePaySession

When the customer clicks the Apple Pay button, open a session. The countryCode is currently hardcoded to US in the Fynex dashboard — see the limitation note below.

   async function startApplePayPayment({ amount, currencyCode }) {
     const paymentRequest = {
       countryCode: 'US',          // current limitation — see note below
       currencyCode,
       merchantCapabilities: ['supports3DS'],
       supportedNetworks: ['visa', 'masterCard', 'amex', 'discover'],
       total: {
         label: 'Your business name',
         amount: String(amount),
       },
     };

     const session = new ApplePaySession(3, paymentRequest);

     // Step 3: merchant validation
     session.onvalidatemerchant = async (event) => {
       try {
         const merchantSession = await validateMerchantWithServer(
           event.validationURL
         );
         session.completeMerchantValidation(merchantSession);
       } catch (err) {
         session.abort();
         throw err;
       }
     };

     // Step 4: customer authorizes — submit to Fynex
     session.onpaymentauthorized = async (event) => {
       const applePayToken = event.payment.token;
       try {
         await submitAndCapture({ applePayToken, amount, currencyCode });
         session.completePayment(ApplePaySession.STATUS_SUCCESS);
       } catch (err) {
         session.completePayment(ApplePaySession.STATUS_FAILURE);
         throw err;
       }
     };

     session.begin();
   }

Caution

Country code limitation. The Fynex dashboard currently hardcodes countryCode: 'US' in the ApplePaySession request. This means the payment request always presents as a US transaction. Custom integrations should pass the correct country code for their merchant account.

  1. Validate the merchant session (server-side)

Apple requires your server to contact Apple's servers and validate the merchant session before the payment sheet is shown to the customer. In the Fynex dashboard this step is currently faked — it calls completeMerchantValidation({}) with an empty object, which will fail against real Apple Pay in production.

For a real integration you must implement a server-side endpoint that: - Accepts the validationURL from the browser - Calls Apple's merchant validation endpoint using your Apple Pay merchant certificate and private key - Returns the opaque merchant session object to the browser

   // Browser: call your own server-side validation endpoint
   async function validateMerchantWithServer(validationURL) {
     const res = await fetch('/api/apple-pay/validate-merchant', {
       method: 'POST',
       headers: { 'Content-Type': 'application/json' },
       body: JSON.stringify({ validationURL }),
     });
     if (!res.ok) throw new Error('Merchant validation failed');
     return res.json(); // returns the opaque merchant session from Apple
   }

Note

Fynex does not expose a merchant-session-validation endpoint for your own button (the hosted checkout page has a session-bound one, /api/v1/checkout/apple-pay-validate-merchant, which requires a live checkout session). Merchant validation must call Apple's validationURL directly from your own server using your Apple Pay merchant certificate and private key — the browser cannot make this call due to CORS. Return the opaque merchant session from your endpoint (as shown in validateMerchantWithServer above) and hand it to session.completeMerchantValidation(...).

  1. Submit the Apple Pay token to Fynex

Once the customer authorizes the payment, onpaymentauthorized fires with an encrypted applePayToken. Submit it to Fynex:

curl

curl -sS -X POST "$FYNEX_LEGACY_API/api/v1/checkout/apple-pay-submit" \
  -H "Content-Type: application/json" \
  -b "session_id=$SESSION_COOKIE" \
  -d '{
    "sessionId": "6f9b84e1-3b83-4fb9-9f42-a8ac27d11d6b",
    "token": { "...": "encrypted token from Apple" }
  }'

JavaScript

async function submitApplePay({ sessionId, token }) {
  const res = await fetch(
    `${FYNEX_LEGACY_API}/api/v1/checkout/apple-pay-submit`,
    {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      credentials: 'include',
      body: JSON.stringify({ sessionId, token }),
    }
  );
  if (!res.ok) throw new Error(`Submit failed: ${res.status}`);
  return res.json();
  // Response: { success, message, status, failureCode, failureDescription, requiresAction?, actionUrl? }
}

The response contains:

| Field | Type | Notes | |-------|------|-------| | success | boolean | Whether the payment was initialized | | status | string | The payment's internal status after initialization — a previous-style value, which on this legacy route can also be initializing or finalizing. This response has no lifecycleStatus; read it from GET /payments/{payment_id} or the webhook | | failureCode / failureDescription | int / string | Set when initialization failed — see Errors | | requiresAction | boolean? | true when the issuer requires 3DS | | actionUrl | string? | Where to send the browser when requiresAction is true |

  1. Let the hosted page finish

There is no completion call. apple-pay-submit already initialized the payment through the same pipeline a card payment takes, and the response's status tells you where it stands. If requiresAction is true, send the browser to actionUrl for 3DS; the hosted checkout page finalizes the payment on return through the shared checkout contract and redirects to your returnUrls. The former complete-apple-pay-payment route bypassed the platform's antifraud and compliance gates and has been removed.

// Putting it together
async function submitAndFinish({ sessionId, token }) {
  const result = await submitApplePay({ sessionId, token });
  if (!result.success) {
    throw new Error(`Apple Pay failed (${result.failureCode}): ${result.failureDescription}`);
  }
  if (result.requiresAction && result.actionUrl) {
    window.location.href = result.actionUrl; // 3DS — the hosted page completes the payment on return
    return;
  }
  return result.status; // a previous status value, e.g. provider_pending — branch on lifecycleStatus from the webhook or GET /payments/{id}
}

Common pitfalls

Warning

Merchant validation must be server-side in production. The Fynex dashboard fakes merchant validation with an empty object. This is a development shortcut that will fail against real Apple Pay. Real merchant validation requires an Apple Pay merchant certificate and a server-side call to Apple's validation URL. Your browser cannot make this call directly due to CORS restrictions.

  • Apple Pay only shows in Safari on Apple devices. Always gate the button on isApplePayAvailable(). Chrome and Firefox do not support ApplePaySession.
  • The country code is currently hardcoded to US. If your merchant account is registered in another country, override countryCode in the ApplePaySession request.
  • 3DS happens on the hosted page. If requiresAction is true, the browser is sent to actionUrl; the hosted checkout page finishes the payment on return through the shared checkout contract and redirects to your returnUrls. There is nothing to store client-side and no completion call to make. See the 3DS guide for the full pattern.
  • Call session.completePayment() always. Whether the payment succeeds or fails, you must call session.completePayment(STATUS_SUCCESS) or session.completePayment(STATUS_FAILURE) to dismiss the Apple Pay sheet gracefully.

See also

  • Hosted Checkout — The lowest-PCI path to accepting payments — Fynex-hosted page handles card entry.
  • Server-to-Server Payments — Direct card submission for integrations that collect card data on their own infrastructure.
  • Errors — HTTP status codes and error response shapes.