Supported methods
- QRIS (all Indonesian banks + e-wallets via one QR)
- GoPay, OVO, DANA, ShopeePay, LinkAja
- Bank transfer (BCA, Mandiri, BNI, BRI, Permata virtual accounts)
- Credit & debit card (Visa, Mastercard, JCB)
- Akulaku PayLater
- Indomaret / Alfamart cash counter
How a checkout works
POST /billing/checkout { "plan": "GROWTH" }
→ { snapToken, snapRedirectUrl }window.snap.pay(snapToken, {
onSuccess: () => refresh(),
onPending: () => showPendingBanner(),
onError: () => showError(),
});The actual plan flip happens server-side when Midtrans calls our webhook with transaction_status: settlement. Never trust client-side success callbacks for billing state — always re-fetch /billing/plan after the callback fires.
Webhook
POST /billing/midtrans/notification Headers: signature_key: SHA-512(orderId + statusCode + grossAmount + serverKey)
On valid signature + settlement / capture (with fraud accept), we upsert the Subscription row to the purchased plan and stamp the BillingTransaction as PAID. Failed / cancelled / expired payments mark the transaction FAILED without touching the subscription.
Mock mode (dev)
When MIDTRANS_SERVER_KEY is unset the wrapper runs in mock mode — /billing/checkout still returns a token, the UI shows a "Simulate payment success" button, and the same webhook code path applies the upgrade. Use this for UAT.