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).
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.
The chargeAutomatically flag indicates whether a payment method supports tokenization for recurring charges. QR-based methods like PayNow cannot be saved for future charges.
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.
3
Create HitPay Recurring Billing Functions
Server-sideAdd helper functions to interact with HitPay’s Recurring Billing API.
Client-side + Server-sideInstead of redirecting to HitPay’s hosted checkout, you can embed the authorization flow directly in your page using the generate_embed: true parameter. The recurring billing creation endpoint already includes this parameter — the response returns an embeddable QR code or a direct app link depending on the payment method.What each method returns with generate_embed: true:
HitPay Method
Response Field
Customer Experience
shopee_recurring
direct_link.direct_link_url
Button that opens the Shopee app
grabpay_direct
direct_link.direct_link_url
Button that opens the Grab app
card
qr_code_data.qr_code
Inline QR code for the customer to scan
Frontend: handle QR code vs. direct linkAfter calling /api/hitpay/recurring-billing/create, check which fields are present in the response and render accordingly:
Copy
Ask AI
// 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 [directLink, setDirectLink] = 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 (generate_embed: true is set server-side) 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-based methods: show inline QR code setQrCode(data.qrCode); } else if (data.directLinkUrl) { // App-based methods (ShopeePay, GrabPay): show open-app button setDirectLink(data.directLinkUrl); } }; // 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 (directLink) { return ( <div> <a href={directLink} target="_blank" rel="noopener noreferrer"> Open App to Authorize </a> <p>Waiting for authorization...</p> </div> ); } return <p>Setting up authorization...</p>;}
Backend: status polling endpointAdd a lightweight endpoint that the frontend polls to check the recurring billing session status:
Copy
Ask AI
// app/api/hitpay/recurring-billing/status/route.tsimport { NextResponse } from 'next/server';import { getRecurringBilling } from '@/lib/hitpay';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 }); } const session = await getRecurringBilling(id); return NextResponse.json({ status: session.status });}
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
After the session reaches active, call /api/subscription/charge-invoice to charge the first invoice — the same endpoint used in the redirect flow. Future renewals are handled automatically by the Stripe webhook.
3
Process Setup Callback
Client-sideAfter the customer authorizes on HitPay, they’re redirected back to your setup page. Process the callback and charge the first invoice.