Skip to content

Developers

The Propology API

Show a trader's accounts in your own app, place and manage orders, and read the evaluation rules with the room left under each one. A versioned REST API, opened with keys the trader creates and controls.

Version 1 · Base URL https://api.propology.trade/v1

Overview

The API is the same account a trader uses in the app, reached from somewhere else — a bot, a dashboard, or another application. Every order placed through it goes through the same margin checks, leverage limits and risk engine as an order placed by hand.

Version 1 is stable. Fields are not removed or renamed within a version; new fields may be added, so ignore ones you do not recognise. A breaking change would ship as /v2 alongside it.

curl https://api.propology.trade/v1/accounts \
  -H "Authorization: Bearer plg_your_key_here"

Authentication

A trader creates a key in the Propology app under Platforms → API keys. Send it as a bearer token on every request:

Authorization: Bearer plg_7Fq2Kx9aB3nR5tYwLm0pQ8sVc4ZdE6hJkU1oXgTy

Keys begin with plg_. Each one carries:

  • A scope. read — accounts, positions, orders and history — or read and trade, which adds placing, cancelling and closing.
  • Optionally, a set of accounts. A key can be limited to specific trading accounts; otherwise it reaches all of them, including ones opened later.
  • Optionally, allowed IP addresses and an expiry. If your app runs from a fixed address, lock the key to it.

There is no withdrawal scope, and there will not be one. A key can trade an account; it can never move money out of it. Withdrawals always require the trader's password and a two-factor code in the app.

Creating a key requires the trader to enter a two-factor code, and they are emailed every time one is created. The key is shown once, at creation — Propology stores only a fingerprint of it and cannot show it again. Treat it like a password: keep it on your server, never in a browser or a mobile app bundle.

Conventions

Every response is JSON wrapped in the same envelope:

{ "success": true,  "data": { ... } }
{ "success": false, "error": { "code": "VALIDATION_ERROR", "message": "price is required for limit orders" } }
  • Money and quantities are strings — "10432.18", never 10432.18. A JSON number is a floating-point value, and a balance that has been through one is no longer the balance. Parse them with a decimal library.
  • Times are epoch milliseconds, UTC.
  • Lists that can grow are paginated as { "items": [...], "nextCursor": "..." }. Pass cursor to get the next page; nextCursor is null on the last one. limit is 1–200, default 50.

Errors

StatusCodeMeaning
400VALIDATION_ERRORThe request body or query is malformed. The message names the first problem.
401TOKEN_INVALIDMissing, unknown, revoked or expired key — or a request from an address the key does not allow.
403FORBIDDENThe key does not have the scope this route needs.
403ACCOUNT_NOT_TRADEABLEThe account is breached, passed, suspended, closed or under review, and cannot take new orders.
404NOT_FOUNDNo such account, order or position — or one outside the key’s account scope.
409CONFLICTAn order with this clientOrderId is already being placed. See Safe retries.
422ORDER_REJECTEDThe risk engine refused the order — not enough available margin, leverage above the instrument’s bracket, or a size outside its limits. The message says which.
429RATE_LIMITEDToo many requests. Wait for the Retry-After header.

A key that is revoked, expired or used from the wrong address gets the same 401 as one that never existed — so the response cannot be used to probe which keys were once valid.

Rate limits

Limits are per trader, and shared with the app: a trader running a bot and trading by hand at the same time has one allowance between them.

ActionLimitScope
Place, cancel, close120 / minutePer trader, across every key and the app.
Cancel all orders30 / minutePer trader.

Responses carry X-RateLimit-Limit and X-RateLimit-Remaining; a 429 carries Retry-After in seconds.

Accounts

GET/v1/accountsread scope

Every trading account the key can reach. The trader's wallet is never included.

GET/v1/accounts/{accountId}read scope
{
  "id": "5b0e…",
  "name": "10K Evaluation",
  "type": "evaluation",            // evaluation | funded | free
  "status": "active",              // active | breached | passed | pending_review | suspended | closed
  "currency": "USDT",
  "balance": "10000",
  "equity": "9812.40",
  "availableBalance": "9412.40",
  "marginUsed": "400",
  "unrealizedPnl": "-187.60",
  "maxLeverage": 250,
  "rules": {
    "phase": "phase_1",
    "profitTarget": { "targetEquity": "10800", "pct": 8 },
    "dailyLoss":    { "limitEquity": "9700", "remaining": "112.40", "pct": 3 },
    "totalLoss":    { "limitEquity": "9000", "remaining": "812.40", "pct": 10, "mode": "trailing" },
    "tradingDays":  { "completed": 2, "required": 5 },
    "minHoldSeconds": 120,
    "endsAt": 1790380800000
  }
}

rules is what a prop-trading integration exists for. Each limitEquity is the equity at which that rule is breached, and remaining is how far the account is above it right now. They are computed by the same functions the risk engine enforces with, so a drawdown bar drawn from them is the bar the engine will honour. rules is null on an account no live evaluation governs — a funded account, or one whose evaluation has ended.

totalLoss.mode is static (measured from the starting balance), trailing (follows the high-water mark), or trailing_to_initial (trails until it reaches the starting balance, then stays there).

Positions

GET/v1/accounts/{accountId}/positionsread scope

Query status: open, closed or liquidated.

POST/v1/accounts/{accountId}/positions/{positionId}/closetrade scope

Closes at market. Send { "quantity": "0.005" } to close part of it, or an empty body to close all of it. Returns the closing order.

Orders

GET/v1/accounts/{accountId}/ordersread scope

Query status: open for the working book or history for everything finished; symbol; limit; cursor.

POST/v1/accounts/{accountId}/orderstrade scope
{
  "symbol": "BTCUSDT",
  "side": "buy",
  "type": "limit",
  "quantity": "0.010",
  "price": "60000",
  "leverage": 10,
  "timeInForce": "gtc",
  "clientOrderId": "my-bot-000123",
  "stopLossPrice": "58500",
  "takeProfitPrice": "63000"
}
FieldTypeNotes
symbolstringAn instrument from GET /v1/markets, e.g. BTCUSDT.
sidestringbuy or sell.
typestringmarket, limit, stop, stop_limit, take_profit or take_profit_limit.
quantitydecimal stringIn the base asset. Greater than zero.
pricedecimal stringRequired for limit, stop_limit and take_profit_limit. Not allowed on market.
stopPricedecimal stringRequired for stop, stop_limit, take_profit and take_profit_limit.
leverageintegerCapped by the account’s maxLeverage and by the instrument’s own risk brackets.
timeInForcestringgtc (default), ioc or fok.
reduceOnlybooleanOnly ever reduce an open position. Default false.
positionSidestringlong or short. Required on a hedge-mode account.
clientOrderIdstringUp to 48 characters, unique per account. Makes retries safe — see below.
stopLossPricedecimal stringOptional. Attaches a protective stop to the resulting position.
takeProfitPricedecimal stringOptional. Attaches a take-profit to the resulting position.

The response is the order, plus fill — the position and trade it produced — when it executed immediately. fill is null for an order that is resting or was rejected.

DELETE/v1/accounts/{accountId}/orders/{orderId}trade scope
DELETE/v1/accounts/{accountId}/orderstrade scope

Cancels every resting order on the account, or on one instrument with ?symbol=BTCUSDT. Returns { "cancelled": 3 }.

Safe retries

Networks fail. If a request to place an order times out, you cannot tell whether it was placed — and sending it again could open a second position.

Send a clientOrderId and retrying is always safe. Retry with the same id and you get back the order the first request placed, instead of a new one. If the first request is still being processed when the retry arrives, the retry gets a 409 — look the order up with GET …/orders before trying again.

const id = `bot-${Date.now()}`;       // one id per intended order

async function place(order) {
  for (let attempt = 0; attempt < 3; attempt++) {
    try {
      const res = await fetch(`https://api.propology.trade/v1/accounts/${ACCOUNT}/orders`, {
        method: 'POST',
        headers: { Authorization: `Bearer ${KEY}`, 'Content-Type': 'application/json' },
        body: JSON.stringify({ ...order, clientOrderId: id }),   // same id on every attempt
      });
      const body = await res.json();
      if (body.success) return body.data;
      if (res.status !== 409 && res.status < 500) throw new Error(body.error.message);
    } catch (networkError) { /* fall through and retry with the same id */ }
    await new Promise((r) => setTimeout(r, 1000 * (attempt + 1)));
  }
}

A clientOrderId is unique per account for good: once used, it always refers to that order.

Trades

GET/v1/accounts/{accountId}/tradesread scope

Every fill, newest first. Query symbol, since (epoch milliseconds, inclusive), limit and cursor.

Market data

Public — no key needed.

GET/v1/marketsNo key

Every tradeable instrument, with its tick size, step size and maximum leverage.

GET/v1/markets/{symbol}/tickerNo key

Last, mark and index price, the best bid and ask, 24-hour range and volume, and the funding rate with the time of the next funding.

Which price means what matters when you model an order:

OrderPrice usedHow
Market orderbestAsk / bestBidBuys fill at the best ask, sells at the best bid.
Limit orderlastPriceFills when the last traded price reaches your limit.
Stop, take-profitmarkPriceTrigger on the mark price, then execute as market or limit.
Liquidation, rulesmarkPriceLiquidation and every evaluation rule are measured on the mark.

The mark is Binance's futures mark price, derived from an index across several exchanges — so a brief spike on one order book does not trigger stops or breach an account on its own.

Rules that still apply

An account traded through the API is the same account, under the same trading rules. Three matter most to automated strategies:

  • Positions must be held at least two minutes. Closing sooner is a violation, whether a person or a bot closed it. The account's rules.minHoldSeconds says the exact figure.
  • Latency arbitrage is prohibited — systematically taking fills against prices the engine has not yet updated.
  • Copy trading across Propology accounts is prohibited — running the same signal on several accounts you control.

Questions, or building something we should know about? Write to support@propx.dev.