# Integrating Midtrans Snap Payments into a Next.js App

> A hands-on guide to wiring Midtrans Snap into Next.js: server-side token creation, the Snap popup, and a webhook that verifies the SHA512 signature key.

URL: https://www.matthewswong.com/en/blog/midtrans-payment-gateway-integration-nextjs/

- Author: Matthews Wong (https://www.matthewswong.com/en/about/)
- Published: 2026-07-14
- Updated: 2026-10-17
- Category: Backend

> **Key takeaway:** Midtrans Snap needs three moving parts: a server route that trades your Server Key for a Snap token, the client-side snap.js popup that opens with that token, and a webhook that verifies every payment by recomputing SHA512 of order_id, status_code, gross_amount and your Server Key. Never trust the browser redirect alone.

Midtrans is an Indonesian payment gateway, and Snap is its drop-in checkout: one popup that covers bank-transfer Virtual Accounts (BCA, BNI, BRI, Permata and more) and Mandiri Bill Payment, QRIS, the big e-wallets (GoPay, OVO, DANA, ShopeePay), and cards. On a recent project for an Indonesian client I wired Snap into a Next.js 15 App Router build, and the flow is cleaner than the old docs make it look, as long as you keep one rule in mind: the money is only real when the webhook says so.

This walkthrough covers the full round trip: creating a transaction token from a server route, opening the Snap popup on the client, receiving the HTTP notification, verifying its signature, and reading the settlement status correctly. The examples use the App Router and plain fetch so you can drop them into any Next.js codebase without the official SDK if you prefer.

## The three keys and two environments

Midtrans gives you a Server Key and a Client Key, and both come in a sandbox pair and a production pair. The Server Key must live only on the server; it is the secret that both authorizes token creation and signs webhook notifications. The Client Key is safe to expose in the browser because it only lets snap.js render the popup. Getting this split wrong is the single most common security mistake I see in Indonesian Midtrans tutorials.

> Sandbox and production are separate environments with separate Server and Client Keys: pick the environment in the dashboard's top-left dropdown, then read the pair under Settings, Access Keys. Midtrans's own sandbox examples use keys starting SB-Mid-, but trust the dashboard rather than the prefix. The snap.js and API hosts differ too (app.sandbox.midtrans.com versus app.midtrans.com), so a mismatched environment is the first thing to check when the popup silently fails.

## Step 1 — Create the Snap token server-side

The backend job is to POST your order details to the Snap transactions endpoint using HTTP Basic Auth, where the username is your Server Key and the password is empty. Midtrans returns a token that the client uses to open the popup. Build the order_id yourself and make it unique and idempotent per order, because you will match on it later in the webhook.

> gross_amount is typed as an integer in whole Rupiah, and Midtrans also says not to put decimals in item prices. If you send item_details, price times quantity across all items must equal gross_amount exactly. Notifications return it as a string such as 10000.00, so hash that string exactly as received.

## Step 2 — Open the Snap popup on the client

On the client you load snap.js once with your Client Key, then call window.snap.pay(token) with the token your route returned. In Next.js the clean way to inject the script is next/script with the correct sandbox or production URL. The popup handles every payment method Midtrans has enabled for your account, so you do not build any card or Virtual Account UI yourself.

> The onSuccess callback fires in the browser and is great for UX, but it is not proof of payment. A user can close the tab, lose signal, or a Virtual Account can be paid hours later. Use these callbacks only to update the screen. The source of truth is always the server-side webhook in Step 3.

## Step 3 — Handle and verify the webhook

Midtrans sends an HTTP notification to a URL you register in the dashboard whenever a transaction changes state. This is the only reliable signal that money moved. Every notification carries a signature_key, and you must recompute it yourself: SHA512 of order_id, status_code, gross_amount and your Server Key concatenated. If your hash does not match, discard the request — it is forged or corrupted. Only after the signature checks out do you read the status.

> Return a 2xx fast and do the heavy work asynchronously. Midtrans waits up to 15 seconds, asks you to answer within about 5, and retries a failed delivery up to five times over several hours (fewer times for 500, 503, 400 and 404). It also warns that the same notification may, rarely, arrive twice, so key your order-update logic on order_id and a retried settlement never double-ships an order or double-credits a wallet.

## Reading transaction statuses correctly

The field that matters is transaction_status, paired with fraud_status for card payments. A common bug is treating pending as success — for a Virtual Account or QRIS, pending simply means the payment instruction was created and the customer has not paid yet. Only settlement (or capture with an accept fraud status for cards) means the funds are confirmed. One documented exception: for Permata VA, Mandiri Bill Payment and Indomaret, a settlement can rarely reverse to deny, usually within minutes, and Midtrans says to treat it as unpaid. When a notification is late or out of order, ask the GET Status API.

| transaction_status | What it means | Action |
| --- | --- | --- |
| pending | VA/QRIS created, customer has not paid | Wait for the next notification |
| settlement | Funds confirmed by bank/provider | Fulfil the order |
| capture | Card authorized; check fraud_status | Fulfil only if fraud_status is accept |
| deny | Rejected by the provider or Midtrans fraud detection | Mark order failed |
| expire | Not paid within the time limit | Release stock, mark failed |
| cancel / refund | Cancelled or money returned | Reverse fulfilment |

## Testing in sandbox before going live

- Use Midtrans's web-based sandbox simulator (simulator.sandbox.midtrans.com) to pay a test Virtual Account or QRIS code without real money, use the documented test cards for accepted, challenged and denied card payments, and set a short custom expiry to watch an expire notification arrive.
- Expose your local webhook with a tunnel (for example ngrok) and register that URL in Settings, Configuration, Payment Notification URL.
- Test the signature check by sending a fake notification with a wrong signature_key and confirm your route returns 403.
- When switching to production, swap both keys, flip the environment flag, and re-register the production notification URL — they do not carry over from sandbox.

## Sources

- Midtrans Docs — Backend Integration (Snap token)
- Midtrans Docs — HTTP(S) Notification / Webhooks
- Midtrans Docs — Transaction Status Cycle

## Source links

- https://docs.midtrans.com/reference/backend-integration
- https://docs.midtrans.com/docs/https-notification-webhooks
- https://docs.midtrans.com/docs/transaction-status-cycle

## FAQ

### Is the Midtrans Client Key safe to expose in the browser?

Yes. The Client Key is designed to be public and is only used by snap.js to render the payment popup. The Server Key is the secret one — it authorizes token creation and signs webhooks, so it must stay server-side and never be prefixed with NEXT_PUBLIC_.

### Why should I not trust the Snap onSuccess callback as proof of payment?

The onSuccess callback runs in the user's browser and can be missed if they close the tab or lose connection. For Virtual Account and QRIS, payment may complete hours later. The only reliable confirmation is the server-side HTTP notification, whose signature you verify before marking an order paid.

### How do I verify the Midtrans webhook signature_key?

Concatenate order_id, status_code, gross_amount and your Server Key into one string, then compute its SHA512 hash as hex. Compare that to the signature_key in the notification body. If they do not match, reject the request with a 403 because it is forged or corrupted.

### What is the difference between pending and settlement in Midtrans?

Pending means the payment instruction (a Virtual Account number or QRIS code) was created but the customer has not paid yet. Settlement means the bank or provider has confirmed the funds. Only fulfil an order on settlement, or on capture with fraud_status accept for card payments.

### What are the sandbox and production endpoints for Midtrans Snap?

Sandbox creates tokens at app.sandbox.midtrans.com/snap/v1/transactions and loads app.sandbox.midtrans.com/snap/snap.js. Production uses app.midtrans.com for both. The two environments have separate keys and dashboards, and a sandbox token will not open in the production popup.

---

- HTML version: https://www.matthewswong.com/en/blog/midtrans-payment-gateway-integration-nextjs/
- Bahasa Indonesia: https://www.matthewswong.com/id/blog/midtrans-payment-gateway-integration-nextjs/
- Site index for agents: https://www.matthewswong.com/llms.txt
- Sitemap: https://www.matthewswong.com/sitemap.xml
- API description (OpenAPI): https://www.matthewswong.com/openapi.json
