Accept Apple Pay™ Recurring Payments

This guide explains how to use Apple Pay Merchant Tokens (MPANs) with MoneyHash APIs and JavaScript SDK to enable recurring, unscheduled, and automatic reload payments using Apple Pay network tokens.

Overview

Apple Pay Merchant Tokens (MPANs) allow you to securely reuse Apple Pay network tokens for recurring and merchant-initiated transactions (MITs).
MoneyHash supports this by converting Apple Pay tokens into Universal Card Tokens, which you can use across payment providers.

This flow lets you:

  1. Tokenize an Apple Pay network token via MoneyHash’s JS SDK.
  2. Store it as a reusable universal card token in the Vault.
  3. Charge it for both CIT and MIT unscheduled payments.

Apple provides MPAN tokens for various payment scenarios:

  • Recurring Payments - Subscriptions, memberships, and regular billing cycles
  • Automatic Reload - Topping up accounts when balances fall below thresholds
  • Deferred Payment - Pre-authorization for services delivered at a later date

When using MPANs, the payment flow includes additional configuration in the Apple Pay™ payment request to specify the type of recurring payment and associated parameters.



Getting Started

Prerequisites

Before starting, make sure you have:

  • A payment flow that exposes self‑serve Apple Pay as an express method (so Apple Pay appears in expressMethods with nativePayData).
  • Integrate MoneyHash JS SDK v2.9.0+ (for tokenizeReceipt).
  • Apple Pay JS (apple-pay-sdk.js) loaded on the page.
  • A customer in MoneyHash.

Step-by-Step Integration

Step 1 — From backend create or get customer

  1. Use the Customer API to either create a new customer (POST /api/v1.4/customers/) or reuse an existing one.
  2. Store the customer ID. You will:
    1. send it when creating the card token intent
    2. reuse it on all CIT / MIT payment intents.

Step 2 — From backend create a card token intent

(send the customer here, and card type universal)

Create a card intent / card token intent so you can tokenize the Apple Pay card into a UNIVERSAL card token. This uses the Card Token APIs mentioned in “Save card for future use”.

Example (shape only, exact schema per your internal API):

POST /api/v1.4/tokens/cards/

{
  "customer": "<CUSTOMER_ID>",
  "card_token_type": "UNIVERSAL", // key bit for cross‑provider usage
  "webhook_url": "https://example.com/webhook",
  "metadata": {
    "source": "apple_pay_network_token"
  }
}

You’ll get back something like:

{
  "id": "<CARD_TOKEN_INTENT_ID>",
  ...
}

Keep cardTokenIntentId – the SDK will need it when calling moneyHash.tokenizeReceipt.

This is conceptually the same “card intent” used in

Save card for future use


Step 3 — Using SDK get all methods, send the flow here

(keep the returned method id here)

On the frontend, call moneyHash.getMethods and pass the flowId of a flow that exposes Apple Pay as an express method.

const {
  paymentMethods,
  expressMethods,
  savedCards,
  savedBankAccounts,
  customerBalances,
} = await moneyHash.getMethods({
  currency: "<currency>",
  amount: "<amount>",
  customer: "<customer_id>",
  flowId: "<flow_id>",
});

// Find Apple Pay express method
const applePayMethod = expressMethods.find(
  (m) => m.id === "APPLE_PAY" || m.type === "APPLE_PAY"
);

if (!applePayMethod) {
  throw new Error("Apple Pay not available for this flow.");
}

// Native Pay config from MoneyHash
const nativePayData = applePayMethod.nativePayData;
// This is what Apple Pay docs refer to as applePayNativeData
const methodId = nativePayData.method_id;
  • Keep methodId (nativePayData.method_id):
    • It’s used today in validateApplePayMerchantSession.
    • In this new flow you will also pass it to tokenizeReceipt.

You can also optionally use fields from nativePayData to pre‑configure the Apple Pay sheet (amount, supported networks, country, currency) instead of hard‑coding them.


Step 4 — Render Apple Pay button (keep the receipt)

Use the Apple Pay JS API to:

  1. render the Apple Pay button,
  2. open an Apple Pay session with a recurring / unscheduled request (via recurringPaymentRequest),
    1. Always Set tokenNotificationURL to this URL "https://vault.moneyhash.io/api/v1/apple_pay_decryption/merchant-token-events
  3. collect the Apple Pay receipt when the customer authorizes.

Example SDK Payload for RegularBilling:

const request = {
  countryCode: nativePayData.country_code,
  currencyCode: nativePayData.currency_code,
  supportedNetworks: nativePayData.supported_networks,
  merchantCapabilities: nativePayData.supported_capabilities,
  total: {
    label: "Your Business",
    amount: "20.00"
  },
  recurringPaymentRequest: {
    paymentDescription: "Usage-based charges",
    regularBilling: {
      label: "Usage-based charges",
      amount: "20.00",
      paymentTiming: "recurring", // or per Apple’s guideline for unscheduled use
      recurringPaymentStartDate: new Date(), // The date of the first payment.
      recurringPaymentIntervalUnit: "month",
      recurringPaymentIntervalCount: 1
    },
    /**
    * A URL to a web page where the user can update or delete the payment method for the recurring payment.
    * https://developer.apple.com/documentation/applepayontheweb/applepayrecurringpaymentrequest/managementurl
    */
    managementURL: "https://your.site/.....",
    tokenNotificationURL:
      "https://vault.moneyhash.io/api/v1/apple_pay_decryption/merchant-token-events"
  }
};

const session = new ApplePaySession(3, request);

// You’ll still need merchant validation using nativePayData.method_id
session.onvalidatemerchant = (event) => {
  moneyHash
    .validateApplePayMerchantSession({
      methodId,
      validationUrl: event.validationURL,
    })
    .then(merchantSession =>
      session.completeMerchantValidation(merchantSession)
    )
    .catch(() => {
      // gracefully close if merchant validation fails
      session.completeMerchantValidation({});
    });
};

session.onpaymentauthorized = (event) => {
  // This is the raw token from Apple Pay
  const receipt = JSON.stringify({ token: e.payment.token });
  // Finish the Apple Pay sheet UX
  session.completePayment(ApplePaySession.STATUS_SUCCESS);

  // Follow `Step 5 moneyHash.tokenizeReceipt`
  const cardTokenId = await moneyHash.tokenizeReceipt({
	  receipt ,  // Receipt generated from ApplePay Sheet
	  methodId,  // nativePayData.method_id
	  cardTokenIntentId, // from backend card intent creation
	});
};

session.begin();

Example SDK Payload for AutomaticReload:

const request = {
  countryCode: nativePayData.country_code,
  currencyCode: nativePayData.currency_code,
  supportedNetworks: nativePayData.supported_networks,
  merchantCapabilities: nativePayData.supported_capabilities,
  total: {
    label: "Your Business",
    amount: "20.00"
  },
  automaticReloadPaymentRequest: {
    paymentDescription: "Coffee Card Auto-Reload",

    automaticReloadBilling: {
      label: "Auto-Reload Amount",
      amount: "25.00",
      type: "final",
      paymentTiming: "automaticReload"
    },

    automaticReloadPaymentThresholdAmount: "5.00",
    managementURL: "https://your.site/account/reloads",
    tokenNotificationURL:
      "https://vault.moneyhash.io/api/v1/apple_pay_decryption/merchant-token-events"
  }
};

const session = new ApplePaySession(3, request);

// You’ll still need merchant validation using nativePayData.method_id
session.onvalidatemerchant = (event) => {
  moneyHash
    .validateApplePayMerchantSession({
      methodId,
      validationUrl: event.validationURL,
    })
    .then(merchantSession =>
      session.completeMerchantValidation(merchantSession)
    )
    .catch(() => {
      // gracefully close if merchant validation fails
      session.completeMerchantValidation({});
    });
};

session.onpaymentauthorized = (event) => {
  // This is the raw token from Apple Pay
  const receipt = JSON.stringify({ token: e.payment.token });
  // Finish the Apple Pay sheet UX
  session.completePayment(ApplePaySession.STATUS_SUCCESS);

  // Follow `Step 5 **moneyHash.tokenizeReceipt`**
  const cardTokenId = await moneyHash.tokenizeReceipt({
	  receipt ,  // Receipt generated from ApplePay Sheet
	  methodId,  // nativePayData.method_id
	  cardTokenIntentId, // from backend card intent creation
	});
};

session.begin();

Step 5 — Submit receipt to moneyHash.tokenizeReceipt

(with card token intent id, method id → get card token id back)

Once you have:

  • cardTokenIntentId (Step 2),
  • methodId (from nativePayData.method_id in Step 3), and
  • applePayReceipt (Step 4),

call the new JS SDK helper:

const cardTokenId = await moneyHash.tokenizeReceipt({
  receipt ,  // Receipt generated from ApplePay Sheet
  methodId,  // nativePayData.method_id
  cardTokenIntentId, // from backend card intent creation
});

// cardTokenId is a MoneyHash "card token" backed by a network token

This mirrors the “Save card for future use” flow (card intent + tokenization), but instead of cardForm.createCardToken, you’re feeding in an Apple Pay receipt via tokenizeReceipt.

Keep cardTokenId — it’s what you’ll pass into the CIT and MIT payment intents.


Step 6 — Create the CIT payment intent

(card token id + customer + CIT data + pay with network token + recurring data, payment type UNSCHEDULED)

Now you create the first unscheduled CIT payment using:

  • the Apple Pay card token (cardTokenId),
  • payment_type: "UNSCHEDULED",
  • "merchant_initiated": false,
  • "paying_with_network_token": true,
  • an agreement_id in recurring_data.

Example:

POST /api/v1.4/payments/intent/

{
  "amount": 20,
  "amount_currency": "USD",
  "operation": "purchase",
  "customer": "8c1a11c0-6ec6-4888-9e5c-8d07d7b5fed0",
  "card_token": "<APPLE_PAY_CARD_TOKEN_ID>",   // from tokenizeReceipt
  "merchant_initiated": false,                 // CIT
  "payment_type": "UNSCHEDULED",
  "paying_with_network_token": true,
  "recurring_data": {
    "agreement_id": "[YOUR_GENERATED_UNSCHEDULED_ID]"
  },
  "webhook_url": "https://example.com/webhook"
}

Compared to the generic Unscheduled payments – Initial transaction example, the main differences are:

  • you already have a card_token from Apple Pay (so you don’t need tokenize_card: true here),
  • and you explicitly mark this as a network token payment with "paying_with_network_token": true.

The customer still participates actively (CIT), but the credential being used is a stored network token, not a raw PAN.


Step 7 — Create the MIT payment intent

(card token id + customer + MIT data + pay with network token + recurring data, payment type UNSCHEDULED)

Every subsequent charge in the agreement is a MIT unscheduled payment that:

  • reuses the same card_token (Apple Pay network token),
  • sets "merchant_initiated": true,
  • keeps "payment_type": "UNSCHEDULED",
  • sets "paying_with_network_token": true,
  • and sends the same agreement_id in recurring_data.

Example:

POST /api/v1.4/payments/intent/

{
  "amount": 20,
  "amount_currency": "USD",
  "operation": "purchase",
  "customer": "8c1a11c0-6ec6-4888-9e5c-8d07d7b5fed0",
  "card_token": "<APPLE_PAY_CARD_TOKEN_ID>",
  "merchant_initiated": true,                 // MIT
  "payment_type": "UNSCHEDULED",
  "paying_with_network_token": true,
  "recurring_data": {
    "agreement_id": "[YOUR_GENERATED_UNSCHEDULED_ID]"
  },
  "webhook_url": "https://example.com/webhook"
}

This is equivalent to “Request an unscheduled payment” in the docs, with the extra hint that:

  • the card token is a network token provisioned via Apple Pay, and
  • the intent is explicitly flagged with "paying_with_network_token": true.