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
HitPay Embedded Recurring Billing
HitPay Save Payment Method & Charge
Stripe Subscriptions API
Third-Party Payment Processing
Payment Method Compatibility
| Method | Auto-Charge Support | HitPay Recurring Method |
|---|---|---|
| ShopeePay | Yes | shopee_recurring |
| GrabPay | Yes | grabpay_direct |
| Cards | Yes | card |
Payment Flow
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.Customer
Customer
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 |
Subscription
Subscription
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 |
Invoice
Invoice
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. Theinvoice.payment_attempt_requiredwebhook fires for these, and your handler charges them via HitPay.
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 |
PaymentMethod (type: custom)
PaymentMethod (type: custom)
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).PaymentMethod is not saved to the customer or reused. A new one is created per charge solely to satisfy the reportPayment() API.PaymentRecord
PaymentRecord
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 IDmetadata.hitpay_payment_id— same value, accessible as plain metadata
| 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 |
invoice.payment_attempt_required (webhook event)
invoice.payment_attempt_required (webhook event)
invoice.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.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.Create Custom Payment Methods on Stripe Dashboard
Create CPM in Stripe Dashboard
Download Payment Icons
Step 2: Create Subscription
When the customer submits the checkout form, two server calls happen in sequence:- Create a Stripe Subscription — creates an incomplete subscription and a pending invoice
- Create a HitPay Recurring Billing session — tokenizes the payment method and returns authorization data (QR code or direct app link)
Configure Environment Variables
Client: Collect customer info and initiate setup
Server: Create Stripe Subscription
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.app/api/create-subscription/route.ts
app/api/create-subscription/route.ts
Server: Create HitPay Recurring Billing Session
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).app/api/hitpay/recurring-billing/create/route.ts
app/api/hitpay/recurring-billing/create/route.ts
Step 3: Handle Payment Authorization
After creating the HitPay recurring billing session in Step 2, present the authorization UI to the customer.- Embedded (Recommended)
- URL Redirect
POST /v1/recurring-billing. For example, for ShopeePay:| 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 |
GET /v1/recurring-billing/{id} every few seconds until status becomes active, then charge the first invoice.components/EmbeddedAuthorizationForm.tsx
components/EmbeddedAuthorizationForm.tsx
app/api/hitpay/recurring-billing/status/route.ts
app/api/hitpay/recurring-billing/status/route.ts
status progresses through:pending— customer has not yet authorizedactive— customer has authorized the payment method (safe to charge)canceled— authorization was canceled or expired
active, the polling component calls /api/subscription/charge-invoice automatically. See Charge the Saved Payment Method for HitPay charge API details.Charge via HitPay
hitpay_recurring_billing_id from their Stripe metadata and call POST /v1/charge/recurring-billing/{id} to debit the saved payment method.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.Report Payment in Stripe
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.Mark Invoice as Paid
paid_out_of_band: true. This tells Stripe the payment was collected externally (via HitPay), transitions the invoice to paid, and activates the subscription.paid, Stripe transitions the subscription from incomplete → active. Future renewals are handled automatically by the webhook in Step 4.Full implementation: app/api/subscription/charge-invoice/route.ts
Full implementation: app/api/subscription/charge-invoice/route.ts
Step 4: Handle Future Invoices
On each billing cycle, Stripe firesinvoice.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.
Setup Stripe Webhook
invoice.payment_attempt_required Stripe webhook event to automatically charge future invoices.Webhook setup:- Go to Stripe Dashboard → Developers → Webhooks → Add endpoint
- Enter your endpoint URL:
https://your-domain.com/api/stripe/webhook - Select the event:
invoice.payment_attempt_required - Copy the Signing secret (
whsec_xxx) and add it asSTRIPE_WEBHOOK_SECRET
app/api/stripe/webhook/route.ts
app/api/stripe/webhook/route.ts
Stripe Webhooks Guide
Testing
- Create a subscription with “Auto-Charge” option
- Select a tokenizable CPM (ShopeePay, GrabPay, or Card)
- Complete authorization on HitPay redirect
- Verify:
- Customer metadata has
hitpay_recurring_billing_id - First invoice is paid
- Subscription is active
- Customer metadata has
FAQ
Which payment methods support auto-charge subscriptions?
Which payment methods support auto-charge subscriptions?
Why is the recurring billing charge failing?
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
Why isn't the webhook triggering charges?
Why isn't the webhook triggering charges?
- Verify webhook endpoint is configured in Stripe Dashboard
- Check webhook signature verification
- Ensure
STRIPE_WEBHOOK_SECRETis set correctly