/api/v1/healthPublicLiveness/readiness probe
Returns `ok` or `degraded` plus database/provider checks. Safe to wire to a container healthcheck or uptime monitor. Public.
Responses
- 200 Service status → object
Developers
65 operations across 16 areas. Every response uses the same envelope: { ok: true, data, meta? } on success, { ok: false, error: { code, message, correlationId } } on failure. Send x-correlation-id to trace a request end-to-end; quote it in support tickets.
Authentication
Web clients ride the vp_access HttpOnly cookie (CSRF-protected). Mobile and server clients send Authorization: Bearer <accessToken> and rotate with the refresh token.
Idempotency
POST /api/v1/orders requires a per-user idempotencyKey. Replaying it returns the original order — never a second charge.
Money
Amounts are integer minor units (amountMinor) computed server-side. The decimal string is display-only. No endpoint accepts a client-computed price.
Health, provider status and the contract itself
/api/v1/healthPublicLiveness/readiness probe
Returns `ok` or `degraded` plus database/provider checks. Safe to wire to a container healthcheck or uptime monitor. Public.
Responses
/api/v1/openapiPublicThis OpenAPI document
Machine-readable contract generated from the live zod validation schemas. Import into Postman/Insomnia/Stoplight.
Responses
/api/v1/provider/healthAuthenticatedUpstream provider health
Performs a live balance call against SMSPool when credentials are configured, otherwise reports the mock provider. Never returns the API key.
Responses
Registration, sign-in, rotating sessions, password reset
/api/v1/auth/registerPublicCreate a customer account
Creates the user plus a zero-balance wallet, sets session cookies (web) and returns an access token + rotating refresh token (mobile). Rate limited per IP.
Request body
| Field | Type | Notes |
|---|---|---|
| email* | string (email) | — |
| password* | string | — |
| displayName | string | — |
| referralCode | string | — |
Responses
/api/v1/auth/loginPublicSign in
argon2id verification, per-account + per-IP throttling. Returns tokens; the web client receives them as HttpOnly cookies and never sees the refresh token in JS.
Request body
| Field | Type | Notes |
|---|---|---|
| email* | string (email) | — |
| password* | string | — |
Responses
/api/v1/auth/refreshAuthenticatedRotate the session
Single-use rotating refresh token. Reuse of an already-rotated token revokes the whole token family and returns 401. Web sends the cookie; mobile sends `refreshToken` in the body.
Request body
| Field | Type | Notes |
|---|---|---|
| refreshToken | string | — |
Responses
/api/v1/auth/logoutAuthenticatedSign out
Revokes the presented refresh token (and clears cookies).
Responses
/api/v1/auth/forgot-passwordPublicRequest a password reset link
Always returns 200 to avoid account enumeration. In DEMO_MODE with the log email transport the response includes `devToken` for local testing; production returns `{ requested: true }` only.
Request body
| Field | Type | Notes |
|---|---|---|
| email* | string (email) | — |
Responses
/api/v1/auth/reset-passwordPublicComplete a password reset
Consumes a single-use reset token, sets the new argon2id hash and revokes every active session for that user.
Request body
| Field | Type | Notes |
|---|---|---|
| token* | string | — |
| newPassword* | string | — |
Responses
Profile, password, notifications
/api/v1/meAuthenticatedCurrent profile
Responses
/api/v1/meAuthenticatedUpdate profile
Update display name and/or email address.
Request body
| Field | Type | Notes |
|---|---|---|
| displayName | string | — |
| string (email) | — |
Responses
/api/v1/me/passwordAuthenticatedChange password
Requires the current password; revokes other sessions after the change.
Request body
| Field | Type | Notes |
|---|---|---|
| currentPassword* | string | — |
| newPassword* | string | — |
Responses
/api/v1/notificationsAuthenticatedList notifications
Query parameters
page (integer)pageSize (integer)unreadOnly ("true" | "false")Responses
/api/v1/notifications/{id}/readAuthenticatedMark one notification read
Path parameters
id — Internal UUID **or** public ref (NX-…, DP-…) where notedResponses
/api/v1/notifications/read-allAuthenticatedMark all notifications read
Responses
Countries and services available upstream
/api/v1/countriesPublicList serviceable countries (public)
Served from the latest catalog snapshot. Availability is populated by the catalog-refresh job via the provider adapter.
Query parameters
search (string)Responses
/api/v1/servicesPublicList services (public)
Query parameters
countryProviderId (integer)search (string)Responses
Margin-adjusted, server-computed prices
/api/v1/pricingAuthenticatedMarketplace price list
Server-computed sell price for every country/service pair (integer math only). Prices already include the configured margin rules; the client must never compute prices.
Query parameters
countryId (string (uuid))search (string)availableOnly ("true" | "false")sort ("price_asc" | "price_desc" | "country" | "service")page (integer)pageSize (integer)Responses
/api/v1/pricing/quoteAuthenticatedPrice quote for one pair
Live cost/stock lookup plus local margin rules. `warnings` carries operator-visible caveats such as demo/mock mode. The quote is advisory — the authoritative charge is re-computed inside the purchase transaction.
Query parameters
countryId (string (uuid))serviceId (string (uuid))Responses
Purchases, OTP retrieval, cancellation, live status
/api/v1/ordersAuthenticatedList your orders
Ownership-scoped. Phone numbers are masked; OTP codes are never included in list responses.
Query parameters
status (string) — Comma-separated order statusessearch (string)from (string (date-time))to (string (date-time))page (integer)pageSize (integer)Responses
/api/v1/ordersAuthenticatedBuy a number / request an OTP
Idempotent: `idempotencyKey` is unique per user, so a double-click or a retried request returns the original order instead of charging twice. Funds are reserved first, then the provider purchase is attempted exactly once. A provider timeout is NOT treated as a failure — the order becomes UNCERTAIN and reconciliation decides (refund only after the provider confirms).
Request body
| Field | Type | Notes |
|---|---|---|
| countryId* | string (uuid) | — |
| serviceId* | string (uuid) | — |
| idempotencyKey* | string | — |
| poolProviderId | integer | null | — |
Responses
/api/v1/orders/exportAuthenticatedExport order history as CSV
Streams CSV of your own orders. OTP codes are intentionally excluded from exports.
Query parameters
from (string (date-time))to (string (date-time))Responses
/api/v1/orders/{id}AuthenticatedOrder detail (by id or public ref)
Owner-scoped. Includes the OTP code, the full phone number and the status timeline once available.
Path parameters
id — stringResponses
/api/v1/orders/{id}/cancelAuthenticatedCancel an active order
Only pre-SMS states can be cancelled. The refund is applied **only** when the provider confirms the cancellation; otherwise the order is released back to its previous state or becomes UNCERTAIN for reconciliation.
Path parameters
id — Internal UUID **or** public ref (NX-…, DP-…) where notedResponses
/api/v1/orders/{id}/eventsAuthenticatedLive order status stream (SSE)
Server-Sent Events stream of status changes for one order, owner-scoped. Clients fall back to polling `GET /api/v1/orders/{id}` when SSE is unavailable.
Path parameters
id — Internal UUID **or** public ref (NX-…, DP-…) where notedResponses
Balances and the immutable ledger
/api/v1/walletAuthenticatedWallet balances
Available/reserved/total in integer minor units. Balances are server-authoritative; no endpoint accepts a client-supplied balance.
Responses
/api/v1/wallet/transactionsAuthenticatedLedger entries
Append-only double-entry-style ledger rows for the caller, newest first.
Query parameters
category ("DEPOSIT_CREDIT" | "PURCHASE_RESERVE" | "PURCHASE_DEBIT" | "PURCHASE_RELEASE" | "REFUND_CREDIT" | "ADJUSTMENT")page (integer)pageSize (integer)Responses
/api/v1/wallet/statementAuthenticatedDownload a statement (CSV)
Query parameters
from (string (date-time))to (string (date-time))Responses
Manual bKash/Nagad top-ups with admin verification
/api/v1/deposits/methodsAuthenticatedDeposit methods + instructions
Returns the operator-configured bKash/Nagad merchant numbers and step-by-step instructions. Manual deposits always require human verification before funds are credited.
Responses
/api/v1/depositsAuthenticatedList your deposits
Query parameters
status ("PENDING" | "APPROVED" | "REJECTED" | "EXPIRED")page (integer)pageSize (integer)Responses
/api/v1/depositsAuthenticatedDeclare a deposit
Creates a PENDING deposit with a unique reference. For Stripe, generates a checkout session URL. For manual methods, is credited upon admin approval.
Request body
| Field | Type | Notes |
|---|---|---|
| method* | "MANUAL_BKASH" | "MANUAL_NAGAD" | "GATEWAY" | "STRIPE" | "COINGATE" | — |
| amount* | string | — |
| senderRef | string | — |
| senderAccount | string | — |
| successUrl | string (uri) | — |
| cancelUrl | string (uri) | — |
Responses
/api/v1/deposits/stripe/verifyAuthenticatedVerify Stripe checkout session
Queries the Stripe API to verify completion of a checkout session and immediately credits the user wallet.
Request body
| Field | Type | Notes |
|---|---|---|
| sessionId* | string | — |
| depositId | string | — |
Responses
/api/v1/deposits/stripe/payment-intentAuthenticatedCreate Stripe in-app payment intent
Initializes a Stripe PaymentIntent for in-app mobile or web checkout with optional instant card confirmation.
Request body
| Field | Type | Notes |
|---|---|---|
| amount* | string | — |
| card | object | — |
Responses
/api/v1/deposits/coingate/verifyAuthenticatedVerify CoinGate crypto payment
Queries the CoinGate API to verify payment of a cryptocurrency order and immediately credits the user wallet.
Request body
| Field | Type | Notes |
|---|---|---|
| orderId* | object | — |
| depositId | string | — |
Responses
Signed gateway webhooks
/api/v1/payments/webhook/{gatewayId}PublicPayment gateway webhook (server-to-server)
HMAC-SHA256 signed webhook ingress (`x-signature`, optional `sha256=` prefix, `x-event-id` for idempotency). Disabled unless configured. Signature verified against the raw body before any credit; duplicate events are ignored; amounts/currencies are re-verified server-side.
Path parameters
gatewayId — stringRequest body
No structured body — see the description.
Responses
/api/v1/payments/webhook/coingatePublicCoinGate cryptocurrency webhook ingress (server-to-server)
Processes status: paid CoinGate webhook events to verify and credit wallet deposits.
Request body
No structured body — see the description.
Responses
/api/v1/payments/webhook/stripePublicStripe webhook ingress (server-to-server)
Processes checkout.session.completed and payment_intent.succeeded Stripe webhook events to verify and credit wallet deposits.
Request body
No structured body — see the description.
Responses
Customer tickets
/api/v1/support/ticketsAuthenticatedList your tickets
Query parameters
status (string)page (integer)pageSize (integer)Responses
/api/v1/support/ticketsAuthenticatedOpen a ticket
Request body
| Field | Type | Notes |
|---|---|---|
| subject* | string | — |
| category* | "ORDER" | "PAYMENT" | "ACCOUNT" | "OTHER" | — |
| priority | "LOW" | "NORMAL" | "HIGH" | "URGENT" | — |
| body* | string | — |
Responses
/api/v1/support/tickets/{id}AuthenticatedTicket thread
Path parameters
id — Internal UUID **or** public ref (NX-…, DP-…) where notedResponses
/api/v1/support/tickets/{id}/replyAuthenticatedReply to a ticket
Path parameters
id — Internal UUID **or** public ref (NX-…, DP-…) where notedRequest body
| Field | Type | Notes |
|---|---|---|
| body* | string | — |
Responses
User administration
/api/v1/admin/usersAuthenticatedSearch users
Requires the `users:read` permission.
Query parameters
search (string)status ("ACTIVE" | "SUSPENDED" | "CLOSED")page (integer)pageSize (integer)Responses
/api/v1/admin/users/{id}AuthenticatedUser detail (wallet, orders, notes)
Path parameters
id — Internal UUID **or** public ref (NX-…, DP-…) where notedResponses
/api/v1/admin/users/{id}/statusAuthenticatedSuspend / reactivate a user
Audited. Suspension blocks authentication and new purchases but never touches balances.
Path parameters
id — Internal UUID **or** public ref (NX-…, DP-…) where notedRequest body
| Field | Type | Notes |
|---|---|---|
| status* | "ACTIVE" | "SUSPENDED" | — |
| reason* | string | — |
Responses
/api/v1/admin/users/{id}/limitsAuthenticatedSet the daily purchase limit
Path parameters
id — Internal UUID **or** public ref (NX-…, DP-…) where notedRequest body
| Field | Type | Notes |
|---|---|---|
| purchaseLimitDaily* | integer | — |
| reason | string | — |
Responses
/api/v1/admin/users/{id}/notesAuthenticatedAttach internal notes
Path parameters
id — Internal UUID **or** public ref (NX-…, DP-…) where notedRequest body
| Field | Type | Notes |
|---|---|---|
| notes* | string | — |
Responses
/api/v1/admin/users/{id}/wallet-adjustAuthenticatedManual wallet adjustment (compensating entry)
Writes an ADJUSTMENT ledger row — the only supported way to correct a balance, since ledger rows are immutable. Requires a reason and the `wallet:adjust` permission; audited. Step-up authentication is enforced on the web UI.
Path parameters
id — Internal UUID **or** public ref (NX-…, DP-…) where notedRequest body
| Field | Type | Notes |
|---|---|---|
| amount* | string | — |
| reason* | string | — |
Responses
Order operations and reconciliation
/api/v1/admin/ordersAuthenticatedSearch orders (all users)
Query parameters
status (string) — Comma-separated order statusessearch (string)providerMode ("LIVE" | "MOCK")uncertainOnly ("true" | "false")page (integer)pageSize (integer)Responses
/api/v1/admin/orders/{id}AuthenticatedOrder detail (all users)
Path parameters
id — Internal UUID **or** public ref (NX-…, DP-…) where notedResponses
/api/v1/admin/orders/{id}/reconcileAuthenticatedReconcile an order against the provider
Re-checks the upstream order. When the provider has no record, the order is marked UNCERTAIN, flagged for review (audit + event) and funds stay committed — they are never released automatically. Second invocation escalates to NEEDS_REVIEW.
Path parameters
id — Internal UUID **or** public ref (NX-…, DP-…) where notedResponses
Deposit approval queue
/api/v1/admin/depositsAuthenticatedDeposit queue
Manual deposits awaiting verification, oldest first, plus history.
Query parameters
status ("PENDING" | "APPROVED" | "REJECTED" | "EXPIRED")page (integer)pageSize (integer)Responses
/api/v1/admin/deposits/{id}/approveAuthenticatedApprove a manual deposit (credits the wallet)
The single place where manual money enters the ledger: validates the reference, credits DEPOSIT_CREDIT, marks the deposit APPROVED and records the reviewer. Requires `deposits:review`.
Path parameters
id — Internal UUID **or** public ref (NX-…, DP-…) where notedRequest body
| Field | Type | Notes |
|---|---|---|
| note* | string | — |
Responses
/api/v1/admin/deposits/{id}/rejectAuthenticatedReject a manual deposit
No ledger effect — a rejected deposit never credits anything.
Path parameters
id — Internal UUID **or** public ref (NX-…, DP-…) where notedRequest body
| Field | Type | Notes |
|---|---|---|
| note* | string | — |
Responses
Margin rules and catalog sync
/api/v1/admin/pricing-rulesAuthenticatedList pricing rules
Responses
/api/v1/admin/pricing-rulesAuthenticatedCreate a pricing rule
Rules are integer-only: PERCENT_BPS (basis points, 2500 = 25%) or FIXED_MINOR (minor units), with optional minimum sell price and minimum margin. Most specific scope wins, then priority.
Request body
| Field | Type | Notes |
|---|---|---|
| scope* | "GLOBAL" | "COUNTRY" | "SERVICE" | "COUNTRY_SERVICE" | — |
| countryId | string | null | — |
| serviceId | string | null | — |
| ruleType* | "PERCENT_BPS" | "FIXED_MINOR" | — |
| value* | string | — |
| minSellPriceMinor | string | null | — |
| minMarginBps | integer | null | — |
| priority | integer | — |
| active | boolean | — |
Responses
/api/v1/admin/pricing-rules/{id}AuthenticatedUpdate a pricing rule
Path parameters
id — Internal UUID **or** public ref (NX-…, DP-…) where notedRequest body
| Field | Type | Notes |
|---|---|---|
| scope | "GLOBAL" | "COUNTRY" | "SERVICE" | "COUNTRY_SERVICE" | — |
| countryId | string | null | — |
| serviceId | string | null | — |
| ruleType | "PERCENT_BPS" | "FIXED_MINOR" | — |
| value | string | — |
| minSellPriceMinor | string | null | — |
| minMarginBps | integer | null | — |
| priority | integer | — |
| active | boolean | — |
Responses
/api/v1/admin/pricing-previewAuthenticatedPreview a rule against the live catalog
Shows the resulting sell price/margin for a country/service pair before saving a rule.
Query parameters
countryId (string (uuid))serviceId (string (uuid))limit (integer)Responses
/api/v1/admin/catalog/syncAuthenticatedForce a catalog refresh
Re-pulls countries/services/stock from the provider adapter and snapshots the result.
Responses
Support queue for staff
/api/v1/admin/ticketsAuthenticatedSupport queue
Query parameters
status (string)priority (string)page (integer)pageSize (integer)Responses
/api/v1/admin/tickets/{id}AuthenticatedTicket thread (staff view)
Path parameters
id — Internal UUID **or** public ref (NX-…, DP-…) where notedResponses
/api/v1/admin/tickets/{id}/replyAuthenticatedStaff reply
Path parameters
id — Internal UUID **or** public ref (NX-…, DP-…) where notedRequest body
| Field | Type | Notes |
|---|---|---|
| body* | string | — |
Responses
/api/v1/admin/tickets/{id}/statusAuthenticatedChange ticket status / assign
Path parameters
id — Internal UUID **or** public ref (NX-…, DP-…) where notedRequest body
| Field | Type | Notes |
|---|---|---|
| status* | "OPEN" | "PENDING_CUSTOMER" | "ASSIGNED" | "RESOLVED" | "CLOSED" | — |
| assignToSelf | boolean | — |
Responses
Metrics and audit trail
/api/v1/admin/dashboardAuthenticatedOperations dashboard metrics
User/order counters, uncertain-order count, wallet liability and margin for the current period.
Responses
/api/v1/admin/metrics/timeseriesAuthenticatedTime series for charts
Query parameters
metric ("orders" | "revenue" | "margin" | "deposits")days (integer)Responses
/api/v1/admin/audit-logsAuthenticatedImmutable audit trail
Every privileged action lands here. `redactedMeta` is sanitised before storage — OTPs, tokens, API keys and passwords never appear.
Query parameters
action (string)targetType (string)actorId (string (uuid))page (integer)pageSize (integer)Responses
# 1. sign in (web cookie flow)
curl -sX POST http://localhost:3000/api/v1/auth/login \
-H 'content-type: application/json' \
-c cookies.txt \
-d '{"email":"you@example.com","password":"…"}'
# 2. see live prices
curl -s "http://localhost:3000/api/v1/pricing?pageSize=5&sort=price_asc" -b cookies.txt
# 3. buy a number (idempotent — safe to retry)
curl -sX POST http://localhost:3000/api/v1/orders -b cookies.txt \
-H 'content-type: application/json' \
-d '{"countryId":"…","serviceId":"…","idempotencyKey":"order-$(date +%s)"}'
# 4. read the OTP once it arrives
curl -s http://localhost:3000/api/v1/orders/NX-XXXXXX -b cookies.txtMachine clients can authenticate with a Bearer token instead of cookies and poll GET /api/v1/orders/{id} if SSE is not available.
Questions or an endpoint you need? Contact support or open a ticket — include the x-correlation-id from the failing call.