SaaS Recurring Billing in Indonesia: Midtrans and Xendit

Credit card ownership is low in Indonesia, and most people pay with e-wallets or QRIS. A card-on-file default would exclude the majority of your subscribers. You need gateways that tokenize local instruments and a fallback flow for rails that are one-time only.
Yes. Midtrans has a Subscription API that auto-deducts on a set interval using card tokenization and GoPay tokenization. Xendit offers a Subscriptions product with fixed-amount and usage-based plans, retry recovery for failed payments, and webhooks for each billing cycle.
No. QRIS is a merchant-presented, one-time rail where the customer scans and approves each payment. For QRIS users you cannot silently re-bill, so you send a renewal notification and a fresh payment link every cycle instead of an automatic deduction.
Dunning is the automated process of retrying failed renewal charges and reminding the customer. In Indonesia, e-wallet balances run dry and cards expire often, so renewals fail routinely. A retry-and-reminder sequence with a grace period is essential to avoid losing paying customers.
Gateways deliver results asynchronously and retry webhooks, so the same event can arrive twice or out of order. If your handler is not idempotent, you might double-charge or double-grant access. Dedupe on the gateway's event id and make reprocessing the same event a no-op.

Key Takeaway
This post explains how to build SaaS recurring billing in Indonesia using the local gateways Midtrans and Xendit. It covers tokenization for card-on-file, the subscription state machine of active, past due, and canceled, proration and dunning retries, handling QRIS and e-wallets that are one-time by nature, plus webhooks and idempotency for reliable renewals.
Charging a card once is easy. Charging the same customer automatically every month, forever, without a human clicking anything, is a different problem. That is what a subscription is, and in Indonesia it is harder than the Stripe tutorials suggest.
Most Indonesian customers do not pay with a saved credit card. They pay with QRIS, GoPay, OVO, DANA, ShopeePay, or a bank transfer. These rails are built for one-off payments, not for a merchant that silently pulls funds next month. Building SaaS billing here means knowing which local rails support recurring charges, and engineering around the ones that do not.
Card penetration in Indonesia is low, and most digital spend flows through e-wallets and QRIS rather than credit cards. Recurring billing assumes the opposite: a stored instrument the merchant can charge again without the customer present. When the dominant payment method is a QR code the user scans fresh each time, the card-on-file model that Western SaaS relies on simply does not map.
The result is that you cannot just copy a Stripe subscription flow. You need a gateway that supports merchant-initiated transactions on local rails, a way to tokenize whatever instrument the customer does have, and a fallback plan for the large share of users whose only payment method is inherently one-time.
Two local gateways cover most of the market. Midtrans exposes a Subscription API that auto-deducts funds at a set interval; its recurring feature is built on card tokenization, where the One Click and Two Clicks flows store card details as a token, and it also supports GoPay tokenization for wallet-based recurring charges. Under the hood, Midtrans treats recurring and one-click as the same stored-token feature, and the only difference is what triggers the charge.
Xendit offers a Subscriptions product that manages fixed-amount and usage-based plans on weekly, monthly, or yearly schedules. You define the amount, schedule, and recovery options for failed payments, and Xendit handles the deductions, emitting webhooks such as a retrying event when an attempt fails and a succeeded event when a cycle clears; it requires at least one payment channel that supports merchant-initiated transactions. QRIS itself, standardized by Bank Indonesia, is a merchant-presented one-time rail, so neither gateway can silently re-bill a QRIS scan and it becomes a manual renewal touchpoint rather than a true recurring instrument.
Whatever gateway you pick, store the gateway's token and subscription id against your own user, never the raw card or wallet credential. Tokenization keeps sensitive data off your servers and is exactly what makes a later merchant-initiated charge possible.
A subscription is a small state machine, and getting the states and transitions right is most of the work. Model it explicitly rather than inferring status from scattered payment rows.
Payments are asynchronous. The customer approves in a wallet app or scans a QR, and your server learns the outcome later through a webhook, not a synchronous API response. Treat the webhook as the source of truth for whether a cycle succeeded, and never mark a subscription active just because you sent a charge request.
Webhooks retry and can arrive more than once or out of order, so every handler must be idempotent: dedupe on the gateway's event or order id and make processing the same event twice a no-op. Midtrans supports an idempotency key when creating a subscription, so a retried create request returns the same result instead of a duplicate. Reconcile daily by pulling the gateway's transaction list and comparing it against your own ledger to catch webhooks you missed.
Never trust a webhook without verifying its signature, and never flip a user to paid on the request alone. A spoofed or replayed webhook that you process blindly can grant free access or double-charge a customer.
The failure modes here are specific to the local market. Design for them from the start rather than patching after the first failed renewal.
SaaS recurring billing in Indonesia is less about the payment API and more about the reality that most customers cannot be silently re-billed. Pick a gateway that tokenizes the instruments your users actually have, model the subscription as an explicit state machine, and make every webhook idempotent and reconciled. Where card-on-file is impossible, turn renewal into a reliable notification-plus-link flow.