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 |
|---|---|---|
| Xendit | https://yourdomain/webhooks/xendit |
you, by hand, once |
| PayPal | (none) | not needed — it completes on the return |
PayPal completes when the buyer comes back, so it needs no webhook at all. Xendit is the one that needs you — see below.
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
There are two ways to set them. Both keep the secret out of sight — neither ever shows a saved value.
From the admin page (recommended)
Admin → Settings → Payment gateway credentials. Enter the PayPal Client ID + Secret (and the
sandbox/live switch) and the Xendit Secret Key + Callback token. They are stored encrypted on the
server; the fields are write-only, so once saved they are never displayed again — the page shows only
Configured / Not set. Leave a field blank to keep it unchanged; use Remove stored credentials to
clear one.
One-time setup: generate the key that encrypts them, kept separate from the app key so that a
database dump and an .env leak are each useless on their own:
php artisan store:secrets-key # writes STORE_SECRETS_KEY to .env — then BACK IT UP off the server
php artisan config:clear
Losing STORE_SECRETS_KEY makes stored credentials unreadable (you would just re-enter them).
Or from .env (alternative)
If you prefer, set them in .env on the server instead, then restart. Admin-page values take precedence
when both are present.
# Xendit
XENDIT_SECRET_KEY=...
XENDIT_CALLBACK_TOKEN=... # from Settings → Developers → Webhooks
# PayPal
PAYPAL_CLIENT_ID=...
PAYPAL_SECRET=...
PAYPAL_ENV=sandbox # or: live
A gateway with no credentials is simply switched off — there is nothing else to enable.
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.