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.
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.
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 instrumentsaccounts.readRead accounts and balancespositions.readRead positions, orders and historyaccounts.createOpen trading accountsaccounts.updateChange leverage and account statusmoney.depositCredit depositsmoney.withdrawDebit withdrawalsbonus.grantGrant bonusespositions.dealOpen and close positions on a client's accountConventions
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.
// 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.
{
"detail": "This account holds 2 open position(s). A leverage change alters
their margin, so close them first."
}Groups
/groupsgroups.readReturns 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.
{
"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
/serversgroups.readThe 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
/instruments?group_id=1groups.readWith a group_id, returns that group’s terms per instrument — markup, commission, swap and trade mode. Without one, the full instrument catalogue.
Currencies
/currenciesAn 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.
{
"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
/accountsaccounts.createCreates 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.
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_idstringYour own identifier for this client. Send it. A retry with the same value returns the existing account instead of creating a second one.
first_namestringrequiredGiven name.
last_namestringFamily name.
emailstringrequiredMatched against existing clients at this broker, so a second account joins the same person rather than duplicating them.
phonestringInclude the country code.
countrystringTwo-letter ISO code.
date_of_birthstringISO date, YYYY-MM-DD.
groupstringThe 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_idintegerThe numeric id, as an alternative to the slug. Either works; neither is required if your key has a default group.
server_idintegerDefaults to the broker’s live or demo server, matching is_demo.
leverageintegerOverrides the group leverage for this account. As 1:N — send 200 for 1:200.
is_demobooleanDemo accounts are funded from a separate ledger and never touch the broker’s real books.
currencystringISO 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_balanceintegerMinor units. On a live account this asserts that money was received, which is why it usually waits for approval.
Response
{
"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
/accounts?external_id={id}&email={email}accounts.readFinds 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:
/accounts/{login}accounts.readReturns the account with live equity, margin and floating P&L, computed from current prices — the same figures the trader sees.
{
"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
/accounts/{login}/leverageaccounts.update/accounts/{login}/statusaccounts.updateA 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
/accounts/{login}/transactionsmoney.deposit / money.withdrawcurl -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"
}'kindstringrequiredOne of deposit, withdrawal, credit_in, credit_out, bonus, correction.
amountintegerrequiredMinor units, always positive. The kind sets the direction.
external_transaction_idstringrequiredYour payment reference. Required, not optional — this is what stops a repeated callback crediting twice.
reasonstringrequiredShown in the broker’s back office and the client’s statement.
Reconciliation
/transactions?since={timestamp}accounts.readEvery 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.
{
"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
/bonus-campaignsbonus.grant/accounts/{login}/bonusbonus.grantCampaigns 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
/accounts/{login}/positions?status=openpositions.read/accounts/{login}/orderspositions.readPositions 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
/deals?since={timestamp}&login={login}positions.readOne 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.
{
"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.
/accounts/{login}/positionspositions.dealOpens at the current market price. external_id is required: a copy engine that times out and retries must not open the same trade twice.
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"
}'symbolstringThe instrument, as your system knows it. Send instrument_id instead if you have it.
sidestringrequiredbuy or sell.
lotsnumberrequiredVolume in lots.
stop_lossnumberRejected 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_idstringrequiredYour reference. A repeat returns the original position with replayed: true rather than opening a second.
/accounts/{login}/positions/{id}/closepositions.dealCloses 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.
/accounts/{login}/positions/{id}positions.dealSets or clears the stop and target on an open position.
Profit and loss
/reports/pnl?date_from={date}&date_to={date}reports.readWhat 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.
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
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 openedaccount.updatedLeverage, group or status changeddeposit.completedFunds creditedwithdrawal.completedFunds debitedbonus.grantedA bonus was appliedposition.openedA trade was opened, by the client or through the APIposition.closedA trade was closed, with the resultmargin.callAn account crossed its margin call levelstop_outPositions were liquidatedWebhooks 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 / providerstringrequiredYour label for the connection and who it points at.
protocolfix | websocket | restFIX 4.4 for the providers above; websocket and rest exist for custom gateways.
quote_host / quote_portstring / intrequiredThe market-data gateway address from your provider’s spec sheet.
quote_sender / quote_targetstringrequiredSenderCompID 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 aboverequiredThe same four for the order session. Providers usually issue different comp IDs per session.
quote_password / trade_passwordstringSent 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 30FIX 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 trueQuotes are stateless, so starting each logon from sequence 1 is safe and avoids replaying stale prices.
reset_seq_tradebool, default falseOrder 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