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 a super-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. 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.
  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 Super-admin bearer required

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.

GET /admin/whitelabel/email-templates Admin bearer required

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.

The path :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.

FieldWhat it is
apiUserIdFrom SendPulse account settings → API
apiSecretFrom SendPulse account settings → API
sendPulseEmailThe verified "from" address your notifications are sent from
supportNameThe 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.

FieldSent whenVariables
registrationTemplateIdrequiredNew user requests a code to verify their email during sign-upotp
loginOtpTemplateIdrequiredUser with email 2FA requests a code to complete loginotp
forgetPasswordOtpTemplateIdrequiredUser with email 2FA requests a password-reset codeotp
profileUpdateOtpTemplateIdrequiredUser requests a code to confirm a profile / security changeotp
withdrawalRequestOtpTemplateIdrequiredUser requests a code to confirm an external (on-chain) withdrawalotp   amount   address
internalTransferOtpTemplateIdoptionalUser requests a code to confirm a platform-to-platform transferotp   amount ("100 USDT")   toName   address
swapWithdrawalOtpTemplateIdoptionalUser requests a code to confirm a token swap withdrawalotp

User money notifications

Delivered to the end user. All optional — configure the ones matching features you expose.

FieldSent whenVariables
depositProcessingTemplateIdoptionalAn on-chain deposit is detected, before it's creditedname   token   amount   network   txid
depositConfirmedTemplateIdoptionalThe deposit is credited to the user's balancename   token   amount
withdrawalSuccessNotificationTemplateIdoptionalAn external withdrawal completes on-chaintoken   amount   amountReceived   address
internalTransferDepositProcessingTemplateIdoptionalAn incoming internal transfer is submitted, awaiting settlement (sent to the recipient)name   toName   token   amount   network   txid
internalWithdrawalSuccessfulNotificationTemplateIdoptionalAn 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.

FieldSent whenVariables
cardFundingPendingTemplateIdoptionalA card top-up is submitted and processingname   cardholderName   amount   bin   type (card network)
cardFundingConfirmedTemplateIdoptionalA card top-up succeeds and funds land on the cardname   cardholderName   amount   bin   type
cardFundingFailedTemplateIdreservedAccepted 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.

FieldSent whenVariables
customPlatinumCardCreationPendingTemplateIdoptionalA custom-holder card request enters processingfirstName
customPlatinumCardCreationCompletedTemplateIdoptionalA custom-holder card is issuedfirstName
defaultPlatinumCardCreationPendingTemplateIdoptionalA default-holder (fast-path) card request enters processingfirstName
defaultPlatinumCardCreationCompletedTemplateIdoptionalA default-holder card is issuedfirstName
cardHolderVerifiedNotificationTemplateIdoptionalA submitted cardholder identity is approvedfirstName   lastName   email   Status (= "approved")
cardHolderRejectedNotificationTemplateIdoptionalA submitted cardholder identity is rejectedfirstName   lastName   email   Status (= "rejected")   rejectionReason

Account verification (KYC)

Delivered to the end user as their account-level KYC submission is reviewed.

FieldSent whenVariables
kycApprovedNotificationTemplateIdoptionalA user's account KYC submission is approved—
kycRejectedNotificationTemplateIdoptionalA user's account KYC submission is rejected—
kycUnderReviewNotificationTemplateIdoptionalA user's account KYC submission enters review—

Referral

FieldSent whenVariables
referralSignUpTemplateIdrequiredSomeone signs up with a user's referral code — sent to the referrername (referrer's first name)
userReferralAcceptedNotificationTemplateIdoptionalA user's referral / affiliate application is approvedemail

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.

FieldSent whenVariables
adminDepositNotificationTemplateIdrequiredA user deposit is receivedclient_name   amount   time   platform
adminWithdrawalNotificationTemplateIdrequiredA user requests a withdrawalclient_name   amount   time   platform
adminCardCreationNotificationTemplateIdrequiredA user requests a card (both standard and platinum paths)client_name   client_email   platform   time (platinum path only)
adminWsbCardWithdrawalNotificationTemplateIdoptionalA platinum card withdrawal is requestedclient_name   client_email   amount   time   platform
adminDefaultCardCreationNotificationTemplateIdreservedAccepted and seeded but not currently dispatched (the platinum-card ops mail uses adminCardCreationNotificationTemplateId)—
adminKycSubmissionNotificationTemplateIdoptionalA user submits an account KYC verification record for review—
Previously-documented fields that no longer exist. adminLoginNotificationTemplateId and adminProfileUpdateNotificationTemplateId have been removed from the setup call's schema — don't send them; they're rejected as unrecognized fields.

Affiliate portal

FieldSent whenVariables
portalLoginOtpTemplateIdoptionalAn affiliate requests a code to log into the referral portalotp
portalRegistrationTemplateIdreservedAccepted by the setup call; portal registration is not wired to it yetotp (expected)
portalForgetPasswordOtpTemplateIdreservedAccepted by the setup call; portal password reset is not wired to it yetotp (expected)
Platform-owned templates. A few internal alerts — high-risk deposit review, gas-wallet-low, card top-up / deletion failures — are sent from fixed templates on the platform's own default tenant. You don't configure these and they aren't part of your payload.

Example: initial setup call

POST /admin/whitelabel/<your-id>/email-templates
// 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.

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

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
swap-withdrawalConfirming a token swap withdrawalYes—
updateConfirming a profile/password changeYes—
verify-emailConfirming ownership of the current email before changing itYes—
change-emailConfirming ownership of a new email addressYes—
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.

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.

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

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.

Nearly every authenticated endpoint can return 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:
403 response
{ "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.

09

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.

10

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.

  1. Request a code for the current email. Call POST /auth/otp with purpose: "verify-email" (skip this if the user authenticates with otpMethod: "authenticator" instead).
  2. Verify ownership of the current email. Call POST /users/email/verify with the code. This unlocks the next step but doesn't change anything yet.
  3. Request a code for the new email. Call POST /auth/otp with purpose: "change-email" and the candidate new address.
  4. Submit the change. Call PUT /users/email with the new email and that code.
POST/users/email/verifyBearer required

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.

PUT/users/emailBearer required

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.

11

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.

POST/users/2fa/authenticator/setupBearer required

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.

POST/users/2fa/authenticator/verifyBearer required

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.

PUT/users/2fa/methodBearer required

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.

DELETE/users/2fa/disabled2fa/authenticatorBearer required

Body: { otp, otpMethod? }. Removes the authenticator app from the account and reverts twoFactorAuthMethod to "email".

12

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;
}
13

Profile & assets

Method & pathPurpose
GET /usersCurrent user's profile, including resolved fee rates
PUT /usersUpdate profile — firstName, lastName, telegram, whatsapp, password, twoFactorAuthMethod, authenticatorOtp, preferredCurrency, otp (otp required). Does not accept email — see Changing email for that.
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 if a refBy value is a valid, usable referral or coupon code (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 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:

FieldWhat it is
isAdminWhether this account has admin rights on the tenant — gates access to POST /auth/admin/login
pendingBalanceBalance tied up in not-yet-settled activity, separate from the spendable balances in /users/assets
depositFee, withdrawalFee, cardCreationFee, referralFeePercentThis user's fully-resolved fee rates, after any per-user or coupon overrides are applied on top of your tenant's defaults
subscriptionEnabledPresent (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, pendingReferralRequestWhether the user has an affiliate application awaiting review, and the application record itself if so
14

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:
15

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.

16

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
17

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 & pathPurpose
GET /users/dex-transactionsPaginated swap history for the caller — query page, limit, status, txnType, route, token, startDate, endDate
POST /users/dex-transactions/:id/startBegins processing a pending swap the user initiated
POST /users/dex-transactions/:id/withdrawWithdraws 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.

18

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.

GET/users/check-cashbackBearer required

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

Use-it-or-lose-it. The eligible pool reflects deposit volume accrued so far; it isn't a running balance you can partially claim — build your UI around "what you'd get if you topped up a cashback-eligible card right now," not a persistent earned balance.
19

Statements

Generate a downloadable activity statement, either for the user's whole account or for one card.

POST/statement/csvBearer required

Body: { cardId?, startDate?, endDate? }. Streams a CSV file. Omit cardId for an account-wide statement; include it to scope to one card.

POST/statement/pdfBearer required

Same body shape as the CSV endpoint; streams a PDF instead.

20

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).
Subscription-priced tenants work differently. If your white label has subscriptions turned on, card creation also requires a 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.
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/bins — not 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
packageIdstringconditionalrequired when your tenant has subscriptions enabled — see Subscription-priced cards. Omit entirely on a non-subscription tenant.
firstName, lastName, email, dateOfBirth—conditionalrequired 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)

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.

21

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 & pathPurpose
GET /cards/subscriptionsList the packages available to the caller (general packages plus any personal overrides)
GET /cards/bin/:binIdA single BIN's pricing, resolved the same way as an entry from GET /cards/bins
GET /cards/subscription/:cardIdThe subscription state backing one of the user's cards
PUT /cards/subscription/:cardId/packageChange which package a card is billed under
POST /cards/subscription/:cardId/retryRetry a failed subscription charge
POST /cards/subscription/:cardId/cancelCancel the subscription on a card
POST /cards/subscription/:cardId/reactivateReactivate 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.

22

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.

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

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.

POST/users/kycBearer required
FieldType / format
firstName, lastName2–32 chars
email5–50 chars
phone5–20 chars
areaCodeoptional
birthdaydate
country, state, citystrings — pull valid country/city values from GET /users/countries / GET /users/cities?regionCode=<country>
address2–100 chars
postalCode2–15 chars
idTypePASSPORT · NATIONAL_ID · DRIVERS_LICENSE · GOVERNMENT_ISSUED_ID_CARD — note this list differs from cardholder verification's idType enum
idIssuingCountryoptional
idNumber2–50 chars
issueDatedate
idExpiryDateoptional date
utilityBillFileId, idDocumentFileId, selfieFileIdfile ids from POST /users/upload-file
nationality, gender, occupation, accountPurposeall optional
annualSalary, expectedMonthlyVolumeoptional 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.

GET/users/kycBearer required

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.

24

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
GET /cards/balance/:cardIdA card's current balance
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. See Revealing card details for a lower-exposure alternative.
25

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.

GET/cards/info-sensitive/:cardIdBearer required

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.

GET/cards/reveal/:tokenNo bearer token (single-use link)

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.

26

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.

27

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.

This is not the full admin surface. The platform's own back office exposes a much larger set of endpoints for platform-wide configuration (creating white labels, editing global card products and supported networks, cross-tenant balance reconciliation, and more). Those are restricted to the platform's own tenant and are not documented here — nothing below will work against another partner's data no matter which account calls it.

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 /permissions lists every valid key; GET /roles / POST /roles / PUT /roles/:roleId let 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 & pathPurposeAccess
GET /admin/depositPaginated deposit/withdrawal list, scoped to your tenant — query page, limit, type, status, startDate, endDate, searchAny admin
GET /admin/statsAggregate user stats for your tenantAny admin
GET /admin/transactionsTransaction list, scoped to your tenant — query startDate, endDate (required), typeAny admin

Users

Method & pathPurposeAccess
GET /admin/usersPaginated user list for your tenant — query page, limit, search, isAdminAny admin
PUT /admin/users/update/:userIdEdit a user on your tenant — any of isAdmin, active, firstName, lastName, canRefer, email, isAffiliate, cardCreationFee, referralFeePercent, withdrawalFee, depositFee, roleIds, permissionKeys, earnsFromDepositAny admin
GET /admin/users/:userId/binsA user's resolved card-product pricing overridesAny admin
PUT /admin/users/:userId/update-binSet a per-user price/top-up-fee override for a BIN — body { binId, price, topUpFee, defaultPrice? }change_card_fees
DELETE /admin/users/:userId/bin/:binIdRemove a per-user BIN overridechange_card_fees
GET /admin/users/:userId/walletA user's deposit wallet addressesAny admin
DELETE /admin/users/:userId/refresh-tokensRevoke all of a user's active sessionsSuper-admin
Per-user BIN pricing floor. 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 & pathPurposeAccess
GET /admin/cardsPaginated card list for your tenantAny admin
GET /admin/cards/infoCard balance lookupAny admin
GET /admin/cards/user/:userIdA specific user's cardsAny admin
GET /admin/cards/balance/:cardIdA specific card's balanceAny admin
GET /admin/cards/transactions / /:cardIdCard transaction history, tenant-wide or for one cardAny admin
GET /admin/cards/pendingIn-flight card creation requests for your tenant — query page, limit, searchAny admin
GET /admin/cards/pending-holdersIn-flight cardholder verification requests for your tenant — query page, limit, search, all, providerAny admin
GET /admin/cards/pending/allIn-flight card creation requests for your tenant, with completed/processing filtersAny admin
PUT /admin/cards/flag/:cardId / /unflag/:cardIdFreeze / unfreeze a cardfreeze_delete_card
DELETE /admin/cards/:cardIdClose a cardfreeze_delete_card

Referrals & KYC review

Method & pathPurposeAccess
GET /admin/referralsReferral commission totals for your tenantAny admin
GET /admin/referrals/:userIdOne user's referral detailAny admin
GET /admin/referral-applicationsPending affiliate applications for your tenantAny admin
PUT /admin/referral-applications/:applicationIdApprove or reject an affiliate applicationreview_referral_application
GET /admin/kyc-submissionsAccount-KYC submissions for your tenant — pairs with account KYCAny admin
GET /admin/kyc-submissions/:submissionIdA single submission's detailAny admin
PUT /admin/kyc-submissions/:submissionIdApprove, reject, or move a submission to reviewSuper-admin

Tenant settings

Method & pathPurposeAccess
PUT /admin/whitelabelUpdate your own tenant's settings — walletAddress, cardCreationFee, userRefferalFeePercent, userDepositFeePercent, userWithdrawalFeePercent, logoAny admin
GET /admin/whitelabel/:whiteLabelIdYour 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-statusWhether subscriptions are enabled for your tenant, and related billing stateAny admin
POST /admin/whitelabel/addressRegister a settlement address for your tenantSuper-admin
GET /admin/whitelabel/addressList your tenant's settlement addressesAny admin
PUT /admin/whitelabel/:whiteLabelId/adminGrant admin rights to one of your usersSuper-admin
PUT /admin/whitelabel/:whiteLabelId/remove-adminRevoke a user's admin rightsSuper-admin
GET /admin/whitelabel/settlements · /settlements/stats · /settle-earningsSettlement history and stats for your tenantAny admin
DELETE /admin/whitelabel/addressRemove a settlement addressSuper-admin
Toggling 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 & pathPurposeAccess
GET /admin/whitelabel/:whiteLabelId/binsYour tenant's BIN pricing overridesAny admin
PUT /admin/whitelabel/:whiteLabelId/update-binSet your tenant-wide price/top-up-fee/default-price for a BINchange_card_fees
DELETE /admin/whitelabel/:whiteLabelId/bin/:binIdRemove a tenant-wide BIN overridechange_card_fees
GET /admin/whitelabel/:whiteLabelId/subscription-packagesYour tenant's subscription packagesAny admin
POST /admin/whitelabel/:whiteLabelId/subscription-packageCreate a package — name, binId, durationMonths, price, defaultPricechange_card_fees
PUT /admin/whitelabel/:whiteLabelId/subscription-package/:packageIdUpdate a packagechange_card_fees
DELETE /admin/whitelabel/:whiteLabelId/subscription-package/:packageIdRemove a packagechange_card_fees
Can't undercut the platform rate. You can't set a BIN price/top-up-fee/default-price, or a subscription package's price/default-price, below the value the platform has configured as the base for that product. And package creation/update never accepts 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 & pathPurposeAccess
GET /admin/couponsList your tenant's couponsAny admin
GET /admin/coupons/:couponId/usersUsers currently on a given couponAny admin
GET /admin/whitelabel/:whiteLabelId/coupon/:couponIdA single coupon's configurationAny admin
POST /admin/whitelabel/:whiteLabelId/couponCreate a couponmanage_coupon
PUT /admin/whitelabel/:whiteLabelId/coupon/:couponIdUpdate a couponmanage_coupon
DELETE /admin/whitelabel/:whiteLabelId/coupon/:couponIdDelete a couponmanage_coupon
A coupon's fee or referral-rate overrides can't go below your tenant's own base rates — the same floor logic as the BIN/package pricing above.

Admin roles & permissions

Method & pathPurposeAccess
GET /admin/permissionsEvery valid permission keyAny admin
GET /admin/rolesRoles defined on your tenantAny admin
POST /admin/rolesCreate a role — name, permissionKeysmanage_admins
PUT /admin/roles/:roleIdUpdate a role's permission keysmanage_admins
A role can't be given the manage_admins key itself unless the account creating or editing it is the tenant's super-admin.
28

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
403Caller is authenticated but blocked by the KYC gate — body includes kycStatus
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.

29

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
POST/users/email/verifyVerify current email
PUT/users/emailChange email
POST/users/2fa/authenticator/setupStart authenticator enrollment
POST/users/2fa/authenticator/verifyConfirm authenticator enrollment
PUT/users/2fa/methodSwitch active 2FA method
DELETE/users/2fa/disabled2fa/authenticatorRemove authenticator app
GET/users/depositsDeposit/withdrawal history
GET/users/deposit/:depositIdSingle record
GET/users/lookupFind a user by email
GET/users/dex-transactionsSwap history
POST/users/dex-transactions/:id/startBegin a pending swap
POST/users/dex-transactions/:id/withdrawWithdraw a swap's output
GET/users/check-cashbackCashback eligibility
GET/users/notificationsUnread transfer count
POST/users/card-holderSubmit cardholder verification
GET/users/card-holderList cardholder verification records
POST/users/kycSubmit account KYC
GET/users/kycAccount KYC status
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/coupon check (public)
POST/users/upload-fileUpload a document
GET/users/countries · /users/citiesReference data
POST/users/referral-codeAttach a referral/coupon code after registration
GET/users/referrals · /referrals/listReferral stats
POST/users/referrals/withdraw · /referrals/applyReferral payout / application
GET/users/referrals/applicationReferral/affiliate application status

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
GET/cards/balance/:cardIdCard balance
GET/cards/info-sensitive/:cardIdOne-time card-detail reveal link
POST/cards/funding/:cardIdFund / withdraw
POST/cards/status/:cardIdFreeze / unfreeze
DELETE/cards/:cardIdClose card
GET/cards/transactions(/:cardId)Transaction history
GET/cards/subscriptionsAvailable subscription packages
GET/cards/bin/:binIdSingle BIN's resolved pricing
GET/cards/subscription/:cardIdA card's subscription state
PUT/cards/subscription/:cardId/packageChange subscription package
POST/cards/subscription/:cardId/retry · /cancel · /reactivateManage a card's subscription

Statements

POST/statement/csv · /statement/pdfGenerate an account or card statement

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.