> ## Documentation Index
> Fetch the complete documentation index at: https://docs.hitpayapp.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Subscriptions (Auto-Charge)

> Implement auto-charge subscriptions with Stripe Custom Payment Methods and HitPay

Auto-charge subscriptions allow customers to authorize and tokenize a payment method once, with future invoices charged automatically via HitPay. This flow only works with tokenizable payment methods (Cards, ShopeePay, GrabPay).

## How It Works

This integration connects Stripe's Subscription API with HitPay's Recurring Billing API, allowing customers to set up automatic payments using local payment methods while keeping all subscription records in Stripe.

### Key Concepts

| Component                    | Purpose                                                                             |
| ---------------------------- | ----------------------------------------------------------------------------------- |
| **Stripe Subscription**      | Manages the subscription lifecycle, billing periods, and invoice generation.        |
| **HitPay Recurring Billing** | Tokenizes the customer's payment method and processes automatic charges.            |
| **Stripe Payment Records**   | Records each HitPay charge back in Stripe for unified reporting and reconciliation. |

### API References

<CardGroup cols={2}>
  <Card title="HitPay Embedded Recurring Billing" icon="code" href="https://docs.hitpayapp.com/apis/guide/embedded-recurring-billing">
    Create recurring billing sessions with embedded authorization — returns QR codes and direct app links instead of redirecting to a hosted checkout page.
  </Card>

  <Card title="HitPay Save Payment Method & Charge" icon="bolt" href="/apis/guide/save-payment-method">
    How to charge a saved payment method via the HitPay API — the same charge endpoint used to bill both the first invoice and all future renewals.
  </Card>

  <Card title="Stripe Subscriptions API" icon="stripe" href="https://docs.stripe.com/billing/subscriptions/overview">
    Manage subscription lifecycle and invoice generation.
  </Card>

  <Card title="Third-Party Payment Processing" icon="stripe" href="https://docs.stripe.com/billing/subscriptions/third-party-payment-processing">
    Stripe's recommended pattern for processing subscription payments via external payment processors.
  </Card>
</CardGroup>

### Payment Method Compatibility

| Method    | Auto-Charge Support | HitPay Recurring Method |
| --------- | ------------------- | ----------------------- |
| ShopeePay | Yes                 | `shopee_recurring`      |
| GrabPay   | Yes                 | `grabpay_direct`        |
| Cards     | Yes                 | `card`                  |

## Payment Flow

```mermaid theme={"system"}
sequenceDiagram
    participant User
    participant Frontend
    participant Backend
    participant Stripe
    participant HitPay

    User->>Frontend: Select subscription + "Auto-Charge"
    Frontend->>Backend: POST /api/create-subscription
    Backend->>Stripe: Create Subscription (charge_automatically)
    Backend-->>Frontend: subscriptionId, customerId
    Frontend->>Backend: POST /api/hitpay/recurring-billing/create
    Backend->>HitPay: Create recurring billing session
    HitPay-->>Backend: redirectUrl
    Backend-->>Frontend: redirectUrl
    Frontend->>User: Redirect to HitPay
    User->>HitPay: Authorize payment method
    HitPay->>Frontend: Redirect to /setup callback
    Frontend->>Backend: POST /api/subscription/charge-invoice
    Backend->>HitPay: Charge saved payment method
    Backend->>Stripe: Mark invoice paid + PaymentRecord
    Backend-->>Frontend: Success
    Note over HitPay,Stripe: Future invoices: auto-charged via HitPay
```

### API Calls Summary

| Step | API                | Endpoint / Method                     | Purpose                                                                                                    |
| ---- | ------------------ | ------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| 1    | **Stripe**         | `customers.create()`                  | Create or retrieve customer                                                                                |
| 2    | **Stripe**         | `subscriptions.create()`              | Create subscription with `charge_automatically`                                                            |
| 3    | **HitPay**         | `POST /recurring-billing`             | Create recurring billing session (method-specific parameter returns QR code, direct link, or instructions) |
| 4a   | **HitPay**         | (redirect)                            | **Redirect flow**: Customer authorizes on HitPay's hosted page                                             |
| 4b   | **HitPay**         | `GET /recurring-billing/{id}`         | **Embedded flow**: Poll until `status === 'active'` (customer scanned QR or opened app)                    |
| 5    | **HitPay**         | `POST /charge/recurring-billing/{id}` | Charge first invoice via saved payment method                                                              |
| 6    | **Stripe**         | `paymentMethods.create()`             | Create CPM PaymentMethod for reporting                                                                     |
| 7    | **Stripe**         | `paymentRecords.reportPayment()`      | Record payment for dashboard visibility                                                                    |
| 8    | **Stripe**         | `invoices.pay()`                      | Mark invoice as paid                                                                                       |
| —    | **Stripe Webhook** | `invoice.payment_attempt_required`    | Triggered by Stripe for each renewal invoice                                                               |
| 9    | **HitPay**         | `POST /charge/recurring-billing/{id}` | Auto-charge renewal invoices via webhook                                                                   |

***

## Stripe Objects

This integration touches six Stripe objects. Understanding each one makes it easier to follow the code and debug issues.

<AccordionGroup>
  <Accordion title="Customer">
    A [Stripe Customer](https://docs.stripe.com/api/customers) represents the subscriber. In this integration, the customer object is the **bridge between Stripe and HitPay** — it stores the HitPay recurring billing session ID in its `metadata` so every future invoice knows which HitPay token to charge.

    **Key fields used:**

    | Field                                  | Purpose                                                                         |
    | -------------------------------------- | ------------------------------------------------------------------------------- |
    | `metadata.hitpay_recurring_billing_id` | HitPay recurring billing session ID — used to charge all invoices               |
    | `metadata.hitpay_payment_method`       | The HitPay recurring method (e.g. `shopee_recurring`, `grabpay_direct`, `card`) |
    | `metadata.hitpay_cpm_type_id`          | Stripe CPM Type ID — used when creating a `PaymentMethod` for reporting         |

    The customer is created (or retrieved by email) when the subscription is set up, and its metadata is populated after the HitPay recurring billing session is created.
  </Accordion>

  <Accordion title="Subscription">
    A [Stripe Subscription](https://docs.stripe.com/billing/subscriptions/overview) manages the **billing lifecycle** — it defines the billing interval, generates invoices on each renewal, and tracks subscription status (`active`, `past_due`, `canceled`, etc.).

    In this integration, subscriptions are created with `collection_method: 'charge_automatically'` and `payment_behavior: 'default_incomplete'`. This tells Stripe to generate invoices for each billing cycle and fire the `invoice.payment_attempt_required` webhook — but **not** to attempt payment via Stripe's own payment methods. Your webhook handler performs the actual charge via HitPay.

    **Key fields used:**

    | Field               | Value / Purpose                                                                      |
    | ------------------- | ------------------------------------------------------------------------------------ |
    | `collection_method` | `charge_automatically` — generates invoices and fires the payment webhook            |
    | `payment_behavior`  | `default_incomplete` — subscription stays incomplete until the first invoice is paid |
    | `latest_invoice`    | ID of the first invoice, retrieved immediately after subscription creation           |
    | `status`            | Tracks whether the subscription is active, incomplete, or past due                   |
  </Accordion>

  <Accordion title="Invoice">
    A [Stripe Invoice](https://docs.stripe.com/billing/invoices/overview) represents **one billing cycle**. Stripe creates an invoice automatically for each period of a `charge_automatically` subscription.

    There are two types of invoices in this flow:

    * **First invoice** (`billing_reason: 'subscription_create'`): Created when the subscription is set up. Charged manually after the customer authorizes their payment method on HitPay. Your webhook handler skips this one to avoid double-charging.
    * **Renewal invoices** (`billing_reason: 'subscription_cycle'`): Generated automatically at each billing period. The `invoice.payment_attempt_required` webhook fires for these, and your handler charges them via HitPay.

    After a successful HitPay charge, the invoice is marked paid using `invoices.pay(invoiceId, { paid_out_of_band: true })` — this closes the invoice without Stripe attempting its own payment.

    **Key fields used:**

    | Field            | Purpose                                                                                       |
    | ---------------- | --------------------------------------------------------------------------------------------- |
    | `amount_due`     | Amount to charge (in the smallest currency unit, e.g. cents)                                  |
    | `currency`       | Currency for the HitPay charge                                                                |
    | `customer`       | Customer ID — used to retrieve `hitpay_recurring_billing_id` from metadata                    |
    | `billing_reason` | `subscription_create` vs `subscription_cycle` — used to skip the first invoice in the webhook |
    | `id`             | Stored in `PaymentRecord.metadata` for reconciliation                                         |
  </Accordion>

  <Accordion title="PaymentMethod (type: custom)">
    A [Stripe PaymentMethod](https://docs.stripe.com/api/payment_methods) of type `custom` represents a **Custom Payment Method** — it is created on the fly for each HitPay charge, purely for reporting purposes.

    This object is required as input to `paymentRecords.reportPayment()`. It carries the CPM Type ID that tells Stripe which custom payment method was used (e.g. your ShopeePay or GrabPay CPM).

    ```typescript theme={"system"}
    const paymentMethod = await stripe.paymentMethods.create({
      type: 'custom',
      custom: { type: cpmTypeId },  // e.g. 'cpmt_xxx'
    });
    ```

    **Note:** This `PaymentMethod` is not saved to the customer or reused. A new one is created per charge solely to satisfy the `reportPayment()` API.
  </Accordion>

  <Accordion title="PaymentRecord">
    A [Stripe PaymentRecord](https://docs.stripe.com/billing/subscriptions/third-party-payment-processing) is how **external charges are made visible in the Stripe Dashboard**. After charging via HitPay, you call `paymentRecords.reportPayment()` to record the charge in Stripe — this gives your team unified reporting and reconciliation without HitPay payments being invisible inside Stripe.

    The `PaymentRecord` stores the HitPay payment ID in two places (for easy retrieval when processing refunds):

    * `processor_details.custom.payment_reference` — the HitPay payment ID
    * `metadata.hitpay_payment_id` — same value, accessible as plain metadata

    **Key fields used:**

    | Field                                        | Value / Purpose                                                |
    | -------------------------------------------- | -------------------------------------------------------------- |
    | `amount_requested.value`                     | Invoice `amount_due` (in smallest currency unit)               |
    | `payment_method_details.payment_method`      | The custom `PaymentMethod` ID created above                    |
    | `processor_details.custom.payment_reference` | HitPay `payment_id` returned by the charge API                 |
    | `customer_presence`                          | `off_session` — charge was made without the customer present   |
    | `outcome`                                    | `guaranteed` — HitPay confirmed the charge succeeded           |
    | `metadata.invoice_id`                        | Stripe invoice ID — links the record back to the billing cycle |
  </Accordion>

  <Accordion title="invoice.payment_attempt_required (webhook event)">
    The [`invoice.payment_attempt_required`](https://docs.stripe.com/billing/subscriptions/third-party-payment-processing#handle-payment-attempt-required) event is fired by Stripe **whenever a subscription invoice needs to be charged** under `collection_method: charge_automatically`.

    This is the trigger for all renewal auto-charges. Your webhook handler receives this event, looks up the customer's `hitpay_recurring_billing_id`, and calls HitPay to charge the invoice.

    **Important:** Stripe fires this event for the **first invoice too** (`billing_reason: 'subscription_create'`). Your handler must skip it — the first invoice is already charged manually after the customer completes HitPay authorization. Charging it again here would double-bill the customer.

    ```typescript theme={"system"}
    if (invoice.billing_reason === 'subscription_create') {
      return NextResponse.json({ received: true }); // skip — already charged manually
    }
    ```
  </Accordion>
</AccordionGroup>

***

## Step 1: Configure Custom Payment Methods

Register each HitPay payment method as a Custom Payment Method (CPM) type in your Stripe Dashboard and map those CPM Type IDs to HitPay method identifiers in a config file.

<Steps>
  <Step title="Create Custom Payment Methods on Stripe Dashboard">
    <Badge color="gray">Stripe Dashboard</Badge>

    Create Custom Payment Method types in your Stripe Dashboard for each HitPay payment method you want to offer.

    <Card title="Create CPM in Stripe Dashboard" icon="stripe" href="https://docs.stripe.com/payments/payment-element/custom-payment-methods#create-cpm-in-dashboard">
      Follow Stripe's guide to create Custom Payment Method types and get your CPM Type IDs.
    </Card>

    <Card title="Download Payment Icons" icon="download" href="/files/payment_icons.zip">
      Download official HitPay payment method icons (PayNow, ShopeePay, GrabPay, FPX, and more) optimized for Stripe Custom Payment Methods.
    </Card>
  </Step>

  <Step title="Create Configuration File">
    <Badge color="gray">Server-side</Badge>

    Map your Stripe CPM Type IDs to HitPay payment methods. Include the `hitpayRecurringMethod` for auto-charge support.

    <Accordion title="config/payment-methods.ts">
      ```typescript theme={"system"}
      // config/payment-methods.ts

      interface CustomPaymentMethodConfig {
        id: string;                     // Stripe CPM Type ID (cpmt_xxx)
        hitpayMethod: string;           // HitPay one-time payment method
        hitpayRecurringMethod?: string; // HitPay recurring billing method
        displayName: string;
        chargeAutomatically: boolean;   // Supports auto-charge subscriptions
      }

      export const CUSTOM_PAYMENT_METHODS: CustomPaymentMethodConfig[] = [
        {
          id: 'cpmt_YOUR_PAYNOW_ID',
          hitpayMethod: 'paynow_online',
          displayName: 'PayNow',
          chargeAutomatically: false,  // QR-based, no tokenization
        },
        {
          id: 'cpmt_YOUR_SHOPEEPAY_ID',
          hitpayMethod: 'shopee_pay',
          hitpayRecurringMethod: 'shopee_recurring',
          displayName: 'ShopeePay',
          chargeAutomatically: true,
        },
        {
          id: 'cpmt_YOUR_GRABPAY_ID',
          hitpayMethod: 'grabpay',
          hitpayRecurringMethod: 'grabpay_direct',
          displayName: 'GrabPay',
          chargeAutomatically: true,
        },
        {
          id: 'cpmt_YOUR_CARD_ID',
          hitpayMethod: 'card',
          hitpayRecurringMethod: 'card',
          displayName: 'Card',
          chargeAutomatically: true,
        },
      ];

      export function supportsAutoCharge(cpmTypeId: string): boolean {
        const config = CUSTOM_PAYMENT_METHODS.find(pm => pm.id === cpmTypeId);
        return config?.chargeAutomatically ?? false;
      }

      export function getAutoChargeCpms(): CustomPaymentMethodConfig[] {
        return CUSTOM_PAYMENT_METHODS.filter(pm => pm.chargeAutomatically);
      }
      ```
    </Accordion>

    <Tip>
      The `chargeAutomatically` flag indicates whether a payment method supports tokenization for recurring charges. QR-based methods like PayNow cannot be saved for future charges.
    </Tip>

    <Tip>
      You'll have **different CPM Type IDs in sandbox and production** — they are separate Stripe accounts. Consider using environment variables or an `ids: { sandbox: string, production: string }` structure so you can switch between environments without code changes.
    </Tip>
  </Step>
</Steps>

***

## Step 2: Create Subscription

When the customer submits the checkout form, two server calls happen in sequence:

1. **Create a Stripe Subscription** — creates an incomplete subscription and a pending invoice
2. **Create a HitPay Recurring Billing session** — tokenizes the payment method and returns authorization data (QR code or direct app link)

The authorization data from step 2 is then used in [Step 3](#step-3-handle-payment-authorization) to let the customer approve the payment method inline.

<Steps>
  <Step title="Configure Environment Variables">
    <Badge color="gray">Server-side</Badge>

    Set up the required API keys and configuration for both Stripe and HitPay.

    ```bash theme={"system"}
    # Stripe
    NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_xxx
    STRIPE_SECRET_KEY=sk_test_xxx

    # HitPay
    HITPAY_API_KEY=xxx
    HITPAY_SALT=xxx
    NEXT_PUBLIC_HITPAY_ENV=sandbox  # or 'production'
    NEXT_PUBLIC_SITE_URL=https://your-domain.com

    # Stripe Webhook (for auto-charge renewals — see Step 4)
    STRIPE_WEBHOOK_SECRET=whsec_xxx
    ```

    <Warning>
      Never expose `STRIPE_SECRET_KEY` or `HITPAY_API_KEY` to the client. These are server-side only.
    </Warning>
  </Step>

  <Step title="Client: Collect customer info and initiate setup">
    <Badge color="blue">Client-side</Badge>

    Collect the customer's email address and selected plan, then make two sequential server calls: first to create the Stripe subscription, then to start the HitPay authorization session.

    ```typescript theme={"system"}
    // 1. Create Stripe subscription — returns subscription + invoice details
    const subRes = await fetch('/api/create-subscription', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        email,
        priceId,           // Stripe Price ID for the selected plan
        cpmTypeId,         // Stripe CPM Type ID for the selected payment method
      }),
    });
    const { subscriptionId, customerId, invoiceId, invoiceAmount, currency } = await subRes.json();

    // 2. Create HitPay recurring billing session — returns authorization embed data
    const hitpayRes = await fetch('/api/hitpay/recurring-billing/create', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        customerId,
        subscriptionId,
        invoiceId,
        amount: invoiceAmount,
        currency,
        customerEmail: email,
        paymentMethod: selectedRecurringMethod, // e.g. 'card', 'shopee_recurring', 'grabpay_direct'
      }),
    });
    const { directLinkUrl, directLinkAppUrl, qrCode } = await hitpayRes.json();

    // → Pass this data to Step 3 to render inline authorization
    ```

    <Tip>
      `selectedRecurringMethod` comes from the CPM config (`hitpayRecurringMethod`) for whichever payment method the customer chose. See the `CUSTOM_PAYMENT_METHODS` config in Step 1.
    </Tip>
  </Step>

  <Step title="Server: Create Stripe Subscription">
    <Badge color="gray">Server-side</Badge>

    Create a Stripe subscription with `charge_automatically` collection method. The subscription starts as `incomplete` — it will become `active` once the first invoice is charged in Step 3.

    **What this returns:** `subscriptionId`, `customerId`, `invoiceId`, and the `invoiceAmount` needed to create the HitPay session.

    <Accordion title="app/api/create-subscription/route.ts">
      ```typescript theme={"system"}
      // app/api/create-subscription/route.ts
      import { NextResponse } from 'next/server';
      import { stripe } from '@/lib/stripe';

      export async function POST(request: Request) {
        const { priceId, email, cpmTypeId } = await request.json();

        // Create or retrieve customer
        let customer;
        const existingCustomers = await stripe.customers.list({ email, limit: 1 });
        if (existingCustomers.data.length > 0) {
          customer = existingCustomers.data[0];
        } else {
          customer = await stripe.customers.create({ email });
        }

        // Create subscription with automatic charging
        const subscription = await stripe.subscriptions.create({
          customer: customer.id,
          items: [{ price: priceId }],
          collection_method: 'charge_automatically',
          payment_behavior: 'default_incomplete',
          payment_settings: {
            payment_method_types: ['card'],
          },
        });

        // Get invoice amount for HitPay setup
        const invoice = await stripe.invoices.retrieve(
          subscription.latest_invoice as string
        );

        return NextResponse.json({
          subscriptionId: subscription.id,
          customerId: customer.id,
          invoiceId: invoice.id,
          invoiceAmount: invoice.amount_due / 100,
          currency: invoice.currency,
          billingType: 'charge_automatically',
        });
      }
      ```
    </Accordion>
  </Step>

  <Step title="Server: Create HitPay Recurring Billing Session">
    <Badge color="gray">Server-side</Badge>

    Call `POST /v1/recurring-billing` with a method-specific generate parameter. This tokenizes the customer's payment method and returns the authorization data needed for inline rendering. Store the `recurring_billing_id` on the Stripe customer — you'll need it for all future charges.

    **What this returns:** A `session.id` (the recurring billing token), plus `qr_code_data` (ZaloPay), `direct_link` (ShopeePay / GrabPay / Touch 'N Go), or `instructions` (GIRO).

    <Accordion title="app/api/hitpay/recurring-billing/create/route.ts">
      ```typescript theme={"system"}
      // app/api/hitpay/recurring-billing/create/route.ts
      import { NextResponse } from 'next/server';
      import { stripe } from '@/lib/stripe';

      const HITPAY_API_URL = process.env.NEXT_PUBLIC_HITPAY_ENV === 'production'
        ? 'https://api.hit-pay.com/v1'
        : 'https://api.sandbox.hit-pay.com/v1';

      export async function POST(request: Request) {
        const {
          customerId,
          subscriptionId,
          invoiceId,
          amount,
          currency,
          customerEmail,
          paymentMethod,  // e.g., 'card', 'shopee_recurring'
        } = await request.json();

        const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000';

        // POST /v1/recurring-billing — tokenize the customer's payment method.
        // Pass a method-specific generate parameter to receive authorization data inline
        // instead of redirecting the customer to HitPay's hosted checkout page.
        // generate_direct_link: ShopeePay / GrabPay / Touch 'N Go
        // generate_qr: ZaloPay
        // generate_instructions: GIRO
        const generateParam = paymentMethod === 'zalopay'
          ? 'generate_qr'
          : paymentMethod === 'giro'
          ? 'generate_instructions'
          : 'generate_direct_link';

        const params = new URLSearchParams({
          customer_email: customerEmail,
          amount: amount.toFixed(2),
          currency,
          redirect_url: `${baseUrl}/subscribe/setup?subscription_id=${subscriptionId}&customer_id=${customerId}&invoice_id=${invoiceId}`,
          reference: subscriptionId,
          [generateParam]: 'true',
        });
        params.append('payment_methods[]', paymentMethod);

        const hitpayRes = await fetch(`${HITPAY_API_URL}/recurring-billing`, {
          method: 'POST',
          headers: {
            'Content-Type': 'application/x-www-form-urlencoded',
            'X-BUSINESS-API-KEY': process.env.HITPAY_API_KEY!,
          },
          body: params.toString(),
        });
        const session = await hitpayRes.json();

        // Store recurring billing ID in customer metadata
        await stripe.customers.update(customerId, {
          metadata: {
            hitpay_recurring_billing_id: session.id,
            hitpay_payment_method: paymentMethod,
          },
        });

        // Extract embed fields for the frontend
        const directLinkUrl = session.direct_link?.direct_link_url ?? null;
        const directLinkAppUrl = session.direct_link?.direct_link_app_url ?? null;
        const qrCode = session.qr_code_data?.qr_code ?? null;

        return NextResponse.json({
          recurringBillingId: session.id,
          // Embed data — use one of these to authorize inline (preferred)
          directLinkUrl,       // ShopeePay / GrabPay: open in browser
          directLinkAppUrl,    // ShopeePay only: open Shopee app directly (mobile)
          qrCode,              // Card: render as QR code
          // Fallback: redirect to HitPay's hosted checkout (not ideal — sends customer away)
          hostedCheckoutUrl: session.url,
          status: session.status,
        });
      }
      ```
    </Accordion>
  </Step>
</Steps>

***

## Step 3: Handle Payment Authorization

After creating the HitPay recurring billing session in Step 2, present the authorization UI to the customer.

<Tabs>
  <Tab title="Embedded (Recommended)">
    Instead of redirecting the customer to HitPay's hosted checkout page, the embedded approach returns authorization data directly in the API response — a QR code or a direct app link — which you render inline on your page. The customer never leaves your site.

    To enable this, pass the method-specific generate parameter when calling `POST /v1/recurring-billing`. For example, for ShopeePay:

    ```bash theme={"system"}
    curl -X POST https://api.sandbox.hit-pay.com/v1/recurring-billing \
      -H "X-BUSINESS-API-KEY: your_api_key" \
      -d "customer_email=customer@example.com" \
      -d "amount=10.00" \
      -d "currency=SGD" \
      -d "payment_methods[]=shopee_pay" \
      -d "redirect_url=https://your-site.com/setup/complete" \
      -d "generate_direct_link=true"
    ```

    With the generate parameter, the response includes authorization data instead of just a hosted checkout URL:

    ```json theme={"system"}
    {
      "id": "recurring_billing_id",
      "status": "pending",
      "url": "https://...",
      "qr_code_data": {
        "qr_code": "<base64 image or raw QR string>"
      },
      "direct_link": {
        "direct_link_url": "https://...",
        "direct_link_app_url": "app://..."
      }
    }
    ```

    Depending on the payment method, use the appropriate field:

    | HitPay Method    | Parameter               | Response Field                                                    | Customer Experience                                                     |
    | ---------------- | ----------------------- | ----------------------------------------------------------------- | ----------------------------------------------------------------------- |
    | `shopee_pay`     | `generate_direct_link`  | `direct_link.direct_link_url` / `direct_link.direct_link_app_url` | Button that opens Shopee app (deep link on mobile, web URL as fallback) |
    | `grabpay_direct` | `generate_direct_link`  | `direct_link.direct_link_url`                                     | Button that opens the Grab app                                          |
    | `touch_n_go`     | `generate_direct_link`  | `direct_link.direct_link_url`                                     | Button that opens Touch 'N Go                                           |
    | `zalopay`        | `generate_qr`           | `qr_code_data.qr_code`                                            | Inline QR code for the customer to scan                                 |
    | `giro`           | `generate_instructions` | `instructions`                                                    | Step-by-step bank portal instructions                                   |

    Poll `GET /v1/recurring-billing/{id}` every few seconds until `status` becomes `active`, then charge the first invoice.

    <Accordion title="components/EmbeddedAuthorizationForm.tsx">
      ```tsx theme={"system"}
      // components/EmbeddedAuthorizationForm.tsx
      'use client';

      import { useState, useEffect } from 'react';
      import { QRCodeSVG } from 'qrcode.react';

      interface Props {
        subscriptionId: string;
        invoiceId: string;
        customerId: string;
        amount: number;
        currency: string;
        customerEmail: string;
        selectedRecurringMethod: string; // e.g., 'shopee_recurring', 'card'
      }

      export function EmbeddedAuthorizationForm({
        subscriptionId, invoiceId, customerId, amount, currency,
        customerEmail, selectedRecurringMethod,
      }: Props) {
        const [qrCode, setQrCode] = useState<string | null>(null);
        const [directLinkUrl, setDirectLinkUrl] = useState<string | null>(null);
        const [directLinkAppUrl, setDirectLinkAppUrl] = useState<string | null>(null);
        const [recurringBillingId, setRecurringBillingId] = useState<string | null>(null);
        const [status, setStatus] = useState<'idle' | 'authorizing' | 'charging' | 'success' | 'error'>('idle');

        const startAuthorization = async () => {
          setStatus('authorizing');

          // Create HitPay recurring billing session — method-specific generate parameter is set server-side,
          // so the response contains QR code, direct link, or instructions instead of a hosted checkout URL.
          const res = await fetch('/api/hitpay/recurring-billing/create', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({
              customerId,
              subscriptionId,
              invoiceId,
              amount,
              currency,
              customerEmail,
              paymentMethod: selectedRecurringMethod,
            }),
          });
          const data = await res.json();

          setRecurringBillingId(data.recurringBillingId);

          if (data.qrCode) {
            // Card: render inline QR code for the customer to scan
            setQrCode(data.qrCode);
          } else if (data.directLinkUrl) {
            // ShopeePay / GrabPay: open in browser or app
            setDirectLinkUrl(data.directLinkUrl);
            setDirectLinkAppUrl(data.directLinkAppUrl ?? null); // ShopeePay only: mobile deep link
          }
        };

        // Poll for authorization status
        useEffect(() => {
          if (!recurringBillingId || status !== 'authorizing') return;

          const poll = setInterval(async () => {
            const res = await fetch(
              `/api/hitpay/recurring-billing/status?id=${recurringBillingId}`
            );
            const { status: sessionStatus } = await res.json();

            if (sessionStatus === 'active') {
              clearInterval(poll);
              setStatus('charging');

              // Charge first invoice using the now-active recurring billing
              await fetch('/api/subscription/charge-invoice', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ invoiceId }),
              });

              setStatus('success');
              window.location.href = `/subscribe/success?subscription_id=${subscriptionId}`;
            }
          }, 3000);

          return () => clearInterval(poll);
        }, [recurringBillingId, status, invoiceId, subscriptionId]);

        if (status === 'idle') {
          return (
            <button onClick={startAuthorization}>
              Set up Auto-Charge
            </button>
          );
        }

        if (qrCode) {
          return (
            <div>
              <h3>Scan to Authorize</h3>
              <QRCodeSVG value={qrCode} size={256} />
              <p>Scan this QR code with your payment app to authorize recurring charges.</p>
            </div>
          );
        }

        if (directLinkUrl) {
          return (
            <div>
              {/* On mobile, prefer the deep link to open the app directly */}
              {directLinkAppUrl ? (
                <a href={directLinkAppUrl}>Open Shopee App to Authorize</a>
              ) : (
                <a href={directLinkUrl} target="_blank" rel="noopener noreferrer">
                  Open App to Authorize
                </a>
              )}
              <p>Waiting for authorization...</p>
            </div>
          );
        }

        return <p>Setting up authorization...</p>;
      }
      ```
    </Accordion>

    **Backend: status polling endpoint**

    Add a lightweight endpoint that the frontend polls to check the recurring billing session status:

    <Accordion title="app/api/hitpay/recurring-billing/status/route.ts">
      ```typescript theme={"system"}
      // app/api/hitpay/recurring-billing/status/route.ts
      import { NextResponse } from 'next/server';

      const HITPAY_API_URL = process.env.NEXT_PUBLIC_HITPAY_ENV === 'production'
        ? 'https://api.hit-pay.com/v1'
        : 'https://api.sandbox.hit-pay.com/v1';

      export async function GET(request: Request) {
        const { searchParams } = new URL(request.url);
        const id = searchParams.get('id');

        if (!id) {
          return NextResponse.json({ error: 'Missing id' }, { status: 400 });
        }

        // GET /v1/recurring-billing/{id} — check authorization status
        const hitpayRes = await fetch(`${HITPAY_API_URL}/recurring-billing/${id}`, {
          headers: { 'X-BUSINESS-API-KEY': process.env.HITPAY_API_KEY! },
        });
        const session = await hitpayRes.json();
        return NextResponse.json({ status: session.status });
      }
      ```
    </Accordion>

    The session `status` progresses through:

    * `pending` — customer has not yet authorized
    * `active` — customer has authorized the payment method (safe to charge)
    * `canceled` — authorization was canceled or expired

    <Note>
      Once `active`, the polling component calls `/api/subscription/charge-invoice` automatically. See [Charge the Saved Payment Method](/apis/guide/save-payment-method) for HitPay charge API details.
    </Note>
  </Tab>

  <Tab title="URL Redirect">
    <Warning>
      This approach redirects the customer away from your site to HitPay's hosted checkout page. It breaks the subscription setup flow and is not recommended. Use the Embedded approach if possible.
    </Warning>

    Use the `hostedCheckoutUrl` from the session response to redirect the customer. HitPay redirects them back to your `redirect_url` after authorization, where you then charge the first invoice.

    ```tsx theme={"system"}
    // Redirect customer to HitPay hosted checkout
    const { hostedCheckoutUrl } = await hitpayRes.json();
    window.location.href = hostedCheckoutUrl;
    ```

    When the customer returns to your `redirect_url`, handle the callback:

    <Accordion title="app/subscribe/setup/page.tsx">
      ```tsx theme={"system"}
      // app/subscribe/setup/page.tsx
      'use client';

      import { useEffect, useState } from 'react';
      import { useSearchParams } from 'next/navigation';

      export default function SetupPage() {
        const searchParams = useSearchParams();
        const [status, setStatus] = useState<'processing' | 'success' | 'error'>('processing');
        const [error, setError] = useState<string | null>(null);

        useEffect(() => {
          const chargeFirstInvoice = async () => {
            const subscriptionId = searchParams.get('subscription_id');
            const invoiceId = searchParams.get('invoice_id');

            if (!invoiceId) {
              setError('Missing invoice ID');
              setStatus('error');
              return;
            }

            try {
              const response = await fetch('/api/subscription/charge-invoice', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ invoiceId }),
              });

              const data = await response.json();

              if (data.success) {
                setStatus('success');
                setTimeout(() => {
                  window.location.href = `/subscribe/success?subscription_id=${subscriptionId}`;
                }, 2000);
              } else {
                setError(data.error || 'Failed to charge invoice');
                setStatus('error');
              }
            } catch (err) {
              setError('An error occurred');
              setStatus('error');
            }
          };

          chargeFirstInvoice();
        }, [searchParams]);

        return (
          <div className="setup-container">
            {status === 'processing' && (
              <div>
                <h2>Setting up your subscription...</h2>
                <p>Please wait while we process your first payment.</p>
              </div>
            )}
            {status === 'success' && (
              <div>
                <h2>Subscription Active!</h2>
                <p>Redirecting to your subscription details...</p>
              </div>
            )}
            {status === 'error' && (
              <div>
                <h2>Setup Failed</h2>
                <p>{error}</p>
                <a href="/subscriptions">Try Again</a>
              </div>
            )}
          </div>
        );
      }
      ```
    </Accordion>
  </Tab>
</Tabs>

<Steps>
  <Step title="Charge via HitPay">
    <Badge color="gray">Server-side</Badge>

    Retrieve the customer's `hitpay_recurring_billing_id` from their Stripe metadata and call `POST /v1/charge/recurring-billing/{id}` to debit the saved payment method.

    ```typescript theme={"system"}
    const HITPAY_API_URL = process.env.NEXT_PUBLIC_HITPAY_ENV === 'production'
      ? 'https://api.hit-pay.com/v1'
      : 'https://api.sandbox.hit-pay.com/v1';

    const invoice = await stripe.invoices.retrieve(invoiceId);
    const customer = await stripe.customers.retrieve(invoice.customer as string);
    const recurringBillingId = customer.metadata.hitpay_recurring_billing_id;

    const hitpayRes = await fetch(
      `${HITPAY_API_URL}/charge/recurring-billing/${recurringBillingId}`,
      {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'X-BUSINESS-API-KEY': process.env.HITPAY_API_KEY!,
        },
        body: JSON.stringify({
          amount: (invoice.amount_due / 100).toFixed(2),
          currency: invoice.currency,
        }),
      }
    );
    const charge = await hitpayRes.json();
    // charge.status: 'succeeded' | 'pending' | 'failed'
    // charge.payment_id: HitPay payment reference
    ```

    <Note>
      Treat both `succeeded` and `pending` as success. `pending` means the charge is processing asynchronously — store `charge.payment_id` in invoice metadata and let the webhook confirm it later.
    </Note>
  </Step>

  <Step title="Report Payment in Stripe">
    <Badge color="gray">Server-side</Badge>

    Record the HitPay charge in Stripe using `stripe.paymentRecords.reportPayment()`. This gives Stripe a record of the payment for reconciliation and unified reporting. Without this step, Stripe has no visibility into what was collected by HitPay.

    ```typescript theme={"system"}
    const paymentMethod = await stripe.paymentMethods.create({
      type: 'custom',
      custom: { type: customer.metadata.hitpay_cpm_type_id },
    });

    const now = Math.floor(Date.now() / 1000);
    const paymentRecord = await stripe.paymentRecords.reportPayment({
      amount_requested: { value: invoice.amount_due, currency: invoice.currency },
      customer_details: { customer: invoice.customer },
      payment_method_details: { payment_method: paymentMethod.id },
      processor_details: {
        type: 'custom',
        custom: { payment_reference: charge.payment_id },
      },
      initiated_at: now,
      customer_presence: 'off_session',
      outcome: 'guaranteed',
      guaranteed: { guaranteed_at: now },
    });
    ```

    <Tip>
      Use `outcome: 'guaranteed'` for `succeeded` charges. If the charge returned `pending`, use `outcome: 'failed'` temporarily — update the PaymentRecord to `guaranteed` once the HitPay webhook confirms the charge.
    </Tip>
  </Step>

  <Step title="Mark Invoice as Paid">
    <Badge color="gray">Server-side</Badge>

    Mark the Stripe invoice as paid with `paid_out_of_band: true`. This tells Stripe the payment was collected externally (via HitPay), transitions the invoice to `paid`, and activates the subscription.

    ```typescript theme={"system"}
    await stripe.invoices.pay(invoiceId, {
      paid_out_of_band: true,
    });
    ```

    Store the HitPay payment ID and Stripe payment record ID in invoice metadata as an idempotency guard — the webhook handler checks for these before charging future invoices to avoid double-charging.

    ```typescript theme={"system"}
    await stripe.invoices.update(invoiceId, {
      metadata: {
        hitpay_payment_id: charge.payment_id,
        stripe_payment_record_id: paymentRecord.id,
      },
    });
    ```

    <Note>
      Once the invoice is `paid`, Stripe transitions the subscription from `incomplete` → `active`. Future renewals are handled automatically by the webhook in Step 4.
    </Note>

    <Accordion title="Full implementation: app/api/subscription/charge-invoice/route.ts">
      ```typescript theme={"system"}
      // app/api/subscription/charge-invoice/route.ts
      import { NextResponse } from 'next/server';
      import { stripe } from '@/lib/stripe';

      const HITPAY_API_URL = process.env.NEXT_PUBLIC_HITPAY_ENV === 'production'
        ? 'https://api.hit-pay.com/v1'
        : 'https://api.sandbox.hit-pay.com/v1';

      export async function POST(request: Request) {
        const { invoiceId } = await request.json();

        const invoice = await stripe.invoices.retrieve(invoiceId);
        const customer = await stripe.customers.retrieve(invoice.customer as string);

        if (customer.deleted) {
          return NextResponse.json({ error: 'Customer not found' }, { status: 404 });
        }

        const recurringBillingId = customer.metadata.hitpay_recurring_billing_id;
        if (!recurringBillingId) {
          return NextResponse.json({ error: 'No recurring billing setup found' }, { status: 400 });
        }

        // Step 1: Charge via HitPay
        const hitpayRes = await fetch(
          `${HITPAY_API_URL}/charge/recurring-billing/${recurringBillingId}`,
          {
            method: 'POST',
            headers: {
              'Content-Type': 'application/json',
              'X-BUSINESS-API-KEY': process.env.HITPAY_API_KEY!,
            },
            body: JSON.stringify({
              amount: (invoice.amount_due / 100).toFixed(2),
              currency: invoice.currency,
            }),
          }
        );
        const charge = await hitpayRes.json();

        if (charge.status !== 'succeeded' && charge.status !== 'pending') {
          return NextResponse.json({ error: 'Charge failed', details: charge }, { status: 400 });
        }

        // Step 2: Report payment in Stripe
        const paymentMethod = await stripe.paymentMethods.create({
          type: 'custom',
          custom: { type: customer.metadata.hitpay_cpm_type_id },
        });

        const now = Math.floor(Date.now() / 1000);
        const paymentRecord = await stripe.paymentRecords.reportPayment({
          amount_requested: { value: invoice.amount_due, currency: invoice.currency },
          customer_details: { customer: invoice.customer },
          payment_method_details: { payment_method: paymentMethod.id },
          processor_details: {
            type: 'custom',
            custom: { payment_reference: charge.payment_id },
          },
          initiated_at: now,
          customer_presence: 'off_session',
          outcome: 'guaranteed',
          guaranteed: { guaranteed_at: now },
        });

        // Step 3: Mark invoice as paid
        await stripe.invoices.pay(invoiceId, { paid_out_of_band: true });

        // Store IDs for idempotency (webhook skips invoices already charged)
        await stripe.invoices.update(invoiceId, {
          metadata: {
            hitpay_payment_id: charge.payment_id,
            stripe_payment_record_id: paymentRecord.id,
          },
        });

        return NextResponse.json({ success: true, hitpayPaymentId: charge.payment_id });
      }
      ```
    </Accordion>
  </Step>
</Steps>

***

## Step 4: Handle Future Invoices

On each billing cycle, Stripe fires `invoice.payment_attempt_required` when a new invoice is created. A webhook handler automatically charges the customer's saved HitPay payment method and records the payment in Stripe — no customer action required.

<Steps>
  <Step title="Setup Stripe Webhook">
    <Badge color="gray">Server-side</Badge>

    Handle the `invoice.payment_attempt_required` Stripe webhook event to automatically charge future invoices.

    **Webhook setup:**

    1. Go to **Stripe Dashboard** → **Developers** → **Webhooks** → **Add endpoint**
    2. Enter your endpoint URL: `https://your-domain.com/api/stripe/webhook`
    3. Select the event: `invoice.payment_attempt_required`
    4. Copy the **Signing secret** (`whsec_xxx`) and add it as `STRIPE_WEBHOOK_SECRET`

    For local development, use the [Stripe CLI](https://docs.stripe.com/stripe-cli):

    ```bash theme={"system"}
    stripe listen --forward-to localhost:3000/api/stripe/webhook
    ```

    <Accordion title="app/api/stripe/webhook/route.ts">
      ```typescript theme={"system"}
      // app/api/stripe/webhook/route.ts
      import { NextResponse } from 'next/server';
      import { stripe } from '@/lib/stripe';

      const HITPAY_API_URL = process.env.NEXT_PUBLIC_HITPAY_ENV === 'production'
        ? 'https://api.hit-pay.com/v1'
        : 'https://api.sandbox.hit-pay.com/v1';

      export async function POST(request: Request) {
        const body = await request.text();
        const signature = request.headers.get('stripe-signature')!;

        let event;
        try {
          event = stripe.webhooks.constructEvent(
            body,
            signature,
            process.env.STRIPE_WEBHOOK_SECRET!
          );
        } catch (err) {
          return NextResponse.json({ error: 'Invalid signature' }, { status: 400 });
        }

        if (event.type === 'invoice.payment_attempt_required') {
          const invoice = event.data.object;

          // Skip first invoice — it's charged manually after HitPay authorization
          // to avoid double-charging the customer on subscription creation
          if (invoice.billing_reason === 'subscription_create') {
            return NextResponse.json({ received: true });
          }

          // Skip if nothing is owed
          if (!invoice.amount_due || invoice.amount_due === 0) {
            return NextResponse.json({ received: true });
          }

          // Get customer's recurring billing ID
          const customer = await stripe.customers.retrieve(invoice.customer as string);
          if (customer.deleted) return NextResponse.json({ received: true });

          const recurringBillingId = customer.metadata.hitpay_recurring_billing_id;
          if (!recurringBillingId) return NextResponse.json({ received: true });

          // POST /v1/charge/recurring-billing/{id} — charge the saved payment method
          // See: /apis/guide/save-payment-method#step-3-charge-the-saved-payment-method
          try {
            const hitpayRes = await fetch(
              `${HITPAY_API_URL}/charge/recurring-billing/${recurringBillingId}`,
              {
                method: 'POST',
                headers: {
                  'Content-Type': 'application/json',
                  'X-BUSINESS-API-KEY': process.env.HITPAY_API_KEY!,
                },
                body: JSON.stringify({
                  amount: (invoice.amount_due / 100).toFixed(2),
                  currency: invoice.currency,
                }),
              }
            );
            const chargeResult = await hitpayRes.json();

            if (chargeResult.status === 'succeeded') {
              // Record payment and mark invoice as paid
              // (Same logic as charge-invoice endpoint)
            }
          } catch (error) {
            console.error('Auto-charge failed:', error);
          }
        }

        return NextResponse.json({ received: true });
      }
      ```
    </Accordion>

    <Card title="Stripe Webhooks Guide" icon="stripe" href="https://docs.stripe.com/webhooks">
      Learn how to configure webhooks in your Stripe Dashboard.
    </Card>
  </Step>
</Steps>

***

## Testing

1. Create a subscription with "Auto-Charge" option
2. Select a tokenizable CPM (ShopeePay, GrabPay, or Card)
3. Complete authorization on HitPay redirect
4. Verify:
   * Customer metadata has `hitpay_recurring_billing_id`
   * First invoice is paid
   * Subscription is active

<Warning>
  In sandbox mode, you may need to manually trigger subsequent invoice payments. In production, the Stripe webhook handles this automatically.
</Warning>

***

## FAQ

<AccordionGroup>
  <Accordion title="Which payment methods support auto-charge subscriptions?">
    Auto-charge requires tokenizable payment methods: **Cards**, **ShopeePay**, and **GrabPay**. PayNow is QR-based and cannot be tokenized — customers using PayNow must pay each invoice manually via the [out-of-band flow](/connections/stripe/out-of-band).
  </Accordion>

  <Accordion title="Why is the recurring billing charge failing?">
    * Verify the recurring billing session is active (not pending or canceled)
    * Check that the customer authorized the payment method
    * Ensure the amount and currency are correct
  </Accordion>

  <Accordion title="Why isn't the webhook triggering charges?">
    * Verify webhook endpoint is configured in Stripe Dashboard
    * Check webhook signature verification
    * Ensure `STRIPE_WEBHOOK_SECRET` is set correctly
  </Accordion>
</AccordionGroup>
