Skip to content

Routing

When you call POST /v1/payments, the gateway chooses which PSP to route to using three inputs from the request: currency, method, and the calling app’s mode.

  1. Method override (absolute) — If method is one of the registered overrides, the gateway routes directly there regardless of currency or app allowlist:
    • method=polar → Polar.sh
    • method=paypal → PayPal
  2. IDR + method match — If currency=IDR, the gateway walks an ordered preference list for that method and returns the first PSP that:
    • is in the app’s allowed_providers allowlist (or the allowlist is empty), AND
    • has a driver registered for the calling app’s mode.
  3. Non-IDR, no override — Falls through to the configured global PSP (Paddle today, when wired). Gated by allowlist.
  4. No match422 no provider supports method=… for currency=… (production) or fall through to mock (when MOCK_NONIDR_FALLBACK=1 in dev).

Today the gateway has only NICEPAY in the IDR list (DOKU has been removed):

MethodPreference order
qrisNICEPAY
va_bcaNICEPAY
va_mandiriNICEPAY
va_bniNICEPAY
va_briNICEPAY
va_permataNICEPAY

If a future direct-PJP integration lands (e.g. a BCA QRIS direct merchant), the table extends naturally — qris would prefer the direct rail first, fall back to NICEPAY for unsupported wallets. Products keep using method=qris; the gateway picks.

The IDR walk skips PSPs that don’t have a driver registered for the calling app’s mode. Practical example:

  • App is mode=sandbox
  • DOKU is registered only for live (no sandbox creds in env)
  • NICEPAY is registered only for sandbox
  • Routing for (IDR, qris, sandbox) returns NICEPAY (skipping DOKU)

This means “what PSP serves your request” depends on (1) the routing table, (2) the app’s allowlist, and (3) which credential sets the operator has populated for which PSP.

Each app has an allowed_providers text array. Empty = “all registered providers allowed” (the multi-tenant default). Non-empty restricts the app to listed providers only.

Useful for narrowing a product to a single PSP:

update apps set allowed_providers = '{nicepay}' where name = 'posz-live';

posz-live will now reject any request that would route to a non-NICEPAY rail (422 provider X not allowed for this app).

The allowed_methods JSONB is finer-grained: {"nicepay": ["qris", "va_bca"]} lets the app use NICEPAY for QRIS and BCA VA only. Empty = no restriction.

Useful when one product should only do QRIS and another only does VA, both on the same NICEPAY merchant.

The gateway has a routing-preview endpoint for debugging without creating a payment:

Terminal window
GET /v1/routing/preview?currency=IDR&method=qris
X-PAY-* signed

Returns the PSP that would be picked + which check filtered out alternatives.

method=polar and method=paypal ignore the per-app allowlist by design. They’re absolute escape hatches — useful when a product has explicit reason to force a rail (e.g., USD product that wants to bill via PayPal even though Polar is the default for non-IDR).

If you don’t want a product to be able to use a method override, just don’t tell its team about it. There’s no per-app blocklist for overrides today.