Connecting a fiat deposit gateway
How to take a customer's first card or bank deposit — the readiness console, the sixteen bundled gateways, the credential test, the webhook URL the vendor needs, and why a saved key does nothing until you restart.
Nothing on a fresh install can take money. Sixteen fiat deposit gateways are seeded into the database, every one of them switched off, and none of them holds a credential. This page is how you turn one of them into a working payment path.
The screen is Admin → Finance → Payment Systems → Gateways
(/admin/finance/deposit/gateway), and the first thing to understand about it
is that it is not a CRUD table.
The readiness console
There is no "create gateway" button and its absence is not a missing
permission. The set of gateways is fixed by what is bundled under
api/finance/deposit/fiat/<alias> in the backend — sixteen integrations, no
more — and the seeder writes one row per integration. You enable, configure and
price them; you do not invent them.
So the page is a gallery of cards, one per gateway, and each card answers the
question a table could not: can this gateway take a payment right now? A
row's status column answers "is it switched on", which is a different
question and the reason a list of sixteen green switches could belong to an
install that cannot process a single deposit.
Each card reports four facts, all of them computed rather than stored:
| Fact | What it means |
|---|---|
supported |
An integration for this alias is bundled in this build |
credentialsComplete |
Every required environment variable is present |
missingRequired |
The exact variable names that are unset |
mode |
test, live or unknown — inferred, not configured |
The header rail counts Gateways, Accepting deposits (switched on and able to authenticate), Need credentials, and Currencies covered. Above the grid, a destructive banner appears when any gateway is switched on with credentials missing — "N gateways are switched on and cannot authenticate" — with a button that filters straight to them. The filters are All, Accepting deposits, Needs attention, Switched off and No integration.
The status toggle is deliberately not blocked when credentials are missing — you may be staging a change. But a gateway in that state is offered to customers on the deposit form and fails at the vendor on every attempt, with a generic error. The toggle raises a destructive toast naming how many variables are unset the moment you do it.
status: true and all three readiness checks failing is a real, reachable
state. On a freshly seeded install every one of the sixteen is status: false
with zero credentials set, so switching one on before setting its variables
produces exactly that.
The sixteen bundled profiles, by alias:
stripe · paypal · paystack · mollie · adyen · klarna ·
authorizenet · dlocal · transfi · eway · paysafe · payu · paytm ·
payfast · ipay88 · 2checkout
They are defined in backend/src/utils/deposit-gateway/registry.ts, which
carries — per gateway — the environment variables it reads, the URLs it builds,
the vendor dashboard pages you have to visit, the order to do them in, and the
traps that make a wrong setup look like a right one. Everything the admin
screens show about a gateway comes from there.
Credentials live in .env, not in the database
This is the single fact that costs operators the most time.
A deposit_gateway row holds the title, logo, fees, limits, currency list and
the status switch. It holds no credentials. Every key, secret, merchant id
and sandbox flag is read from the backend process's environment — which is
loaded from .env at the repository root, once, when the backend starts.
Editing .env while the platform is running changes nothing. The running
process still holds the values it read at boot, so the gateway carries on
failing exactly as it did before and the readiness card carries on reporting the
variable as unset.
Restart with pnpm restart from the project root after every credential change.
For PayPal specifically, NEXT_PUBLIC_APP_PAYPAL_CLIENT_ID is inlined into the
frontend bundle at build time — that one needs a rebuild, not a restart.
Nothing sends a credential value back to the browser. The readiness endpoint
returns a boolean for "is it set", plus at most a leading key-type marker
(sk_live, pk_test, AQE) for secrets, and the full value only for things
that are public by construction: URL paths, true/false flags, and publishable
keys the frontend already ships.
The per-gateway screen
Open a card and you land on /admin/finance/deposit/gateway/[id]. The left
column is the setup guide, in a deliberate order — verdict, then the test that
can change the verdict, then the values the test needs, then the URLs that have
to leave the page, then the walkthrough. The right column is the row itself:
title, description, image, currencies, and the fee and limit fields
(fixedFee, percentageFee, minAmount, maxAmount), which can be set once
for all currencies or overridden per currency.
The four panels that matter, in the order they appear:
-
Connection — the verdict, and the Test button that can change it.
-
Environment Variables — every variable this gateway reads, split into required and optional, each with whether it is set and what it is for. It also builds a paste-ready
.envblock containing the keys you still need. -
URLs to give the vendor — the webhook and return URLs, derived from your public site URL. Copy buttons, because these have to leave the page and be pasted into a vendor dashboard.
-
Set up <vendor> — the ordered walkthrough for this specific gateway, several of whose steps depend on the one before.
Below those sit What goes wrong with this one — the vendor-specific traps — and a reference panel with regions, settlement, pricing and links.
The Test button
Testing before saving is the point. Every one of these vendors fails a bad credential somewhere you are not looking — at a customer's checkout, or in PayPal's case not at all until the buttons silently fail to render. The only moment a wrong key is cheap to find is while it is still on your clipboard.
Type candidate values into the Connection panel and press Test. Those values
overlay the configured environment for the duration of one request and are
then discarded. Nothing is written to .env, nothing is written to the database,
no payment or order is created, and no credential value is ever written to the
log — only the variable names you supplied. Only keys the gateway's own
profile declares are accepted; anything else in the body is dropped.
The answer is one of four verdicts:
| Verdict | Meaning |
|---|---|
valid |
The vendor accepted the credential on a read-only call |
invalid |
The vendor explicitly rejected it — a 401/403, or an authentication error in the body |
unknown |
The check could not be completed: a timeout, a 5xx, no outbound network, an unexpected response shape |
unsupported |
This vendor exposes no read-only authenticated endpoint, so presence and shape were checked instead |
The distinction is there on purpose. On several of these vendors, regenerating a
key immediately invalidates the one in production — so reporting "the vendor was
down" as "your key is wrong" sends you to break a working integration. Only an
explicit rejection is reported as invalid.
Six of the sixteen are format-only — paysafe, payu, paytm,
payfast, ipay88 and 2checkout — because they authenticate per-payment
signatures and have no endpoint that can be read without creating something.
For those, Test proves the values are present and well formed; a sandbox
deposit is what proves them correct.
The test is deliberately kept off the audit trail: it tests values you have typed but not saved, so there is no record for an audit entry to point at.
The webhook URL
For the gateways that have one, the address the vendor must call back is:
https://your-site.example/api/finance/deposit/fiat/<alias>/webhookIt is built from the gateway's alias, and the URLs panel shows the
absolute form for your install.
The registry is keyed on alias because id cannot be joined against anything
— the seeder gives some rows a literal id (stripe, mollie) and mints a UUID
for others. alias is what the deposit form puts in the request path and what
the integration folder is named, so it is the only stable link between a
database row and its handler.
The origin comes from APP_PUBLIC_URL, falling back to NEXT_PUBLIC_SITE_URL,
then FRONTEND_URL, then http://localhost:3000. If the URLs on this page look
wrong, that chain is what to fix — not the vendor dashboard.
Some gateways expose an override variable (APP_PAYSTACK_WEBHOOK_ENDPOINT,
APP_MOLLIE_WEBHOOK_ENDPOINT and similar). When one is set, the page shows the
override, because that is the URL the vendor will actually be given.
Stripe, PayPal and eWAY have no webhook route at all. They confirm the payment by reading it back from the vendor when the customer returns, so there is nothing to configure on their side and the panel says so rather than showing an empty field.
Where a gateway has a webhook, it is what credits the wallet independently of the customer's browser. Several vendors keep a separate webhook URL per mode — Paystack is the notable one — so going live with only the test webhook configured means every real payment succeeds at the vendor and is never credited here.
Adyen is worse in one respect: without APP_ADYEN_HMAC_KEY set, notifications
arrive, fail verification and are dropped silently. Set the HMAC key at the same
time as the webhook, never later.
Test mode is inferred, not switched
There is no global "test mode" switch for deposits. Each gateway's mode is worked out from evidence, in this order:
-
No credentials at all — reported as
unknown. A flag or a URL path is configuration, not a credential, so neither counts as evidence. -
An explicit base-URL override — where the integration has one (
APP_TRANSFI_BASE_URL), a host containingsandbox,testorstagingmeans test. It beats the sandbox flag, because that is the precedence the integration itself uses. -
The sandbox flag, where the integration has one — seven do:
APP_PAYSTACK_SANDBOX,APP_TRANSFI_SANDBOX,APP_PAYSAFE_SANDBOX,APP_PAYU_SANDBOX,APP_PAYTM_SANDBOX,APP_PAYFAST_SANDBOX, andAPP_ADYEN_ENVIRONMENT. -
The key prefix —
sk_test_/sk_live_,test_/live_. This is what decides for Stripe and Mollie, which have no flag at all.
So a Stripe install pointed at sk_test_ charges nothing and declines real
customers' cards with no useful message, and the only thing that tells you is
the prefix. Check it after every deployment.
Getting one live
The specifics differ per vendor and the per-gateway screen carries them. The shape is always this:
-
Choose a gateway your customers can actually pay with. Regions and settlement are on each card's reference panel — Paystack for West Africa, PayU or Paytm for India, Mollie for the EEA, dLocal for Latin America.
-
Get the credentials from the vendor, starting with their test or sandbox pair. Several vendors — dLocal, TransFi, Paysafe, Klarna — onboard by contract rather than self-service, so start that conversation early.
-
Write them into
.envon the server. The Environment Variables panel gives you a paste-ready block containing exactly the keys still missing. -
Give the vendor the webhook URL from the URLs panel, on the correct mode tab, and copy back any signing secret it hands you.
-
Restart the backend —
pnpm restartfrom the project root. Nothing you wrote in step 3 exists to the platform until you do. -
Press Test credentials on the Connection panel and read the verdict.
-
Switch the gateway on with the toggle, and set its currency list, fees and limits on the right-hand column. A currency you add here is not enabled at the vendor — the list can only narrow what they already accept.
-
Make one real test deposit and confirm it lands in the wallet. This is the only step that proves the webhook, and no amount of credential testing substitutes for it.
-
Swap in the live credentials, update the live webhook, and restart again.
Related
- Environment variables — the full
.envreference, including every gateway variable and the two rules that decide whether an edit takes effect. - Settings reference — the Deposits master switch under Wallet, which refuses deposits platform-wide regardless of gateway state.
- The admin panel — where the deposit queue and the manual deposit methods sit.