Integration Guide · v1

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.

Base URL: https://<your-assigned-host> Content-Type: application/json Auth: Bearer JWT + x-api-key
01

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:

HTTP 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.

Consistency matters. An access token is minted for one tenant. If you send it back alongside a different 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).

02

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.

This gates everything else. Registration and login both depend on an emailed one-time code. If your tenant's templates aren't configured yet, POST /auth/otp will have nothing to deliver — treat template setup as step zero, not a launch-week detail.
You need an admin account to do this. Submitting your template configuration is an administrative action, not a regular end-user one. It requires logging in through 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.
  1. 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.
  2. 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.
  3. 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.
  4. Log in as an admin. Call POST /auth/admin/login with the email/password of a user on your tenant that has admin rights, to get an access token.
  5. Submit your configuration. Call POST /admin/whitelabel/:whiteLabelId/email-templates with the admin access token, your SendPulse credentials, and every template ID from the tables below. This is what actually enables POST /auth/otp to deliver mail for your tenant.
POST /auth/admin/login No bearer token

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.

POST /admin/whitelabel/:whiteLabelId/email-templates Admin bearer required

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

FieldWhat it is
apiUserIdFrom SendPulse account settings → API
apiSecretFrom SendPulse account settings → API
sendPulseEmailThe verified "from" address your notifications will be sent from
supportNameThe support contact name shown to your users inside emails

Required templates

The setup call is rejected if any of these are missing:

FieldTemplateSent when
registrationTemplateIdRegistrationA new user requests a code to verify their email during sign-up
loginOtpTemplateIdLogin OTPA user requests a code to complete login
forgetPasswordOtpTemplateIdForgot password OTPA user requests a code to reset their password
profileUpdateOtpTemplateIdProfile update OTPA user requests a code to confirm a profile or security change
withdrawalRequestOtpTemplateIdWithdrawal request OTPA user requests a code to confirm an external withdrawal
referralSignUpTemplateIdReferral sign-upSent when someone signs up using a user's referral code
adminWithdrawalNotificationTemplateIdOps: withdrawal notificationInternal notice to your support/ops team when a withdrawal is requested
adminCardCreationNotificationTemplateIdOps: card creation notificationInternal notice to your support/ops team when a card is requested
adminDepositNotificationTemplateIdOps: deposit notificationInternal notice to your support/ops team when a deposit is received
adminLoginNotificationTemplateIdOps: login notificationInternal notice to your support/ops team when a user logs in
adminProfileUpdateNotificationTemplateIdOps: profile update notificationInternal 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:

TemplateSent when
Affiliate portal login OTP / registration / forgot passwordAn affiliate requests a code to log into, register for, or reset a password on the referral portal
Deposit processing / confirmedA deposit is detected on-chain, then credited to the user's balance
Card funding pending / confirmed / failedA 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 notificationInternal notice when a card is created via the simplified onboarding path
Identity verification approved / rejectedA user's submitted verification record is reviewed
Referral acceptedA user's referral application is approved
Internal transfer OTPA user requests a code to confirm a platform-to-platform transfer
Internal transfer processingAn internal transfer is submitted and awaiting settlement
Withdrawal successfulAn external withdrawal completes
Internal transfer successfulAn internal transfer completes

Once POST /admin/whitelabel/:whiteLabelId/email-templates returns success, move on to Authentication model to start integrating.

03

Authentication model

Two independent credentials travel on every authenticated request: the tenant header described above, and a bearer token that identifies the end user.

HeaderApplies toValue
x-api-keyEvery requestYour white-label identifier
AuthorizationAny endpoint requiring a logged-in userBearer <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.

04

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.

  1. Request an OTP. Call POST /auth/otp with purpose: "register" and the candidate email. The backend checks the email isn't already registered under your tenant and emails a one-time code.
  2. Submit registration. Call POST /auth/register with 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.
POST /auth/otp No bearer token

Requests a one-time code for a given purpose. For registration, body is:

FieldTypeRequired
purpose"register"required
emailstringrequired

Response: { success: true, message: "Otp sent", type: "email" }. 400 if the email is already registered under your tenant.

POST /auth/register No bearer token
FieldTypeRequiredNotes
firstNamestringrequiredmin 3 chars
lastNamestringrequiredmin 3 chars
emailstringrequiredlowercased & trimmed automatically
passwordstringrequiredmin 8 chars; needs upper, lower, digit, and special character
otpstringrequiredfrom the previous step
refBystringoptionala 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.

JavaScript — registration
// 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.
05

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

  1. Collect credentials, check them silently. Call POST /auth/check-credentials with email + password. This validates the password without starting a session, so you can show a real-time error before asking for a second factor.
  2. Check whether a code is required. Call POST /auth/login-otp-required with the email. Nearly every account has this on, but it lets you skip the extra field for the rare account that doesn't.
  3. Request the code, if needed. Call POST /auth/otp with purpose: "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.
  4. Submit the full login. Call POST /auth/login with email, password, and the code. On success you receive an access token, a refresh token, and the user's profile.
POST /auth/login No bearer token
FieldTypeRequired
emailstringrequired
passwordstringrequired
otpstringrequired

Response: { accessToken, refreshToken, userData }, where userData includes id, email, firstName, lastName, balance, totalCommisions, refBy, isAdmin, whiteLabelId. 400 on any credential or code mismatch.

JavaScript — full login flow
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.

06

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.

PurposeUsed forRequires bearer tokenExtra fields
registerNew account email verificationNoemail
loginStandard login second factorNoemail
portal-loginAffiliate portal loginNoemail
forget-passwordPassword resetNoemail
withdrawalConfirming an external transferYesamount, address
updateConfirming a profile/password changeYes
internal-transferConfirming a platform-to-platform transferYesamount, 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.

Don't cache "OTP sent" as success. A 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.
07

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:

  1. User reviews the action (e.g. "Withdraw 100 USDT to 0xabc...").
  2. Request the OTP for the relevant purpose (withdrawal, update, or internal-transfer) — this call needs the bearer token.
  3. User confirms with the code, and you submit the action endpoint with otp included in the body.

Endpoints that follow this pattern: POST /users/transfer, POST /users/internal-transfer, PUT /users, PUT /users/password.

08

Password management

POST/auth/reset-passwordNo bearer token

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.

PUT/users/passwordBearer required

Body: { password, newPassword, otp } — changes the password for a logged-in user (request the code with purpose: "update" first).

POST/auth/confirm-passwordBearer required

Body: { password }. Returns { valid: boolean } — use to re-confirm identity before showing a sensitive settings screen, without a full OTP round trip.

09

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.

POST/auth/refresh-tokenNo bearer token

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.

POST/auth/logoutBearer required

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.

JavaScript — silent refresh wrapper
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;
}
10

Profile & assets

Method & pathPurpose
GET /usersCurrent user's profile, including resolved fee rates
PUT /usersUpdate profile — firstName, lastName, email, telegram, whatsapp, password, twoFactorAuthMethod, preferredCurrency, otp (otp required)
GET /users/assetsPer-token balances
PUT /users/preferred-currencySet display currency — { preferredCurrency }
GET /users/rateExchange rate lookup — query from, to
GET /users/checkPublic — check if an email is taken or a referral code is valid (no bearer token needed)
GET /users/countriesFull country reference list — covers every country, not a limited subset
GET /users/citiesCity reference list — pass ?regionCode=<country code> to scope to one country; omit it to get every city
POST /users/upload-fileMultipart 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.

11

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.

GET/users/supported-tokensBearer required

Optional query: network to filter. Returns an array of tokens, each including a nested network object:

Response shape
[
  {
    "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:

NetworkChain typeNotes
Ethereum MainnetethereumUSDT (ERC-20), USDC (ERC-20)
Tron MainnettronUSDT (TRC-20)

Test networks (Sepolia, Shasta) are available in non-production environments only.

Development environment. Against the development/staging host, only test tokens on Ethereum Sepolia and Tron Shasta are active — never mainnet USDT/USDC. Fund a test wallet from a faucet before testing deposits, transfers, or card funding:
12

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.

GET/users/wallet/:tokenIdBearer required

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.

JavaScript — show a deposit address
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 & pathPurpose
GET /users/depositsPaginated deposit/withdrawal history — query page, limit, type
GET /users/deposit/:depositIdSingle 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.

13

Withdrawals & transfers

POST/users/transferBearer required · step-up OTP

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.

POST/users/internal-transferBearer required · step-up OTP

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 & pathPurpose
GET /users/lookupLook up a user by email before an internal transfer — query email
GET /users/beneficiariesPaginated list of past internal-transfer recipients
GET /users/notificationsUnread internal-transfer notification count
PUT /users/notifications/internal-transfer/increment / /decrementAdjust the unread badge counter
14

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 no holderId.
  • needCardHolder: true — a card holder must exist for that BIN before a card can be created against it. Either pass an already-approved holderId, or omit holderId entirely 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).
GET/cards/binsBearer required

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.

POST/cardsBearer required
FieldTypeRequiredNotes
binnumberrequiredthe id field of a card product from GET /cards/binsnot its bin field (the six-digit card number prefix)
useTypestringrequired
costnumberrequiredamount to load onto the card, ≥ 0
assetIdnumberrequiredtoken to debit for cost + fee
quantitynumberoptionaldefaults to 1
firstName, lastName, email, dateOfBirthconditionalrequired when the selected BIN has needCardHolder: false
addressobjectoptionaladdressLine1, city, state, country, postalCode required within it if present
holderIdstringconditionalan 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.

15

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.

  1. Upload documents. Call POST /users/upload-file three times — for the front of the ID, the back of the ID, and a selfie holding the ID — collecting a fileId each time.
  2. Submit the verification record. Call POST /users/card-holder with the applicant's personal details and the three file ids.
  3. Poll for approval. Call GET /users/card-holder until the record's status indicates it's ready, then use its id as holderId in POST /cards.
POST/users/card-holderBearer required
FieldType / format
firstName, lastName2–32 chars, letters and spaces only
email5–50 chars
phone5–20 chars
areaCode2–5 chars (phone country code)
birthdaydate
nationality, country2-letter ISO country code — pull valid values from GET /users/countries
citystring — pull valid values from GET /users/cities?regionCode=<country>
address2–40 chars, letters/numbers/hyphen/space
postalCode2–15 chars, alphanumeric
gender"M" or "F"
occupationstring
annualSalarynumber > 0
accountPurposestring
expectedMonthlyVolumenumber > 0
idTypePASSPORT · HK_HKID · DLN · GOVERNMENT_ISSUED_ID_CARD
idNumber2–50 chars
issueDate, idExpiryDatedate
binIdnumber — 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)
ipAddressthe applicant's IP address
idFrontId, idBackId, idHoldIdfile 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.

Approval is mandatory. A verification record must reach an approved status before its id can be used as 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.
16

Card lifecycle

Method & pathPurpose
GET /cardsList the user's cards, paginated (page, limit)
GET /cards/pendingList in-flight card creation requests — query page, limit, completed
GET /cards/:cardIdBasic card details
GET /cards/info/:cardIdFull card details, including sensitive card data (PAN/expiry) — render behind an explicit "reveal" action
POST /cards/funding/:cardIdLoad or withdraw funds — body { amount, side: "fund" | "withdraw", assetId } (assetId required when funding)
POST /cards/status/:cardIdFreeze or unfreeze — body { action: "enable" | "suspend" }
DELETE /cards/:cardIdClose a card permanently
GET /cards/transactions/:cardIdTransaction history for one card, paginated
GET /cards/transactionsTransaction history across all of the user's cards, paginated
PAN handling. Full card numbers and CVV are only returned from /cards/info/:cardId. Never log this response, and avoid persisting it client-side beyond the current view.
17

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 & pathPurpose
GET /portal/statsAggregate stats across the affiliate's referred users
GET /portal/usersPaginated list of referred users — query page, limit, search
GET /portal/cardsCards belonging to referred users
GET /portal/transactionsDeposits/withdrawals of referred users
POST /portal/userCreate a new end user under this affiliate — body firstName, lastName, email, password, contact
PUT /portal/userAdjust a referred user's fee overrides — userId plus any of depositFee, withdrawalFee, cardCreationFee, referralFeePercent
PUT /portal/referralToggle whether a referred user can themselves refer others
Plaintext password in the response. 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.

18

Error handling

Errors follow a consistent shape across the API:

Error response
{ "error": "Insufficient balance" }
// or, on validation failures:
{ "error": [ { "path": "email", "message": "Invalid email" } ] }
StatusMeaning
400Validation failure, business-rule rejection (insufficient balance, invalid OTP, duplicate email, etc.)
401Missing, malformed, or expired bearer token; or a token whose tenant doesn't match x-api-key
404Resource not found (e.g. a card id that doesn't belong to the caller)
500Unexpected 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.

19

Endpoint index

Every partner-facing endpoint in one table, grouped by area.

Auth

POST/auth/otpRequest a one-time code
POST/auth/registerCreate an account
POST/auth/check-credentialsPre-validate email + password
POST/auth/login-otp-requiredCheck whether login needs an OTP
POST/auth/loginComplete login
POST/auth/reset-passwordReset a forgotten password
POST/auth/confirm-passwordRe-confirm current password
POST/auth/refresh-tokenRotate to a new access token
POST/auth/logoutRevoke a refresh token

Users

GET/usersProfile
PUT/usersUpdate profile
GET/users/assetsBalances
POST/users/transferExternal withdrawal
POST/users/internal-transferPlatform-to-platform transfer
GET/users/beneficiariesPast transfer recipients
PUT/users/passwordChange password
GET/users/depositsDeposit/withdrawal history
GET/users/deposit/:depositIdSingle record
GET/users/lookupFind a user by email
GET/users/notificationsUnread transfer count
POST/users/card-holderSubmit identity verification
GET/users/card-holderList verification records
PUT/users/preferred-currencySet display currency
GET/users/supported-tokensActive tokens/networks
GET/users/wallet/:tokenIdDeposit address
GET/users/rateExchange rate
GET/users/checkEmail/referral check (public)
POST/users/upload-fileUpload a document
GET/users/countries · /users/citiesReference data
GET/users/referrals · /referrals/listReferral stats
POST/users/referrals/withdraw · /referrals/applyReferral payout / application

Cards

GET/cards/binsAvailable card products
POST/cardsCreate a card
GET/cardsList cards
GET/cards/pendingPending card requests
GET/cards/:cardIdCard details
GET/cards/info/:cardIdFull card details
POST/cards/funding/:cardIdFund / withdraw
POST/cards/status/:cardIdFreeze / unfreeze
DELETE/cards/:cardIdClose card
GET/cards/transactions(/:cardId)Transaction history

Portal (affiliate accounts only)

GET/portal/stats · /users · /cards · /transactionsDashboards
POST/portal/userCreate a referred user
PUT/portal/user · /referralAdjust fee overrides / referral flag
Not covered here. Beyond the admin login and email-template setup call described in Set up your email templates, administrative endpoints under /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.