All articles

Integrating a Payment Gateway Like Midtrans or Xendit Safely

How to integrate an Indonesian payment gateway such as Midtrans or Xendit: choosing payment methods, hosted checkout versus direct API, verifying webhooks, handling duplicate notifications, order status design, expiry, refunds, testing in sandbox, and daily reconciliation.

Software Development|Published |10 min read
A customer paying with a phone at a shop counter

Adding online payments looks simple in a demo: call the API, show the payment page, done. The problems show up later in production. A customer pays but the order stays unpaid, an order is marked paid twice, a notification arrives before the order is saved, or finance cannot match the gateway report with the orders in the system. Most of these issues come from treating payment as a single request instead of a process with several steps that can arrive late, twice, or out of order.

Choose the Payment Methods Your Customers Use

MethodFitsNotes
Virtual accountB2B invoices and larger amounts paid by bank transferEach order gets its own account number, so payments are matched automatically
QRISRetail and small amounts, paid from any bank or e-wallet appOne standard QR code works across many apps
E-wallet such as GoPay, OVO, DANA, or ShopeePayConsumer apps and mobile checkoutUsually redirects to or opens the e-wallet app to confirm
Credit and debit cardInternational customers and subscriptionsCard data should stay with the gateway, with 3D Secure for verification
Retail outletsCustomers who prefer to pay cash at a convenience storePayment can happen hours later, so expiry times matter

Fees differ per method and per gateway, and they change, so compare the current price lists for the methods you plan to offer. Offering every method is not always better. A short list of the methods your customers actually use keeps checkout simple and reconciliation easier.

Hosted Checkout or Direct API

Gateways offer a hosted payment page or popup, such as Midtrans Snap or Xendit Invoice, and a direct API where you build the payment screens yourself. The hosted option is faster to build, handles each method's flow for you, and keeps card data off your servers, which greatly reduces PCI DSS scope. The direct API gives full control over the look and flow, but you take on more screens, more edge cases, and more testing. For most business applications, start with the hosted checkout and move specific methods to the direct API only when there is a clear reason.

The Webhook Is the Source of Truth

After paying, the customer is redirected back to your site. That redirect is only for the user experience. The customer can close the browser before it happens, and the URL can be opened by anyone, so it must never mark an order as paid. The payment status comes from the gateway's server-to-server notification, the webhook, which your backend receives and verifies.

  • Verify every notification. Midtrans includes a signature key built from the order ID, status code, gross amount, and your server key, which you recompute and compare. Xendit sends a callback verification token in a request header that you compare with the token in your dashboard.
  • For extra certainty, call the gateway's status API with the transaction ID before updating the order.
  • Check that the amount and currency in the notification match the order in your database.
  • Respond quickly with a success status, and move slow work such as sending emails or creating shipments to a background job.
  • Log every notification you receive, including the raw body, so disputes and bugs can be investigated later.

Handle Duplicates and Out-of-Order Notifications

Gateways retry notifications when your server does not answer in time, and the same payment can produce several notifications as its status changes. Your handler must be idempotent: processing the same notification twice must not ship the order twice or add credit twice. Store the gateway transaction ID with a unique constraint, update the order inside a database transaction, and only allow valid status changes. A paid order should not move back to pending because an older notification arrived late.

Design the Order Status Carefully

Order statusMeaningAllowed next status
Awaiting paymentPayment created and shown to the customerPaid, expired, or cancelled
PaidVerified settlement from the gatewayProcessing, refunded, or partially refunded
ExpiredThe payment window closed without paymentA new payment attempt can be created
CancelledCancelled by the customer or the system before paymentNone
RefundedMoney returned to the customerNone

Keep the gateway's own transaction statuses in a separate table and map them to your order status in one place. Set an expiry time that suits each method, and release reserved stock when a payment expires. For card payments, also handle fraud review statuses, where the gateway holds a payment for checking before it is final.

Test Thoroughly in Sandbox

  1. 1Use the sandbox keys and simulate each payment method you offer, including success, failure, and expiry.
  2. 2Send the same notification twice and confirm the order is processed only once.
  3. 3Send a notification with a wrong signature or token and confirm it is rejected.
  4. 4Make the webhook endpoint fail temporarily and confirm the order is updated correctly when the gateway retries.
  5. 5Test a refund and a partial refund, and check what customers and finance see.
  6. 6Keep sandbox and production keys in separate environment variables, and never commit either to Git.

Reconcile Every Day

Even with a correct integration, mismatches happen: a webhook that failed for hours, a manual refund in the dashboard, or a settlement that arrives later than expected. A daily job that compares the gateway's transaction report with your orders catches these before customers complain. Report paid transactions without a paid order, paid orders without a settled transaction, and amount differences. Finance teams also need settlement reports that show gross amount, fees, and the net amount transferred to the bank.

Add an admin screen that shows the full notification history for one order. When a customer says they paid but the order is still pending, support can answer in a minute instead of asking a developer to search the logs.

Key takeaways

  • Offer the payment methods your customers actually use, and compare current fees per method.
  • Start with the gateway's hosted checkout to keep card data off your servers and reduce integration work.
  • Treat the verified webhook as the only source of payment status, never the browser redirect.
  • Make notification handling idempotent, check amounts, and allow only valid order status changes.
  • Test duplicates, bad signatures, retries, and refunds in sandbox, and reconcile transactions every day.

Related articles

More articles on software development, AI, cloud, and infrastructure.

A desktop screen showing landing page designs next to a tablet and a phone
Web Development|

How to Build a Company Profile Website That Brings in Leads

What a company profile website needs to bring in enquiries: clear service pages, proof such as case studies and client logos, easy contact options, fast loading on mobile, SEO basics, the right platform, and tracking that shows which pages produce leads.

A green speech bubble cut from paper on a yellow background
AI Development|

WhatsApp Business API for Companies: How It Works and How to Start

What the WhatsApp Business Platform offers compared with the WhatsApp Business app, how the 24-hour customer service window and message templates work, going through Meta or a provider, number setup, integrating with your systems, adding a chatbot, and why unofficial tools are risky.

Looking for a software development partner?

Tell us about your project, what you need to build, and the challenges you are facing. We can discuss the technical approach, scope, timeline, and estimated cost.

Start a conversation