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 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 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

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.

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.

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.

group_idinteger

Which group to place them in. Defaults to your key’s default group. Must be one your key is permitted to use.

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.

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/{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.

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 client opened a trade
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.

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