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 must be restricted to specific IP addresses — the platform refuses to create one otherwise. Without that, a leaked key is a leaked bank account.
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 bonusesConventions
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.
{
"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.
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.
group_idintegerWhich group to place them in. Defaults to your key’s default group. Must be one your key is permitted to use.
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.
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/{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.
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 client opened a tradeposition.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.
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