Building a frontend on the white-label wallet API
This guide documents every endpoint available to a white-label partner: account
creation, two-factor login, deposit addresses, transfers, and prepaid card issuance.
It corresponds to the interactive reference at /docs — use this guide for
the flow and reasoning, and /docs for exact schemas.
How the white-label system works
The platform is multi-tenant: every partner's data — users, balances, cards, fee configuration — is fully isolated from every other partner's. Your tenant is called a white label, and it is identified by a single string that the platform issues to you at onboarding.
The tenant header
Every request, authenticated or not, must include your tenant identifier in the
x-api-key header:
x-api-key: your-whitelabel-id
This header is what scopes every query on the backend: it determines which pool of users a login checks against, which fee schedule applies to a deposit, and which card price list a BIN resolves to. Two different partners can have two different users registered under the same email address, because the account is namespaced by tenant, not globally unique.
x-api-key value than the one active when it was
issued, the request is rejected as unauthorized. Store your API key once, as a build-time
constant or server-side config — never let it vary per request.
What varies per tenant
- Fees — deposit, withdrawal, and card-creation fee percentages are configured per white label (with a platform-wide fallback). You never calculate or submit a fee: every endpoint that touches money returns the net amount after the applicable fee has already been resolved server-side.
- Card pricing — the price and top-up fee for a given card BIN can be overridden per white label, so the same BIN may be priced differently for you than for another partner.
- Branding — every transactional email (OTP codes, deposit/withdrawal/card confirmations, etc.) is sent from templates you design and register yourself in SendPulse — see Set up email templates, the first thing to do before going live.
What does not vary per tenant: supported blockchain networks and tokens are global infrastructure — every partner sees the same active set (see Networks & tokens).
Set up your email templates first
Do this before writing a single line of integration code. Every OTP and account notification described in this guide — registration codes, login codes, password resets, deposit/withdrawal/card confirmations — is delivered through email templates that you design and own in SendPulse, not a shared platform template. Until these are built and submitted, no email in your tenant can be sent — which means no user can complete registration, login, or any OTP-gated action.
POST
/auth/otp will have nothing to deliver — treat template setup as step zero, not
a launch-week detail.
POST /auth/admin/login with a user on your tenant that has admin rights
(not the regular /auth/login), and using the resulting access token as the
bearer token for the setup call below. Creating or updating the configuration
(POST/PUT) requires the
caller to hold super-admin rights specifically — a plain admin account that can't pass that
check gets 403. Reading the current configuration back with
GET only needs a regular admin bearer token.
- Create a SendPulse account for your brand (or use an existing one) and verify the sender email address you want your notifications to come from.
- Generate API credentials. In SendPulse, open the API section of your account settings and generate an API User ID and API Secret — these authorize the platform to send mail through your account on your users' behalf.
- Build each template below in the SendPulse template editor, using your own branding, copy, and layout. Publish each one and record its numeric template ID.
- Log in as an admin. Call
POST /auth/admin/loginwith the email/password of a user on your tenant that has admin rights, to get an access token. - Submit your configuration. Call
POST /admin/whitelabel/:whiteLabelId/email-templateswith the admin access token, your SendPulse credentials, and every template ID from the tables below. This is what actually enablesPOST /auth/otpto deliver mail for your tenant.
Same shape as the regular login call — body: { email, password, otp } — but the account must have admin rights on your tenant, or it returns 400 "Access denied". Returns { accessToken, userData }; use accessToken as the bearer token for the setup call below.
Creates your tenant's email configuration. The :whiteLabelId path segment is required by the route but not authoritative — see the note below. Once created, use PUT on the same path to update any field later, sending only the fields you're changing.
Returns the currently stored configuration for the calling tenant (resolved from your x-api-key, no path parameter). Use it to check what's already set before sending a PUT.
:whiteLabelId is advisory. Unless you authenticate as the platform
default tenant, the server ignores whatever id you put in the URL and always writes to
your own tenant (the one your x-api-key resolves to). You cannot configure another
partner's templates, and you don't need to look your own id up — any placeholder in the path works.
Account-level fields
All four are required on the initial POST.
| Field | What it is |
|---|---|
apiUserId | From SendPulse account settings → API |
apiSecret | From SendPulse account settings → API |
sendPulseEmail | The verified "from" address your notifications are sent from |
supportName | The sender / support display name shown on your emails |
How template variables work
Every template is one you build and publish in SendPulse. When the platform sends a
message it passes a fixed set of merge variables into that template — reference
them in your SendPulse content as {{otp}}, {{amount}}, and so on. The
subject line is set by the platform, not your template, so design for the body only.
Variable names are case-sensitive and exactly as listed below (note Status is capitalised).
Any variable your template doesn't reference is simply ignored; referencing one that wasn't passed
renders empty.
Authentication & OTP
Delivered to the end user. Every code email carries the same otp variable; withdrawal and
transfer codes add context so the user can verify what they're approving.
| Field | Sent when | Variables | |
|---|---|---|---|
registrationTemplateId | required | New user requests a code to verify their email during sign-up | otp |
loginOtpTemplateId | required | User with email 2FA requests a code to complete login | otp |
forgetPasswordOtpTemplateId | required | User with email 2FA requests a password-reset code | otp |
profileUpdateOtpTemplateId | required | User requests a code to confirm a profile / security change | otp |
withdrawalRequestOtpTemplateId | required | User requests a code to confirm an external (on-chain) withdrawal | otp amount address |
internalTransferOtpTemplateId | optional | User requests a code to confirm a platform-to-platform transfer | otp amount ("100 USDT") toName address |
swapWithdrawalOtpTemplateId | optional | User requests a code to confirm a token swap withdrawal | otp |
User money notifications
Delivered to the end user. All optional — configure the ones matching features you expose.
| Field | Sent when | Variables | |
|---|---|---|---|
depositProcessingTemplateId | optional | An on-chain deposit is detected, before it's credited | name token amount network txid |
depositConfirmedTemplateId | optional | The deposit is credited to the user's balance | name token amount |
withdrawalSuccessNotificationTemplateId | optional | An external withdrawal completes on-chain | token amount amountReceived address |
internalTransferDepositProcessingTemplateId | optional | An incoming internal transfer is submitted, awaiting settlement (sent to the recipient) | name toName token amount network txid |
internalWithdrawalSuccessfulNotificationTemplateId | optional | An internal transfer completes (sent to the sender) | token amount toName address |
name on the deposit templates. The deposit-processing and deposit-confirmed emails
currently pass the account holder's last name into name — write the copy so it
reads correctly either way (e.g. "Hi {{name}}," rather than assuming a first name).
Card funding
Delivered to the end user when a card top-up changes state.
| Field | Sent when | Variables | |
|---|---|---|---|
cardFundingPendingTemplateId | optional | A card top-up is submitted and processing | name cardholderName amount bin type (card network) |
cardFundingConfirmedTemplateId | optional | A card top-up succeeds and funds land on the card | name cardholderName amount bin type |
cardFundingFailedTemplateId | reserved | Accepted by the setup call but not yet dispatched — register a placeholder so the field is ready when it goes live | — |
Card creation & cardholder verification
Delivered to the end user. "Custom" = the applicant submitted their own cardholder identity; "default" = the simplified fast-path using a shared default holder.
| Field | Sent when | Variables | |
|---|---|---|---|
customPlatinumCardCreationPendingTemplateId | optional | A custom-holder card request enters processing | firstName |
customPlatinumCardCreationCompletedTemplateId | optional | A custom-holder card is issued | firstName |
defaultPlatinumCardCreationPendingTemplateId | optional | A default-holder (fast-path) card request enters processing | firstName |
defaultPlatinumCardCreationCompletedTemplateId | optional | A default-holder card is issued | firstName |
cardHolderVerifiedNotificationTemplateId | optional | A submitted cardholder identity is approved | firstName lastName email Status (= "approved") |
cardHolderRejectedNotificationTemplateId | optional | A submitted cardholder identity is rejected | firstName lastName email Status (= "rejected") rejectionReason |
Account verification (KYC)
Delivered to the end user as their account-level KYC submission is reviewed.
| Field | Sent when | Variables | |
|---|---|---|---|
kycApprovedNotificationTemplateId | optional | A user's account KYC submission is approved | — |
kycRejectedNotificationTemplateId | optional | A user's account KYC submission is rejected | — |
kycUnderReviewNotificationTemplateId | optional | A user's account KYC submission enters review | — |
Referral
| Field | Sent when | Variables | |
|---|---|---|---|
referralSignUpTemplateId | required | Someone signs up with a user's referral code — sent to the referrer | name (referrer's first name) |
userReferralAcceptedNotificationTemplateId | optional | A user's referral / affiliate application is approved | email |
Ops notifications (to your team)
Delivered to your configured admin email addresses, not to end users. Use them for internal monitoring and manual-review queues.
| Field | Sent when | Variables | |
|---|---|---|---|
adminDepositNotificationTemplateId | required | A user deposit is received | client_name amount time platform |
adminWithdrawalNotificationTemplateId | required | A user requests a withdrawal | client_name amount time platform |
adminCardCreationNotificationTemplateId | required | A user requests a card (both standard and platinum paths) | client_name client_email platform time (platinum path only) |
adminWsbCardWithdrawalNotificationTemplateId | optional | A platinum card withdrawal is requested | client_name client_email amount time platform |
adminDefaultCardCreationNotificationTemplateId | reserved | Accepted and seeded but not currently dispatched (the platinum-card ops mail uses adminCardCreationNotificationTemplateId) | — |
adminKycSubmissionNotificationTemplateId | optional | A user submits an account KYC verification record for review | — |
adminLoginNotificationTemplateId
and adminProfileUpdateNotificationTemplateId have been removed from the setup call's schema —
don't send them; they're rejected as unrecognized fields.
Affiliate portal
| Field | Sent when | Variables | |
|---|---|---|---|
portalLoginOtpTemplateId | optional | An affiliate requests a code to log into the referral portal | otp |
portalRegistrationTemplateId | reserved | Accepted by the setup call; portal registration is not wired to it yet | otp (expected) |
portalForgetPasswordOtpTemplateId | reserved | Accepted by the setup call; portal password reset is not wired to it yet | otp (expected) |
default tenant. You don't configure these and they aren't part of your payload.
Example: initial setup call
// headers: x-api-key: <your-whitelabel-id>, Authorization: Bearer <admin-access-token> { "apiUserId": "sp_user_xxx", "apiSecret": "sp_secret_xxx", "sendPulseEmail": "noreply@yourbrand.com", "supportName": "YourBrand Support", // required templates "registrationTemplateId": 100001, "loginOtpTemplateId": 100002, "forgetPasswordOtpTemplateId": 100003, "profileUpdateOtpTemplateId": 100004, "withdrawalRequestOtpTemplateId": 100005, "referralSignUpTemplateId": 100006, "adminDepositNotificationTemplateId": 100007, "adminWithdrawalNotificationTemplateId": 100008, "adminCardCreationNotificationTemplateId": 100009, // optional — add the ones you use "depositProcessingTemplateId": 100012, "depositConfirmedTemplateId": 100013, "withdrawalSuccessNotificationTemplateId": 100014, "cardFundingPendingTemplateId": 100015, "cardFundingConfirmedTemplateId": 100016 }
To change anything later, PUT the same path with
only the fields you're updating — the call is validated as a partial, and
untouched fields keep their stored values. Sending null for an optional id is
rejected; omit the key instead.
Once the setup call returns success, verify with
GET /admin/whitelabel/email-templates, then move on to
Authentication model to start integrating.
Authentication model
Two independent credentials travel on every authenticated request: the tenant header described above, and a bearer token that identifies the end user.
| Header | Applies to | Value |
|---|---|---|
x-api-key | Every request | Your white-label identifier |
Authorization | Any endpoint requiring a logged-in user | Bearer <accessToken> |
The access token is a signed JWT returned by /auth/login. It encodes the
user's identity and tenant scope, and is valid for 24 hours. There is no
separate session store to check — the token itself is the credential, so treat it like
one: keep it in memory or secure storage, never in a location readable by third-party
scripts (i.e. avoid localStorage if you can use an httpOnly cookie proxied
through your own backend instead).
Alongside the access token, login also returns a longer-lived refresh token (30 days) used solely to mint new access tokens without asking the user to log in again — see Sessions & tokens.
Registration
New accounts are created in two calls: request a one-time code by email, then submit the registration form together with that code. This confirms the email address belongs to the person registering before any account exists.
- Request an OTP. Call
POST /auth/otpwithpurpose: "register"and the candidate email. The backend checks the email isn't already registered under your tenant and emails a one-time code. - Submit registration. Call
POST /auth/registerwith the user's details and the code they received. On success, the account is created — the user still needs to log in afterward; registration does not return a session token.
Requests a one-time code for a given purpose. For registration, body is:
| Field | Type | Required |
|---|---|---|
purpose | "register" | required |
email | string | required |
Response: { success: true, message: "Otp sent", type: "email" }. 400 if the email is already registered under your tenant.
| Field | Type | Required | Notes |
|---|---|---|---|
firstName | string | required | min 3 chars |
lastName | string | required | min 3 chars |
email | string | required | lowercased & trimmed automatically |
password | string | required | min 8 chars; needs upper, lower, digit, and special character |
otp | string | required | from the previous step |
refBy | string | optional | a referring user's id (that user must have referrals enabled) or a coupon id — check either kind up front with GET /users/check?refBy=... |
Success returns 200 with { newUser: { id, email, firstName, lastName, whiteLabelId } }. Errors are 400 with a message: expired code, invalid code, duplicate email, or invalid referral.
A user who registered without a refBy can still attach one afterward, once, via
POST /users/referral-code (body { refBy }) — it fails with 400
if the account already has a referral or coupon code set.
// 1. Request a one-time code by email await fetch(`${BASE_URL}/auth/otp`, { method: "POST", headers: { "Content-Type": "application/json", "x-api-key": API_KEY }, body: JSON.stringify({ purpose: "register", email }) }); // 2. User enters the code they received, then submit registration const res = await fetch(`${BASE_URL}/auth/register`, { method: "POST", headers: { "Content-Type": "application/json", "x-api-key": API_KEY }, body: JSON.stringify({ firstName, lastName, email, password, otp }) }); const { newUser } = await res.json(); // Registration does not log the user in — send them to the login screen next.
Login & two-factor authentication
Every account has 2FA on by default. A user authenticates with email + password, then confirms a second factor — either a code emailed to them, or a code from an authenticator app, depending on what they've configured.
Recommended UX
- Collect credentials, check them silently. Call
POST /auth/check-credentialswith email + password. This validates the password without starting a session, so you can show a real-time error before asking for a second factor. - Check whether a code is required. Call
POST /auth/login-otp-requiredwith the email. Nearly every account has this on, but it lets you skip the extra field for the rare account that doesn't. - Request the code, if needed. Call
POST /auth/otpwithpurpose: "login". If the account uses email 2FA, a code is sent; if it uses an authenticator app, nothing is sent — the user reads the code from their app instead. - Submit the full login. Call
POST /auth/loginwith email, password, and the code. On success you receive an access token, a refresh token, and the user's profile.
| Field | Type | Required |
|---|---|---|
email | string | required |
password | string | required |
otp | string | required |
Response: { accessToken, refreshToken, userData }, where userData includes id, email, firstName, lastName, balance, totalCommisions, refBy, isAdmin, whiteLabelId. 400 on any credential or code mismatch.
const headers = { "Content-Type": "application/json", "x-api-key": API_KEY }; // Step 1 — validate credentials before asking for a 2FA code const check = await fetch(`${BASE_URL}/auth/check-credentials`, { method: "POST", headers, body: JSON.stringify({ email, password }) }); if (!check.ok) throw new Error("Invalid email or password"); // Step 2 — send the OTP (no-op email if the account uses an authenticator app) await fetch(`${BASE_URL}/auth/otp`, { method: "POST", headers, body: JSON.stringify({ purpose: "login", email }) }); // Step 3 — complete login once the user has entered their code const res = await fetch(`${BASE_URL}/auth/login`, { method: "POST", headers, body: JSON.stringify({ email, password, otp }) }); const { accessToken, refreshToken, userData } = await res.json();
Choosing a second factor
A logged-in user can switch their 2FA method via PUT /users by setting
twoFactorAuthMethod to "email" or "authenticator".
When switching to an authenticator app, the user's profile carries the secret needed to
render a QR code (or manual entry key) for enrolling in an authenticator app such as
Google Authenticator or Authy — display it once during setup and let the user scan it.
The OTP system
A single endpoint, POST /auth/otp, issues one-time codes for every purpose in
the product. The purpose field determines what's validated and whether the
call requires a bearer token.
| Purpose | Used for | Requires bearer token | Extra fields |
|---|---|---|---|
register | New account email verification | No | email |
login | Standard login second factor | No | email |
portal-login | Affiliate portal login | No | email |
forget-password | Password reset | No | email |
withdrawal | Confirming an external transfer | Yes | amount, address |
swap-withdrawal | Confirming a token swap withdrawal | Yes | — |
update | Confirming a profile/password change | Yes | — |
verify-email | Confirming ownership of the current email before changing it | Yes | — |
change-email | Confirming ownership of a new email address | Yes | — |
internal-transfer | Confirming a platform-to-platform transfer | Yes | amount, toUserId, assetId |
Codes are short-lived and delivered by email for accounts on email-based 2FA. Accounts using an authenticator app never receive an emailed code for any purpose above except password reset — always read the current code from the app instead.
Overriding the 2FA method per call
Login, password reset, and every step-up action that accepts an otp also accept an
optional otpMethod field ("email" or "authenticator") in the
same request body. When present, it tells that one call which kind of code to verify against,
regardless of the account's stored default — useful for a user who has an authenticator app set up
but wants to fall back to an emailed code for a specific action (or vice versa). Omit it to use the
account's stored twoFactorAuthMethod.
200 from /auth/otp
only means the request was accepted, not that the subsequent action will succeed — always
surface the real error from the follow-up call (login, register, transfer, etc.) if the
code is rejected.
Step-up authentication for sensitive actions
A valid bearer token alone is not enough to move money or change security-relevant profile fields. Withdrawals, internal transfers, password changes, and general profile updates all require a fresh OTP submitted with the request itself, on top of the existing session.
Build this into your UI as a deliberate last step rather than an afterthought:
- User reviews the action (e.g. "Withdraw 100 USDT to 0xabc...").
- Request the OTP for the relevant purpose (
withdrawal,update, orinternal-transfer) — this call needs the bearer token. - User confirms with the code, and you submit the action endpoint with
otpincluded in the body.
Endpoints that follow this pattern: POST /users/transfer, POST /users/internal-transfer, PUT /users, PUT /users/password.
The KYC gate
Separate from account KYC submission and cardholder verification, a white label can turn on a blanket identity-verification requirement for every money-moving and account-management action. If your tenant has this on, build it into your onboarding flow deliberately — don't treat it as an edge case.
403 for this reason. Transfers,
deposits history, card creation and funding, password/profile changes, swaps, statements, and more
all check KYC status before proceeding. The response shape is:
{ "error": "KYC verification required before you can perform this action", "kycStatus": "not-submitted" }
kycStatus is one of not-submitted, pending,
under-review, approved, or rejected, letting you route the
user straight to the right screen — a KYC form, a "we're reviewing it" message, or a rejection
reason — instead of showing a generic error. Treat any 403 with this shape as a signal
to redirect into the KYC submission flow, not as a fatal error.
Whether this gate is active at all is a per-tenant setting controlled by your platform admin; a
small number of individual accounts can also be exempted from it directly. Not every white label
has it on — check with your onboarding contact, or simply detect it live via the 403
shape above.
Password management
Body: { email, newPassword, otp } — request the code first via POST /auth/otp with purpose: "forget-password". Returns { success: true } or 400 on an invalid code or unknown email.
Body: { password, newPassword, otp } — changes the password for a logged-in user (request the code with purpose: "update" first).
Body: { password }. Returns { valid: boolean } — use to re-confirm identity before showing a sensitive settings screen, without a full OTP round trip.
Changing email
Changing the email on an account is a three-step flow: confirm the user still controls the
current email (via a code sent to it, or an authenticator code), then confirm they
control the new one. This can't be done through PUT /users.
- Request a code for the current email. Call
POST /auth/otpwithpurpose: "verify-email"(skip this if the user authenticates withotpMethod: "authenticator"instead). - Verify ownership of the current email. Call
POST /users/email/verifywith the code. This unlocks the next step but doesn't change anything yet. - Request a code for the new email. Call
POST /auth/otpwithpurpose: "change-email"and the candidate new address. - Submit the change. Call
PUT /users/emailwith the new email and that code.
Body: { otp, otpMethod? }. Confirms the caller still controls their current email (or passes an authenticator code). 400 if skipped before the next step — PUT /users/email checks that this was done first.
Body: { email, otp, otpMethod? }. email is the new address; the code must have been requested with purpose: "change-email" for that same address. 400 if the current email wasn't verified first, the new address is already taken on your tenant, or it matches the existing email.
Authenticator app 2FA
Beyond switching the stored 2FA method via PUT /users, dedicated endpoints manage the
authenticator app itself — setting it up, confirming it, and removing it.
Starts (or restarts) authenticator enrollment. Returns { secret, otpauthUrl } — render otpauthUrl as a QR code, or show secret for manual entry. The secret isn't saved on the account until POST /users/2fa/authenticator/verify confirms it. To replace an already-active authenticator, pass { update: true, otp, otpMethod? } — this requires confirming with the account's current 2FA method first.
Body: { otp } — the 6-digit code currently showing in the app. On success, the setup secret becomes the account's active authenticator and twoFactorAuthMethod switches to "authenticator". The pending setup secret expires 10 minutes after /setup was called — restart if you take too long.
Body: { method: "email" | "authenticator", otp, otpMethod? }. Switches the account's active 2FA method. Switching to "authenticator" requires it to already be set up (via the two endpoints above); otp must verify against the account's current method before the switch takes effect.
Body: { otp, otpMethod? }. Removes the authenticator app from the account and reverts twoFactorAuthMethod to "email".
Sessions & tokens
Access tokens are short-lived by design (24 hours) so that a stolen token has a bounded blast radius. Refresh tokens exist to renew a session silently in the background.
Body: { refreshToken }. Returns a brand-new { accessToken, refreshToken } pair — the old refresh token is invalidated immediately (rotation), so always persist the new one. Returns 401 if the refresh token is expired or unknown; your client should fall back to a full login in that case.
Body: { refreshToken }. Revokes that refresh token. The current access token remains technically valid until its 24-hour expiry, so treat logout as "stop renewing," not an instant global kill — discard the access token client-side immediately on logout.
async function authorizedFetch(path, options = {}) { let res = await fetch(`${BASE_URL}${path}`, { ...options, headers: { ...options.headers, "x-api-key": API_KEY, "Authorization": `Bearer ${accessToken}` } }); if (res.status === 401) { const refreshed = await fetch(`${BASE_URL}/auth/refresh-token`, { method: "POST", headers: { "Content-Type": "application/json", "x-api-key": API_KEY }, body: JSON.stringify({ refreshToken }) }); if (!refreshed.ok) throw new Error("SESSION_EXPIRED"); ({ accessToken, refreshToken } = await refreshed.json()); res = await fetch(`${BASE_URL}${path}`, { ...options, headers: { ...options.headers, "x-api-key": API_KEY, "Authorization": `Bearer ${accessToken}` } }); } return res; }
Profile & assets
| Method & path | Purpose |
|---|---|
GET /users | Current user's profile, including resolved fee rates |
PUT /users | Update profile — firstName, lastName, telegram, whatsapp, password, twoFactorAuthMethod, authenticatorOtp, preferredCurrency, otp (otp required). Does not accept email — see Changing email for that. |
GET /users/assets | Per-token balances |
PUT /users/preferred-currency | Set display currency — { preferredCurrency } |
GET /users/rate | Exchange rate lookup — query from, to |
GET /users/check | Public — check if an email is taken, or if a refBy value is a valid, usable referral or coupon code (no bearer token needed) |
GET /users/countries | Full country reference list — covers every country, not a limited subset |
GET /users/cities | City reference list — pass ?regionCode=<country code> to scope to one country; omit it to get every city |
POST /users/upload-file | Multipart upload (max 10MB) for KYC documents — form field file, returns { fileId } |
Use these two endpoints to populate country/city dropdowns anywhere you collect an
address or nationality — most notably the account KYC and
cardholder verification forms. Always fetch cities with the
selected country's regionCode rather than loading the full unscoped list, both
for a faster picker and to keep the two fields consistent.
What GET /users actually returns
Beyond the stored profile columns, the response is enriched server-side on every call — don't assume it's a flat mirror of the database row:
| Field | What it is |
|---|---|
isAdmin | Whether this account has admin rights on the tenant — gates access to POST /auth/admin/login |
pendingBalance | Balance tied up in not-yet-settled activity, separate from the spendable balances in /users/assets |
depositFee, withdrawalFee, cardCreationFee, referralFeePercent | This user's fully-resolved fee rates, after any per-user or coupon overrides are applied on top of your tenant's defaults |
subscriptionEnabled | Present (and reflects your tenant's setting) for any white label other than the platform's own default tenant — tells you whether to route card creation through the subscription pricing flow |
hasPendingReferralRequest, pendingReferralRequest | Whether the user has an affiliate application awaiting review, and the application record itself if so |
Networks & tokens
Supported blockchain networks and tokens are global — every white label sees the same active set. Use this endpoint to build your asset picker rather than hardcoding a list, since it reflects what's actually enabled server-side.
Optional query: network to filter. Returns an array of tokens, each including a nested network object:
[
{
"id": 1, "symbol": "USDT", "name": "Tether USD",
"contractAddress": "0xdAC17F958D2ee523a2206206994597C13D831ec7",
"decimals": 6, "type": "ERC20", "isActive": true,
"network": { "id": 1, "name": "Ethereum Mainnet", "symbol": "ETH", "isActive": true }
}
]
Networks currently live in production:
| Network | Chain type | Notes |
|---|---|---|
| Ethereum Mainnet | ethereum | USDT (ERC-20), USDC (ERC-20) |
| Tron Mainnet | tron | USDT (TRC-20) |
Test networks (Sepolia, Shasta) are available in non-production environments only.
- Tron Shasta faucet — shasta.tronex.io/join/getJoinPage
- Ethereum Sepolia faucet (Alchemy) — alchemy.com/faucets/ethereum-sepolia
- Ethereum Sepolia faucet (Google Cloud) — cloud.google.com/application/web3/faucet/ethereum/sepolia
Deposit addresses
Each user is issued a deposit address per blockchain network — not per token — the first time it's requested. One Ethereum-network address can receive both USDT and USDC, for example; you resolve which network to provision by passing any token id on that network.
Returns the user's deposit address for the network that tokenId belongs to, creating it on first request. Response includes walletAddress and network metadata. 400 if the token or its network is inactive.
const res = await authorizedFetch(`/users/wallet/${usdtTokenId}`); const { walletAddress } = await res.json(); // Render walletAddress as text + QR code for the user to send funds to.
Deposit lifecycle
Deposits are detected automatically once funds arrive at a user's address — there is no
endpoint to "notify" the backend of an incoming transfer. Ethereum-network deposits are
typically credited within moments of confirmation; Tron-network deposits go through an
additional confirmation step before crediting, so may take a little longer. Poll
GET /users/deposits or GET /users/assets to reflect the credited
balance in your UI.
| Method & path | Purpose |
|---|---|
GET /users/deposits | Paginated deposit/withdrawal history — query page, limit, type |
GET /users/deposit/:depositId | Single deposit/withdrawal record |
Each record has a status of waiting, pending,
completed, failed, rejected, or
refunded, and a type of deposit,
withdrawal, card-topup, card-withdrawal,
card-creation, referral-reward, or
internal-transfer — use these to render a unified activity feed.
Withdrawals & transfers
Sends crypto to an external address. Body: token, chain, amount, to, assetId, otp (all required). Creates a withdrawal record with status: "pending" — external withdrawals go through a manual review step before funds are actually released, so surface this as "processing" rather than instant in your UI.
Moves funds directly to another user on the platform (any tenant). Body: amount, toUserId, assetId, otp. Settles faster than an external withdrawal since no on-chain review queue is involved.
| Method & path | Purpose |
|---|---|
GET /users/lookup | Look up a user by email before an internal transfer — query email |
GET /users/beneficiaries | Paginated list of past internal-transfer recipients |
GET /users/notifications | Unread internal-transfer notification count |
PUT /users/notifications/internal-transfer/increment / /decrement | Adjust the unread badge counter |
Token swaps
Separate from a plain external withdrawal, a user can swap one supported token for another and withdraw the result. A swap is tracked as its own record with a lifecycle you drive explicitly — it doesn't start moving funds until you call the start endpoint.
| Method & path | Purpose |
|---|---|
GET /users/dex-transactions | Paginated swap history for the caller — query page, limit, status, txnType, route, token, startDate, endDate |
POST /users/dex-transactions/:id/start | Begins processing a pending swap the user initiated |
POST /users/dex-transactions/:id/withdraw | Withdraws the swap's output — step-up OTP: body { toAddress, otp, otpMethod? } |
GET /users/dex-transactions/:id/start and .../withdraw both 404 if the
transaction doesn't belong to the caller, so there's no need to filter by user yourself beyond
passing the right id.
Cashback
Some card BINs carry a cashback rate on deposit volume. Use this endpoint to show a user what they're eligible for before they commit to a top-up.
Returns { eligible: false } if the user has no eligible deposit volume. Otherwise returns per-BIN breakdowns keyed by both the BIN's id and its bin (card-number-prefix) value, each shaped as { cashbackRate, cashbackAmount, depositAmount }.
Statements
Generate a downloadable activity statement, either for the user's whole account or for one card.
Body: { cardId?, startDate?, endDate? }. Streams a CSV file. Omit cardId for an account-wide statement; include it to scope to one card.
Same body shape as the CSV endpoint; streams a PDF instead.
Card issuance overview
Prepaid cards are created against a BIN (a card product configuration) —
list available BINs first, then create a card against one of them. Not every BIN
requires a card holder. Whether one is required is controlled entirely by a single
field on the BIN, needCardHolder, from GET /cards/bins — check
it per BIN and branch your UI on it, rather than assuming one onboarding path fits every
card product:
needCardHolder: false— no card holder step at all. The cardholder's basic details (name, email, date of birth) are submitted directly in the card creation call, and the BIN can be used with noholderId.needCardHolder: true— a card holder must exist for that BIN before a card can be created against it. Either pass an already-approvedholderId, or omitholderIdentirely to let the backend auto-create a lightweight "default" card holder — a simplified fast path that bypasses full identity verification for BINs that allow it (see below).
packageId and is priced from that package instead
of the BIN's flat price — see Subscription-priced cards before
building your card-creation form. Check subscriptionEnabled on
GET /users to know which flow applies to a given user.
Returns active card products with your tenant's pricing applied: id, bin, network, cardType, price, defaultPrice, topUpFee, needDepositForActiveCard, needCardHolder, minDepositAmount, maxDepositAmount, isActive. needCardHolder is the field that decides everything in the next section — read it per BIN, don't hardcode an assumption per card network or product name.
| Field | Type | Required | Notes |
|---|---|---|---|
bin | number | required | the id field of a card product from GET /cards/bins — not its bin field (the six-digit card number prefix) |
useType | string | required | |
cost | number | required | amount to load onto the card, ≥ 0 |
assetId | number | required | token to debit for cost + fee |
quantity | number | optional | defaults to 1 |
packageId | string | conditional | required when your tenant has subscriptions enabled — see Subscription-priced cards. Omit entirely on a non-subscription tenant. |
firstName, lastName, email, dateOfBirth | — | conditional | required when the selected BIN has needCardHolder: false |
address | object | optional | addressLine1, city, state, country, postalCode required within it if present |
holderId | string | conditional | an approved identity-verification record id, required when the selected BIN has needCardHolder: true — unless you omit it to use the default-holder fast path (see below) |
On a subscription tenant, the fee resolved for cardCreationFee purposes comes
from the chosen package's price instead of the BIN's flat price — everything else about the
cost calculation below is the same. The full cost
((cost + resolved fee) × quantity) is deducted from the
user's balance for assetId immediately — before the physical/virtual card
is actually live — and refunded if issuance ultimately fails. Response is
200 with a confirmation message and the user's updated profile; card
activation itself happens asynchronously, so poll GET /cards/pending until
the request completes.
When needCardHolder is true and no holderId is
supplied, the backend auto-creates a lightweight "default" holder from
firstName/lastName/email instead of rejecting the request — this is a
fast path for BINs that allow it and bypasses full identity verification, so treat it
as a distinct, simpler onboarding option rather than a fallback for a missing id.
Subscription-priced cards
Some tenants price card creation through subscription packages rather than a flat per-BIN fee.
Check subscriptionEnabled on GET /users before
assuming either model — the two are mutually exclusive for a given tenant.
| Method & path | Purpose |
|---|---|
GET /cards/subscriptions | List the packages available to the caller (general packages plus any personal overrides) |
GET /cards/bin/:binId | A single BIN's pricing, resolved the same way as an entry from GET /cards/bins |
GET /cards/subscription/:cardId | The subscription state backing one of the user's cards |
PUT /cards/subscription/:cardId/package | Change which package a card is billed under |
POST /cards/subscription/:cardId/retry | Retry a failed subscription charge |
POST /cards/subscription/:cardId/cancel | Cancel the subscription on a card |
POST /cards/subscription/:cardId/reactivate | Reactivate a previously cancelled subscription |
Use the package's id from GET /cards/subscriptions as
packageId when calling POST /cards — pick packages scoped to the
target binId only.
Cardholder verification
Required before issuing a card against a BIN with needCardHolder: true
(unless using the "default holder" shortcut above). It's a document-upload plus a data
form, reviewed asynchronously. This verifies a specific cardholder identity for card
issuance — it's separate from the account-level KYC submission some
tenants require just to use the platform at all.
- Upload documents. Call
POST /users/upload-filethree times — for the front of the ID, the back of the ID, and a selfie holding the ID — collecting afileIdeach time. - Submit the verification record. Call
POST /users/card-holderwith the applicant's personal details and the three file ids. - Poll for approval. Call
GET /users/card-holderuntil the record's status indicates it's ready, then use its id asholderIdinPOST /cards.
| Field | Type / format |
|---|---|
firstName, lastName | 2–32 chars, letters and spaces only |
email | 5–50 chars |
phone | 5–20 chars |
areaCode | 2–5 chars (phone country code) |
birthday | date |
nationality, country | 2-letter ISO country code — pull valid values from GET /users/countries |
city | string — pull valid values from GET /users/cities?regionCode=<country> |
address | 2–40 chars, letters/numbers/hyphen/space |
postalCode | 2–15 chars, alphanumeric |
gender | "M" or "F" |
occupation | string |
annualSalary | number > 0 |
accountPurpose | string |
expectedMonthlyVolume | number > 0 |
idType | PASSPORT · HK_HKID · DLN · GOVERNMENT_ISSUED_ID_CARD |
idNumber | 2–50 chars |
issueDate, idExpiryDate | date |
binId | number — the target card product's id from GET /cards/bins (e.g. 111072), not its bin field, the six-digit card number prefix (e.g. 49387519) |
ipAddress | the applicant's IP address |
idFrontId, idBackId, idHoldId | file ids from POST /users/upload-file |
Returns the created verification record with an initial pending status. GET /users/card-holder (optionally filtered by ?binId=) lists a user's own records so you can poll for a status change.
holderId in POST /cards — submitting a
holderId that is still pending, or that was rejected, fails with
a 400. Don't let a user proceed to card creation from a pending record; gate
that step in your UI on the polled status.
Account KYC submission
This is the account-level verification checked by the KYC gate — it's distinct from cardholder verification, which is scoped to a specific card product. Only relevant for tenants that have the KYC gate enabled.
| Field | Type / format |
|---|---|
firstName, lastName | 2–32 chars |
email | 5–50 chars |
phone | 5–20 chars |
areaCode | optional |
birthday | date |
country, state, city | strings — pull valid country/city values from GET /users/countries / GET /users/cities?regionCode=<country> |
address | 2–100 chars |
postalCode | 2–15 chars |
idType | PASSPORT · NATIONAL_ID · DRIVERS_LICENSE · GOVERNMENT_ISSUED_ID_CARD — note this list differs from cardholder verification's idType enum |
idIssuingCountry | optional |
idNumber | 2–50 chars |
issueDate | date |
idExpiryDate | optional date |
utilityBillFileId, idDocumentFileId, selfieFileId | file ids from POST /users/upload-file |
nationality, gender, occupation, accountPurpose | all optional |
annualSalary, expectedMonthlyVolume | optional numbers ≥ 0 |
400 if the user already has a submission that's pending or approved — the response in that case includes the existing record under data so you can show its status instead of a blind error.
Returns the caller's latest submission: { status, reviewNote, data }. status is not-submitted if nothing's been submitted yet, otherwise one of pending, under-review, approved, or rejected — the same values the KYC gate's 403 responses use, so you can drive one status-handling code path from both.
Card lifecycle
| Method & path | Purpose |
|---|---|
GET /cards | List the user's cards, paginated (page, limit) |
GET /cards/pending | List in-flight card creation requests — query page, limit, completed |
GET /cards/:cardId | Basic card details |
GET /cards/info/:cardId | Full card details, including sensitive card data (PAN/expiry) — render behind an explicit "reveal" action |
GET /cards/balance/:cardId | A card's current balance |
POST /cards/funding/:cardId | Load or withdraw funds — body { amount, side: "fund" | "withdraw", assetId } (assetId required when funding) |
POST /cards/status/:cardId | Freeze or unfreeze — body { action: "enable" | "suspend" } |
DELETE /cards/:cardId | Close a card permanently |
GET /cards/transactions/:cardId | Transaction history for one card, paginated |
GET /cards/transactions | Transaction history across all of the user's cards, paginated |
/cards/info/:cardId. Never log this response, and avoid persisting it client-side beyond the current view. See Revealing card details for a lower-exposure alternative.
Revealing card details
As an alternative to GET /cards/info/:cardId — where the full PAN and CVV travel
straight into your own frontend's memory — this flow hands you a short-lived, one-time link to a
hosted page that displays the sensitive details itself. Embed that link in an iframe
rather than parsing the data out of your own API response.
Returns { success: true, iframeUrl, expiresIn: 120 }. iframeUrl points at /cards/reveal/:token on a reveal-specific host — load it in an iframe for the user to view (and typically copy) their PAN/CVV/expiry without those values ever reaching your own application code.
Not an API call your app parses — it's the page the iframe above loads. The token is valid for 120 seconds and only once: reading it (whether by loading the page or letting it expire) invalidates it immediately, so call /cards/info-sensitive/:cardId fresh each time a user wants to view a card's details.
Affiliate portal
Separate from the main end-user app: a dashboard for users who have been granted affiliate
status, letting them onboard referred users and see referral performance. Gated by
isAffiliate on the caller's account — building this is only relevant if your
product includes an affiliate program.
| Method & path | Purpose |
|---|---|
GET /portal/stats | Aggregate stats across the affiliate's referred users |
GET /portal/users | Paginated list of referred users — query page, limit, search |
GET /portal/cards | Cards belonging to referred users |
GET /portal/transactions | Deposits/withdrawals of referred users |
POST /portal/user | Create a new end user under this affiliate — body firstName, lastName, email, password, contact |
PUT /portal/user | Adjust a referred user's fee overrides — userId plus any of depositFee, withdrawalFee, cardCreationFee, referralFeePercent |
PUT /portal/referral | Toggle whether a referred user can themselves refer others |
POST /portal/user returns the
password you sent in plaintext so the portal can relay it to the newly created user —
display it once and do not store it.
Affiliate portal login uses the same OTP mechanism with purpose: "portal-login"
against POST /auth/otp, followed by the standard POST /auth/login.
Your own back office
Beyond the email-template setup covered earlier, a set of /admin/* endpoints is
available to an account with admin rights on your own tenant — useful for
building an internal ops dashboard for your own team, scoped to your own users. Everything here
requires POST /auth/admin/login first; use the resulting access token as the bearer
token, same as the email-template setup call.
Two kinds of access control
Every call below needs a bearer token from an admin account on your tenant. Some additionally require that account to hold a specific permission, or to be flagged as your tenant's super-admin:
- Permission-gated — the account needs a specific permission key (listed per endpoint below) assigned to it, unless it's the super-admin.
GET /permissionslists every valid key;GET /roles/POST /roles/PUT /roles/:roleIdlet you define roles bundling permission keys and assign them to your team's accounts. - Super-admin only — the account must be flagged as your tenant's super-admin specifically; holding every individual permission key isn't equivalent. A super-admin automatically passes every permission check too.
Deposits & transactions
| Method & path | Purpose | Access |
|---|---|---|
GET /admin/deposit | Paginated deposit/withdrawal list, scoped to your tenant — query page, limit, type, status, startDate, endDate, search | Any admin |
GET /admin/stats | Aggregate user stats for your tenant | Any admin |
GET /admin/transactions | Transaction list, scoped to your tenant — query startDate, endDate (required), type | Any admin |
Users
| Method & path | Purpose | Access |
|---|---|---|
GET /admin/users | Paginated user list for your tenant — query page, limit, search, isAdmin | Any admin |
PUT /admin/users/update/:userId | Edit a user on your tenant — any of isAdmin, active, firstName, lastName, canRefer, email, isAffiliate, cardCreationFee, referralFeePercent, withdrawalFee, depositFee, roleIds, permissionKeys, earnsFromDeposit | Any admin |
GET /admin/users/:userId/bins | A user's resolved card-product pricing overrides | Any admin |
PUT /admin/users/:userId/update-bin | Set a per-user price/top-up-fee override for a BIN — body { binId, price, topUpFee, defaultPrice? } | change_card_fees |
DELETE /admin/users/:userId/bin/:binId | Remove a per-user BIN override | change_card_fees |
GET /admin/users/:userId/wallet | A user's deposit wallet addresses | Any admin |
DELETE /admin/users/:userId/refresh-tokens | Revoke all of a user's active sessions | Super-admin |
PUT /admin/users/:userId/update-bin won't let you
set price or defaultPrice below the BIN's own resolved price, and rejects
a topUpFee over 50%.
Cards
| Method & path | Purpose | Access |
|---|---|---|
GET /admin/cards | Paginated card list for your tenant | Any admin |
GET /admin/cards/info | Card balance lookup | Any admin |
GET /admin/cards/user/:userId | A specific user's cards | Any admin |
GET /admin/cards/balance/:cardId | A specific card's balance | Any admin |
GET /admin/cards/transactions / /:cardId | Card transaction history, tenant-wide or for one card | Any admin |
GET /admin/cards/pending | In-flight card creation requests for your tenant — query page, limit, search | Any admin |
GET /admin/cards/pending-holders | In-flight cardholder verification requests for your tenant — query page, limit, search, all, provider | Any admin |
GET /admin/cards/pending/all | In-flight card creation requests for your tenant, with completed/processing filters | Any admin |
PUT /admin/cards/flag/:cardId / /unflag/:cardId | Freeze / unfreeze a card | freeze_delete_card |
DELETE /admin/cards/:cardId | Close a card | freeze_delete_card |
Referrals & KYC review
| Method & path | Purpose | Access |
|---|---|---|
GET /admin/referrals | Referral commission totals for your tenant | Any admin |
GET /admin/referrals/:userId | One user's referral detail | Any admin |
GET /admin/referral-applications | Pending affiliate applications for your tenant | Any admin |
PUT /admin/referral-applications/:applicationId | Approve or reject an affiliate application | review_referral_application |
GET /admin/kyc-submissions | Account-KYC submissions for your tenant — pairs with account KYC | Any admin |
GET /admin/kyc-submissions/:submissionId | A single submission's detail | Any admin |
PUT /admin/kyc-submissions/:submissionId | Approve, reject, or move a submission to review | Super-admin |
Tenant settings
| Method & path | Purpose | Access |
|---|---|---|
PUT /admin/whitelabel | Update your own tenant's settings — walletAddress, cardCreationFee, userRefferalFeePercent, userDepositFeePercent, userWithdrawalFeePercent, logo | Any admin |
GET /admin/whitelabel/:whiteLabelId | Your tenant's current settings (the path segment is ignored the same way as the email-templates endpoint — it always resolves to your own tenant) | Any admin |
GET /admin/whitelabel/:whiteLabelId/subscription-status | Whether subscriptions are enabled for your tenant, and related billing state | Any admin |
POST /admin/whitelabel/address | Register a settlement address for your tenant | Super-admin |
GET /admin/whitelabel/address | List your tenant's settlement addresses | Any admin |
PUT /admin/whitelabel/:whiteLabelId/admin | Grant admin rights to one of your users | Super-admin |
PUT /admin/whitelabel/:whiteLabelId/remove-admin | Revoke a user's admin rights | Super-admin |
GET /admin/whitelabel/settlements · /settlements/stats · /settle-earnings | Settlement history and stats for your tenant | Any admin |
DELETE /admin/whitelabel/address | Remove a settlement address | Super-admin |
subscriptionEnabled or kycEnabled isn't self-service.
Those live on the platform-only tenant-configuration endpoint, not
PUT /admin/whitelabel — talk to your onboarding contact to turn either on.
Card products & subscription packages
Manage your tenant's own pricing on top of the platform's card products — see Card issuance overview and Subscription-priced cards for how these surface to end users.
| Method & path | Purpose | Access |
|---|---|---|
GET /admin/whitelabel/:whiteLabelId/bins | Your tenant's BIN pricing overrides | Any admin |
PUT /admin/whitelabel/:whiteLabelId/update-bin | Set your tenant-wide price/top-up-fee/default-price for a BIN | change_card_fees |
DELETE /admin/whitelabel/:whiteLabelId/bin/:binId | Remove a tenant-wide BIN override | change_card_fees |
GET /admin/whitelabel/:whiteLabelId/subscription-packages | Your tenant's subscription packages | Any admin |
POST /admin/whitelabel/:whiteLabelId/subscription-package | Create a package — name, binId, durationMonths, price, defaultPrice | change_card_fees |
PUT /admin/whitelabel/:whiteLabelId/subscription-package/:packageId | Update a package | change_card_fees |
DELETE /admin/whitelabel/:whiteLabelId/subscription-package/:packageId | Remove a package | change_card_fees |
basePrice or
platformFeePercent from a tenant admin — those stay platform-controlled regardless of
what you send.
Coupons
Coupons let you override pricing and referral eligibility for a specific group of users on your
tenant — see the couponId references in card creation
and subscriptions for how a coupon affects what an end user is
actually charged.
| Method & path | Purpose | Access |
|---|---|---|
GET /admin/coupons | List your tenant's coupons | Any admin |
GET /admin/coupons/:couponId/users | Users currently on a given coupon | Any admin |
GET /admin/whitelabel/:whiteLabelId/coupon/:couponId | A single coupon's configuration | Any admin |
POST /admin/whitelabel/:whiteLabelId/coupon | Create a coupon | manage_coupon |
PUT /admin/whitelabel/:whiteLabelId/coupon/:couponId | Update a coupon | manage_coupon |
DELETE /admin/whitelabel/:whiteLabelId/coupon/:couponId | Delete a coupon | manage_coupon |
Admin roles & permissions
| Method & path | Purpose | Access |
|---|---|---|
GET /admin/permissions | Every valid permission key | Any admin |
GET /admin/roles | Roles defined on your tenant | Any admin |
POST /admin/roles | Create a role — name, permissionKeys | manage_admins |
PUT /admin/roles/:roleId | Update a role's permission keys | manage_admins |
manage_admins key itself unless the account creating or
editing it is the tenant's super-admin.
Error handling
Errors follow a consistent shape across the API:
{ "error": "Insufficient balance" }
// or, on validation failures:
{ "error": [ { "path": "email", "message": "Invalid email" } ] }
| Status | Meaning |
|---|---|
400 | Validation failure, business-rule rejection (insufficient balance, invalid OTP, duplicate email, etc.) |
401 | Missing, malformed, or expired bearer token; or a token whose tenant doesn't match x-api-key |
403 | Caller is authenticated but blocked by the KYC gate — body includes kycStatus |
404 | Resource not found (e.g. a card id that doesn't belong to the caller) |
500 | Unexpected server error — safe to retry once, then surface a generic failure message |
Note that several success responses return HTTP 200 where you might expect
201 (e.g. registration, card creation) — check the response body rather than
assuming a status code convention.
Endpoint index
Every partner-facing endpoint in one table, grouped by area.
Auth
| POST | /auth/otp | Request a one-time code |
| POST | /auth/register | Create an account |
| POST | /auth/check-credentials | Pre-validate email + password |
| POST | /auth/login-otp-required | Check whether login needs an OTP |
| POST | /auth/login | Complete login |
| POST | /auth/reset-password | Reset a forgotten password |
| POST | /auth/confirm-password | Re-confirm current password |
| POST | /auth/refresh-token | Rotate to a new access token |
| POST | /auth/logout | Revoke a refresh token |
Users
| GET | /users | Profile |
| PUT | /users | Update profile |
| GET | /users/assets | Balances |
| POST | /users/transfer | External withdrawal |
| POST | /users/internal-transfer | Platform-to-platform transfer |
| GET | /users/beneficiaries | Past transfer recipients |
| PUT | /users/password | Change password |
| POST | /users/email/verify | Verify current email |
| PUT | /users/email | Change email |
| POST | /users/2fa/authenticator/setup | Start authenticator enrollment |
| POST | /users/2fa/authenticator/verify | Confirm authenticator enrollment |
| PUT | /users/2fa/method | Switch active 2FA method |
| DELETE | /users/2fa/disabled2fa/authenticator | Remove authenticator app |
| GET | /users/deposits | Deposit/withdrawal history |
| GET | /users/deposit/:depositId | Single record |
| GET | /users/lookup | Find a user by email |
| GET | /users/dex-transactions | Swap history |
| POST | /users/dex-transactions/:id/start | Begin a pending swap |
| POST | /users/dex-transactions/:id/withdraw | Withdraw a swap's output |
| GET | /users/check-cashback | Cashback eligibility |
| GET | /users/notifications | Unread transfer count |
| POST | /users/card-holder | Submit cardholder verification |
| GET | /users/card-holder | List cardholder verification records |
| POST | /users/kyc | Submit account KYC |
| GET | /users/kyc | Account KYC status |
| PUT | /users/preferred-currency | Set display currency |
| GET | /users/supported-tokens | Active tokens/networks |
| GET | /users/wallet/:tokenId | Deposit address |
| GET | /users/rate | Exchange rate |
| GET | /users/check | Email/referral/coupon check (public) |
| POST | /users/upload-file | Upload a document |
| GET | /users/countries · /users/cities | Reference data |
| POST | /users/referral-code | Attach a referral/coupon code after registration |
| GET | /users/referrals · /referrals/list | Referral stats |
| POST | /users/referrals/withdraw · /referrals/apply | Referral payout / application |
| GET | /users/referrals/application | Referral/affiliate application status |
Cards
| GET | /cards/bins | Available card products |
| POST | /cards | Create a card |
| GET | /cards | List cards |
| GET | /cards/pending | Pending card requests |
| GET | /cards/:cardId | Card details |
| GET | /cards/info/:cardId | Full card details |
| GET | /cards/balance/:cardId | Card balance |
| GET | /cards/info-sensitive/:cardId | One-time card-detail reveal link |
| POST | /cards/funding/:cardId | Fund / withdraw |
| POST | /cards/status/:cardId | Freeze / unfreeze |
| DELETE | /cards/:cardId | Close card |
| GET | /cards/transactions(/:cardId) | Transaction history |
| GET | /cards/subscriptions | Available subscription packages |
| GET | /cards/bin/:binId | Single BIN's resolved pricing |
| GET | /cards/subscription/:cardId | A card's subscription state |
| PUT | /cards/subscription/:cardId/package | Change subscription package |
| POST | /cards/subscription/:cardId/retry · /cancel · /reactivate | Manage a card's subscription |
Statements
| POST | /statement/csv · /statement/pdf | Generate an account or card statement |
Portal (affiliate accounts only)
| GET | /portal/stats · /users · /cards · /transactions | Dashboards |
| POST | /portal/user | Create a referred user |
| PUT | /portal/user · /referral | Adjust fee overrides / referral flag |
/admin/* (tenant configuration, manual deposit/withdrawal review, card
fleet management, platform-wide settings) are restricted to your organization's own
back-office and are intentionally excluded from this guide.