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.

11 min readUpdated 6 August 2026deposits, payments, gateways, stripe, webhooks

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:

  1. Connection — the verdict, and the Test button that can change it.

  2. 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 .env block containing the keys you still need.

  3. 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.

  4. 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

Tests a gateway's credentials against the vendor

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-onlypaysafe, 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>/webhook

It 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:

  1. No credentials at all — reported as unknown. A flag or a URL path is configuration, not a credential, so neither counts as evidence.

  2. An explicit base-URL override — where the integration has one (APP_TRANSFI_BASE_URL), a host containing sandbox, test or staging means test. It beats the sandbox flag, because that is the precedence the integration itself uses.

  3. 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, and APP_ADYEN_ENVIRONMENT.

  4. The key prefixsk_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:

  1. 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.

  2. 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.

  3. Write them into .env on the server. The Environment Variables panel gives you a paste-ready block containing exactly the keys still missing.

  4. Give the vendor the webhook URL from the URLs panel, on the correct mode tab, and copy back any signing secret it hands you.

  5. Restart the backendpnpm restart from the project root. Nothing you wrote in step 3 exists to the platform until you do.

  6. Press Test credentials on the Connection panel and read the verdict.

  7. 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.

  8. 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.

  9. Swap in the live credentials, update the live webhook, and restart again.

  • Environment variables — the full .env reference, 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.