Wallet
Panel Guide
Programs

Panel — Programs

Who calls these endpoints?Merchant panel
Base path
/api/panel/v1/wallet
Identity
Panel session (JWT) — Authorization: Bearer <panel access token>. The token comes from panel login; an s2s signing key does NOT open these endpoints.
Authorization
RBAC permission: wallet:program:read · wallet:program:manage · wallet:limit:manage · wallet:sponsor-policy:manage · wallet:sponsor-policy:approveFour-eyes: the requester and the approver cannot be the same person — this is enforced by identity comparison, not by a role gate.
Cross-surface
The endpoints on this page have NO equivalent in the s2s API; you cannot call them from your own panel. These capabilities exist only in the merchant panel, behind a panel session.

A program is the definition of your wallet product. Every user, account and operation under it is governed by its policies.

Creating and editing

GET   programs                 # list
POST  programs                 # create
GET   programs/:id             # detail
PATCH programs/:id             # edit
FieldDescription
nameProgram name
loopModeClosed-loop / open-loop mode
currenciesSupported currencies
railPolicyIntegration policy
brandingBranding fields — see Portal — Branding & Theme
statusStatus
licenceHolderLicence-holder axis
⚠️

Use expectedVersion when editing. If two operators edit the same program at once, the request without the expected version is rejected instead of silently overwriting.

Program status and archiving

From the merchant panel, status accepts only ACTIVE and SUSPENDED.

ARCHIVED cannot be set from the merchant panel. Archiving a program is a platform-management operation. Use SUSPENDED to take a program out of service; if it genuinely has to be archived, contact the platform team.

An archive request passes two gates and both are fail-closed. You need to understand what they report, because you are the one who has to drain the queues:

Error codeWhat it saysWhat you must do
WALLET_PROGRAM_ARCHIVE_BLOCKED_BALANCEThe program still has accounts holding a balanceSettle the balances (refund or pay out to the user)
WALLET_PROGRAM_ARCHIVE_BLOCKED_IN_FLIGHTEven at zero balance, there is money in flightClose the relevant queue (axis table below)
WALLET_PROGRAM_ARCHIVE_UNVERIFIABLEThe program is above the inline balance-verification capThe platform drain procedure is required; balances are verified out of band
⚠️

Do not render these two codes with the same message. ..._BALANCE means "settle the balances"; ..._IN_FLIGHT means "close the queue". They are different jobs, and a single "the program cannot be archived" string sends the operator to the wrong one. The balance gate runs first.

Money in flight — the axis breakdown

WALLET_PROGRAM_ARCHIVE_BLOCKED_IN_FLIGHT reports structurally which queue holds how many records:

{
  "code": "WALLET_PROGRAM_ARCHIVE_BLOCKED_IN_FLIGHT",
  "details": {
    "inFlightByAxis": { "OPERATION": 12, "CASHOUT_CODE": 500 },
    "capped": true,
    "probeCap": 500,
    "axes": [
      { "axis": "OPERATION", "count": 12, "capped": false, "remedy": "…" },
      { "axis": "CASHOUT_CODE", "count": 500, "capped": true, "remedy": "…" }
    ]
  }
}
AxisWhat blocksRemedy
OPERATIONAn operation saga that has not reached a terminal stateLet the sagas finish, or reverse them
CASHOUT_CODEOutstanding cash-out codesRelease them or let them expire
CARD_AUTHORIZATIONOutstanding card authorizationsClear or reverse them
BULK_PAYOUT_BATCHOutstanding bulk payout batchesCancel or finish them
⚠️

Counting is capped at 500 (probeCap). When an axis reports capped: true, the real figure is not 500 but "500+" — never render it as an exact number.

The capping flag is per axis (axes[].capped). Do not infer which axis was capped from the top-level capped boolean: an axis holding exactly 500 records that was not capped would be shown as "500+" because a different axis overflowed. axes[].capped exists precisely to remove that guess.

The reason the balance gate cannot see money in flight is technical and important: the balance read looks at the customer wallet account, while blocked funds have been moved into a separate blocked bucket that read never touches. And a saga that has not posted yet appears in neither bucket. That is why the second axis exists.

OTP policy

GET programs/:id/otp-policy
PUT programs/:id/otp-policy

Defines when the program asks for an OTP. There are two rules:

FieldMeaning
otpAmountThresholdOTP is required for transactions above this amount
newCounterpartyOtpWhen the user pays a business or recipient for the first time, OTP is required regardless of the amount: ON or OFF

newCounterpartyOtp can be turned off only on a closed-loop program; on an open-loop program the rule always applies and an OFF request is rejected. If you omit the field the stored value does not change. The default is ON.

In the response, newCounterpartyOtpConfigurable shows whether the rule can be turned off on this program, and newCounterpartyOtpEffective shows whether it currently applies.

⚠️

Setting the threshold to 0 means "no threshold", not "OTP on everything". If newCounterpartyOtp is also OFF, the program never asks for an OTP. After saving, verify the behaviour with one small and one large test transaction.

⚠️

If otpAmountThreshold is omitted, the program-specific threshold is removed and the platform default applies. When you only change newCounterpartyOtp, send the current threshold as well.

Assigning a limit scheme

POST programs/:id/limit-scheme

Binds a limit scheme to the program. Scheme contents: Panel — Limit & Fee Schemes.

Cross-channel shared cap

GET programs/:id/shared-limit-caps
PUT programs/:id/shared-limit-caps/:capCode/tiers

Defines the umbrella cap shared by selected outbound channels. It is off by default; agree the thresholds with your compliance team before enabling. See Limits.

Binding bank integrations

GET    programs/:id/bank-rails
PUT    programs/:id/bank-rails/:railId
DELETE programs/:id/bank-rails/:railId
Coming soon — requires a connected bank integration

The binding screen is built; what is needed is a bank integration to bind. Without an integration, bank transfer top-up, bank withdrawal, IBAN transfer and IBAN-targeted bulk payouts do not work. Status: Roadmap.

A program can run with several bank integrations at once; direction, status and credential reference are configured per integration.

Sponsor policy

GET  programs/:id/sponsor-policy
PUT  programs/:id/sponsor-policy
POST programs/:id/sponsor-policy/validate
POST programs/:id/sponsor-policy/versions/:versionId/submit
POST programs/:id/sponsor-policy/versions/:versionId/approve
POST programs/:id/sponsor-policy/versions/:versionId/reject
GET  sponsor-policy/providers

The sponsor policy declares which licence and providers the program runs on: e-money provider, enabled integrations, ATM provider, crypto provider and assets, card issuer and BIN sponsor.

⚠️

The policy is validated before approval: a policy naming a provider that is not connected will not be approved. Run validate before saving — it tells you exactly which binding is missing.

The policy is versioned and subject to four-eyes approval.

Provider switching

GET  programs/:id/provider-switches
POST programs/:id/provider-switches
GET  programs/:id/provider-switches/:switchId/readiness
POST programs/:id/provider-switches/:switchId/drained
POST programs/:id/provider-switches/:switchId/abort

Moving from one provider to another requires an explicit switch record; you cannot simply edit the provider field.

A switch has three stages: open the switch record → drain in-flight operations on the old provider (drained) → complete. The readiness endpoint shows which stage you are at and what is still pending. If it goes wrong, abort rolls it back.

Agents

GET  agents
POST agents

Agent (dealer) definition: float account, daily credit limit and KYC reference. See Top-up → Agent top-up.

Things to watch

TopicWhy it matters
Adding a currencyUsers must open new accounts; existing accounts are not converted
Changing integration policyCan affect in-flight operations; do it in a low-volume window
Program statusDeactivating stops users from transacting
Branding fieldsThey also appear on receipts and notifications