Accept Apple Pay™ Recurring Payments

This guide explains how to use Apple Pay Merchant Tokens (MPANs) with MoneyHash APIs and Flutter 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 Flutter 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 Flutter SDK v4.0.0+ (for tokenizeReceipt).
  • Apple Pay configured in your iOS app with proper entitlements.
  • 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 moneyHashSDK.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)

In your Flutter app, call getMethods and pass the flowId of a flow that exposes Apple Pay as an express method.

final MoneyHashSDK moneyHashSDK = MoneyHashSDKBuilder().build();
try {
  final methods = await moneyHashSDK.getMethods(
        GetMethodsParams.withCurrency(
          amount: 20.0,
          currency: "USD",
          customer: "<customer_id>",
          flowId: "<flow_id>",
        ),
      );

  // Find Apple Pay express method
  final applePayMethod = methods.expressMethods?.firstWhere(
        (method) => method.id == "APPLE_PAY" || method.type == "APPLE_PAY",
        orElse: () => throw Exception('Apple Pay method not available'),
      );

  if (applePayMethod == null || applePayMethod.nativePayData is! ApplePayData) {
        throw Exception('Apple Pay data not found');
      }

  final applePayData = applePayMethod.nativePayData as ApplePayData;

  // Native Pay config from MoneyHash
  // This is what Apple Pay docs refer to as applePayNativeData
  final methodId = applePayData.methodID!;

} catch (error) {
  throw error;
}
  • Keep methodId (applePayData.methodID):
    • It's used today in Apple Pay merchant validation.
    • In this new flow you will also pass it to tokenizeReceipt.

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



Step 4 — Generate Apple Pay receipt using MoneyHash SDK

MoneyHash provides two ways to generate an Apple Pay receipt:

  1. Simple flow (recommended when you don’t need recurring details displayed on the Apple Pay sheet)
  2. Advanced flow (required when you want Apple Pay to explicitly show Recurring Payments or Automatic Reload details to the user)

Option A — Simple Apple Pay receipt (default)

Use this option if you don’t need Apple Pay to display recurring or reload information in the native sheet.


// MoneyHash SDK handles the entire Apple Pay flow including:
// 1. Presenting the Apple Pay sheet
// 2. Handling user authorization
// 3. Processing the payment token
// 4. Returning the receipt for tokenization
try {
  var receipt = await moneyHashSDK.generateApplePayReceipt(
        ApplePayReceiptParams.withApplePayData(TEST_AMOUNT, applePayData),
      );
  print('✅ Apple Pay receipt generated successfully');
} catch (error) {
  print('❌ Apple Pay flow error: $error');
}

This method automatically:

  • Configures the Apple Pay request using applePayData
  • Presents the Apple Pay authorization sheet
  • Handles user authentication (Face ID / Touch ID / passcode)
  • Converts the Apple Pay payment token into a NativePayReceipt
  • Returns a receipt string ready for tokenizeReceipt

Option B — Apple Pay receipt with recurring / automatic reload details

Use this option when you want Apple Pay to clearly show recurring payment or automatic reload information in the Apple Pay sheet (e.g. subscriptions, wallet top-ups).

This requires iOS 16.0+.

Method signature

NOTE: This method will work only from iOS 16

Future<NativePayReceipt?> generateApplePayReceipt(
  ApplePayReceiptParams generateApplePayParams) async
class ApplePayReceiptParams {
  double depositAmount;
  ApplePayData? applePayData;
  String? merchantIdentifier;
  String? currencyCode;
  String? countryCode;
  List<String>? supportedNetworks;
  List<String>? merchantCapabilities;
  RecurringPaymentRequest? recurringPayment;
  AutomaticReloadPaymentRequest? automaticReload;

  // Constructor for intentId and intentType
  ApplePayReceiptParams.withApplePayData(this.depositAmount, this.applePayData)
      : assert(applePayData != null);

  // Constructor for currency and optional params
  ApplePayReceiptParams.withCustomData({
    required this.depositAmount,
    required this.merchantIdentifier,
    required this.currencyCode,
    required this.countryCode,
    this.supportedNetworks,
    this.merchantCapabilities,
    this.recurringPayment,
    this.automaticReload,
  }) : assert(merchantIdentifier != null), assert(currencyCode != null), assert(countryCode != null);
}

Example — Recurring payment (subscription)

try {
  // Create recurring payment configuration
  final recurringBilling = RecurringPaymentSummaryItem(
        label: 'Monthly Subscription',
        amount: 9.99,
        startDate: DateTime.now(),
        endDate: DateTime.now().add(const Duration(days: 365)),
        intervalCount: 1,
        intervalUnit: IntervalUnit.month,
      );

  final recurringPayment = RecurringPaymentRequest(
        paymentDescription: 'Monthly Subscription Service',
        regularBilling: recurringBilling,
        managementURL: 'https://example.com/manage',
        billingAgreement: 'You will be charged monthly until cancelled.',
        tokenNotificationURL: 'https://example.com/terms',
      );

  print('🔄 Recurring payment configuration: $recurringPayment');

  // Generate receipt with recurring payment
  var receipt = await moneyHashSDK.generateApplePayReceipt(
        ApplePayReceiptParams.withCustomData(
          depositAmount: TEST_AMOUNT,
          merchantIdentifier: applePayData.merchantId!,
          currencyCode: applePayData.currencyCode!,
          countryCode: applePayData.countryCode!,
          supportedNetworks: applePayData.supportedNetworks ?? ['visa', 'mastercard'],
          merchantCapabilities: applePayData.merchantCapabilities ?? ['supports3DS'],
          recurringPayment: recurringPayment,
        ),
      );

  print('✅ Apple Pay recurring payment receipt generated successfully');
  print('📢 Subscription: Monthly \$9.99');
  print('📢 Receipt: ${receipt?.receipt?.substring(0, 50)}...');
} catch (error) {
  print('❌ Apple Pay recurring payment error: $error');
}

Example — Automatic reload (wallet top-up)

try {
  // Create automatic reload configuration
  final automaticReloadBilling = AutomaticReloadPaymentSummaryItem(
        label: 'Wallet Auto-Reload',
        amount: 25.00,
        thresholdAmount: 5.00,
      );

  final automaticReload = AutomaticReloadPaymentRequest(
        paymentDescription: 'Automatic Wallet Reload',
        automaticReloadBilling: automaticReloadBilling,
        managementURL: 'https://example.com/wallet',
        billingAgreement: 'Your wallet will be automatically reloaded when balance is low.',
        tokenNotificationURL: 'https://example.com/wallet-terms',
      );

  print('💰 Automatic reload configuration: $automaticReload');

  // Generate receipt with automatic reload
  var receipt = await moneyHashSDK.generateApplePayReceipt(
        ApplePayReceiptParams.withCustomData(
          depositAmount: TEST_AMOUNT,
          merchantIdentifier: applePayData.merchantId!,
          currencyCode: applePayData.currencyCode!,
          countryCode: applePayData.countryCode!,
          supportedNetworks: applePayData.supportedNetworks ?? ['visa', 'mastercard'],
          merchantCapabilities: applePayData.merchantCapabilities ?? ['supports3DS'],
          automaticReload: automaticReload,
        ),
      );

  print('✅ Apple Pay automatic reload receipt generated successfully');
  print('📢 Reload: \$25.00 when below \$5.00');
  print('📢 Receipt: ${receipt?.receipt?.substring(0, 50)}...');
} catch (error) {
  print('❌ Apple Pay automatic reload error: $error');
}

Step 5 — Submit receipt to moneyHashSDK.tokenizeReceipt

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

Once you have:

  • cardTokenIntentId (Step 2),
  • methodId (from applePayData.methodID in Step 3), and
  • applePayReceipt (Step 4),

call the Flutter SDK helper:

try {
  final cardTokenId = await moneyHashSDK.tokenizeReceipt(
      receipt.receipt,              // Receipt generated from Apple Pay
      methodId,             // applePayData.methodID
      cardTokenIntentId,    // from backend card intent creation
    );

  // cardTokenId is a MoneyHash "card token" backed by a network token
  print('✅ Card token created: $cardTokenId');
  print(cardTokenId);

} catch (error) {
  print('❌ Failed to tokenize receipt: $error');
  throw error;
}

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.