Guide

Provision users & accounts

Your system is the source of truth for who a client is. This guide creates the client in Octolit under your own id, opens their trading accounts, and covers the day-two operations: pausing, signing out, keeping records in sync and honouring deletion requests.

#The model

User
one CRM client, addressed by external_id (your id)
Brand
chosen at creation (brand_id), never changes
Trading account
live or demo, one currency, one account type
Limits
max live / demo accounts per client = brand setting

Your id is stored on the client as external_user_id and comes back in every webhook as data.client.external_id. It must be 1 - 191 characters from letters, digits and . _ @ : + -. Choose an id that never changes for a person - a database key, not an e-mail.

#Before you start

  • The brand switch Partner API is on; your key carries users:write, clients:read and accounts:write (plus accounts:read, trading:control, sso:issue for the later sections).
  • You know the brand_id of the brand the client belongs to (shown in the Octolit CRM; every client the API returns carries it as brand.id).
  • You know the account_type_id values you want to open accounts with (the CRM's account types; the company default is used when you omit it).

#Onboarding flow

  1. your siteUser registers (or passes your KYC) on your side
  2. step 1PUT …/users/cust_10042 with brand_id, names, e-mail, phone, country → 201
  3. step 2POST …/users/cust_10042/accounts for the live account (and one more call for a demo) → 201
  4. step 3fund it with money posting, or let the client deposit in the trader
  5. step 4sign the user in with single sign-on; the client is trading
  1. Create the user

    One idempotent call. If no client with this external_id exists in your company it is created in the brand named by brand_id and the answer is 201. country is ISO 3166-1 alpha-2; phone carries the country code.

    PUT /api/external/v1/users/cust_10042
    curl -X PUT "https://api.your-brand.com/api/external/v1/users/cust_10042" \
      -H "X-Api-Key: $OCTOLIT_PARTNER_KEY" \
      -H "Content-Type: application/json" \
      -d '{
      "brand_id": 13,
      "email": "anna@example.com",
      "first_name": "Anna",
      "last_name": "Lindqvist",
      "phone": "+46 70 123 45 67",
      "country": "SE"
    }'
    201 Created
    {
      "id": 10042,
      "external_id": "cust_10042",
      "first_name": "Anna",
      "last_name": "Lindqvist",
      "full_name": "Anna Lindqvist",
      "email": "a***@example.com",
      "phone": "+46 70 *** ** 67",
      "brand": {
        "id": 13,
        "name": "Your brand"
      },
      "country": "SE",
      "active": true,
      "is_lead": false,
      "deleted": false,
      "login_enabled": true,
      "trading_enabled": true,
      "accounts": 1,
      "kyc": {
        "id_status": "approved",
        "billing_status": "pending"
      },
      "created_at": "2026-11-11T09:30:00Z",
      "updated_at": "2026-11-11T09:30:00Z"
    }

    email and phone come back masked unless the key also carries clients:read_email / clients:read_phone.

  2. Open the live account

    The account type decides live or demo and which pricing applies. The brand's maximum number of live accounts per client is enforced (409 limit_reached). The new account starts at balance 0.

    POST /api/external/v1/users/cust_10042/accounts
    curl -X POST "https://api.your-brand.com/api/external/v1/users/cust_10042/accounts" \
      -H "X-Api-Key: $OCTOLIT_PARTNER_KEY" \
      -H "Content-Type: application/json" \
      -d '{
      "account_type_id": 4,
      "currency": "EUR",
      "name": "Main EUR"
    }'
    201 Created
    {
      "account": {
        "id": 88213,
        "name": "Main EUR",
        "currency": "EUR",
        "demo": false,
        "type": {
          "id": 4,
          "name": "Standard",
          "provider": "internal"
        },
        "balance": "0",
        "equity": "0",
        "margin": "0",
        "free_margin": "0",
        "margin_level": null,
        "open_pl": "0",
        "closed_pl": "0",
        "credit": "0",
        "mt_login": null,
        "created_at": "2026-11-11T09:31:00Z"
      }
    }
  3. Open a demo account (optional)

    A demo account type always gives a demo account; demo: true on the default live type gives a demo account on live pricing. Demo accounts have their own per-client maximum.

    POST /api/external/v1/users/cust_10042/accounts
    curl -X POST "https://api.your-brand.com/api/external/v1/users/cust_10042/accounts" \
      -H "X-Api-Key: $OCTOLIT_PARTNER_KEY" \
      -H "Content-Type: application/json" \
      -d '{
      "demo": true,
      "currency": "USD",
      "name": "Practice"
    }'
  4. Fund and sign in

    Book the first deposit from your cashier with money posting (or let the client use the trader's own deposit page), then hand the user over with a one-time login link. The platform sends account.created, deposit.status and client.login to your webhook along the way.

#Users: update and read

The same PUT updates an existing user (200). Send only the fields that changed - every field is optional. Values are added, never removed: a new e-mail joins the client's emails list, a new phone the phones list. There is no delete; see export & erase.

PUT /api/external/v1/users/cust_10042 (update)
curl -X PUT "https://api.your-brand.com/api/external/v1/users/cust_10042" \
  -H "X-Api-Key: $OCTOLIT_PARTNER_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "phone": "+46 70 765 43 21"
}'
FieldRule
first_name, last_name, full_nameUp to 120 characters each. Send either the pair or full_name.
emailUp to 254 characters. An address that belongs to another client of your company answers 409 email_taken.
phoneWith the country code, e.g. +357 99 123456.
countryISO 3166-1 alpha-2 (SE, DE, …).
brand_idRead only when the user is created. A later call with another brand answers 409 brand_fixed.

Read the user back with GET …/users/{external_id}. Three fields matter for your own UI: login_enabled (the client may sign in), trading_enabled (false after a pause, see below) and kyc.id_status / kyc.billing_status.

GET /api/external/v1/users/cust_10042
curl "https://api.your-brand.com/api/external/v1/users/cust_10042" \
  -H "X-Api-Key: $OCTOLIT_PARTNER_KEY"

#Trading accounts

List a user's accounts with their live figures, or read one combined balance across all accounts with GET …/balance. Each account carries type.provider: internal for accounts the platform executes, mt for accounts on your MetaTrader server (then mt_login is set - see the MT4 / MT5 guide).

GET /api/external/v1/users/cust_10042/accounts
curl "https://api.your-brand.com/api/external/v1/users/cust_10042/accounts" \
  -H "X-Api-Key: $OCTOLIT_PARTNER_KEY"
  • currency on creation is a 3-letter code (USD, EUR, GBP, …) or its id; default USD. It must be one the platform supports (422 otherwise).
  • name (up to 64 characters) is what the client sees in the account switcher.
  • Leverage, spreads and margin rules come from the account type and the symbol settings in the CRM, not from the API call. Pick the type that carries the leverage you want.
  • Accounts are not deleted through the API. Stop using one by not funding it, or pause the whole user.

#Pause and resume trading

Pausing is the compliance lever: new positions, balance changes and transfers are refused for the user until you resume. Open positions stay open and are still managed - stop loss, take profit and stop-out keep working. The pause is recorded in the client's CRM history with the key that did it.

POST /api/external/v1/users/cust_10042/trading/disable
curl -X POST "https://api.your-brand.com/api/external/v1/users/cust_10042/trading/disable" \
  -H "X-Api-Key: $OCTOLIT_PARTNER_KEY"
POST /api/external/v1/users/cust_10042/trading/enable
curl -X POST "https://api.your-brand.com/api/external/v1/users/cust_10042/trading/enable" \
  -H "X-Api-Key: $OCTOLIT_PARTNER_KEY"

Both answer 200; the user's trading_enabled flips. Calling disable twice is harmless. Typical triggers: a failed KYC re-check, a chargeback, a self-exclusion request, a sanctions hit on your side.

#Sessions

A pause does not sign the user out. When the account must go dark at once, end the sessions as well: the user is signed out of the trader and the dashboard everywhere.

DELETE /api/external/v1/users/cust_10042/sessions
curl -X DELETE "https://api.your-brand.com/api/external/v1/users/cust_10042/sessions" \
  -H "X-Api-Key: $OCTOLIT_PARTNER_KEY"

List sessions first if you want to show "signed in on 2 devices" in your own UI. Both need the sso:issue scope.

#Keep both systems in sync

Changes made in the Octolit CRM by your staff (a new e-mail, a KYC decision, an account opened by an agent) do not go through your API calls. Two ways to pick them up:

  • Webhooks - client.created, client.updated, client.deleted, account.created and the rest arrive within seconds. See the webhooks guide.
  • Polling by change time - GET /clients?updated_since=… pages through every client changed since your last run (cursor based, up to 100 per page). Run it nightly as a safety net, and GET /clients/deleted for clients erased in the CRM.
GET /api/external/v1/clients?updated_since=…
curl "https://api.your-brand.com/api/external/v1/clients?updated_since=2026-11-10T00:00:00Z&per_page=100" \
  -H "X-Api-Key: $OCTOLIT_PARTNER_KEY"
One owner for client creation
Clients your staff create by hand in the CRM have no external_user_id. A PUT under a new id with that client's e-mail does not link the two - it answers 409 email_taken. Let your system create every client, and when a hand-made client shows up in the change feed with an e-mail you know, settle it in the CRM first (your staff decide which record stays), then PUT again.

#Data export and erasure

Subject-access and deletion requests are handled in the Octolit CRM by your staff; the API does not delete clients. What you need to know when your own process runs:

  • Export: the CRM produces a per-client data export as a zip (client page > Data management). Your own records are yours to add.
  • Erasure of personal data is refused while the client has open positions, pending money or a balance. Close, settle and withdraw first (pausing alone is not enough). The client row stays as "Erased client <id>".
  • After erasure the client is listed by GET /clients/deleted as { id, deleted_at } and client.deleted is sent - so store the CRM id from the PUT answer next to your external_id. A later PUT with the same external_id creates a fresh client.
  • Reduced-data mode is a per-brand setting: clients created through single sign-on get no name or e-mail, and sign-in rows keep only the network part of the IP. Set it before go-live if your contract requires it.
  • Retention of the sign-in history and the other logs is a CRM setting; the webhook delivery log keeps 30 days.

#Idempotency and retries

  • PUT …/users/{external_id} is idempotent by design: the same body twice gives the same client. Retry freely on a network error.
  • POST …/accounts is not: two calls open two accounts (until the brand's limit stops you). On a timeout, list the accounts and look for the name you sent before you retry.
  • Pause, resume and session calls are safe to repeat.
  • Money posting has a required idempotency key - see that guide.
  • On 429 wait for Retry-After; on 5xx retry with backoff (1 s, 2 s, 4 s, three times).

#Go-live checklist

  • external_id is a stable internal key; a test shows the same id maps to the same client after a second PUT.
  • brand_id is sent on creation and the right account types are used for live and demo.
  • Your UI reads login_enabled and trading_enabled and explains a pause to the client.
  • A compliance action on your side calls trading/disable and DELETE …/sessions.
  • A nightly updated_since run (or webhooks) keeps e-mails and KYC status in sync.
  • Your deletion process checks GET /clients/deleted and removes your copy.

Reference: create or update a user, open an account, list accounts, pause / resume trading, end sessions, login link.