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.
- 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. Use your own white-label id (the same value you send as x-api-key) as the path parameter. Once created, use PUT on the same path to update any field later — send only the fields you're changing.
Account-level fields
| Field | What it is |
|---|---|
apiUserId | From SendPulse account settings → API |
apiSecret | From SendPulse account settings → API |
sendPulseEmail | The verified "from" address your notifications will be sent from |
supportName | The support contact name shown to your users inside emails |
Required templates
The setup call is rejected if any of these are missing:
| Field | Template | Sent when |
|---|---|---|
registrationTemplateId | Registration | A new user requests a code to verify their email during sign-up |
loginOtpTemplateId | Login OTP | A user requests a code to complete login |
forgetPasswordOtpTemplateId | Forgot password OTP | A user requests a code to reset their password |
profileUpdateOtpTemplateId | Profile update OTP | A user requests a code to confirm a profile or security change |
withdrawalRequestOtpTemplateId | Withdrawal request OTP | A user requests a code to confirm an external withdrawal |
referralSignUpTemplateId | Referral sign-up | Sent when someone signs up using a user's referral code |
adminWithdrawalNotificationTemplateId | Ops: withdrawal notification | Internal notice to your support/ops team when a withdrawal is requested |
adminCardCreationNotificationTemplateId | Ops: card creation notification | Internal notice to your support/ops team when a card is requested |
adminDepositNotificationTemplateId | Ops: deposit notification | Internal notice to your support/ops team when a deposit is received |
adminLoginNotificationTemplateId | Ops: login notification | Internal notice to your support/ops team when a user logs in |
adminProfileUpdateNotificationTemplateId | Ops: profile update notification | Internal notice to your support/ops team when a user changes their profile |
Recommended templates
Optional — skip only if the related feature won't be exposed in your product. Field names follow the same pattern as the required table above (e.g. depositProcessingTemplateId); if you're ever unsure of an exact key, the setup call's validation error names it for you:
| Template | Sent when |
|---|---|
| Affiliate portal login OTP / registration / forgot password | An affiliate requests a code to log into, register for, or reset a password on the referral portal |
| Deposit processing / confirmed | A deposit is detected on-chain, then credited to the user's balance |
| Card funding pending / confirmed / failed | A card top-up or withdrawal changes state |
| Card creation pending / completed (standard onboarding) | A card request that went through full identity verification changes state |
| Card creation pending / completed (fast-path onboarding) | A card request that used the simplified "default holder" path changes state |
| Ops: fast-path card creation notification | Internal notice when a card is created via the simplified onboarding path |
| Identity verification approved / rejected | A user's submitted verification record is reviewed |
| Referral accepted | A user's referral application is approved |
| Internal transfer OTP | A user requests a code to confirm a platform-to-platform transfer |
| Internal transfer processing | An internal transfer is submitted and awaiting settlement |
| Withdrawal successful | An external withdrawal completes |
| Internal transfer successful | An internal transfer completes |
Once POST /admin/whitelabel/:whiteLabelId/email-templates returns success,
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 on their account |
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.
// 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 |
update | Confirming a profile/password change | 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.
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.
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.
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, email, telegram, whatsapp, password, twoFactorAuthMethod, preferredCurrency, otp (otp required) |
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 a referral code is valid (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 identity verification form.
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.
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 |
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).
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 |
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) |
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.
Identity 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.
- 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.
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 |
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.
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.
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 |
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 |
| GET | /users/deposits | Deposit/withdrawal history |
| GET | /users/deposit/:depositId | Single record |
| GET | /users/lookup | Find a user by email |
| GET | /users/notifications | Unread transfer count |
| POST | /users/card-holder | Submit identity verification |
| GET | /users/card-holder | List verification records |
| 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 check (public) |
| POST | /users/upload-file | Upload a document |
| GET | /users/countries · /users/cities | Reference data |
| GET | /users/referrals · /referrals/list | Referral stats |
| POST | /users/referrals/withdraw · /referrals/apply | Referral payout / application |
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 |
| POST | /cards/funding/:cardId | Fund / withdraw |
| POST | /cards/status/:cardId | Freeze / unfreeze |
| DELETE | /cards/:cardId | Close card |
| GET | /cards/transactions(/:cardId) | Transaction history |
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.