> ## 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 (Out-of-Band)

> Implement out-of-band subscriptions with Stripe Custom Payment Methods and HitPay

Out-of-band subscriptions allow customers to pay each invoice manually using any HitPay payment method. This is the same flow as one-time payments, repeated per billing cycle. This approach works with all payment methods including QR-based ones like PayNow that cannot be tokenized.

## How It Works

This integration connects Stripe's Subscription API with HitPay's Payment Request API, allowing customers to pay each invoice using local payment methods while keeping all subscription records in Stripe.

### Key Concepts

| Component                  | Purpose                                                                                                      |
| -------------------------- | ------------------------------------------------------------------------------------------------------------ |
| **Stripe Subscription**    | Manages subscription lifecycle with `send_invoice` collection method. Generates invoices each billing cycle. |
| **HitPay Payment Request** | Creates one-time payment requests (QR codes or redirect) for each invoice.                                   |
| **Stripe Payment Records** | Records each HitPay payment in Stripe for unified reporting and reconciliation.                              |

### API References

<CardGroup cols={2}>
  <Card title="HitPay Payment Request API" icon="code" href="https://docs.hitpayapp.com/apis/guide/payment-requests">
    Create payment requests with QR codes for one-time payments.
  </Card>

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

<Tip>
  Out-of-band subscriptions work with **all payment methods** since each invoice is paid as a one-time payment. This is ideal for QR-based methods like PayNow that cannot be tokenized.
</Tip>

## Payment Flow

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

    User->>Frontend: Select subscription + "Pay Each Invoice"
    Frontend->>Backend: POST /api/create-subscription
    Backend->>Stripe: Create Subscription (send_invoice)
    Stripe-->>Backend: subscriptionId + invoiceClientSecret
    Backend-->>Frontend: clientSecret
    Frontend->>User: Show Payment Element
    User->>Frontend: Select CPM & scan QR
    User->>HitPay: Complete payment
    Frontend->>Backend: POST /api/subscription/pay-invoice
    Backend->>Stripe: Mark invoice paid_out_of_band
    Backend->>Stripe: Create PaymentRecord
    Backend-->>Frontend: Success
    Note over User,Stripe: Next billing cycle: new invoice sent
```

***

## 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 so they appear as options in your checkout.

<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. For out-of-band subscriptions, you only need the `hitpayMethod` (one-time payment method).

    <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 (optional)
        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 getHitpayMethod(cpmTypeId: string): string | undefined {
        const config = CUSTOM_PAYMENT_METHODS.find(pm => pm.id === cpmTypeId);
        return config?.hitpayMethod;
      }
      ```
    </Accordion>
  </Step>
</Steps>

***

## Step 2: Create Subscription

Create a Stripe Subscription with `send_invoice` collection method, which tells Stripe to issue invoices that customers pay manually — rather than charging a saved payment method automatically.

<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 (Standard Account Keys)
    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'
    ```

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

  <Step title="Create Subscription with Send Invoice">
    <Badge color="gray">Server-side</Badge>

    Create a Stripe subscription with `send_invoice` collection method. This creates invoices that customers pay manually each billing cycle.

    <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 } = 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 manual invoice payment
        const subscription = await stripe.subscriptions.create({
          customer: customer.id,
          items: [{ price: priceId }],
          collection_method: 'send_invoice',
          days_until_due: 0,  // Invoice due immediately
          payment_settings: {
            payment_method_types: ['card'],
          },
        });

        // Finalize invoice to get PaymentIntent
        const invoice = await stripe.invoices.retrieve(subscription.latest_invoice as string);
        const finalizedInvoice = await stripe.invoices.finalizeInvoice(invoice.id);

        // Get client secret for Payment Element
        let clientSecret: string;
        if (finalizedInvoice.payment_intent) {
          const pi = await stripe.paymentIntents.retrieve(
            finalizedInvoice.payment_intent as string
          );
          clientSecret = pi.client_secret!;
        } else {
          // Create a UI PaymentIntent if needed
          const pi = await stripe.paymentIntents.create({
            amount: finalizedInvoice.amount_due,
            currency: finalizedInvoice.currency,
            customer: customer.id,
            metadata: { invoice_id: finalizedInvoice.id },
          });
          clientSecret = pi.client_secret!;
        }

        return NextResponse.json({
          subscriptionId: subscription.id,
          invoiceId: finalizedInvoice.id,
          clientSecret,
          customerId: customer.id,
          billingType: 'out_of_band',
          type: 'payment',
        });
      }
      ```
    </Accordion>

    <Note>
      The key difference from auto-charge is `collection_method: 'send_invoice'` with `days_until_due: 0`, which creates an invoice due immediately that customers pay manually.
    </Note>
  </Step>
</Steps>

***

## Step 3: Handle Invoice Payment

When the customer selects a HitPay payment method for an invoice, create a HitPay payment request, display the QR code for them to scan, and mark the invoice as paid once HitPay confirms the payment.

<Steps>
  <Step title="Create HitPay Payment Request">
    <Badge color="gray">Server-side</Badge>

    Use the same HitPay Payment Request endpoint from the one-time payments flow. When a customer selects a custom payment method, create a payment request for the invoice amount.

    <Card title="HitPay Payment Request" icon="code" href="/connections/stripe/one-time-payments#step-2-create-payment-request">
      See the one-time payments guide for the HitPay payment request implementation.
    </Card>
  </Step>

  <Step title="Display QR Code and Poll for Payment">
    <Badge color="gray">Client-side</Badge>

    Show the QR code or redirect URL and poll for payment completion. This is the same flow as one-time payments.

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

      import { useState, useCallback } from 'react';
      import { PaymentElement, useStripe, useElements } from '@stripe/react-stripe-js';
      import { QRCodeSVG } from 'qrcode.react';
      import { CUSTOM_PAYMENT_METHODS, getHitpayMethod } from '@/config/payment-methods';

      interface Props {
        amount: number;
        currency: string;
        invoiceId: string;
        subscriptionId: string;
      }

      export function SubscriptionCheckoutForm({
        amount,
        currency,
        invoiceId,
        subscriptionId,
      }: Props) {
        const stripe = useStripe();
        const elements = useElements();

        const [selectedCpm, setSelectedCpm] = useState<string | null>(null);
        const [qrCode, setQrCode] = useState<string | null>(null);
        const [paymentRequestId, setPaymentRequestId] = useState<string | null>(null);
        const [isPolling, setIsPolling] = useState(false);

        const isCustomPaymentMethod = (type: string) => {
          return CUSTOM_PAYMENT_METHODS.some(pm => pm.id === type);
        };

        const handlePaymentElementChange = async (event: any) => {
          const paymentType = event.value?.type;

          if (paymentType && isCustomPaymentMethod(paymentType)) {
            setSelectedCpm(paymentType);
            await createHitPayRequest(paymentType);
          } else {
            setSelectedCpm(null);
            setQrCode(null);
          }
        };

        const createHitPayRequest = async (cpmTypeId: string) => {
          const hitpayMethod = getHitpayMethod(cpmTypeId);
          if (!hitpayMethod) return;

          const response = await fetch('/api/hitpay/create', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({
              amount,
              currency,
              paymentMethod: hitpayMethod,
              paymentIntentId: invoiceId,  // Use invoice ID as reference
            }),
          });

          const data = await response.json();

          if (data.qrCode) {
            setQrCode(data.qrCode);
            setPaymentRequestId(data.paymentRequestId);
            startPolling(data.paymentRequestId, cpmTypeId);
          }
        };

        const startPolling = useCallback((requestId: string, cpmTypeId: string) => {
          setIsPolling(true);
          let attempts = 0;

          const poll = async () => {
            if (attempts >= 60) {
              setIsPolling(false);
              return;
            }

            // Check HitPay status
            const statusRes = await fetch('/api/hitpay/status', {
              method: 'POST',
              headers: { 'Content-Type': 'application/json' },
              body: JSON.stringify({ paymentRequestId: requestId }),
            });
            const { status } = await statusRes.json();

            if (status === 'completed') {
              // Mark invoice as paid
              await fetch('/api/subscription/pay-invoice', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({
                  invoiceId,
                  paymentRequestId: requestId,
                  cpmTypeId,
                }),
              });

              setIsPolling(false);
              window.location.href = '/subscribe/success';
            } else {
              attempts++;
              setTimeout(poll, 3000);
            }
          };

          poll();
        }, [invoiceId]);

        return (
          <div>
            <PaymentElement onChange={handlePaymentElementChange} />

            {qrCode && (
              <div className="qr-container">
                <h3>Scan to Pay Invoice</h3>
                <QRCodeSVG value={qrCode} size={256} />
                {isPolling && <p>Waiting for payment...</p>}
              </div>
            )}

            {!selectedCpm && (
              <button
                onClick={() => stripe?.confirmPayment({
                  elements: elements!,
                  confirmParams: { return_url: `${window.location.origin}/subscribe/success` },
                })}
                disabled={!stripe}
              >
                Subscribe
              </button>
            )}
          </div>
        );
      }
      ```
    </Accordion>
  </Step>

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

    After verifying the HitPay payment, create a Payment Record in Stripe and mark the invoice as paid out-of-band.

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

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

        // Verify HitPay payment completed
        const { status, payment_id } = await getHitPayPaymentStatus(paymentRequestId);
        if (status !== 'completed') {
          return NextResponse.json(
            { error: 'Payment not completed' },
            { status: 400 }
          );
        }

        // Create PaymentMethod with CPM type
        const paymentMethod = await stripe.paymentMethods.create({
          type: 'custom',
          custom: { type: cpmTypeId },
        });

        // Get invoice details
        const invoice = await stripe.invoices.retrieve(invoiceId);

        // Record the payment
        const timestamp = Math.floor(Date.now() / 1000);
        await (stripe as any).paymentRecords.reportPayment({
          amount_requested: {
            value: invoice.amount_due,
            currency: invoice.currency,
          },
          payment_method_details: {
            payment_method: paymentMethod.id,
          },
          processor_details: {
            type: 'custom',
            custom: {
              payment_reference: payment_id || paymentRequestId,
            },
          },
          initiated_at: timestamp,
          customer_presence: 'on_session',
          outcome: 'guaranteed',
          guaranteed: { guaranteed_at: timestamp },
          metadata: {
            invoice_id: invoiceId,
            hitpay_payment_request_id: paymentRequestId,
          },
        });

        // Mark invoice as paid out-of-band
        await stripe.invoices.pay(invoiceId, {
          paid_out_of_band: true,
        });

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

***

## Step 4: Handle Future Invoices

Each billing cycle, Stripe automatically generates a new invoice. Since out-of-band subscriptions don't use tokenized payment methods, customers pay each invoice manually using the same QR code flow — no webhook or automatic charge required.

<Steps>
  <Step title="Future Invoice Flow">
    <Badge color="gray">Info</Badge>

    Each billing cycle, Stripe automatically generates a new invoice. Since out-of-band subscriptions don't use tokenized payment methods, customers pay each invoice manually using the same one-time payment flow.

    **How it works:**

    1. Stripe generates a new invoice at the start of each billing cycle
    2. You notify the customer (via email or your app) that a new invoice is ready
    3. Customer returns to your payment page and pays the invoice using any HitPay payment method
    4. The payment is recorded and the invoice is marked as paid

    <Note>
      Unlike auto-charge subscriptions, there's no webhook to automatically charge the customer. You can optionally set up a webhook for `invoice.created` to send payment reminders.
    </Note>
  </Step>
</Steps>

***

## Testing

1. Create a subscription with "Pay Each Invoice" option
2. Select a CPM and scan the QR code
3. Complete the mock payment in HitPay sandbox
4. Verify:
   * Invoice marked as `paid_out_of_band` in Stripe
   * Payment Record created (`prec_*`)
   * Subscription is active

***

## FAQ

<AccordionGroup>
  <Accordion title="Why isn't the invoice being marked as paid?">
    * Check that `paid_out_of_band: true` is passed to `invoices.pay()`
    * Verify the Payment Record was created successfully
    * Check server logs for API errors
  </Accordion>

  <Accordion title="Why isn't the QR code appearing?">
    * Verify the HitPay API key is correct
    * Check that the payment method supports QR codes
    * Ensure the amount and currency are valid
  </Accordion>

  <Accordion title="Why isn't the payment status updating?">
    * Check that polling is running correctly
    * Verify HitPay webhook is configured for real-time updates
    * Check browser console for network errors
  </Accordion>
</AccordionGroup>
