API and data model
Every gateway endpoint with its method, path and permission; the payment, payout, refund and webhook status values; the seven database tables; the twelve permission keys; and the three scheduled jobs.
Everything the addon exposes, in one place. Paths are as registered by the
router: a file named index serves its folder, a bracketed folder becomes a path
parameter, and any other filename becomes a path segment — which is why creating
a payment is POST /v1/payment/create and not POST /v1/payment.
Merchant API — API-key authenticated
Base: /api/gateway/v1. Authentication is X-API-Key. No session, no cookie.
Key permissions are the six values a merchant may put on a key:
payment.create, payment.read, payment.cancel, refund.create,
refund.read and the wildcard *. Anything else submitted is silently dropped,
and an empty result becomes ["*"].
Merchant dashboard — session authenticated
Base: /api/gateway. These are what the merchant's own screens call. They
require a logged-in user who owns the merchant record; there is no permission
key involved.
Checkout and public endpoints
The confirm endpoint takes a bare JSON array as its body — one object per
wallet the buyer is paying from, each with walletId, walletType, currency,
amount and equivalentInPaymentCurrency. The server recomputes the rate and
settles on its own figure; the buyer's equivalentInPaymentCurrency is only
checked for staleness against a 2% tolerance.
Admin API
Base: /api/admin/gateway. Session authenticated plus a permission key.
Identifier formats
All generated from a cryptographically secure RNG over a 62-character alphabet.
| Prefix | Length after prefix | Used for |
|---|---|---|
pi_ |
24 | Payment intent, public — it is in the checkout URL |
re_ |
24 | Refund |
po_ |
24 | Payout |
pk_live_ · pk_test_ |
48 | Public API key |
sk_live_ · sk_test_ |
48 | Secret API key |
evt_ + payment or refund id |
— | Webhook event id. Stable across retries |
API keys are stored as SHA-256 hashes with only the last four characters kept in clear. They cannot be recovered — only rotated.
Status values
Payment — PENDING · PROCESSING · COMPLETED · FAILED · CANCELLED ·
EXPIRED · REFUNDED · PARTIALLY_REFUNDED
Payout — PENDING · PROCESSING · COMPLETED · FAILED · CANCELLED.
A rejected payout is recorded as CANCELLED.
Refund — PENDING · COMPLETED · FAILED · CANCELLED. In practice a
refund is created already COMPLETED or the transaction rolls back.
Refund reason — REQUESTED_BY_CUSTOMER · DUPLICATE · FRAUDULENT ·
OTHER
Webhook delivery — PENDING · SENT · RETRYING · FAILED
Merchant status — PENDING · ACTIVE · SUSPENDED · REJECTED
Merchant verification — UNVERIFIED · PENDING · VERIFIED
Fee type — PERCENTAGE · FIXED · BOTH
Payout schedule — INSTANT · DAILY · WEEKLY · MONTHLY
Wallet type — FIAT · SPOT · ECO
Webhook events
Emitted: payment.created, payment.completed, payment.cancelled,
payment.expired, payment.failed, refund.completed.
Defined but never emitted: refund.created, refund.failed.
Headers: X-Gateway-Signature (sha256= + hex), X-Gateway-Timestamp (Unix
seconds), X-Gateway-Event, User-Agent: PaymentGateway-Webhook/1.0. The
signed string is <timestamp>.<raw body>, keyed with the merchant's
webhookSecret. Full recipe in Webhooks.
Database tables
Seven tables, all prefixed gateway_. They are auto-synced with the rest of the
schema; there is no separate migration step.
| Table | Holds |
|---|---|
gateway_merchant |
One row per merchant. Soft-deleted (paranoid). Unique on slug, apiKey and secretKey |
gateway_api_key |
Key hashes, mode, type, permissions, IP allowlist, last-used stamp |
gateway_payment |
Sessions and their outcome, including allocations (which wallets paid) and lineItems |
gateway_refund |
One row per refund, linked to a payment |
gateway_payout |
One row per proposed payout, with period, gross, fee, net and counts |
gateway_merchant_balance |
One row per merchant × currency × wallet type. pending is the source of truth |
gateway_webhook |
One row per delivery, with payload, signature sent, response status, body, timing and attempt count |
Money columns are DECIMAL(30,8). The driver returns decimals as strings —
anything reading these tables directly must parse before it does arithmetic.
Permission keys
| Key | Grants |
|---|---|
access.gateway.merchant |
Open the admin dashboard and merchant screen |
view.gateway.merchant |
Read merchants |
edit.gateway.merchant |
Edit, approve, suspend, verify |
delete.gateway.merchant |
Soft-delete a merchant |
create.gateway.merchant |
Seeded; no route uses it |
access.gateway.payment |
Open the payment screen |
view.gateway.payment |
Read payments |
manage.gateway.payment |
Issue an admin refund |
create.gateway.payment · delete.gateway.payment · edit.gateway.payment |
Seeded; no route uses them |
view.gateway.payout |
Read payouts |
edit.gateway.payout |
Approve and reject payouts |
create.gateway.payout · delete.gateway.payout |
Seeded; no route uses them |
view.gateway.refund |
The refund list endpoint |
access.gateway.settings |
Open the settings screen |
view.gateway.settings |
Read settings and the currency picker |
edit.gateway.settings |
The parallel settings endpoint the UI does not call |
Do not confuse these with access.deposit.gateway, view.deposit.gateway and
edit.deposit.gateway, which belong to the core fiat deposit providers.
Scheduled jobs
All three run in the cron worker, under the gateway category.
| Job | Interval | Behaviour |
|---|---|---|
processGatewayPayouts |
60 min | Proposes PENDING payouts for unclaimed balance. Skips the whole run when gatewayEnabled is off. Concurrency 3 |
processGatewayWebhookRetries |
60 s | Redelivers RETRYING rows past nextRetryAt, plus PENDING rows older than 2 minutes. Batch of 100 |
processGatewayPaymentExpiry |
5 min | Expires PENDING sessions past expiresAt and emits payment.expired. Batch of 500, with a 60-second wall-clock budget per run |
Environment
| Variable | Effect |
|---|---|
APP_PUBLIC_URL |
Origin used to build every checkout URL. Empty in .env.example; falls back to http://localhost:3000 |
APP_DEFAULT_LOCALE |
Locale segment in the checkout URL. Not in .env.example; defaults to en |
Where the fee goes
The platform fee is credited to the first Super Admin user's wallet, in the
same currency and wallet type the buyer paid in, as a PLATFORM_FEE operation
with reference <paymentId>_fee. An adminProfit row is written with type
GATEWAY_PAYMENT. If no Super Admin role or user exists, the fee collection
logs a warning and is skipped — the payment still completes.