Payments setup — and the one step that silently loses money
Payment credentials are server-side only: they live in .env on your machine and never in the
database, never in a form, never in this admin. That is deliberate. What follows is where each value
goes — and the one step that is easy to miss and expensive to miss.
The step that costs money if you skip it
Most Indonesian payment methods are asynchronous. A buyer picks a virtual account, an e-wallet, or pays at a retail outlet, and then closes the tab. They may finish paying an hour later, from their banking app, on a different device. Nothing about that ever returns to your website.
So the purchase is completed by the provider calling your server — a webhook — and by nothing else.
If that call never arrives:
- the buyer's money reaches you,
- their licence is never issued,
- the order sits at "pending",
- and nothing anywhere says why.
This is not a rare edge case. On the Indonesian methods it is the normal path.
Where each provider gets its URL
| Provider | Webhook URL | Who sets it |
|---|---|---|
| Midtrans | https://yourdomain/webhooks/midtrans |
automatic — sent with every transaction |
| Xendit | https://yourdomain/webhooks/xendit |
you, by hand, once |
| PayPal | (none) | not needed — it completes on the return |
Midtrans is handled for you: each transaction carries its own notification URL, so there is nothing to configure. PayPal completes when the buyer comes back, so it needs no webhook at all.
Xendit is the one that needs you. Its "invoice paid" callback is account-wide, not per-invoice, so it is set once in Xendit's own dashboard.
Xendit, click by click
- Sign in at dashboard.xendit.co.
- Settings → Developers → Webhooks.
- Under Invoices paid, paste the URL your admin page shows for Xendit
(
https://yourdomain/webhooks/xendit). - Save, and copy the webhook verification token shown on the same page.
- Put that token in your server's
.envasXENDIT_CALLBACK_TOKEN, alongsideXENDIT_SECRET_KEY. - Restart the site so the new values are read.
Both halves are required. The URL without the token means deliveries arrive and are refused; the token without the URL means nothing arrives at all.
Proving it actually works
Do not trust "I pasted it". Open Admin → Settings → Payment webhooks. It reports what has genuinely reached your server:
| What it says | What it means | What to do |
|---|---|---|
| Never received | Nothing has ever arrived from this provider | The URL is missing or wrong in the provider's dashboard |
| Not matching | Deliveries arrive but name no order of yours | The URL points at you from a different provider account than the store uses |
| Working | At least one delivery matched a real order | Nothing — it is wired up |
The counters only move for calls that carry the provider's own credentials, so a random probe from the internet can never make an unconfigured gateway look healthy.
Test it end to end before you take real money. Xendit's dashboard has a "test webhook" button on the
same page; use it, then reload the admin card. If it still says Never received, the URL is wrong —
check for a typo, a missing https://, or a trailing slash.
The credentials themselves
All of these go in .env on the server, then restart:
# Midtrans
MIDTRANS_ENV=sandbox # or: production
MIDTRANS_SERVER_KEY=...
MIDTRANS_CLIENT_KEY=...
# Xendit
XENDIT_SECRET_KEY=...
XENDIT_CALLBACK_TOKEN=... # from Settings → Developers → Webhooks
# PayPal
PAYPAL_CLIENT_ID=...
PAYPAL_SECRET=...
A gateway with no key is simply switched off — there is nothing else to enable. Admin → Settings → Server-side secrets shows which are configured, without ever showing a value.
If your server is behind a proxy or firewall
The webhook path is a public, server-to-server URL. It has no login, by necessity — the provider cannot log in as you. It is protected instead by the provider's own signature or token, an authoritative re-read of the payment status directly from the provider, and a rate limit.
That means POST /webhooks/* must be reachable from the public internet. If you block it, or put the
whole site behind an IP allow-list, asynchronous payments stop being fulfilled — with exactly the same
silent symptom as never having configured the URL.