API documentation

The SteraTrader CRM API lets your back office open trading accounts, process deposits and withdrawals, read group configuration, and follow what clients are trading. It is a REST API over HTTPS with JSON bodies, scoped API keys, and signed webhooks.

Two conventions are worth reading before anything else, because they are where integrations with other platforms usually go wrong.

base url
https://api.steratrader.com/crm/v1

Authentication

Every request carries an API key as a bearer token. Your broker creates the key in their back office and chooses its scopes, which groups it may use, and which IP addresses may present it.

bash
curl https://api.steratrader.com/crm/v1/whoami \
  -H "Authorization: Bearer stk_your_key_here"

Call /whoami first. It returns the broker, the scopes your key holds, and which groups it may place accounts into — which saves a support ticket when something returns 403 later.

A key that can withdraw funds or place trades must be restricted to specific IP addresses — the platform refuses to create one otherwise. A leaked withdrawal key costs the broker money once; a leaked dealing key can trade an account to zero.

Scopes

groups.readRead groups, servers and instruments
accounts.readRead accounts and balances
positions.readRead positions, orders and history
accounts.createOpen trading accounts
accounts.updateChange leverage and account status
money.depositCredit deposits
money.withdrawDebit withdrawals
bonus.grantGrant bonuses
positions.dealOpen and close positions on a client's account

Conventions

Money is always integer minor units

$10.50 is 1050. There is no scaling factor to look up and no decimal to lose a cent to. Amounts are always positive; the kind field decides direction, so a withdrawal of $250 is {"kind": "withdrawal", "amount": 25000}.

Every write is idempotent

Account creation takes an external_id; money movements require an external_transaction_id. Repeat either and you get the original result with a 200 and replayed: true — not a second account, and not a second credit.

This matters more than it sounds. A payment gateway firing its callback three times is normal, not exceptional, and a platform that credits three times is a platform that loses money.

json
// First call
{ "success": true, "data": { "balance": 525000, "replayed": false } }

// Same external_transaction_id again
{ "success": true, "message": "Already applied.",
  "data": { "balance": 525000, "replayed": true } }

Errors

Errors return a non-2xx status and a detail field written for a person rather than a log. The status codes are conventional: 400 for a bad request, 401 for an unrecognised key, 403 for a missing scope or an unlisted IP, 404 for something that does not exist, 429 when rate limited.

json
{
  "detail": "This account holds 2 open position(s). A leverage change alters
             their margin, so close them first."
}

Groups

GET/groupsgroups.read

Returns only the groups your key may place accounts into. A group carries leverage, margin levels, execution model and the markup clients on it are quoted.

Slugs, not paths

Every group has a slug and a separate name. Reference the slug in API calls: it is stable and never changes. The name is what the broker and their clients see, and they can change it whenever they like without breaking your integration.

This is deliberate. On platforms where the group path is the identifier, renaming a group breaks every system referencing it — so nobody renames anything, and installations fill up with groups called real\NEW2. Separating the two avoids that entirely.

Migrating from another platform

If your system already sends group names from somewhere else, the broker can add those as aliases on the matching group. Your calls keep working unchanged while you move to slugs at your own pace.

json
{
  "success": true,
  "data": [
    {
      "id": 1,
      "name": "Standard",
      "currency": "USD",
      "leverage": 200,
      "margin_call": "100.00",
      "stop_out": "50.00",
      "execution_model": "b_book",
      "is_default": true
    }
  ]
}

Servers

GET/serversgroups.read

The server is what a trader picks in the terminal. Live and demo are separate servers, and an account must sit on the one matching its type.

Instruments

GET/instruments?group_id=1groups.read

With a group_id, returns that group’s terms per instrument — markup, commission, swap and trade mode. Without one, the full instrument catalogue.

Currencies

GET/currencies

An account is denominated in its own currency, for life. Every amount your integration sees for that account — balance, equity, transactions, deals — is in integer minor units of that currency, and a transaction must be sent in it: a deposit in USD to an NGN account is refused, not silently converted. Margin and P&L on cross-currency instruments are converted by the platform at the broker’s configured rates, and an operation with no rate available is refused rather than guessed.

json
{
  "enabled": true,
  "settlement": "USD",
  "currencies": ["USD", "NGN", "GHS", "ZAR", "EUR", "GBP"]
}

Read this once and render a currency picker — or none, when the broker offers a single currency. Which currencies exist and their rates are configured by the broker in their back office.

Open an account

POST/accountsaccounts.create

Creates a trading account, or queues it for approval. If your key has auto-approve, the account is created immediately and the response carries its credentials. Otherwise the request waits for a person at the broker, and you are told when it is approved.

bash
curl -X POST https://api.steratrader.com/crm/v1/accounts \
  -H "Authorization: Bearer stk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "external_id": "crm-client-0001",
    "first_name": "Adaeze",
    "last_name": "Okonkwo",
    "email": "adaeze@example.com",
    "phone": "+2348030001111",
    "country": "NG",
    "group_id": 1,
    "initial_balance": 500000
  }'

Request

external_idstring

Your own identifier for this client. Send it. A retry with the same value returns the existing account instead of creating a second one.

first_namestringrequired

Given name.

last_namestring

Family name.

emailstringrequired

Matched against existing clients at this broker, so a second account joins the same person rather than duplicating them.

phonestring

Include the country code.

countrystring

Two-letter ISO code.

date_of_birthstring

ISO date, YYYY-MM-DD.

groupstring

The group’s slug — standard, raw. Stable, so a broker renaming the group does not break your integration. A name from another platform works too if the broker has added it as an alias.

group_idinteger

The numeric id, as an alternative to the slug. Either works; neither is required if your key has a default group.

server_idinteger

Defaults to the broker’s live or demo server, matching is_demo.

leverageinteger

Overrides the group leverage for this account. As 1:N — send 200 for 1:200.

is_demoboolean

Demo accounts are funded from a separate ledger and never touch the broker’s real books.

currencystring

ISO code, like NGN. Must be one the broker offers — read /currencies. Defaults to USD, and cannot be changed later: balances, statements and deals all report in it.

initial_balanceinteger

Minor units. On a live account this asserts that money was received, which is why it usually waits for approval.

Response

json
{
  "success": true,
  "message": "Account 100001 opened.",
  "data": {
    "account_id": 1,
    "login": 100001,
    "password": "gkyuAnJXtZ%9",
    "investor_password": "3qSGKegHwh*5",
    "server": "Velton Markets Live",
    "group": "Standard",
    "currency": "USD",
    "is_demo": false,
    "balance": 500000
  }
}

The passwords are shown once. They are hashed on our side and cannot be recovered — pass them to the client immediately or reset them.

Read an account

GET/accounts?external_id={id}&email={email}accounts.read

Finds accounts when you do not know the login — by the external_id you sent at creation, or by the client’s email. For one account:

GET/accounts/{login}accounts.read

Returns the account with live equity, margin and floating P&L, computed from current prices — the same figures the trader sees.

json
{
  "login": 100001,
  "group_name": "Standard",
  "leverage": 200,
  "balance": 525000,
  "credit": 0,
  "floating": -1240,
  "equity": 523760,
  "margin": 43380,
  "free_margin": 480380,
  "margin_level": 1207.38,
  "open_positions": 1,
  "currency": "USD"
}

Leverage and status

PATCH/accounts/{login}/leverageaccounts.update
PATCH/accounts/{login}/statusaccounts.update

A leverage change is refused while positions are open. Changing it re-prices the margin on an existing trade, and a client can find themselves stopped out by an administrative action they never saw.

Status can be active, disabled (cannot open, can still close) or archived. Both take a mandatory reason, which appears in the broker’s audit log.

Deposits and withdrawals

POST/accounts/{login}/transactionsmoney.deposit / money.withdraw
bash
curl -X POST \
  https://api.steratrader.com/crm/v1/accounts/100001/transactions \
  -H "Authorization: Bearer stk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "kind": "deposit",
    "amount": 25000,
    "currency": "USD",
    "external_transaction_id": "psp-99821",
    "reason": "Card deposit via Paystack"
  }'
kindstringrequired

One of deposit, withdrawal, credit_in, credit_out, bonus, correction.

amountintegerrequired

Minor units, always positive. The kind sets the direction.

external_transaction_idstringrequired

Your payment reference. Required, not optional — this is what stops a repeated callback crediting twice.

reasonstringrequired

Shown in the broker’s back office and the client’s statement.

Reconciliation

GET/transactions?since={timestamp}accounts.read

Every money movement across the broker, in order, with a cursor for the next call. If your system missed webhooks during an outage, call this and catch up — rather than discovering the gap in a client’s balance months later.

json
{
  "transactions": [
    {
      "id": 2,
      "login": 100001,
      "kind": "deposit",
      "amount": 25000,
      "currency": "USD",
      "reason": "Card deposit via Paystack",
      "reference": "psp-99821",
      "performed_at": "2026-09-16T13:31:04Z"
    }
  ],
  "next_since": "2026-09-16T13:31:04Z",
  "count": 1
}

Run it nightly even when nothing appears wrong. A reconciliation that only runs after a problem is a reconciliation that finds the problem late.

Bonuses

GET/bonus-campaignsbonus.grant
POST/accounts/{login}/bonusbonus.grant

Campaigns are configured by the broker, including whether the bonus pays as credit (counts toward margin, not withdrawable) or balance (the client’s money immediately). Your call names the campaign and, for a deposit match, the deposit it is matching.

Positions and orders

GET/accounts/{login}/positions?status=openpositions.read
GET/accounts/{login}/orderspositions.read

Positions carry the symbol, side, lots, open price, stop and target, swap, commission and — once closed — the realised result and why it closed. Orders carry the full submission including rejections, which is what you need when a client asks why a trade did not go through.

Deals and history

GET/deals?since={timestamp}&login={login}positions.read

One immutable row per execution: opens, closes, partial closes, stop loss and take profit hits, stop-outs and copy-trading fills. A position tells you what is; a deal tells you what happened and when. Profit, swap and commission are integers in the minor unit of the account’s own currency, so an account held in NGN reports NGN. The closing deal’s side is the opposite of the position’s: closing a buy is a sell execution.

json
{
  "deals": [
    { "id": 912, "login": 100001, "symbol": "XAUUSD",
      "action": "open", "reason": null, "side": "buy",
      "lots": "0.50", "price": "2384.610",
      "profit": 0, "swap": 0, "commission": 350, "currency": "USD",
      "position_id": 455, "order_id": 1201,
      "created_at": "2026-10-11T09:14:02Z" },
    { "id": 913, "login": 100001, "symbol": "XAUUSD",
      "action": "close", "reason": "tp", "side": "sell",
      "lots": "0.50", "price": "2391.820",
      "profit": 36050, "swap": -120, "commission": 0, "currency": "USD",
      "position_id": 455, "order_id": null,
      "created_at": "2026-10-11T14:02:40Z" }
  ],
  "next_since": "2026-10-11T14:02:40Z",
  "count": 2
}

The feed pages with the same since / next_since cursor as /transactions, and the same advice applies: reconcile on a schedule, not after a dispute. There is no edit or delete — deals are history, and a wrong one is answered with a correcting deal, never a rewrite. That is the property an auditor, an IB commission run and an LP reconciliation all depend on.

Placing trades

These routes open and close positions on a client’s account. Most integrations should not have this scope. It exists for copy trading and similar systems that place trades on a client’s behalf, and a key holding it is restricted to specific IP addresses.

POST/accounts/{login}/positionspositions.deal

Opens at the current market price. external_id is required: a copy engine that times out and retries must not open the same trade twice.

bash
curl -X POST \
  https://api.steratrader.com/crm/v1/accounts/100001/positions \
  -H "Authorization: Bearer stk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "symbol": "XAUUSD",
    "side": "buy",
    "lots": 0.10,
    "stop_loss": 4300.00,
    "take_profit": 4380.00,
    "external_id": "copy-7f3a9c21"
  }'
symbolstring

The instrument, as your system knows it. Send instrument_id instead if you have it.

sidestringrequired

buy or sell.

lotsnumberrequired

Volume in lots.

stop_lossnumber

Rejected if it sits on the wrong side of the market — a stop that would fire on the next tick is never what was meant.

external_idstringrequired

Your reference. A repeat returns the original position with replayed: true rather than opening a second.

POST/accounts/{login}/positions/{id}/closepositions.deal

Closes at the current market price. Pass lots to close part of it. Closing a position the client already closed returns already_closed rather than an error — a copy engine doing that is behaving correctly.

PATCH/accounts/{login}/positions/{id}positions.deal

Sets or clears the stop and target on an open position.

Profit and loss

GET/reports/pnl?date_from={date}&date_to={date}reports.read

What the book made and what it owes, for a date window, computed on the platform. Your CRM could reassemble this by walking every account; one call here beats hundreds there, and the numbers that reach a broker’s accounts should come from one place. Demo activity is excluded unless you pass include_demo=true — demo money is fictional and never belongs in the real figures.

Webhooks

Your broker configures a URL and the events you want. Each delivery is signed with HMAC-SHA256 over the raw body, using the secret shown when the connection was created.

http
POST /your-webhook-endpoint
X-Stera-Event: deposit.completed
X-Stera-Signature: sha256=8f3c2a...

{ "event": "deposit.completed", "at": 1758030664.2,
  "data": { "login": 100001, "amount": 25000, "balance": 525000,
            "external_transaction_id": "psp-99821" } }

Verifying a signature

python
import hmac, hashlib

def verify(raw_body: bytes, header: str, secret: str) -> bool:
    expected = "sha256=" + hmac.new(
        secret.encode(), raw_body, hashlib.sha256).hexdigest()
    # Constant time: a timing difference here leaks the secret slowly.
    return hmac.compare_digest(expected, header)

Events

account.createdA trading account was opened
account.updatedLeverage, group or status changed
deposit.completedFunds credited
withdrawal.completedFunds debited
bonus.grantedA bonus was applied
position.openedA trade was opened, by the client or through the API
position.closedA trade was closed, with the result
margin.callAn account crossed its margin call level
stop_outPositions were liquidated

Webhooks can be delayed, duplicated or missed. Treat them as a prompt to act rather than as the record — and reconcile against /transactions on a schedule. Every integration that trusted webhooks alone has a story about the day they stopped arriving.

Broker connections (FIX API)

Brokers connect their own liquidity from the back office under Administration → Liquidity. SteraTrader is a FIX 4.4 taker: it dials out to your provider’s gateway — PrimeXM, oneZero, Centroid, or an LP’s own bridge — using two sessions, as every provider structures it: one for market data, one for orders. They are separate connections with separate credentials and separate sequence numbers, so a quote session that drops does not take order entry down with it.

The connection form, in your provider’s terms:

name / providerstringrequired

Your label for the connection and who it points at.

protocolfix | websocket | rest

FIX 4.4 for the providers above; websocket and rest exist for custom gateways.

quote_host / quote_portstring / intrequired

The market-data gateway address from your provider’s spec sheet.

quote_sender / quote_targetstringrequired

SenderCompID and TargetCompID for the quote session — Sender is the comp ID the provider assigned to you, Target is theirs.

trade_host / trade_port / trade_sender / trade_targetas aboverequired

The same four for the order session. Providers usually issue different comp IDs per session.

quote_password / trade_passwordstring

Sent once and held in the secret store. They are never shown again and never land in a column a support session could read.

heartbeat_secsint, default 30

FIX heartbeat interval. On silence the session sends a test request rather than assuming a quiet market — a dead TCP connection looks identical to one otherwise.

reset_seq_quotebool, default true

Quotes are stateless, so starting each logon from sequence 1 is safe and avoids replaying stale prices.

reset_seq_tradebool, default false

Order flow is not stateless. Keeping sequence numbers means a gap after a disconnect triggers a resend request, and no execution report is silently lost.

Ask your provider for their FIX 4.4 specification with hosts, ports, comp IDs, credentials and the tradable symbol list. Price streams are consumed as incremental market data or mass quotes (how PrimeXM streams), and reconnects are backed off rather than immediate — hammering a gateway that just dropped you is how an IP gets blocked.

Need a key?

API keys are created by the broker in their back office, under Administration. If you are integrating on a broker’s behalf, ask them to create a connection for you and choose the scopes it needs.

Talk to us