Docs

Payments · Midtrans

QRIS, e-wallets, virtual accounts, cards, and PayLater — all through Midtrans Snap.

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

server side
POST /billing/checkout  { "plan": "GROWTH" }
→ { snapToken, snapRedirectUrl }
client side
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.