Xendit vs Midtrans: Which Payment Gateway to Pick in Indonesia

Photo by Wainuio via Wikimedia Commons (CC BY-SA 4.0)
For most core methods they are close, and QRIS is identical at 0.7% because Bank Indonesia regulates it. Virtual Account is around IDR 4,000 flat on both, and cards sit near 2.9% + IDR 2,000. The real cost differences show up in negotiated e-wallet rates and disbursement fees, so compare your own signed rate card rather than headline numbers.
Yes, but not with a native app hand-off. GoPay users on Xendit pay through universal QRIS by scanning a code. Midtrans, being part of the GoTo group, offers a native GoPay redirect that feels more seamless inside the GoPay app.
Midtrans has offered payouts through a separate product (Iris), but disbursement is not its core focus. If paying out to sellers, drivers, or freelancers is central to your product, Xendit's disbursement API — which reaches 450+ banks and e-wallets — is the stronger and more mature choice.
Xendit is widely preferred for a clean, consistent REST API and engineer-friendly docs with official SDKs. Midtrans is faster to ship if you use its Snap drop-in or an existing CMS/Laravel plugin, but its API surface is older and split across Core API, Snap, and Iris.
If you only collect payments and want to launch quickly on a CMS or framework with ready plugins, Midtrans is usually faster. If you build custom, care about API quality, or need to pay money out, choose Xendit. Many teams start on Midtrans and migrate to Xendit once disbursement or regional expansion becomes a requirement.

Photo by Wainuio via Wikimedia Commons (CC BY-SA 4.0)
Key Takeaway
Pick Midtrans if you mainly collect payments and want native GoPay plus ready CMS plugins; pick Xendit if you build custom on a clean API or need programmatic disbursement to pay out sellers, drivers, or refunds. Both cover Virtual Account, QRIS, e-wallets, cards, and retail at similar fees.
Every Indonesian app I ship eventually hits the same fork in the road: Xendit or Midtrans. They are the two payment gateways that actually matter here, and on paper they look almost identical — both do Virtual Account, QRIS, e-wallets, cards, and retail outlets. The real differences only show up once you are deep in the code, reading webhooks at 2am and reconciling settlements. This post is the comparison I wish I had before wiring up either one on a recent project for an Indonesian client.
I will keep this grounded in what a backend engineer cares about: which payment methods each one covers, how the fees actually stack up, what the developer experience and docs feel like, whether you can pay money out (disbursement), and finally a concrete rule for choosing between them for a startup versus an enterprise. Fees change often, so treat the exact numbers as a snapshot from the official pricing pages linked at the end and always re-check before you sign.
For the core Indonesian payment methods, coverage is close to a tie. Both gateways give you bank Virtual Accounts across BCA, BNI, BRI, Mandiri, Permata and CIMB, universal QRIS that any e-money app can scan, direct e-wallet channels, domestic and international cards, and retail over-the-counter payments at Alfamart and Indomaret. The one channel that genuinely differs is GoPay.
Both gateways charge zero setup and zero monthly fees — you only pay per successful transaction. The headline rates are close because a lot of Indonesian pricing is regulated or set by the underlying channel. QRIS, for example, is capped by Bank Indonesia at 0.7% for regular merchants, so it is the same everywhere. The table below is the snapshot I work from; the important nuance is VAT (PPN, currently 11%), which is added on top of most fees except QRIS, GoPay, and ShopeePay where it is already baked in.
| Payment method | Midtrans | Xendit |
|---|---|---|
| Virtual Account | ~ IDR 4,000 flat per transaction | Flat fee per transaction (see pricing page) |
| QRIS | 0.7% (BI-regulated) | 0.7% (BI-regulated) |
| E-wallet (OVO/DANA) | ~ 1.5% per transaction | ~ 3% and up, varies by merchant type |
| GoPay / ShopeePay | ~ 2% (native GoPay hand-off) | Via QRIS 0.7% or direct e-wallet rate |
| Cards (Visa/Mastercard) | 2.9% + IDR 2,000 | 2.9% + IDR 2,000 |
| Disbursement / payout | Limited / not a core product | Payout to 450+ banks and e-wallets, fee per payout |
Do not quote these rates to a client as final. E-wallet fees in particular are negotiated per merchant category and change with volume, and VAT is added on top of most of them. Always pull your own numbers from the live pricing page and, for real deals, from a signed rate card.
This is where Xendit earns its reputation. Its API is clean, REST-first, and consistent — you create an invoice or a specific payment-channel object, you get back a well-shaped JSON, and the webhook payloads are predictable. The docs read like they were written by engineers, with copy-paste examples and official SDKs for Node, PHP, Python, Java, and Go. If you are building a custom checkout from scratch, Xendit is simply the more pleasant place to live.
Midtrans is not bad — its Snap drop-in is genuinely fast to ship, and it has the widest set of ready-made CMS and framework plugins (WooCommerce, Laravel packages, and more). The trade-off is that the API surface feels older and more fragmented across its Core API, Snap, and Iris products, and the webhook signature verification trips up first-timers. Here is a minimal Snap token request to show the shape:
// Midtrans Snap: create a transaction token (Node.js)
const midtransClient = require('midtrans-client');
const snap = new midtransClient.Snap({
isProduction: false,
serverKey: process.env.MIDTRANS_SERVER_KEY,
});
const token = await snap.createTransaction({
transaction_details: {
order_id: 'ORDER-101',
gross_amount: 150000, // IDR
},
credit_card: { secure: true },
});
// token.redirect_url -> open Snap checkout# Xendit: create an invoice (any channel) via REST
curl https://api.xendit.co/v2/invoices \
-u xnd_production_XXXX: \
-H 'Content-Type: application/json' \
-d '{
"external_id": "order-101",
"amount": 150000,
"payer_email": "[email protected]",
"description": "Order 101"
}'
# Returns invoice_url + all enabled payment channelsWhichever you pick, always verify the webhook signature server-side and treat the webhook — not the browser redirect — as the source of truth for a paid order. Users close tabs; webhooks are how you actually learn a Virtual Account was funded.
This is the single biggest architectural difference. If your product needs to send money out — paying sellers on a marketplace, settling drivers, paying freelancers, or processing bulk refunds — Xendit treats disbursement as a first-class product with an API that pays out to 450+ banks and e-wallets, including single and batch payouts. Midtrans historically offered payouts through a separate product (Iris), but disbursement is not its center of gravity the way it is for Xendit. On a recent marketplace build I chose Xendit specifically because collecting and paying out through one provider kept reconciliation sane.
# Xendit: disburse money to a bank account
curl https://api.xendit.co/disbursements \
-u xnd_production_XXXX: \
-H 'Content-Type: application/json' \
-d '{
"external_id": "payout-seller-88",
"bank_code": "BCA",
"account_holder_name": "BUDI SANTOSO",
"account_number": "1234567890",
"amount": 500000
}'There is no universally better gateway — there is only the right fit for what you are building and how fast you need to move. Here is the decision rule I actually use.
My default: if I only collect payments, I reach for Midtrans first for speed. The moment the product needs to pay anyone out, I switch to Xendit without hesitation — one provider for both money-in and money-out is worth more than a small fee difference.