API Reference

32 sections · 245 endpoints

Base URL

https://<broker-domain>

Authentication

Bearer token via Authorization: Bearer <token>. Two types: user JWT (broker staff) and customer JWT (traders).

Rate Limits

300 requests / 60s per authenticated user. Registration: 600 req/IP/60s.

Idempotency

Send Idempotency-Key header on mutations for at-most-once processing. 24h TTL. 5xx responses are NOT cached.

API Namespaces

NamespaceAudienceExample
/api/customer/*Trading platform frontendPOST /api/customer/orders
/api/crm/*CRM staff (sales, retention)GET /api/crm/customers
/api/backoffice/*Operations (payments, compliance)PATCH /api/backoffice/transactions/{id}/approve
/api/platform/*Platform config (groups, workflows)POST /api/platform/groups
/api/admin/*Administration (users, roles)POST /api/admin/users
/api/prices/*Price dataGET /api/prices/EURUSD

Legacy flat paths (e.g., GET /customers, POST /orders) continue working but log a deprecation warning.

1. Gateway

All client traffic goes through the API Gateway on port 8000 -- the only externally exposed port. **Cross-cutting behaviour:** - **X-Request-ID** -- assigned to every request; echoed in response headers. Pass your own to correlate across services. - **Idempotency-Key** -- send on any mutation (POST/PATCH/PUT/DELETE) to ensure at-most-once processing. Same key + same user = same response. Replays return the cached response with `X-Idempotent-Replay: true` header. Key: UUID or string, max 128 chars. TTL: 24 hours. 5xx responses are NOT cached (allow retry). No header = no protection. - **Rate limiting** -- 300 requests/60s per authenticated user. Exceeding returns `429` with `Retry-After` header. Idempotent replays do not consume rate quota. - **Registration rate limit** -- 600 requests per IP per 60s on `POST /customers/register`. - **CORS** -- configured once at the gateway. - **JWT errors** -- `401 {"detail": "Token expired"}` or `401 {"detail": "Invalid token"}` or `401 {"detail": "Missing or invalid Authorization header"}`.

GET
/health
PUBLIC

Gateway health check

Returns gateway health status including Redis connectivity and idempotency cache stats. Includes `permission_checks_skipped` counter (> 0 = RBAC cache degraded).

Response

{
  "status": "ok",
  "service": "api-gateway",
  "checks": {
    "redis": "ok",
    "idempotency": {
      "hits": 0,
      "misses": 0,
      "hit_rate": 0
    }
  },
  "timestamp": "2026-03-24T10:00:00Z"
}
GET
/health/all
PUBLIC

Aggregated health check for all services

Polls all internal services and returns combined status. Returns `"status": "degraded"` if any service is down or unhealthy. No authentication required.

Response

{
  "status": "ok",
  "services": {
    "api-gateway": {
      "status": "ok",
      "details": {
        "status": "ok",
        "service": "api-gateway"
      }
    },
    "price-feed": {
      "status": "ok",
      "details": {
        "status": "ok",
        "service": "price-feed"
      }
    },
    "order-service": {
      "status": "ok",
      "details": {
        "status": "ok",
        "service": "order-service"
      }
    }
  },
  "timestamp": "2026-03-11T22:00:00Z"
}

2. Authentication

Authentication endpoints for broker staff (CRM users) and customers (traders). JWTs use HS256 with 15-minute access tokens and 7-day refresh tokens.

POST
/api/admin/users/loginLegacy: /users/login
PUBLIC

Broker staff login

Authenticate as a broker staff user. Returns short-lived access token (15 min) + long-lived refresh token (7 days). If user has 2FA enabled and no `totp_code` is provided, returns `{"requires_2fa": true}` -- re-send with `totp_code` to complete login.

Request

{
  "email": "[email protected]",
  "password": "secret123",
  "totp_code": "123456"
}

Response

{
  "access_token": "eyJ...",
  "refresh_token": "abc123...",
  "expires_in": 900,
  "token_type": "bearer",
  "user": {
    "id": "uuid",
    "email": "[email protected]",
    "first_name": "Jane",
    "last_name": "Smith",
    "full_name": "Jane Smith",
    "team_id": null,
    "role_id": 2,
    "is_active": true,
    "created_at": "2026-03-09T10:00:00Z",
    "last_login_at": "2026-03-09T10:00:00Z"
  }
}
  • `totp_code` is required only if user has 2FA enabled. Omit for users without 2FA.
  • 200 OK with `{"requires_2fa": true, "message": "Enter your 2FA code"}` when 2FA is enabled but no code provided.
  • Token TTL: access 15 min, refresh 7 days. Algorithm: HS256.
401Wrong credentials or invalid 2FA code403Account disabled
POST
/api/customer/loginLegacy: /customers/login
PUBLIC

Customer (trader) login

Authenticate as a trader and receive a JWT with `type: "customer"`. JWT payload includes `login` (primary active trading account), `logins` (all active account logins for multi-account switcher), `compliance_status`, and `force_password_change`.

Request

{
  "email": "[email protected]",
  "password": "secret123"
}

Response

{
  "access_token": "eyJ...",
  "refresh_token": "abc123...",
  "expires_in": 900,
  "token_type": "bearer",
  "customer": {
    "id": "uuid",
    "email": "[email protected]",
    "first_name": "John",
    "last_name": "Doe",
    "full_name": "John Doe",
    "force_password_change": false,
    "...": "all CustomerResponse fields"
  }
}
  • Token TTL: access 15 min, refresh 7 days.
  • `force_password_change` restriction: when `true`, gateway restricts customer to only: `POST /customers/me/password`, `GET /customers/me`, `POST /auth/refresh`. All other routes return 403.
401Wrong credentials or password not set403Account disabled
GET
/api/customer/autologinLegacy: /auth/autologin
PUBLIC

Redeem one-time autologin token

Redeem a one-time autologin token. Returns the same shape as `POST /customers/login`. Token is burned atomically via Redis Lua GET+DEL.

Query Parameters

NameTypeDescriptionDefault
token*uuidOne-time autologin token
  • TTL: 15 minutes for organic registrations. 72 hours for affiliate registrations (configurable via `AUTOLOGIN_EXPIRY_HOURS` env var).
  • If customer has `force_password_change=true`, the issued JWT carries that claim and the gateway restricts routes.
401Token invalid, already used, or expired403Customer account disabled
POST
/api/customer/password-reset/requestLegacy: /customers/password-reset/request
CUSTOMER

Request password reset link (authenticated)

Request a password reset link. Customer must be logged in -- email is derived from JWT.

Request

{}

Response

{
  "message": "...",
  "reset_url": "https://..."
}
429Rate limited (max 3 active tokens per 15 minutes)
POST
/api/customer/password-reset/forgotLegacy: /customers/password-reset/forgot
PUBLIC

Forgot password (public)

Public. Always returns 200 to prevent email enumeration.

Request

{
  "email": "[email protected]"
}

Response

{
  "message": "If an account exists with that email, a reset link has been generated"
}
GET
/api/customer/password-reset/verifyLegacy: /customers/password-reset/verify
PUBLIC

Verify password reset token

Public. Verify token validity without burning it. Returns masked email.

Query Parameters

NameTypeDescriptionDefault
token*uuidPassword reset token

Response

{
  "valid": true,
  "email": "tr***@example.com"
}
401Token invalid/expired/used
POST
/api/customer/password-reset/confirmLegacy: /customers/password-reset/confirm
PUBLIC

Confirm password reset

Public. Burns token atomically and updates password.

Request

{
  "token": "<uuid>",
  "new_password": "NewPass123"
}

Response

{
  "message": "Password updated successfully"
}
401Token invalid/expired/used422Password <8 or >72 chars
POST
/api/customer/me/passwordLegacy: /customers/me/password
CUSTOMER

Set new password

Set a new password. Clears `force_password_change` flag and issues a fresh JWT. Used after affiliate/import registration where password was auto-generated. This endpoint is allowed even when `force_password_change` is `true` (gateway exempts it). Password must be 8-72 characters.

Request

{
  "new_password": "MySecurePass99"
}

Response

{
  "access_token": "eyJ...",
  "message": "Password updated successfully"
}
  • The new token has `force_password_change: false`, removing the gateway restriction.
403Not a customer JWT422Password too short or too long
POST
/api/admin/auth/2fa/setupLegacy: /auth/2fa/setup
AUTH

Generate TOTP secret for 2FA setup

Generate a TOTP secret for the authenticated user. Returns a QR code URI for authenticator apps (Google Authenticator, Authy, etc.).

Response

{
  "secret": "JBSWY3DPEHPK3PXP",
  "qr_uri": "otpauth://totp/JumpTech:[email protected]?secret=...",
  "message": "Scan QR code, then verify"
}
POST
/api/admin/auth/2fa/verifyLegacy: /auth/2fa/verify
AUTH

Verify TOTP code to enable 2FA

Verify a TOTP code to enable 2FA. Generates 10 one-time backup codes (shown once -- user must save them).

Request

{
  "code": "123456"
}

Response

{
  "enabled": true,
  "backup_codes": [
    "a1b2c3d4",
    "e5f6g7h8",
    "..."
  ],
  "message": "2FA enabled. Save backup codes."
}
POST
/api/admin/auth/2fa/disableLegacy: /auth/2fa/disable
AUTH

Disable 2FA

Disable 2FA. Requires a valid current TOTP code to confirm.

Request

{
  "code": "123456"
}

Response

{
  "enabled": false,
  "message": "2FA has been disabled"
}
GET
/api/admin/auth/sessionsLegacy: /auth/sessions
AUTH

List active sessions

List all active sessions for the authenticated user.

Response

[
  {
    "session_id": "uuid",
    "device": "Mozilla/5.0...",
    "ip": "1.2.3.4",
    "created_at": "2026-03-20T10:00:00Z"
  }
]
DELETE
/api/admin/auth/sessions/{session_id}
AUTH

Revoke a specific session

Revoke a specific session. The session's JWT becomes invalid immediately.

DELETE
/api/admin/auth/sessionsLegacy: /auth/sessions
AUTH

Revoke all other sessions

Revoke all sessions except the current one (identified by the JWT's `session_id` claim).

Response

{
  "revoked": 3
}
POST
/api/admin/auth/refreshLegacy: /auth/refresh
PUBLIC

Refresh access token

Exchange a valid refresh token for a new short-lived access token. No JWT required -- auth via the refresh token itself. Works for both CRM users and customers.

Request

{
  "refresh_token": "abc123..."
}

Response

{
  "access_token": "eyJ...",
  "expires_in": 900,
  "token_type": "bearer"
}
401Invalid, expired, or revoked refresh token

3. Customers

Customer management endpoints including registration, CRM lookup, deduplication, and online presence.

POST
/api/customer/registerLegacy: /customers/register
PUBLIC

Customer registration

Public customer registration. Creates a Customer record and a linked TradingAccount in one transaction. Runs fraud checks: disposable email block -> affiliate_lead_id duplicate -> email duplicate -> phone duplicate -> jurisdiction gate. Supports four registration paths: Organic (password required, country->group), Affiliate (auto-generated password, 72h autologin link, force_password_change=true), Import (auto-generated password, CSV group or default), Promo (password required, promo code's group).

Request

{
  "email": "[email protected]",
  "password": "TestPass1234",
  "first_name": "John",
  "last_name": "Doe",
  "phone": "+447700900000",
  "country": "GB",
  "gender": "male",
  "source": "organic",
  "promo_code": null,
  "affiliate_id": "aff_123",
  "campaign_id": "camp_spring2026",
  "funnel": "lp-gold-v3",
  "affiliate_lead_id": "portal_lead_99871"
}

Response

{
  "id": "uuid",
  "email": "[email protected]",
  "phone": "+447700900000",
  "country": "GB",
  "gender": "male",
  "owner_id": null,
  "ftd_sales_agent_id": null,
  "sales_status_id": null,
  "kyc_status_id": null,
  "affiliate_id": "aff_123",
  "campaign_id": "camp_spring2026",
  "funnel": "lp-gold-v3",
  "affiliate_lead_id": "portal_lead_99871",
  "manager_counter": 0,
  "reg_time": "2026-03-09T10:00:00Z",
  "is_deposited": false,
  "is_active": true,
  "retention_status": null,
  "risk_classification": "normal",
  "registration_source": "organic",
  "force_password_change": false,
  "trading_accounts": [
    {
      "id": "uuid",
      "login": "10007",
      "currency": "USD",
      "account_type": "real",
      "balance": 0,
      "credit": 0,
      "is_active": true,
      "created_at": "2026-03-09T10:00:00Z"
    }
  ],
  "autologin_token": null,
  "autologin_url": null,
  "source": "organic"
}
  • `source`: `organic` (default), `affiliate`, or `import`. Set automatically to `promo` when `promo_code` is provided.
  • `promo_code`: optional -- validated against `promo_codes` table. Overrides source to `promo` and assigns customer to the promo code's group.
  • `password`: required for `organic` and `promo`. Auto-generated for `affiliate` and `import`.
  • `autologin_token` / `autologin_url` are only populated for `affiliate` registrations. URL valid for 72 hours (configurable via `AUTOLOGIN_EXPIRY_HOURS`).
  • Duplicate email (Organic/Promo): `200 {"duplicate": true, "customer_id": "uuid", "message": "Email already registered."}` (anti-enumeration).
  • Duplicate email (Affiliate): `200 {"duplicate": true, "customer_id": null, "message": "Lead already registered"}` (no customer_id exposed).
  • Duplicate email (Import): Rejection CSV row (not HTTP error).
400Invalid, expired, or exhausted promo code403Restricted jurisdiction409Duplicate affiliate_lead_id OR duplicate phone422Disposable email domain blocked, or password missing for organic/promo429Gateway registration rate limit exceeded (600 req/IP/60s)
GET
/api/crm/customersLegacy: /customers
view_customers

List customers with CRM filters

List customers with CRM filters. Results are scoped by the caller's `customer_view_scope` (own/team/all). Ordered by `reg_time DESC`. Customer responses are PII-masked based on the caller's role -- `email` and `phone` fields may be masked or hidden depending on the role's `pii_email` and `pii_phone` settings.

Query Parameters

NameTypeDescriptionDefault
owner_idUUIDCustomers assigned to this owner
countrystringISO 3166-1 alpha-2
kyc_status_idintKYC status filter
sales_status_idintSales status filter
affiliate_idstringAffiliate portal ID
campaign_idstringCampaign ID
has_ftdbool`true` = deposited, `false` = never deposited
is_activeboolActive/inactive filter
retention_statusstringactive / churning / dormant / null
reg_fromdateRegistration date from (inclusive)
reg_todateRegistration date to (inclusive)
pageintPage number1
page_sizeintPage size (max 500)50
GET
/api/crm/customers/{id}Legacy: /customers/{id}
view_customers

Get single customer profile

Single customer profile with embedded agent names and trading accounts.

PATCH
/api/crm/customers/{id}Legacy: /customers/{id}
edit_customer_details

Update customer details

CRM agent update. Scoped by caller's `customer_edit_scope`.

Request

{
  "phone": "+44...",
  "country": "DE",
  "owner_id": "uuid",
  "sales_status_id": 3,
  "kyc_status_id": 1
}
  • All fields are optional.
POST
/api/crm/customers/dedupe-checkLegacy: /customers/dedupe-check
view_customers

Check for duplicate customers

Check for potential duplicate customers using scoring-based matching. Scoring: `affiliate_lead_id` = 10, `email` = 4, `phone` = 3, `country` = 1.

Request

{
  "email": "[email protected]",
  "phone": "+447700900000",
  "country": "GB",
  "affiliate_lead_id": "portal_lead_99871"
}

Response

{
  "duplicates": [
    {
      "customer_id": "uuid",
      "email": "[email protected]",
      "phone": "+447700900000",
      "country": "GB",
      "score": 8,
      "matched_fields": [
        "email",
        "phone",
        "country"
      ]
    }
  ]
}
POST
/api/crm/customers/{id}/pingLegacy: /customers/{id}/ping
CUSTOMER

Customer heartbeat

Heartbeat -- refresh Redis online presence TTL (300s). Returns 204 No Content.

GET
/api/crm/customers/{id}/onlineLegacy: /customers/{id}/online
view_customers

Check customer online status

Check Redis online status.

Response

{
  "customer_id": "uuid",
  "online": true
}

4. Promo Codes

Manage promo codes that assign registering customers to a specific trading group.

POST
/api/platform/promo-codesLegacy: /promo-codes
manage_promo_codes

Create a promo code

Create a promo code that assigns registering customers to a specific group.

Request

{
  "code": "SPRING2026",
  "group_id": "uuid",
  "expires_at": "2026-06-30T23:59:59Z",
  "max_uses": 500
}
  • `code` is normalized to uppercase.
  • `expires_at` -- optional. `null` = no expiry.
  • `max_uses` -- optional. `null` = unlimited.
404Group not found409Promo code already exists
GET
/api/platform/promo-codesLegacy: /promo-codes
manage_promo_codes

List all promo codes

List all promo codes, ordered by `created_at DESC`.

GET
/api/platform/promo-codes/{id}Legacy: /promo-codes/{id}
manage_promo_codes

Get single promo code

Single promo code by ID.

PATCH
/api/platform/promo-codes/{id}Legacy: /promo-codes/{id}
manage_promo_codes

Update promo code

Update promo code fields: `is_active`, `expires_at`, `max_uses`.

DELETE
/api/platform/promo-codes/{id}Legacy: /promo-codes/{id}
manage_promo_codes

Deactivate promo code

Soft-deactivate (sets `is_active=false`). Returns 204 No Content.

5. Customer Import

Bulk customer import via CSV upload. Processes rows asynchronously at a drip rate of ~10/sec.

GET
/api/backoffice/customers/import/templateLegacy: /customers/import/template
import_customers

Download CSV import template

Download CSV template for customer import. Returns a CSV file with headers: `email,phone,first_name,last_name,country,group_name,assigned_to_email,comment`.

POST
/api/backoffice/customers/importLegacy: /customers/import
import_customers

Upload CSV to import customers

Upload a CSV to import customers. Creates an async job and processes rows in the background at a drip rate of ~10/sec. Each row is registered with `source=import`, auto-generated password, and `force_password_change=true`. Duplicate emails produce a rejection row (not an HTTP error).

Request

multipart form with `file` field (UTF-8 CSV, max 50,000 rows)

Response

{
  "job_id": "uuid",
  "status": "pending",
  "total_rows": 1500,
  "message": "Import job created. Processing 1500 rows."
}
  • Required CSV columns: `email`, `phone`, `first_name`, `last_name`, `country`.
  • Optional CSV columns: `group_name`, `assigned_to_email`, `comment`.
400No file, bad encoding, missing required columns, unknown columns, empty CSV, or >50,000 rows
GET
/api/backoffice/customers/import/{job_id}/statusLegacy: /customers/import/{job_id}/status
import_customers

Get import job progress

Job progress for a customer import.

Response

{
  "id": "uuid",
  "status": "processing",
  "filename": "leads_march.csv",
  "total_rows": 1500,
  "processed_rows": 750,
  "success_count": 740,
  "rejection_count": 10,
  "created_at": "2026-03-24T10:00:00Z"
}
GET
/api/backoffice/customers/import/{job_id}/rejectionsLegacy: /customers/import/{job_id}/rejections
import_customers

Download rejection report

Download rejection report as CSV. Each row includes the original data and the rejection reason.

6. Accounts

Trading account management -- live state, balances, transactions, group reassignment, and account closure.

GET
/api/crm/accounts/{login}/stateLegacy: /accounts/{login}/state
view_customers

Live account equity, PnL, and margin

Live equity, PnL, and margin data from Redis. Includes staleness indicators for frontend display. Falls back to Postgres balance/credit when Redis state is missing (e.g., after Redis restart).

Response

{
  "login": "10007",
  "equity": 10250.5,
  "pnl": 250.5,
  "free_margin": 9750.5,
  "margin_level": 1025.05,
  "margin_used": 500,
  "max_withdrawable": 9750.5,
  "gross_exposure": 10854.2,
  "net_exposure": 5427.1,
  "currency": "USD",
  "stale": false,
  "age_s": 0.2
}
  • `stale` (boolean): `true` if Redis state is older than 5 seconds -- frontend should show "data delayed" warning.
  • `age_s` (float): seconds since the PnL engine last updated this account's state in Redis.
GET
/api/crm/accounts/{account_id}/balanceLegacy: /accounts/{account_id}/balance
view_customers

Get derived account balance

Derived balance: `SUM(amount) WHERE status='APPROVED'`.

GET
/api/crm/accounts/{account_id}/transactionsLegacy: /accounts/{account_id}/transactions
view_customers

List account transactions

All transactions for a trading account.

Query Parameters

NameTypeDescriptionDefault
statusstringFilter by status (e.g., APPROVED)
limitintMax results100
offsetintPagination offset0
PATCH
/api/platform/accounts/{login}/groupLegacy: /accounts/{login}/group
manage_groups

Reassign account to a different group

Reassign a trading account to a different group. Open positions retain their snapshotted terms from the original group.

Request

{
  "group_id": "uuid"
}
POST
/api/backoffice/accounts/{login}/closeLegacy: /accounts/{login}/close
close_accounts

Close a trading account

Close a trading account. Pre-checks: no open positions, no pending transactions. Pending orders are auto-cancelled on closure. `force=true` allows closure with remaining balance (broker acknowledges offline handling). Sets `closure_status='closed'`, `is_active=false`. Deactivates customer if no other active accounts.

Request

{
  "reason": "Customer requested closure",
  "force": false
}

Response

{
  "login": 10007,
  "status": "closed",
  "reason": "Customer requested closure",
  "balance_at_closure": 0
}

7. Financial Ledger

The financial ledger is an append-only audit trail of every monetary event on a trading account, stored in a TimescaleDB hypertable (`ledger_events`). Each event records atomic deltas for balance, credit, and margin, with DB-computed running totals.

GET
/api/backoffice/accounts/{login}/ledgerLegacy: /accounts/{login}/ledger
view_customers

Query financial ledger events

Query the financial ledger for a trading account. Returns atomic financial events in reverse chronological order.

Query Parameters

NameTypeDescriptionDefault
event_typestringFilter: `deposit`, `withdrawal`, `chargeback`, `credit_in`, `credit_out`, `order_open`, `margin_release`, `trade_pnl`, `commission`, `swap`, `nbp`, `correction_in`, `correction_out`
from_datestringISO 8601 start time
to_datestringISO 8601 end time
limitintMax 500100
offsetintPagination offset0

Response

[
  {
    "id": "uuid",
    "account_id": 10007,
    "event_type": "deposit",
    "reference_type": "transaction",
    "reference_id": "uuid",
    "balance_delta": 500,
    "credit_delta": 0,
    "margin_delta": 0,
    "balance_after": 10500,
    "credit_after": 0,
    "margin_after": 500,
    "sequence_num": 42,
    "description": "Deposit approved",
    "idempotency_key": "deposit:tx-uuid",
    "created_at": "2026-03-24T10:00:00Z"
  }
]
GET
/api/backoffice/accounts/{login}/ledger/summaryLegacy: /accounts/{login}/ledger/summary
view_customers

Aggregated ledger summary

Aggregated ledger summary -- total deposits, withdrawals, P&L, commissions, swaps.

Query Parameters

NameTypeDescriptionDefault
from_datestringISO 8601 start time (optional)
to_datestringISO 8601 end time (optional)

Response

{
  "account_id": 10007,
  "total_deposits": 5000,
  "total_withdrawals": -1000,
  "net_trading_pnl": 250.5,
  "total_commissions": -45,
  "total_swaps": -12.3,
  "total_chargebacks": 0,
  "total_nbp": 0,
  "net_credit": 0,
  "net_balance_change": 4193.2,
  "total_events": 87
}

8. Transactions

All monetary movements -- deposits, withdrawals, bonuses, credits, corrections. **Type values:** DEPOSIT, WITHDRAWAL, BONUS, BONUS_CREDIT_IN, BONUS_CREDIT_OUT, CREDIT_IN, CREDIT_OUT, TRANSFER_IN, TRANSFER_OUT, CORRECTION_IN, CORRECTION_OUT, IB_PAYMENT, REWARD, PROFIT_SHARING_IN, PROFIT_SHARING_OUT **Status values:** PENDING, IN_PROGRESS, PROCESSING, APPROVED, DECLINE, REJECT, DELETED **Source values:** cashier (Praxis), manual (CRM operator), system (platform-generated)

POST
/api/backoffice/transactions/manualLegacy: /transactions/manual
create_deposits

Create a manual transaction

Create a manual transaction.

Request

{
  "type": "DEPOSIT",
  "account_id": "uuid",
  "amount": "500.00",
  "currency": "USD",
  "account_amount": "500.00",
  "usd_amount": "500.00",
  "exchange_rate_usd": "1.0",
  "gate_name": "Bank Wire",
  "proof_reference": "WIRE-REF-20260309",
  "comment": "Bank wire confirmed",
  "status": "PENDING"
}
  • `status` can be `PENDING` or `APPROVED` -- approve in one step.
PATCH
/api/backoffice/transactions/{id}/approveLegacy: /transactions/{id}/approve
approve_deposits

Approve a pending transaction

Approve a pending manual transaction.

  • Free margin guard: when account has open positions (`margin_used > 0`), withdrawals limited to `min(balance, free_margin)`, credit removals require `equity - removal >= margin_used`.
  • Maker-checker: when `broker_settings.maker_checker_enabled = 'true'`, the user who created the transaction cannot approve it.
403Same user attempted to create and approve (maker-checker violation)422Withdrawal/credit removal exceeds free margin
PATCH
/api/backoffice/transactions/{id}/declineLegacy: /transactions/{id}/decline
approve_deposits

Decline a pending transaction

Decline a pending manual transaction.

PATCH
/api/backoffice/transactions/{id}/deleteLegacy: /transactions/{id}/delete
approve_deposits

Soft-delete a transaction

Soft-delete -- excluded from balance computation, preserved for audit.

POST
/api/backoffice/transactions/cashierLegacy: /transactions/cashier
create_deposits

Create a cashier (Praxis) transaction

Create a cashier (Praxis) transaction. Idempotent on `cashier_id`.

POST
/webhooks/praxis
PUBLIC

Praxis webhook

Legacy path -- routes to integration-service for HMAC verification and forwarding.

POST
/api/backoffice/transactions/{tx_id}/chargebackLegacy: /transactions/{tx_id}/chargeback
process_chargebacks

Process a chargeback

Process a chargeback against an approved deposit. Creates a negative CHARGEBACK transaction.

Request

{
  "reason": "Card dispute by cardholder"
}

Response

{
  "chargeback_id": "uuid",
  "original_transaction_id": "uuid",
  "amount": -500,
  "balance_after": -200,
  "account_frozen": true
}
  • Only approved `DEPOSIT` transactions can be charged back.
  • Idempotent: cannot chargeback the same deposit twice.
  • If balance goes negative, customer is auto-frozen (`compliance_status='frozen'`).
GET
/api/backoffice/transactions/pending-approvalLegacy: /transactions/pending-approval
approve_deposits

List transactions awaiting approval

List transactions awaiting approval (status `PENDING` or `PENDING_APPROVAL`). Max 200.

GET
/api/backoffice/transactionsLegacy: /transactions
view_customers

List transactions

List transactions with filters.

Query Parameters

NameTypeDescriptionDefault
account_idUUIDFilter by account
customer_idUUIDFilter by customer
typestringTransaction type filter
statusstringStatus filter
sourcestringSource filter (cashier, manual, system)
date_fromstringDate range start
date_tostringDate range end
limitintMax results
offsetintPagination offset
GET
/api/backoffice/transactions/{id}Legacy: /transactions/{id}
view_customers

Get single transaction

Single transaction by UUID.

POST
/transactions/payment-callback
PUBLIC

Provider payment callback (internal)

Provider-agnostic internal endpoint. Called by integration-service after webhook verification.

Request

{
  "cashier_id": "...",
  "status": "APPROVED",
  "gateway_payload": {}
}

9. Payment Routing & BridgerPay

Per-country payment gateway routing. Brokers configure which cashier handles deposits/withdrawals for each country. Unconfigured countries fall back to `DEFAULT_PAYMENT_GATEWAY` env var (default: `praxis`).

GET
/api/platform/payments/gateways/availableLegacy: /payments/gateways/available
view_customers

List available payment gateways

List all known payment gateways with their enabled status. A gateway is "enabled" only if activated in integration-service with credentials configured.

Response

[
  {
    "id": "praxis",
    "name": "Praxis",
    "type": "redirect",
    "enabled": true
  },
  {
    "id": "bridgerpay",
    "name": "BridgerPay",
    "type": "embed",
    "enabled": false
  }
]
GET
/api/platform/payments/routingLegacy: /payments/routing
view_customers

List payment routing rules

List all per-country payment gateway routing rules.

Response

[
  {
    "country": "BR",
    "deposit_gateway": "bridgerpay",
    "withdrawal_gateway": "praxis",
    "is_active": true,
    "note": "Brazil uses BridgerPay",
    "updated_at": "2026-03-19T10:00:00"
  }
]
PUT
/api/platform/payments/routing/{country}Legacy: /payments/routing/{country}
manage_payment_routing

Create or update country routing

Create or update payment routing for a country (ISO 3166-1 alpha-2). Rejects if gateway is not enabled.

Request

{
  "deposit_gateway": "bridgerpay",
  "withdrawal_gateway": "praxis",
  "is_active": true,
  "note": "Brazil uses BridgerPay for deposits"
}
DELETE
/api/platform/payments/routing/{country}Legacy: /payments/routing/{country}
manage_payment_routing

Delete country routing

Delete routing -- country reverts to default gateway. Returns 204.

GET
/payments/gateway
PUBLIC

Resolve payment gateway for country

Resolve which payment gateway to use for a customer's country. Public (no auth) -- called by the frontend before initializing the cashier.

Query Parameters

NameTypeDescriptionDefault
country*stringISO 3166-1 alpha-2 country code
directionstring`deposit` or `withdrawal`deposit

Response

{
  "gateway": "bridgerpay",
  "country": "BR",
  "direction": "deposit",
  "type": "embed",
  "script_url": "https://embed.bridgerpay.com/connector.min.js",
  "cashier_key": "..."
}
POST
/payments/bridgerpay/session
CUSTOMER

Create BridgerPay cashier session

Create a BridgerPay cashier session. Requires customer JWT. Creates a PENDING transaction and returns the cashier token for frontend iframe initialization.

Request

{
  "amount": 500,
  "currency": "USD"
}

Response

{
  "transaction_id": "uuid",
  "cashier_token": "abc123...",
  "cashier_session_id": "...",
  "cashier_key": "...",
  "script_url": "https://embed.bridgerpay.com/connector.min.js"
}
POST
/integrations/webhooks/bridgerpay
PUBLIC

BridgerPay payment webhook

BridgerPay payment webhook. Public (no auth). Handled by integration-service -- connector verifies HMAC, maps status, and forwards to `POST /transactions/payment-callback` on account-service.

  • Status mapping: `approved` -> APPROVED, `declined`/`error`/`cancelled` -> DECLINE, `in_process` -> PROCESSING.

10. Withdrawal Approval Rules

Threshold-based role requirements for withdrawal approval. Default rules: agent < $1K, manager < $10K, compliance $10K+.

GET
/api/platform/withdrawal-approval-rulesLegacy: /withdrawal-approval-rules
approve_deposits

List withdrawal approval rules

List all active rules, ordered by `min_amount_usd`.

POST
/api/platform/withdrawal-approval-rulesLegacy: /withdrawal-approval-rules
manage_broker_config

Create a withdrawal approval rule

Create a new rule. Returns 201 Created.

Request

{
  "min_amount_usd": 0,
  "max_amount_usd": 1000,
  "required_role": "agent",
  "is_active": true
}
PUT
/api/platform/withdrawal-approval-rules/{rule_id}Legacy: /withdrawal-approval-rules/{rule_id}
manage_broker_config

Update a withdrawal approval rule

Update an existing rule (same fields as POST).

DELETE
/api/platform/withdrawal-approval-rules/{rule_id}Legacy: /withdrawal-approval-rules/{rule_id}
manage_broker_config

Delete a withdrawal approval rule

Soft-delete (sets `is_active=false`). Returns 204 No Content.

11. Orders (Trading)

Market order placement, position management, and stop-loss/take-profit modification.

POST
/api/customer/ordersLegacy: /orders
view_orders

Place a market order (async)

Place a market order -- async execution. Poll `GET /orders/request/{request_id}` for execution result.

Request

{
  "login": 10007,
  "symbol": "EURUSD",
  "cmd": 0,
  "volume": 0.1
}

Response

{
  "request_id": "uuid",
  "status": "queued"
}
  • `cmd`: `0` = buy, `1` = sell.
GET
/api/customer/orders/request/{request_id}Legacy: /orders/request/{request_id}
view_orders

Poll async order execution status

Poll async execution status. Key expires after 5 minutes.

Response

{
  "request_id": "uuid",
  "status": "executed",
  "order": 100001
}
  • Possible statuses: `executed`, `rejected` (with `reason` field, e.g., "Insufficient margin", "EXPOSURE_LIMIT_EXCEEDED").
GET
/api/crm/ordersLegacy: /orders
view_orders

List open positions

List open positions.

Query Parameters

NameTypeDescriptionDefault
login*intTrading account login
GET
/api/crm/orders/{ticket}Legacy: /orders/{ticket}
view_orders

Get single open position

Single open position with live profit.

Query Parameters

NameTypeDescriptionDefault
login*intTrading account login
DELETE
/api/crm/orders/{ticket}Legacy: /orders/{ticket}
view_orders

Close a position

Close a position. Supports partial close.

Query Parameters

NameTypeDescriptionDefault
login*intTrading account login
volumefloatPartial close volume (omit for full close)
  • Partial close creates a new `order_history` record linked via `parent_order`.
  • `volume` must be a multiple of 0.001; remaining must be >= 0.001 or zero.
  • Margin, commission, and storage split proportionally.
PATCH
/api/customer/orders/{ticket}/sltpLegacy: /orders/{ticket}/sltp
view_orders

Modify stop-loss / take-profit

Modify stop-loss / take-profit on an open position.

Query Parameters

NameTypeDescriptionDefault
login*intTrading account login

Request

{
  "sl": 1.08,
  "tp": 1.12
}
  • At least one of `sl` or `tp` required; set to `0` to remove.

12. Pending Orders

Pending orders (Limit / Stop / Stop-Limit). Pending orders do not reserve margin -- margin is checked at execution time only.

POST
/api/customer/orders/pendingLegacy: /orders/pending
view_orders

Place a pending order

Place a pending order.

Request

{
  "login": 10007,
  "symbol": "EURUSD",
  "cmd": 2,
  "volume": 0.1,
  "trigger_price": 1.085,
  "limit_price": null,
  "sl": 1.08,
  "tp": 1.09,
  "expiry_at": "2026-03-15T00:00:00Z",
  "comment": "Buy limit on EUR"
}
  • `cmd`: `2` = BUY_LIMIT, `3` = SELL_LIMIT, `4` = BUY_STOP, `5` = SELL_STOP, `6` = BUY_STOP_LIMIT, `7` = SELL_STOP_LIMIT.
  • `limit_price`: required for cmd 6/7, rejected for cmd 2-5. The limit price used after the stop triggers.
  • `expiry_at`: optional -- `null` = GTC (good till cancelled).
  • Trigger price validation: BUY_LIMIT < ask, SELL_LIMIT > bid, BUY_STOP > ask, SELL_STOP < bid, BUY_STOP_LIMIT > ask, SELL_STOP_LIMIT < bid.
  • Stop-limit execution: when the stop triggers, if price is within the limit -> fill immediately. If price gapped past the limit -> converts to a regular limit order and waits.
PATCH
/api/customer/orders/pending/{order_id}Legacy: /orders/pending/{order_id}
view_orders

Modify a pending order

Modify an active pending order.

Query Parameters

NameTypeDescriptionDefault
login*intTrading account login

Request

{
  "sl": 1.075,
  "tp": 1.095,
  "trigger_price": 1.086,
  "limit_price": 1.087
}
  • At least one field required.
  • `limit_price` can only be set on stop-limit orders (cmd 6/7).
  • `trigger_price` validated against current market price.
  • Uses `FOR UPDATE` lock to prevent race conditions.
GET
/api/customer/orders/pendingLegacy: /orders/pending
view_orders

List pending orders

List pending orders.

Query Parameters

NameTypeDescriptionDefault
login*intTrading account login
statusstringFilter by status (e.g., `active`)
DELETE
/api/customer/orders/pending/{order_id}Legacy: /orders/pending/{order_id}
view_orders

Cancel a pending order

Cancel a pending order. Only active orders.

Query Parameters

NameTypeDescriptionDefault
login*intTrading account login

13. Trade History

Closed trade history with currency conversion rates.

GET
/api/crm/historyLegacy: /history
view_orders

List closed trades

List closed trades. Response includes `open_fx_rate` and `close_fx_rate` for margin/profit currency conversion.

Query Parameters

NameTypeDescriptionDefault
login*intTrading account login
GET
/api/crm/history/{ticket}Legacy: /history/{ticket}
view_orders

Get single closed trade

Single closed trade.

Query Parameters

NameTypeDescriptionDefault
login*intTrading account login

14. Prices

Real-time and historical price data, FX rates, price health monitoring, and WebSocket streaming.

GET
/api/pricesLegacy: /prices
PUBLIC

Get all latest prices

Returns latest cached price for every active symbol.

Response

{
  "prices": [
    {
      "symbol": "BTCUSDT",
      "bid": 67152.53,
      "ask": 67152.54,
      "mid": 67152.535,
      "timestamp_ms": 1773033231279,
      "provider": "platform"
    }
  ],
  "count": 3
}
GET
/api/prices/{symbol}Legacy: /prices/{symbol}
PUBLIC

Get single symbol price

Single symbol lookup. URL-encode slashes for forex: `EUR%2FUSD`.

404Symbol not active or cache expired (TTL 60s)
GET
/api/prices/historyLegacy: /prices/history
PUBLIC

OHLC bars for charting

OHLC bars for charting (TradingView Charting Library datafeed).

Query Parameters

NameTypeDescriptionDefault
symbol*stringe.g. `EURUSD`
timeframestring`M1`, `M5`, `M15`, `H1`, `H4`, `D1`M1
fromstringISO 8601 start time
tostringISO 8601 end time
limitintMax 5000500

Response

{
  "symbol": "EURUSD",
  "timeframe": "H1",
  "bars": [
    {
      "time": 1711234567,
      "open": 1.085,
      "high": 1.086,
      "low": 1.084,
      "close": 1.0855,
      "volume": 147
    }
  ],
  "count": 500
}
GET
/api/prices/fx-ratesLegacy: /fx-rates
PUBLIC

Get all USD-based FX rates

Returns all USD-based exchange rates from the in-memory cache (refreshed hourly).

Response

{
  "base": "USD",
  "rates": {
    "EUR": 0.92,
    "GBP": 0.79,
    "JPY": 149.5
  },
  "last_updated": 1710000000,
  "count": 161
}
GET
/api/prices/fx-rates/{currency}Legacy: /fx-rates/{currency}
PUBLIC

Get single currency FX rate

Single currency rate lookup.

Response

{
  "currency": "GBP",
  "rate": 0.79,
  "base": "USD"
}
GET
/api/prices/healthLegacy: /health/prices
PUBLIC

Per-symbol price freshness check

Per-symbol price freshness check. Each symbol reports its last tick age against its configured `stale_threshold_ms` (default 10000ms). Status is `"ok"` when no symbols are stale, `"degraded"` when at least one is stale.

Response

{
  "status": "ok",
  "symbols": {
    "EURUSD": {
      "last_tick_ms": 1773033231279,
      "age_ms": 312,
      "stale": false,
      "threshold_ms": 10000
    },
    "BTCUSDT": {
      "last_tick_ms": 1773033230100,
      "age_ms": 1491,
      "stale": false,
      "threshold_ms": 5000
    }
  },
  "summary": {
    "total": 2,
    "stale": 0,
    "healthy": 2
  },
  "timestamp": "2026-03-12T10:00:31Z"
}
GET
/api/prices/outagesLegacy: /price-outages
view_audit

Historical price outage log

Historical price outage log. Each row represents a detected staleness window for a symbol. `ended_at` is `null` for ongoing outages.

Query Parameters

NameTypeDescriptionDefault
symbolstringFilter by symbol (exact match)
limitintMax rows returned (max 500)50

Response

{
  "outages": [
    {
      "id": "uuid",
      "symbol": "XAUUSD",
      "started_at": "2026-03-12T09:55:00Z",
      "ended_at": "2026-03-12T09:57:12Z",
      "duration_ms": 132000,
      "threshold_ms": 10000,
      "last_tick_at": "2026-03-12T09:54:50Z"
    }
  ],
  "count": 1
}
WS
/api/customer/ws/prices
PUBLIC

WebSocket price streaming

WebSocket endpoint for real-time price streaming.

GET
/api/prices/providers
PUBLIC

List price providers

List configured price data providers.

GET
/api/prices/symbols
PUBLIC

List all price symbols

List all symbols available in the price feed.

15. Instruments

Instrument catalog and broker-level instrument configuration.

GET
/api/customer/instrumentsLegacy: /instruments
PUBLIC

List all active instruments

List all active instruments.

Query Parameters

NameTypeDescriptionDefault
group_bystring`asset_class` -- returns grouped response

Response

[
  {
    "id": "uuid",
    "symbol": "EURUSD",
    "display_name": "Euro / US Dollar",
    "category": "forex",
    "asset_class": "forex_majors",
    "base_currency": "EUR",
    "quote_currency": "USD",
    "digits": 5,
    "pip_size": 0.0001,
    "contract_size": 100000,
    "min_lot": 0.01,
    "max_lot": 100,
    "lot_step": 0.01,
    "margin_rate": 0.01,
    "spread": 0.2,
    "stale_threshold_ms": 10000,
    "is_active": true,
    "provider": "none",
    "last_bid": 1.08542,
    "last_ask": 1.08556,
    "last_tick_at": "2026-03-12T10:30:00Z"
  }
]
GET
/api/customer/instruments/asset-classesLegacy: /instruments/asset-classes
PUBLIC

List active asset classes

Active asset classes for frontend tabs.

GET
/api/customer/instruments/{symbol}Legacy: /instruments/{symbol}
PUBLIC

Get single instrument

Single instrument by symbol.

POST
/api/platform/instrumentsLegacy: /instruments
manage_instruments

Add a new instrument

Add a new instrument.

409Symbol already exists
GET
/api/platform/instrumentsLegacy: /broker/instruments
manage_instruments

List broker-level instrument config

List broker-level configuration for all instruments.

GET
/api/platform/instruments/availableLegacy: /broker/instruments/available
PUBLIC

List broker-enabled instruments

Instruments enabled at broker level and active in master catalog. Use for trading UI.

PATCH
/api/platform/instruments/{symbol}Legacy: /broker/instruments/{symbol}
manage_instruments

Update broker-level instrument config

Update broker-level config: `is_enabled`, `tradeability` (`close_only`, `null` to clear).

16. Groups (Trading Configuration)

Trading group management including per-group instrument overrides and swap configuration.

GET
/api/platform/groupsLegacy: /groups
PUBLIC

List all trading groups

List all active trading groups (public).

POST
/api/platform/groupsLegacy: /groups
manage_groups

Create a trading group

Create a trading group.

Request

{
  "name": "vip",
  "description": "VIP clients",
  "currency": "USD",
  "default_leverage": 200,
  "margin_call_level": 50,
  "stopout_level": 20,
  "commission_type": "per_lot",
  "commission_value": 5,
  "swap_free": false,
  "swap_enabled": true,
  "swap_multiplier": 1,
  "swap_free_days": 0,
  "is_islamic": false,
  "is_default": false,
  "max_exposure": 500000,
  "net_max_exposure": false
}
  • `stopout_level` must be < `margin_call_level`.
  • `is_islamic=true` forces `swap_free=true`.
  • `swap_enabled` -- default `true`; set `false` to disable swap charges for all accounts in this group.
  • `swap_multiplier` -- default `1.0`; multiplier applied to all swap rates.
  • `swap_free_days` -- default `0`; days after account creation during which swaps are not charged.
  • `max_exposure` -- per-account exposure limit in USD; `null` = unlimited. When `net_max_exposure=false` (default), this is gross exposure. When `true`, hedged positions on the same symbol reduce net exposure.
GET
/api/platform/groups/{id}Legacy: /groups/{id}
PUBLIC

Get single group

Single group by ID.

PATCH
/api/platform/groups/{id}Legacy: /groups/{id}
manage_groups

Update group fields

Update group fields. All fields optional.

DELETE
/api/platform/groups/{id}Legacy: /groups/{id}
manage_groups

Soft-delete a group

Soft-delete (sets `is_active=false`).

409Active accounts assigned; reassign them first
PUT
/api/platform/groups/{id}/instruments/{symbol}Legacy: /groups/{id}/instruments/{symbol}
manage_groups

Create or update per-group instrument config

Create or update per-group instrument config. All fields optional and nullable -- `null` = inherit from master catalog.

Request

{
  "spread": 2,
  "margin_rate": 0.015,
  "tradeability": "close_only",
  "commission_type": "per_lot",
  "commission_value": 5,
  "min_lot": 0.01,
  "max_lot": 50
}
  • `spread` in pips -- converted to price delta at fill time.
  • `tradeability`: `tradable` | `close_only` | `non_tradable`.
GET
/api/platform/groups/{id}/instrumentsLegacy: /groups/{id}/instruments
view_customers

List group instrument configs

List all instrument configs for a group.

PUT
/api/platform/groups/{id}/swap-configLegacy: /broker/groups/{id}/swap-config
manage_groups

Bulk upsert swap rates

Bulk upsert swap rates for multiple instruments on a group.

Request

{
  "instruments": {
    "BTCUSDT": {
      "swap_type": "percent",
      "swap_long": -0.05,
      "swap_short": -0.05
    },
    "EURUSD": {
      "swap_type": "points",
      "swap_long": -5.5,
      "swap_short": -3.2
    }
  }
}

Response

{
  "group_id": "uuid",
  "instruments_updated": 1,
  "instruments_created": 1
}
GET
/api/platform/groups/{id}/swap-ratesLegacy: /broker/groups/{id}/swap-rates
view_customers

Get resolved swap rates

Resolved swap rates for all active instruments in a group. Shows source and effective rates after applying `swap_multiplier`.

17. Users (Broker Staff)

POST
/api/admin/usersLegacy: /users
edit_users

Create a broker staff account

Request

{
  "email": "[email protected]",
  "first_name": "Jane",
  "last_name": "Smith",
  "password": "secret123",
  "team_id": "uuid-or-null",
  "role_id": 2
}

Response

UserResponse
  • 201 Created -- returns UserResponse.
409Email already registered.
GET
/api/admin/users/meLegacy: /users/me
AUTH

Current user profile from JWT

Returns the profile of the currently authenticated user. No special permission required -- any authenticated user can call this.

GET
/api/admin/usersLegacy: /users
view_users

List broker staff with filters

Query Parameters

NameTypeDescriptionDefault
team_idUUIDFilter by team
role_idintFilter by role
is_activeboolFilter by active status
searchstringilike match on first_name, last_name, email
pageintPage number (default 1)1
page_sizeintPage size (default 50, max 500)50
GET
/api/admin/users/{id}Legacy: /users/{id}
view_users

Single user profile

PATCH
/api/admin/users/{id}Legacy: /users/{id}
edit_users

Update user fields

Request

{
  "first_name": "Jane",
  "last_name": "Smith",
  "team_id": "uuid",
  "role_id": 3,
  "is_active": false
}
  • All fields are optional.
POST
/api/admin/users/{id}/pingLegacy: /users/{id}/ping
AUTH

Heartbeat -- refresh online presence

Refresh Redis online presence TTL (300s) and stamp `last_login_at`. Any authenticated user can call this.

  • 204 No Content.
GET
/api/admin/users/{id}/onlineLegacy: /users/{id}/online
view_users

Check Redis online status

Response

{
  "user_id": "uuid",
  "online": true
}

18. Teams

Organisational hierarchy for broker staff. Teams support nested parent-child relationships with materialized path lookups.

POST
/api/admin/teamsLegacy: /teams
edit_users

Create a team

`path` and `depth` are computed automatically.

Request

{
  "name": "Europe",
  "parent_team_id": "root-uuid-or-null"
}
  • 201 Created -- returns TeamResponse.
GET
/api/admin/teamsLegacy: /teams
view_users

Flat list of teams

Query Parameters

NameTypeDescriptionDefault
is_activeboolFilter by active status
depthintFilter by depth level
parent_team_idUUIDFilter by parent team
GET
/api/admin/teams/treeLegacy: /teams/tree
view_users

Full hierarchy as nested JSON

Returns the complete team hierarchy as nested JSON for UI rendering.

GET
/api/admin/teams/{id}Legacy: /teams/{id}
view_users

Single team by ID

GET
/api/admin/teams/{id}/childrenLegacy: /teams/{id}/children
view_users

Direct children only (one level down)

GET
/api/admin/teams/{id}/subtreeLegacy: /teams/{id}/subtree
view_users

All descendants at any depth

Uses `path LIKE` -- one index scan.

GET
/api/admin/teams/{id}/usersLegacy: /teams/{id}/users
view_users

All active users directly in this team

GET
/api/admin/teams/{id}/subtree/usersLegacy: /teams/{id}/subtree/users
view_users

All active users in this team and all descendants

PATCH
/api/admin/teams/{id}Legacy: /teams/{id}
edit_users

Update a team

If `parent_team_id` changes, all descendant paths are recomputed atomically.

400Team cannot be its own parent, or re-parenting under own descendant.
GET
/api/admin/teams/{id}/permissionsLegacy: /teams/{id}/permissions
view_users

List permission overrides for a team

PUT
/api/admin/teams/{id}/permissions/{permission_id}Legacy: /teams/{id}/permissions/{permission_id}
manage_roles

Set a permission override for a team

DELETE
/api/admin/teams/{id}/permissions/{permission_id}Legacy: /teams/{id}/permissions/{permission_id}
manage_roles

Remove a permission override (reverts to role default)

19. Roles & Permissions

RBAC system for broker staff. Roles define customer visibility scopes, PII masking rules, and permission grants.

GET
/api/admin/rolesLegacy: /roles
view_users

List all roles

Query Parameters

NameTypeDescriptionDefault
include_inactiveboolInclude inactive rolesfalse
POST
/api/admin/rolesLegacy: /roles
manage_roles

Create a role

Request

{
  "name": "retention_agent",
  "display_name": "Retention Agent",
  "description": "Handles churning customers",
  "customer_view_scope": "own",
  "customer_edit_scope": "none",
  "pii_email": "masked",
  "pii_phone": "masked",
  "can_export_pii": false,
  "level": 0
}
  • Role RBAC columns:
  • `customer_view_scope` (own / team / all) -- whose customers this role can see.
  • `customer_edit_scope` (none / own / team / all) -- whose customers this role can edit.
  • `pii_email` (full / masked / hidden) -- how customer email is displayed.
  • `pii_phone` (full / masked / hidden) -- how customer phone is displayed.
  • `can_export_pii` (true / false) -- whether PII can be exported (CSV, etc.).
GET
/api/admin/roles/{role_id}Legacy: /roles/{role_id}
view_users

Single role by ID

PATCH
/api/admin/roles/{role_id}Legacy: /roles/{role_id}
manage_roles

Update role fields

Updatable fields: `display_name`, `description`, `customer_view_scope`, `customer_edit_scope`, `pii_email`, `pii_phone`, `can_export_pii`, `level`, `is_active`.

  • Privilege escalation guard: a user cannot set a scope value higher than their own role's scope. For example, a user with `customer_edit_scope=own` cannot create a role with `customer_edit_scope=all`.
GET
/api/admin/roles/{role_id}/permissionsLegacy: /roles/{role_id}/permissions
view_users

List all permissions with granted status for a role

PUT
/api/admin/roles/{role_id}/permissions/{permission_id}Legacy: /roles/{role_id}/permissions/{permission_id}
manage_roles

Grant or revoke a permission

Request

{
  "granted": true
}
  • Privilege escalation guard: a user cannot grant a permission they do not hold themselves.
DELETE
/api/admin/roles/{role_id}/permissions/{permission_id}Legacy: /roles/{role_id}/permissions/{permission_id}
manage_roles

Remove a permission grant

GET
/api/admin/permissionsLegacy: /permissions
view_users

List all defined permissions

Query Parameters

NameTypeDescriptionDefault
categorystringFilter by permission category
POST
/api/admin/permissionsLegacy: /permissions
manage_roles

Create a new permission definition

20. CRM — Statuses, Assignments, Activities, Tasks

CRM building blocks: status management, customer-to-agent assignments, activity feeds, and task tracking.

POST
/api/crm/statusesLegacy: /statuses
manage_statuses

Create a status

`type` is normalised to lowercase.

Request

{
  "type": "finance",
  "name": "Deposited"
}
GET
/api/crm/statusesLegacy: /statuses
manage_statuses

List statuses

Query Parameters

NameTypeDescriptionDefault
typestringFilter by status type
active_onlyboolOnly active statuses
GET
/api/crm/statuses/groupedLegacy: /statuses/grouped
manage_statuses

All active statuses grouped by type

Call once on page load.

GET
/api/crm/statuses/{id}Legacy: /statuses/{id}
manage_statuses

Single status

PATCH
/api/crm/statuses/{id}Legacy: /statuses/{id}
manage_statuses

Update name or soft-delete

Request

{
  "name": "Funded",
  "is_active": false
}
DELETE
/api/crm/statuses/{id}Legacy: /statuses/{id}
manage_statuses

Hard delete a status

  • Rejected if any customer holds this status.
POST
/api/crm/customers/assignLegacy: /customers/assign
assign_customers

Assign customers to an agent

Request

{
  "customer_ids": [
    "uuid1",
    "uuid2"
  ],
  "role": "sales",
  "new_agent_id": "agent-uuid",
  "source": "rotation",
  "note": "Round-robin batch"
}
  • `new_agent_id: null` = unassign.
GET
/api/crm/customers/{id}/assignmentsLegacy: /customers/{id}/assignments
view_customers

Full assignment history

Query Parameters

NameTypeDescriptionDefault
rolestringFilter by assignment role
limitintMax results
GET
/api/crm/customers/{id}/assignments/currentLegacy: /customers/{id}/assignments/current
view_customers

Current agent per role

PATCH
/api/admin/users/{id}/teamLegacy: /users/{id}/team
edit_users

Move a user to a new team

Request

{
  "new_team_id": "uuid-or-null",
  "note": "Q2 restructure"
}
GET
/api/admin/users/{id}/team-historyLegacy: /users/{id}/team-history
view_users

Full team membership history

GET
/api/crm/customers/{id}/activitiesLegacy: /customers/{id}/activities
view_customers

Activity feed with keyset pagination

Query Parameters

NameTypeDescriptionDefault
typestringComma-separated activity types
limitint1-200
before_idintCursor for keyset pagination
include_deletedboolInclude soft-deleted notes

Response

{
  "items": [
    "..."
  ],
  "next_cursor": "1187"
}
POST
/api/crm/customers/{id}/activities/notesLegacy: /customers/{id}/activities/notes
view_customers

Create a note

Request

{
  "body": "Called customer, interested in gold trading"
}
PATCH
/api/crm/customers/{id}/activities/{act_id}Legacy: /customers/{id}/activities/{act_id}
view_customers

Edit a note body

Only the author may edit. Previous body saved to `note_edits`.

DELETE
/api/crm/customers/{id}/activities/{act_id}Legacy: /customers/{id}/activities/{act_id}
view_customers

Soft-delete a note

Only the author may delete.

GET
/api/crm/customers/{id}/activities/{act_id}/editsLegacy: /customers/{id}/activities/{act_id}/edits
view_customers

Full edit history for a note

POST
/api/crm/customers/{id}/tasksLegacy: /customers/{id}/tasks
create_tasks

Create a task

Request

{
  "title": "Follow up call Friday 3pm",
  "due_at": "2026-03-13T13:00:00Z",
  "assigned_to": "user-uuid"
}
GET
/api/crm/customers/{id}/tasksLegacy: /customers/{id}/tasks
view_customers

List tasks for a customer

Query Parameters

NameTypeDescriptionDefault
statusstringFilter by task status
PATCH
/api/crm/customers/{id}/tasks/{task_id}Legacy: /customers/{id}/tasks/{task_id}
create_tasks

Update a task

Status transitions: `-> done` (sets `completed_at`), `-> cancelled` (sets `cancelled_at`).

GET
/api/crm/tasks/mineLegacy: /tasks/mine
AUTH

Tasks assigned to current user

Query Parameters

NameTypeDescriptionDefault
statusstringFilter by task status
overdueboolOnly overdue tasks
GET
/api/crm/tasks/teamLegacy: /tasks/team
view_users

Tasks for all users in caller's team

Query Parameters

NameTypeDescriptionDefault
statusstringFilter by task status
assigned_toUUIDFilter by assignee

21. Broker Settings

Key-value configuration store for broker-wide settings.

GET
/api/platform/broker-settingsLegacy: /broker-settings
manage_broker_config

List all broker settings

Response

[
  {
    "key": "maker_checker_enabled",
    "value": "true",
    "description": "Require different user for tx approval",
    "updated_at": "2026-03-20T10:00:00Z"
  },
  {
    "key": "platform_name",
    "value": "MyBroker",
    "description": "Broker display name",
    "updated_at": "2026-03-19T08:00:00Z"
  }
]
PUT
/api/platform/broker-settings/{key}Legacy: /broker-settings/{key}
manage_broker_config

Create or update a broker setting

Upserts on key.

Request

{
  "value": "true",
  "description": "Require different user for transaction approval"
}

Response

{
  "key": "maker_checker_enabled",
  "value": "true"
}
  • Notable settings:
  • `maker_checker_enabled` -- when `"true"`, the user who created a transaction cannot approve it.
  • `platform_name` -- broker display name.
  • `platform_domain` -- used for autologin URLs, password reset links.

22. Email & SMS Templates

Manage email and SMS templates with multi-locale translations and placeholder support.

GET
/api/platform/email-templates/schema/placeholdersLegacy: /email-templates/schema/placeholders
manage_templates

List all available placeholders for email templates

Returns available placeholders (e.g., `{{customer.first_name}}`, `{{transaction.amount}}`).

POST
/api/platform/email-templatesLegacy: /email-templates
manage_templates

Create a new email template

Creates the parent record with no translations yet.

Request

{
  "name": "Welcome Email",
  "event_type": "customer.registered",
  "description": "Sent on signup",
  "default_locale": "en"
}
  • 201 Created -- returns template with ID.
409event_type already has a template.
GET
/api/platform/email-templatesLegacy: /email-templates
manage_templates

List all email templates with their translation locales

GET
/api/platform/email-templates/{template_id}Legacy: /email-templates/{template_id}
manage_templates

Single template with all translations

PUT
/api/platform/email-templates/{template_id}Legacy: /email-templates/{template_id}
manage_templates

Update template metadata

Update name, description, default_locale.

DELETE
/api/platform/email-templates/{template_id}Legacy: /email-templates/{template_id}
manage_templates

Soft-delete an email template

  • 204 No Content.
PUT
/api/platform/email-templates/{template_id}/translations/{locale}Legacy: /email-templates/{template_id}/translations/{locale}
manage_templates

Create or update a locale-specific translation

Request

{
  "subject": "Welcome, {{customer.first_name}}!",
  "body_html": "<p>Hello {{customer.first_name}}</p>",
  "body_text": "Hello {{customer.first_name}}"
}
DELETE
/api/platform/email-templates/{template_id}/translations/{locale}Legacy: /email-templates/{template_id}/translations/{locale}
manage_templates

Remove a translation

  • 204 No Content.
POST
/api/platform/email-templates/{template_id}/previewLegacy: /email-templates/{template_id}/preview
manage_templates

Preview a rendered template with sample data

Returns resolved HTML with placeholders filled.

POST
/api/platform/sms-templates/analyzeLegacy: /sms-templates/analyze
manage_templates

Analyze SMS content

Returns encoding (GSM-7 or UCS-2), character count, and segment count.

Request

{
  "body": "Your code is {{code}}"
}
POST
/api/platform/sms-templatesLegacy: /sms-templates
manage_templates

Create a new SMS template

Request

{
  "name": "Verification Code",
  "event_type": "verification",
  "description": "OTP SMS",
  "default_locale": "en"
}
  • 201 Created.
GET
/api/platform/sms-templatesLegacy: /sms-templates
manage_templates

List all SMS templates with translation locales

GET
/api/platform/sms-templates/{template_id}Legacy: /sms-templates/{template_id}
manage_templates

Single SMS template with all translations

PUT
/api/platform/sms-templates/{template_id}Legacy: /sms-templates/{template_id}
manage_templates

Update SMS template metadata

DELETE
/api/platform/sms-templates/{template_id}Legacy: /sms-templates/{template_id}
manage_templates

Soft-delete an SMS template

  • 204 No Content.
PUT
/api/platform/sms-templates/{template_id}/translations/{locale}Legacy: /sms-templates/{template_id}/translations/{locale}
manage_templates

Create or update a locale-specific SMS translation

Request

{
  "body": "Your verification code is {{code}}"
}
DELETE
/api/platform/sms-templates/{template_id}/translations/{locale}Legacy: /sms-templates/{template_id}/translations/{locale}
manage_templates

Remove an SMS translation

  • 204 No Content.
POST
/api/platform/sms-templates/{template_id}/previewLegacy: /sms-templates/{template_id}/preview
manage_templates

Preview a rendered SMS with sample data

23. Workflow Builder

Trigger -> conditions -> actions automation engine. Data-driven rules that execute on Kafka events.

POST
/api/platform/workflowsLegacy: /workflows
manage_workflows

Create a workflow rule

Request

{
  "name": "Auto-assign new deposits to retention",
  "description": "When a customer deposits, assign to retention team",
  "trigger_event": "customer.deposited",
  "trigger_config": {},
  "conditions": {
    "match": "all",
    "rules": [
      {
        "field": "event.amount",
        "operator": "gte",
        "value": 1000
      },
      {
        "field": "customer.retention_status",
        "operator": "eq",
        "value": "churning"
      }
    ]
  },
  "actions": [
    {
      "type": "handover_to_retention",
      "params": {}
    },
    {
      "type": "add_note",
      "params": {
        "body": "Auto-assigned: high-value deposit from churning customer"
      }
    },
    {
      "type": "send_email",
      "params": {
        "subject": "Welcome back!",
        "body_html": "<p>We noticed your deposit...</p>"
      }
    }
  ],
  "priority": 10,
  "stop_after": false
}
  • 201 Created.
  • `conditions`: Expression tree with `match` (`all`/`any`/`none`) and `rules` array -- supports unlimited nesting.
  • `priority`: Lower number = higher priority. All matching workflows execute in order; `stop_after=true` skips lower-priority workflows.
GET
/api/platform/workflowsLegacy: /workflows
manage_workflows

List all workflows

Query Parameters

NameTypeDescriptionDefault
triggerstringFilter by trigger event type
is_activeboolFilter by active status
GET
/api/platform/workflows/{id}Legacy: /workflows/{id}
manage_workflows

Single workflow with execution stats

PUT
/api/platform/workflows/{id}Legacy: /workflows/{id}
manage_workflows

Full replace of workflow definition

PATCH
/api/platform/workflows/{id}/toggleLegacy: /workflows/{id}/toggle
manage_workflows

Toggle is_active on/off

Response

{
  "id": "uuid",
  "is_active": true
}
DELETE
/api/platform/workflows/{id}Legacy: /workflows/{id}
manage_workflows

Soft-delete a workflow

Sets `is_active=false`.

POST
/api/platform/workflows/{id}/cloneLegacy: /workflows/{id}/clone
manage_workflows

Duplicate workflow

Clones the workflow with "(copy)" appended to name.

  • 201 Created.
GET
/api/platform/workflows/{id}/historyLegacy: /workflows/{id}/history
manage_workflows

Execution log for a specific workflow

Query Parameters

NameTypeDescriptionDefault
limitintMax results100
offsetintOffset for pagination0

Response

[
  {
    "id": "uuid",
    "workflow_id": "uuid",
    "workflow_name": "Auto-assign new deposits to retention",
    "trigger_event": "customer.deposited",
    "customer_id": "uuid",
    "customer_email": "[email protected]",
    "status": "completed",
    "actions_executed": 3,
    "error_message": null,
    "started_at": "2026-03-19T10:00:00Z",
    "completed_at": "2026-03-19T10:00:01Z"
  }
]
  • Status: `completed`, `failed`, or `delayed` (waiting for a `delay` action).
GET
/api/platform/workflows/schema/triggersLegacy: /workflows/schema/triggers
manage_workflows

Available trigger event types

Returns trigger event types with descriptions and field categories. Trigger events: `customer.registered`, `customer.deposited`, `customer.ftd`, `customer.withdrew`, `customer.traded`, `customer.status_changed`, `customer.retention_changed`, `call.no_answer`, `call.completed`, `task.overdue`, `customer.inactive_days`.

Response

[
  {
    "event": "customer.deposited",
    "description": "Customer deposit approved",
    "available_categories": [
      "customer",
      "event",
      "transaction"
    ]
  },
  {
    "event": "customer.traded",
    "description": "Customer opened a trade",
    "available_categories": [
      "customer",
      "event",
      "order"
    ]
  }
]
GET
/api/platform/workflows/schema/fieldsLegacy: /workflows/schema/fields
manage_workflows

All fields available in workflow conditions

Returns fields with value source metadata for UI rendering. Field categories: `customer.*`, `event.*`, `transaction.*`, `order.*`. Custom status types auto-discovered from the `statuses` table -- broker-created statuses appear without code changes.

Response

[
  {
    "path": "customer.country",
    "type": "string",
    "description": "Customer country (ISO 3166-1)",
    "category": "customer",
    "value_source": null
  },
  {
    "path": "customer.sales_status_id",
    "type": "integer",
    "description": "Sales status",
    "category": "customer",
    "value_source": {
      "type": "status",
      "status_type": "sales"
    }
  }
]
GET
/api/platform/workflows/schema/actionsLegacy: /workflows/schema/actions
manage_workflows

Registered action types with parameter schemas

Available actions (14): `assign_to_team`, `assign_to_agent`, `reassign_round_robin`, `handover_to_retention`, `set_status`, `send_email`, `send_sms`, `create_task`, `add_note`, `increment_manager_counter`, `set_customer_active`, `freeze_account`, `delay`, `webhook`.

Response

[
  {
    "type": "send_email",
    "description": "Send email via integration service",
    "params": {
      "subject": {
        "type": "string",
        "required": true
      },
      "body_html": {
        "type": "string",
        "required": true
      }
    }
  }
]
  • Team hierarchy in assignment actions: `assign_to_team`, `reassign_round_robin`, and `handover_to_retention` support hierarchical teams. When a parent team (desk) is selected, agents from ALL descendant sub-teams are included in the round-robin pool. Uses `GET /teams/{id}/subtree/users` (materialized path query). Round-robin counter persisted in Redis for even distribution across service restarts.
GET
/api/platform/workflows/schema/operatorsLegacy: /workflows/schema/operators
manage_workflows

All condition operators

16 operators: `eq`, `ne`, `gt`, `lt`, `gte`, `lte`, `in`, `not_in`, `is_null`, `is_not_null`, `contains`, `not_contains`, `starts_with`, `ends_with`, `between`, `regex`.

24. Risk, Retention, FTD & Trader Days

Risk classification, retention lifecycle, first-time deposit tracking, and trader engagement metrics.

GET
/api/admin/risk/rulesLegacy: /risk/rules
view_audit

Return current risk rule config

Returns the current risk rule configuration, or null.

PUT
/api/admin/risk/rulesLegacy: /risk/rules
manage_broker_config

Update risk classification thresholds

Triggers full batch recompute.

GET
/api/crm/risk/profilesLegacy: /risk/profiles
view_customers

List risk profiles

Query Parameters

NameTypeDescriptionDefault
classificationstringFilter by risk classification
min_tradesintMinimum trade count
sort_bystringSort field
limitintMax results
offsetintOffset for pagination
GET
/api/crm/risk/profiles/{customer_id}Legacy: /risk/profiles/{customer_id}
view_customers

Single customer risk profile

404No profile yet.
PUT
/api/platform/retention/rulesLegacy: /retention/rules
manage_broker_config

Create or replace the retention classification rule

Triggers full recompute.

Request

{
  "signals": [
    "last_deposit_at",
    "last_trade_at"
  ],
  "operator": "OR",
  "thresholds": {
    "active": {
      "max_days": 35
    },
    "churning": {
      "min_days": 35,
      "max_days": 60
    },
    "dormant": {
      "min_days": 60
    }
  }
}
  • Thresholds must be contiguous. Boundary: max_days is exclusive, min_days is inclusive.
GET
/api/platform/retention/rulesLegacy: /retention/rules
view_audit

Return the current retention rule

Returns the current rule, or null if not yet configured.

POST
/api/platform/ftd/minimumsLegacy: /ftd/minimums
manage_broker_config

Set a new per-country minimum deposit threshold

Append-only.

Request

{
  "country": "BR",
  "minimum_usd": 300,
  "effective_from": "2026-03-11T00:00:00Z",
  "note": "Q2 Brazil campaign"
}
GET
/api/platform/ftd/minimumsLegacy: /ftd/minimums
view_customers

Full FTD minimum history

Query Parameters

NameTypeDescriptionDefault
countrystringFilter by country code
GET
/api/platform/ftd/minimums/currentLegacy: /ftd/minimums/current
view_customers

Currently active minimum per country

GET
/api/crm/ftdLegacy: /ftd
view_customers

Confirmed FTDs

Query Parameters

NameTypeDescriptionDefault
countrystringFilter by country
affiliate_idstringFilter by affiliate
manager_team_idUUIDFilter by manager team
from_datestringStart date
to_datestringEnd date
pageintPage number
page_sizeintPage size
GET
/api/crm/ftd/incompleteLegacy: /ftd/incomplete
view_customers

In-progress FTDs (deposited but not reached minimum)

Query Parameters

NameTypeDescriptionDefault
countrystringFilter by country
affiliate_idstringFilter by affiliate
pageintPage number
page_sizeintPage size
GET
/api/crm/ftd/customers/{customer_id}Legacy: /ftd/customers/{customer_id}
view_customers

FTD status for a specific customer

GET
/api/crm/customers/{id}/trader-daysLegacy: /customers/{id}/trader-days
view_customers

Customer trading activity calendar

Returns dates when the customer opened at least one order.

Query Parameters

NameTypeDescriptionDefault
fromdateStart date (default: first of current month)
todateEnd date (default: last of current month). Max range: 90 days.

Response

{
  "customer_id": "uuid",
  "from": "2026-03-01",
  "to": "2026-03-31",
  "total_days": 12,
  "days": [
    {
      "date": "2026-03-01",
      "first_order_at": "2026-03-01T09:14:22Z",
      "symbol": "BTCUSDT",
      "login": "10007"
    }
  ]
}
GET
/api/crm/trader-days/summaryLegacy: /trader-days/summary
view_customers

Aggregated trader counts per agent and team

For a date range. Respects `customer_view_scope`.

Query Parameters

NameTypeDescriptionDefault
fromdateStart date (default: current month)
todateEnd date (default: current month). Max: 90 days.

Response

{
  "from": "2026-03-01",
  "to": "2026-03-31",
  "total_trader_days": 4520,
  "unique_traders": 890,
  "by_owner": [
    {
      "owner_id": "uuid",
      "owner_name": "John Smith",
      "trader_days": 320,
      "unique_traders": 45
    }
  ],
  "by_team": [
    {
      "team_id": "uuid",
      "team_name": "Sales Team A",
      "trader_days": 1200,
      "unique_traders": 150
    }
  ]
}
GET
/api/crm/trader-days/dailyLegacy: /trader-days/daily
view_customers

Day-by-day breakdown for charting engagement trends

Respects `customer_view_scope`.

Response

{
  "from": "2026-03-01",
  "to": "2026-03-31",
  "days": [
    {
      "date": "2026-03-01",
      "trader_days": 145,
      "unique_traders": 145
    }
  ]
}

25. Compliance & Jurisdiction

KYC webhook processing, jurisdiction restrictions, compliance events, account freezing, and fraud prevention. **Compliance Gates:** | Gate | Tier 1 | Tier 2 | Tier 3 | |------|--------|--------|--------| | Registration | Block | Set `restricted_jurisdiction` | Log | | Login | Freeze at 3rd login | Record | Record | | Deposit | Reject (frozen) | Hold for review | Allow | | Trade | Reject (frozen) | Allow | Allow | | KYC | Retroactive freeze | Set restricted | Log | **Frozen accounts:** can login, can withdraw, cannot trade, cannot deposit.

POST
/webhooks/kyc
PUBLIC

Provider-agnostic KYC webhook

Accepts results from SumSub, Shufti, Jumio, or manual verification. Public endpoint, HMAC-verified.

Request

{
  "customer_id": "uuid",
  "provider": "sumsub",
  "provider_id": "ext-ref-123",
  "status": "approved",
  "doc_type": "passport",
  "doc_country": "US",
  "raw_result": {}
}
GET
/api/backoffice/compliance/jurisdictionsLegacy: /compliance/jurisdictions
manage_compliance

List restricted jurisdictions

Query Parameters

NameTypeDescriptionDefault
tierintFilter by tier (e.g., 1)
POST
/api/backoffice/compliance/jurisdictionsLegacy: /compliance/jurisdictions
manage_compliance

Add a restricted jurisdiction

PATCH
/api/backoffice/compliance/jurisdictions/{country_code}Legacy: /compliance/jurisdictions/{country_code}
manage_compliance

Update tier, reason, or is_active

GET
/api/backoffice/compliance/eventsLegacy: /compliance/events
manage_compliance

Compliance events

Query Parameters

NameTypeDescriptionDefault
customer_idUUIDFilter by customer
event_typestringFilter by event type
gatestringFilter by compliance gate
limitintMax results
GET
/api/backoffice/compliance/logins/{customer_id}Legacy: /compliance/logins/{customer_id}
manage_compliance

Login audit trail

Query Parameters

NameTypeDescriptionDefault
limitintMax results
POST
/api/backoffice/compliance/freeze/{customer_id}Legacy: /compliance/freeze/{customer_id}
manage_compliance

Manually freeze a customer account

Request

{
  "reason": "Suspicious activity"
}
POST
/api/backoffice/compliance/unfreeze/{customer_id}Legacy: /compliance/unfreeze/{customer_id}
manage_compliance

Manually unfreeze a customer account

Request

{
  "reason": "KYC verified",
  "new_status": null
}
GET
/api/backoffice/admin/disposable-domainsLegacy: /admin/disposable-domains
manage_compliance

List all blocked disposable email domains

POST
/api/backoffice/admin/disposable-domainsLegacy: /admin/disposable-domains
manage_compliance

Add a domain to the blocklist

Request

{
  "domain": "tempmail.com"
}
DELETE
/api/backoffice/admin/disposable-domains/{domain}Legacy: /admin/disposable-domains/{domain}
manage_compliance

Remove a domain from the blocklist

GET
/api/backoffice/admin/fraud-logLegacy: /admin/fraud-log
manage_compliance

Query the fraud audit log

Query Parameters

NameTypeDescriptionDefault
date_fromstringStart date filter
date_tostringEnd date filter
fraud_typestringFilter by fraud type
affiliate_idstringFilter by affiliate
limitintMax results (default 100)100

26. Consents & Communications

Track customer consent records for legal documents and view communication history.

POST
/api/crm/customers/{customer_id}/consentsLegacy: /customers/{customer_id}/consents
view_customers

Record customer acceptance of a legal document

Captures IP address and User-Agent automatically. Upserts on `(customer_id, document_type, document_version)`.

Request

{
  "document_type": "terms_and_conditions",
  "document_version": "2.3",
  "accepted": true
}
  • 201 Created.
GET
/api/crm/customers/{customer_id}/consentsLegacy: /customers/{customer_id}/consents
view_customers

List all consent records for a customer

Ordered by `accepted_at DESC`. For compliance review.

GET
/api/crm/customers/{customer_id}/communicationsLegacy: /customers/{customer_id}/communications
view_customers

Communication log -- all emails and SMS sent to this customer

Query Parameters

NameTypeDescriptionDefault
channelstringFilter: `email` or `sms`
limitintMax 500, default 5050

27. Country Default Groups

Per-country default trading group assigned at registration. Customers from a mapped country get that group; unmapped countries fall back to the platform default group.

GET
/api/platform/country-default-groupsLegacy: /country-default-groups
manage_broker_config

List all per-country group assignments

Response

[
  {
    "country": "DE",
    "group_id": "uuid",
    "group_name": "EU Standard",
    "note": "EU regulation",
    "updated_at": "..."
  }
]
PUT
/api/platform/country-default-groups/{country}Legacy: /country-default-groups/{country}
manage_broker_config

Set or update the default group for a country

ISO 3166-1 alpha-2 (2 chars). Validates group exists. Upserts on country code.

Request

{
  "group_id": "uuid",
  "note": "EU regulation"
}
DELETE
/api/platform/country-default-groups/{country}Legacy: /country-default-groups/{country}
manage_broker_config

Remove mapping -- country reverts to platform default group

  • 204 No Content.

28. Integration Service

Connector marketplace for VoIP, email, SMS, payments, and KYC integrations. One active provider per category per broker (V1). Credentials encrypted at rest (AES-256-GCM). V1 catalog: `zadarma` (VoIP), `voiso` (VoIP), `sendgrid` (email), `gmail` (email), `praxis` (payment), `bridgerpay` (payment), `twilio_voice` (VoIP), `twilio_sms` (SMS), `sumsub` (KYC).

GET
/api/platform/integrations/catalogLegacy: /integrations/catalog
manage_broker_config

List all available integrations in the platform catalog

Response

[
  {
    "slug": "sendgrid",
    "category": "email",
    "display_name": "SendGrid",
    "description": "Transactional email via SendGrid API",
    "credential_schema": {
      "api_key": {
        "type": "string",
        "required": true
      }
    },
    "config_schema": {},
    "is_available": true
  }
]
GET
/api/platform/integrations/catalog/{slug}Legacy: /integrations/catalog/{slug}
manage_broker_config

Single catalog entry by slug

GET
/api/platform/integrations/activeLegacy: /integrations/active
manage_broker_config

List all activated integrations for this broker

Credentials are masked in the response.

GET
/api/platform/integrations/active/{slug}Legacy: /integrations/active/{slug}
manage_broker_config

Single active integration with masked credentials and health status

Response

{
  "id": "uuid",
  "slug": "sendgrid",
  "category": "email",
  "display_name": "SendGrid",
  "is_active": true,
  "health_status": "healthy",
  "health_message": "OK",
  "last_health_check": "2026-03-19T10:00:00Z",
  "config": {},
  "credentials_summary": {
    "api_key": "SG.***redacted***"
  },
  "activated_at": "2026-03-10T12:00:00Z"
}
POST
/api/platform/integrations/{slug}/activateLegacy: /integrations/{slug}/activate
manage_broker_config

Activate an integration with credentials

One provider per category -- activating a second email provider returns 409.

Request

{
  "credentials": {
    "api_key": "SG.xxx"
  },
  "config": {}
}

Response

{
  "slug": "sendgrid",
  "category": "email",
  "is_active": true,
  "activated_at": "..."
}
409Category already has an active integration.422Credential/config schema validation failed.
POST
/api/platform/integrations/{slug}/deactivateLegacy: /integrations/{slug}/deactivate
manage_broker_config

Deactivate an integration

400Already inactive.
PUT
/api/platform/integrations/{slug}/credentialsLegacy: /integrations/{slug}/credentials
manage_broker_config

Update credentials for an active integration

Re-encrypts at rest, resets health status, invalidates connector pool cache.

Request

{
  "credentials": {}
}
PUT
/api/platform/integrations/{slug}/configLegacy: /integrations/{slug}/config
manage_broker_config

Update configuration for an active integration

Non-credential settings.

Request

{
  "config": {}
}
POST
/api/platform/integrations/{slug}/testLegacy: /integrations/{slug}/test
manage_broker_config

Test connector health

If credentials provided in body, tests with those temporarily (no storage). Otherwise tests with stored credentials.

Request

{
  "credentials": {}
}

Response

{
  "status": "success",
  "message": "..."
}
  • Request body is optional. Status: `success` or `failure`.
POST
/api/platform/integrations/execute/{category}/{action}Legacy: /integrations/execute/{category}/{action}
manage_broker_config

Execute an action through the active provider for a category

404No integration configured for category.501Action not implemented by provider.502Provider timeout or error.
POST
/api/platform/integrations/email/sendLegacy: /integrations/email/send
manage_broker_config

Send an email via the active email provider

Provider-agnostic send endpoint. All fields except `to`, `subject` are optional. `from_email`/`from_name` fall back to `EMAIL_FROM_ADDRESS`/`EMAIL_FROM_NAME` env vars.

Request

{
  "to": "[email protected]",
  "subject": "Welcome to trading",
  "body_html": "<p>Hello John</p>",
  "body_text": "Hello John",
  "from_email": "[email protected]",
  "from_name": "Broker Support",
  "reply_to": "[email protected]",
  "cc": [],
  "bcc": [],
  "tags": {
    "campaign": "onboarding"
  },
  "customer_id": "uuid",
  "event_type": "welcome"
}

Response

{
  "message_id": "...",
  "status": "sent",
  "provider": "sendgrid"
}
502Provider error.
POST
/api/platform/integrations/sms/sendLegacy: /integrations/sms/send
manage_broker_config

Send an SMS via the active SMS provider

`from_number` falls back to `SMS_FROM_NUMBER` env var.

Request

{
  "to": "+447700900000",
  "body": "Your verification code is 123456",
  "from_number": "+15551234567",
  "tags": {
    "type": "verification"
  },
  "customer_id": "uuid",
  "event_type": "verification"
}

Response

{
  "message_id": "...",
  "status": "sent",
  "provider": "twilio_sms"
}
POST
/api/platform/integrations/call/initiateLegacy: /integrations/call/initiate
manage_broker_config

Initiate a phone call via the active VoIP provider

`from_number` falls back to `CALL_FROM_NUMBER` env var.

Request

{
  "to": "+447700900000",
  "from_number": "+15551234567",
  "from_agent_id": "uuid",
  "customer_id": "uuid",
  "metadata": {
    "reason": "follow_up"
  }
}

Response

{
  "call_id": "...",
  "status": "initiated",
  "provider": "zadarma"
}
GET
/api/platform/integrations/configLegacy: /integrations/config
manage_broker_config

Provider names and sender defaults

No credentials exposed. Used by frontend to show current configuration.

Response

{
  "email": {
    "provider": "sendgrid",
    "from_address": "[email protected]",
    "from_name": "Broker"
  },
  "sms": {
    "provider": "twilio_sms",
    "from_number": "+15551234567"
  },
  "call": {
    "provider": "zadarma",
    "from_number": "+15559876543"
  }
}
POST
/integrations/email/webhook/{provider}
PUBLIC

Inbound delivery status from email provider

Provider delivery status callback (e.g., `sendgrid`). Verified via provider-specific HMAC signatures.

POST
/integrations/sms/webhook/{provider}
PUBLIC

Inbound delivery status from SMS provider

Provider delivery status callback (e.g., `twilio_sms`). Verified via provider-specific HMAC signatures.

POST
/integrations/call/webhook/{provider}
PUBLIC

Inbound call completion status from VoIP provider

Provider callback (e.g., `zadarma`, `voiso`). Verified via provider-specific HMAC signatures.

POST
/integrations/webhooks/{slug}
PUBLIC

Generic webhook handler for payment and KYC providers

Connector verifies HMAC, processes payload, forwards to account-service. - **Payment flow**: Verifies -> forwards to `POST /transactions/payment-callback` with `{cashier_id, status, gateway_payload}` - **KYC flow (sumsub)**: Verifies `x-payload-digest` -> maps status -> fetches doc details -> signs and forwards to `POST /webhooks/kyc`

POST
/api/platform/integrations/bridgerpay/create-sessionLegacy: /integrations/bridgerpay/create-session
manage_broker_config

Create a BridgerPay cashier session

Called internally by account-service during `POST /payments/bridgerpay/session`. Requires active BridgerPay integration.

Request

{
  "order_id": "bp_abc123",
  "amount": 500,
  "currency": "USD",
  "email": "[email protected]",
  "first_name": "Omar",
  "last_name": "Al-Rashid",
  "country": "AE",
  "phone": "+971501234567",
  "customer_id": "uuid"
}

Response

{
  "cashier_token": "abc123...",
  "cashier_session_id": "...",
  "cashier_key": "..."
}
GET
/api/platform/integrations/auditLegacy: /integrations/audit
manage_broker_config

Integration activity log

Actions: `activated`, `deactivated`, `credentials_updated`, `config_updated`, `health_check_passed`, `health_check_failed`, `execute_success`, `execute_failure`, `webhook_received`.

Query Parameters

NameTypeDescriptionDefault
slugstringFilter by integration slug
actionstringFilter by action type
limitintMax results50
offsetintOffset for pagination0

29. Affiliate Webhooks

Outbound webhook management for affiliate portals. Affiliate portals receive HTTP POST with HMAC-SHA256 signature in `X-Webhook-Signature`. Batched every 30 seconds. **Outbound payload examples:** `customer.status_updated` -- status change notification. `lead.ftd` -- final webhook for this lead (all subsequent events permanently suppressed).

POST
/api/platform/affiliates/webhooksLegacy: /webhooks
manage_broker_config

Register a new affiliate webhook endpoint

Request

{
  "affiliate_id": "partner_acme",
  "endpoint_url": "https://acme-affiliates.com/api/webhook",
  "secret": "hmac-secret-key",
  "batch_size_max": 500
}
GET
/api/platform/affiliates/webhooksLegacy: /webhooks
manage_broker_config

List all registered webhooks

GET
/api/platform/affiliates/webhooks/{webhook_id}Legacy: /webhooks/{webhook_id}
manage_broker_config

Single webhook

PATCH
/api/platform/affiliates/webhooks/{webhook_id}Legacy: /webhooks/{webhook_id}
manage_broker_config

Update a webhook

Updatable fields: `endpoint_url`, `secret`, `is_active`, `batch_size_max`.

DELETE
/api/platform/affiliates/webhooks/{webhook_id}Legacy: /webhooks/{webhook_id}
manage_broker_config

Delete a webhook

GET
/api/platform/affiliates/webhooks/{webhook_id}/deliveriesLegacy: /webhooks/{webhook_id}/deliveries
manage_broker_config

Delivery attempts (most recent first)

  • Limit 100.
GET
/api/platform/affiliates/webhooks/{webhook_id}/deliveries/statsLegacy: /webhooks/{webhook_id}/deliveries/stats
manage_broker_config

Aggregate delivery statistics

30. Calendar

Upcoming economic events calendar.

GET
/calendar
PUBLIC

Upcoming economic events

Query Parameters

NameTypeDescriptionDefault
fromstringStart date `YYYY-MM-DD`today
tostringEnd date (inclusive)+7 days
currencystringComma-separated: `USD,EUR,GBP`
impactstring`high` or `medium`

Response

[
  {
    "id": 1,
    "event_name": "Business NZ PMI",
    "country": "New Zealand",
    "currency": "NZD",
    "category": "Manufacturing PMI",
    "impact": "medium",
    "event_date": "2026-03-13T03:30:00Z",
    "actual": null,
    "forecast": null,
    "previous": 55.2
  }
]

31. Stopout Events & Audit Log

Stopout force-close event monitoring and immutable audit trail of financial state changes.

GET
/api/backoffice/stopout-eventsLegacy: /stopout-events
view_audit

List recent stopout force-close events

For dealing room / risk monitoring.

Query Parameters

NameTypeDescriptionDefault
loginstringFilter by account login
limitintMax results (default 50, max 500)50
offsetintOffset for pagination

Response

{
  "id": 1,
  "login": 10001,
  "order_ticket": 100042,
  "symbol": "EURUSD",
  "margin_level_at_trigger": 18.5,
  "margin_level_after_close": 45.2,
  "equity_at_trigger": 450.2,
  "profit_realized": -125.3,
  "close_price": 1.08542,
  "stopout_level": 20,
  "positions_remaining": 2,
  "created_at": "2026-03-11T14:30:00Z"
}
GET
/api/backoffice/audit-logLegacy: /audit-log
view_audit

Immutable audit trail of financial state changes

Query Parameters

NameTypeDescriptionDefault
entity_typestringFilter by entity type
entity_idstringFilter by entity ID
loginstringFilter by account login
actionstringFilter by action type
limitintMax results (default 100, max 1000)100
offsetintOffset for pagination

Response

{
  "id": 1,
  "entity_type": "order",
  "entity_id": "100042",
  "action": "open",
  "login": "10001",
  "details": {
    "symbol": "EURUSD",
    "volume": 0.1,
    "cmd": 0
  },
  "balance_before": 10000,
  "balance_after": 10000,
  "credit_before": 0,
  "credit_after": 0,
  "margin_used_before": 0,
  "margin_used_after": 108.54,
  "source_service": "order-service",
  "created_at": "2026-03-11T14:30:00Z"
}

32. Account SSE Stream

Server-Sent Events stream of live account state. Browser-compatible via `EventSource`. - Initial snapshot from Redis `HGETALL account:{login}:state` - Subsequent updates via Redis Pub/Sub, throttled to max 500ms between events - 25s keepalive ping - Headers: `Cache-Control: no-cache`, `X-Accel-Buffering: no`

GET
/api/crm/accounts/{login}/streamLegacy: /accounts/{login}/stream
view_customers

Live account state via Server-Sent Events

Opens an SSE stream for real-time account state updates. Connect using `EventSource` in the browser. **Event format:** ``` event: state data: {"balance": 10000.0, "credit": 0.0, "equity": 10250.5, "pnl": 250.5, "margin_used": 500.0, "free_margin": 9750.5, "margin_level": 2050.1, "gross_exposure": 10854.2, "net_exposure": 5427.1, "currency": "USD"} ```

Response

{
  "balance": 10000,
  "credit": 0,
  "equity": 10250.5,
  "pnl": 250.5,
  "margin_used": 500,
  "free_margin": 9750.5,
  "margin_level": 2050.1,
  "gross_exposure": 10854.2,
  "net_exposure": 5427.1,
  "currency": "USD"
}
  • Content-Type: text/event-stream.
  • Initial snapshot sent immediately on connection.
  • Subsequent updates pushed via Redis Pub/Sub, throttled to max 500ms between events.
  • 25s keepalive ping to prevent proxy timeouts.