Skip to main content
BlogBackend

SaaS Recurring Billing in Indonesia: Midtrans and Xendit

How to build recurring SaaS billing in Indonesia with Midtrans, Xendit, and QRIS when most customers do not pay by card.

Matthews Wong

Software Engineer

April 20268 min read
SaaS Recurring Billing in Indonesia: Midtrans and Xendit

Key Takeaway

In Indonesia only linked cards and e-wallets can be charged automatically: Midtrans's Subscription API bills saved cards and GoPay, Xendit Subscriptions bills any channel that supports merchant-initiated transactions, and QRIS, which neither can charge automatically, needs a fresh payment link every cycle. Model the subscription as an explicit state machine of active, past due and canceled, and treat only signed, idempotent webhooks as proof that a cycle was paid.

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. Most of 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.

Why Recurring Billing Is Hard Here

Card penetration in Indonesia is low, while QRIS keeps growing: Bank Indonesia counted 65.77 million QRIS users and 44.86 million QRIS merchants by June 2026. Recurring billing assumes the opposite: a stored instrument the merchant can charge again without the customer present. When the method a customer reaches for is a QR code they scan 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.

  • Credit card ownership is a minority behavior, so a card-on-file default excludes most of your potential subscribers.
  • E-wallets can be linked for merchant-initiated charges, but only through specific products: Midtrans's Subscription API takes GoPay alongside cards, and Xendit lists GoPay (through a separate recurring channel that needs partner activation), OVO, DANA and ShopeePay as supporting them.
  • QRIS, the national QR standard, needs the customer to act in their own app for every payment, whether they scan the merchant's code or show their own, so it is not built for silent renewals.

Gateways and Their Recurring Features

Midtrans and Xendit both document recurring billing. Midtrans's Subscription API charges a stored token on a day, week or month interval and can retry a failed charge up to three times, but it accepts only cards and GoPay. Card recurring reuses the One Click token, since Midtrans says recurring and one click are the same feature and differ only in what triggers the charge, and it needs extra bank approval plus a successful 3DS first payment. Published fees shape the choice as much as the APIs do. On 17 October 2026 Midtrans listed QRIS at 0.7%, cards at 2.9% plus Rp 2.000, a flat Rp 4.000 for virtual accounts, GoPay and ShopeePay at 2%, and DANA and OVO at 1.5%, with different e-wallet and QRIS rates for gaming and digital-product companies. Bank Indonesia has set the QRIS MDR at 0% up to Rp 100.000 for all merchants since 1 October 2026, while Midtrans's page still showed 0.7% that day, so confirm which rate your account is billed. The flat virtual-account fee undercuts the card rate on any charge above roughly Rp 69.000.

Xendit offers a Subscriptions product for fixed-amount and usage-based plans on weekly, monthly or yearly schedules. You define the amount, schedule and recovery options for failed payments, including a payment link the customer can use when an attempt fails, and Xendit handles the deductions, emitting webhooks such as recurring.cycle.retrying when an attempt fails and recurring.cycle.succeeded when a cycle clears; it requires at least one payment channel that supports merchant-initiated transactions. QRIS, standardized by Bank Indonesia, is not one of them: neither Midtrans's Subscription API nor Xendit's QRIS channel supports a merchant-initiated charge, so a QRIS payer 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.

The Subscription State Machine

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.

  • Active: the current cycle is paid and the customer has access, with the next charge scheduled at the period end.
  • Past due: a renewal charge failed, so access may continue during a grace period while dunning retries run on a backoff schedule.
  • Canceled: the customer or an exhausted dunning sequence ended the subscription, and access stops at the end of the paid period, not immediately.
  • Proration: when a plan changes mid-cycle, credit the unused portion of the old plan and charge the prorated remainder of the new one.

Reliability: Webhooks and Idempotency

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 header when creating a subscription, so a create request retried with the same key within three minutes returns the same response 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.

Pitfalls to Plan For

The failure modes here are specific to the local market. Design for them from the start rather than patching after the first failed renewal.

  • Failed renewals are normal, because e-wallet balances run dry and cards expire, so a dunning sequence with retries and reminders is mandatory, not optional.
  • QRIS and unlinked e-wallet payments are one-time by design, so for those users you cannot silently re-bill: send a renewal notice with a fresh payment link each cycle, which can carry a QRIS code, since Bank Indonesia's remote QRIS lets a merchant share the code in an image or invoice.
  • Tax and invoicing matter: if you are a VAT-registered business (PKP), Indonesian VAT, known as PPN, is 12% on a DPP of 11/12 for non-luxury services, and tax invoices have run through Coretax DJP and its connected channels since 1 January 2025, so bake invoice numbering and tax fields into your billing records and issue a proper tax invoice per charge.

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.

FAQ

Frequently Asked Questions

Short answers to what readers ask most about this topic.

Why can't I just use Stripe-style card-on-file billing in Indonesia?

Credit card ownership is low in Indonesia, so a card-on-file default leaves out many of your potential subscribers. You need gateways that can link local instruments such as e-wallets for merchant-initiated charges, and a fallback flow for rails like QRIS that are one-time only.

Do Midtrans and Xendit support recurring subscriptions?

Yes, with limits. Midtrans's Subscription API charges a saved card or a linked GoPay account on a day, week or month interval, and card recurring needs extra bank approval. Xendit Subscriptions supports fixed-amount and usage-based plans, retries for failed payments and webhooks for each cycle, on any channel that supports merchant-initiated transactions.

Can I charge a QRIS payment automatically every month?

No. QRIS needs the customer to act in their own app for every payment, and neither Midtrans's Subscription API nor Xendit's QRIS channel supports merchant-initiated charges. For QRIS users, send a renewal notification with a fresh payment link or QRIS code every cycle instead of an automatic deduction.

What is dunning and why do I need it?

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.

Why do payment webhooks need to be idempotent?

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.