# 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](#hosted-checkout-flow-legacy-surface) 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`:

```bash
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`:

```json
{
  "paymentId": "ORDER-1042",
  "status": "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).

```html
<!-- 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

```
Customer browser            Your backend               Fynex
─────────────────           ─────────────              ─────
check ApplePaySession
  available?
show "Apple Pay" button
customer clicks button
  ──────────────────────►  validate merchant session  (call Apple from
                           (production: call Apple     your own server —
                           from your server)           no Fynex endpoint)
                           ◄── merchantSession
  completeMerchantValidation
  (Apple Pay sheet opens)
  customer authorizes
  onpaymentauthorized fires
  applePaymentToken
  ──────────────────────►  POST /api/v1/checkout/     apple-pay-submit
                           ◄── { success, status,
                                 requiresAction?,
                                 actionUrl? }
                           if requiresAction (3DS):
                             redirect browser to
                             actionUrl (customer
                             authenticates); the
                             hosted page finalizes
                             through the shared
                             checkout contract
  session.completePayment(ApplePaySession.STATUS_SUCCESS)
```

## API calls in this flow

| Step | Method | Path |
|------|--------|------|
| 1 | POST | Your own server-side merchant-validation endpoint (calls Apple — there is no Fynex endpoint for this) |
| 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.

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

   if (isApplePayAvailable()) {
     document.getElementById('apple-pay-button').style.display = 'block';
   }
   ```

2. **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.

   ```js
   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.

3. **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

   ```js
   // 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. 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(...)`.

4. **Submit the Apple Pay token to Fynex**

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

#### curl

```bash
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

```js
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 lifecycle status after initialization |
   | `failureCode` / `failureDescription` | int / string | Set when initialization failed — see [Errors](https://api.fynex.ai/payments-api/v2/docs#tag/errors) |
   | `requiresAction` | boolean? | `true` when the issuer requires 3DS |
   | `actionUrl` | string? | Where to send the browser when `requiresAction` is true |

5. **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.

```js
// 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; // e.g. provider_pending / provider_completed — confirm via 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](https://api.fynex.ai/payments-api/v2/docs#tag/3ds) 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](https://api.fynex.ai/payments-api/v2/docs#tag/hosted-checkout)** — The lowest-PCI path to accepting payments — Fynex-hosted page handles card entry.
- **[Server-to-Server Payments](https://api.fynex.ai/payments-api/v2/docs#tag/server-to-server)** — Direct card submission for integrations that collect card data on their own infrastructure.
- **[Errors](https://api.fynex.ai/payments-api/v2/docs#tag/errors)** — HTTP status codes and error response shapes.
