Binary options API and data reference

Every binary route — nineteen admin endpoints with their permission keys, twelve trader endpoints, the binaryOrder and binaryMarket columns, all five enums and the three settings rows.

9 min readUpdated 6 August 2026binary, api, permissions, enums, reference

Binary options is core, not an addon, so none of this is gated on an extension row. The routes live in three trees:

Tree Who calls it Gate
/api/admin/finance/binary/** operator — markets and settings a permission key on every route
/api/admin/finance/order/binary/** operator — the order book a permission key on every route
/api/exchange/binary/** the trading page and your customers sign-in, and KYC on order placement

Nineteen admin routes, twelve trader routes and one WebSocket. This page is the whole surface with its permission keys, the two tables behind it and every enum value those tables accept.

For what the screens do, see Binary markets, switching binary on and order types and payouts.

Admin — markets

Nine routes. The screen is /admin/finance/binary/market, whose own page gate is access.binary.market — a separate key that no API route checks.

Paginated, filterable market list, sorted by currency by default
Create one market. Accepts source; infers EXCHANGE when the body omits it
Exchange and ecosystem pairs not yet added, each flagged for AI market maker and Binary AI Engine
Create every missing active exchange pair, all disabled. Skips colon pairs
Bulk enable or disable. Body is ids and status
One market, the whole row including source
Update currency, pair, minAmount, maxAmount, isTrending, isHot, status — and nothing else
Delete one market. Permanent — binary_market has no soft-delete column
Toggle one market

Two shapes to know before you script against these. Import is a GET that creates rows — it is wired to a button, not to REST convention, so do not pre-flight it expecting a read. And the update route silently ignores source: it destructures seven named fields off the body and discards the rest, so a PUT carrying a new price feed returns 200 and changes nothing.

Admin — settings

Three routes over one screen, /admin/finance/binary/settings, page gate access.binary.settings.

Settings JSON, both flat switches, a validation result, and isDefault
Validate and save all three rows, then invalidate the caches platform-wide
The three shipped presets, each with its own validation result

Read and write are different keys. access.binary.settings covers the GET and the presets list; only edit.binary.settings covers the PUT. A role holding just the first loads the whole console and gets a 403 on Save.

The PUT returns 400 with a joined error list when validation fails, and writes nothing. Warnings never block a save.

Admin — orders

Seven routes over /admin/finance/order/binary, page gate access.binary.order.

Paginated order list with the placing user joined in
Bulk soft-delete. Body is ids; query force=true destroys, restore=true undeletes
Bulk status write. Body is ids and status
One order with its user
Overwrite symbol, price, amount, profit, side, type, status, isDemo, closePrice
Soft-delete one order; same force and restore query flags
Status write on one order

PUT .../status calls the platform's generic status helper: it writes the status column and returns "updated successfully". It does not release a hold, credit a payout, refund a stake or record platform profit — every one of those lives in BinaryOrderService, which these routes never call.

Setting a PENDING order to WIN therefore produces a customer who is told they won and whose stake is still sitting in inOrder on their wallet. The same is true of the bulk variant and of the full PUT, which will happily rewrite amount, profit and even isDemo on a settled row.

The screen does not expose any of this. /admin/finance/order/binary ships with create, edit and delete all switched off — it is a read-only table with a view dialog. These three write routes are reachable only by calling them directly, and there is no supported reason to. To fix a contract that did not settle, use Binary settlement troubleshooting.

Two more things about that screen worth knowing before you go looking for a button that is not there:

  • The status filter offers five of the six values. PENDING, WIN, LOSS, DRAW and CANCELED are in the dropdown; ERROR is not. Filter for it through the API, or read the Settlement errors backlog tile in the page's analytics, which counts it.
  • There is no order-type column. side is on the table (with only RISE and FALL in its filter), type is not shown at all. If you run more than Rise/Fall, the table cannot tell two contract types apart.

GET on the list and on one order both carry demoMask: user.email. That masks the customer's address only when the whole install runs with NEXT_PUBLIC_DEMO_STATUS=true — it is the demo install flag and has nothing to do with a customer's practice orders. See Practice mode and demo orders.

Trader routes

Everything under /api/exchange/binary. Four are genuinely public — no sign-in, no permission — because the market list, the payout ladder, the published settings and the leaderboard are all read before a visitor has an account.

Method Path Auth
GET /api/exchange/binary/market public
GET /api/exchange/binary/duration public
GET /api/exchange/binary/settings public
GET /api/exchange/binary/leaderboard public
GET /api/exchange/binary/health signed in
GET /api/exchange/binary/leaderboard/me signed in
GET /api/exchange/binary/order signed in
GET /api/exchange/binary/order/last signed in
POST /api/exchange/binary/order signed in + KYC
GET /api/exchange/binary/order/{id} signed in
DELETE /api/exchange/binary/order/{id} signed in
WS /api/exchange/binary/order signed in

None of them takes a permission key. Nothing on the Roles screen changes what a customer can call here.

What each one returns

/market — every binaryMarket row with status true, ordered isTrending descending, then isHot, then currency. Disabled markets are absent, so this is also the fastest check that a market you enabled is live.

/duration — the enabled durations out of the binarySettings JSON, sorted ascending, each carrying five computed payouts: profitPercentageRiseFall, profitPercentageHigherLower, profitPercentageTouchNoTouch, profitPercentageCallPut, profitPercentageTurbo, plus profitPercentage as an alias of the first for older clients. The adjustments cascade: a −5% set on the 3-minute row applies to every longer duration as well, and a further −10% on the 10-minute row makes that one −15%. It reads no database table — the binaryDuration model this endpoint replaced no longer exists.

/settings — a deliberately trimmed copy of the JSON: global, display, orderTypes, durations and cancellation. riskManagement, _preset and _lastModified are not published.

/health — five checks (system, database, durations, markets, orders) folded into healthy / degraded / down. Two of the five are known to misreport; read the settlement troubleshooting page before you act on the verdict.

/leaderboard and /leaderboard/meperiod is daily, weekly (default), monthly or alltime; metric is profit (default), winRate or volume; limit defaults to 100 and is capped at 100. Both filter isDemo: false, so practice trading never appears on a leaderboard.

/order — the caller's own orders for one symbol. The filter is built as currency/pair, so both are needed — omit either and the query matches nothing rather than erroring. type=OPEN returns PENDING rows; any other value returns everything that is not pending. limit defaults to 50 and is capped at 200; offset paginates. It does not filter isDemo — a customer's history mixes practice and live contracts.

/order/last — 30 days of non-pending orders, already split into practiceOrders and nonPracticeOrders, plus livePercentageChange and practicePercentageChange against the preceding 30 days.

DELETE /order/{id} — early cancellation. The body may carry a percentage; the server ignores it and derives the penalty from the Cancellation tab, precisely so a crafted request cannot buy a free exit.

The WebSocket carries ORDER_COMPLETED for the subscriber's own fills. It requires auth, because the subscribe payload is { type: "order", symbol, userId } and without the gate any socket could name someone else's userId.

Placing an order

Create a binary order. Ten per minute per user
{
  "currency": "BTC",
  "pair": "USDT",
  "amount": 25,
  "side": "RISE",
  "type": "RISE_FALL",
  "durationId": "d_1m",
  "closedAt": "2026-08-06T12:34:00.000Z",
  "isDemo": false
}

currency, pair, amount, side, closedAt, durationId and type are required. durationType, barrier, barrierLevelId, strikePrice, strikeLevelId, payoutPerPoint and isDemo are conditional — which ones depends on type, and the rules are on order types and payouts.

Two headers-and-limits facts that break integrations:

  • idempotency-key is a required header. No header is a flat 400, "Missing idempotency-key header". Re-sending the same key for the same user returns the existing order rather than opening a second one.
  • Keys beginning ct_ are refused with 400. That namespace belongs to copy-trading replication, and a user-supplied key inside it could pre-empt a follower's copy.
  • Ten orders per minute per user, enforced at the route.

The refusals you will be asked about:

Code Message Cause
403 Binary trading is currently disabled binaryStatus is off
403 Binary practice mode is currently disabled isDemo true while binaryPracticeStatus is off
400 Order type X is not available in demo/live mode that type's tradingModes switch
400 Amount must be between {min} and {max} {pair} the market's stake limits
400 Order must be placed at least N seconds before expiry global.orderExpirationBuffer
429 maximum of N concurrent open orders / daily limit of N / please wait N second(s) the three per-user throttles — live orders only
400 Binary market {symbol} is disabled status false on the market row
404 Ecosystem market {symbol} not found source is ECOSYSTEM with no matching ecosystem market

The binary_order table

Model binaryOrder, table binary_order, paranoid — a delete sets deletedAt and the row survives. (The admin table is configured isParanoid: false, so it never shows you the deleted rows it is hiding.)

Column Type Notes
id UUID primary key
userId UUID the trader. ON DELETE CASCADE — deleting a customer deletes their whole binary history
symbol string(191) free text BTC/USDT. There is no currency column and no market foreign key
price double entry price at placement
amount double the stake, denominated in the quote half of symbol
profit double payout amount. 0 until settlement
side enum direction — see below
type enum contract type — see below
durationType enum TIME or TICKS, default TIME
barrier double, null barrier level for HIGHER_LOWER, TOUCH_NO_TOUCH, TURBO
strikePrice double, null strike for CALL_PUT
payoutPerPoint double, null for CALL_PUT and TURBO
profitPercentage double, null the payout percentage this contract was sold at, frozen at placement
status enum lifecycle — see below
isDemo boolean, default false practice contract. No wallet is touched
closedAt datetime(3) the expiry instant, not the settlement instant
closePrice double, null settlement price
metadata JSON, null idempotencyKey, walletType, copiedFromOrderId, and on a cancel refundedAmount and cancelPenalty
createdAt · updatedAt · deletedAt datetime deletedAt is the soft-delete marker

Indexes worth knowing when you write a report: (status, closedAt) — which is what makes "unsettled past expiry" cheap — and (userId, metadata(255)) for the idempotency lookup.

A binary stake is denominated in the quote half of symbol, and nothing on the row records which asset that is. Summing amount across a book holding BTC/USDT and ETH/EUR produces a number with no unit. The shipped analytics render these as plain totals for exactly this reason — do not put a currency symbol in front of them, and do not convert without joining on the symbol yourself.

side — 10 values

Two per contract type, and they only ever appear with their own type.

type Its two sides
RISE_FALL RISE, FALL
HIGHER_LOWER HIGHER, LOWER
TOUCH_NO_TOUCH TOUCH, NO_TOUCH
CALL_PUT CALL, PUT
TURBO UP, DOWN

type — 5 values

RISE_FALL · HIGHER_LOWER · TOUCH_NO_TOUCH · CALL_PUT · TURBO.

Only RISE_FALL is enabled on a fresh install. TOUCH_NO_TOUCH and TURBO are refused outright on an ECOSYSTEM-sourced market, because both settle on intra-period highs and lows that only the exchange feed publishes.

durationType — 2 values

TIME is stored for every contract type except TURBO; the service forces it. TICKS is reachable only on TURBOvalidateCreateOrderInput rejects any durationType other than TIME on every other type. The Turbo card's allowTicksBased switch does not gate it: the flag exists in the settings JSON, the presets and the admin toggle, and no backend path reads it, so a TURBO order carrying durationType: "TICKS" is accepted with the switch off.

status — 6 values

Value Means Money
PENDING open, expiry not yet processed stake held in inOrder
WIN the prediction was correct stake released, payout credited
LOSS the prediction was wrong stake executed from hold, platform profit recorded
DRAW close equals entry, or the barrier/strike test tied stake released, nothing else
CANCELED closed early by the customer stake released less the configured penalty
ERROR commented on the model as "data fetch failed, needs manual review" nothing

ERROR is worth a note. It is a legal value, the admin analytics counts it as Settlement errors backlog, and a repository-wide search finds no code path in the current backend that writes it. A non-zero count means rows written by an older version or by hand — investigate them, do not assume the settlement path produced them.

The binary_market table

Model binaryMarket, table binary_market. Not paranoid and not timestamped — no createdAt, no deletedAt, and a delete is gone.

Column Type Default Notes
id UUID generated
currency string(191) base symbol, BTC
pair string(191) quote symbol, USDT
source enum EXCHANGE|ECOSYSTEM EXCHANGE which price series backs entry, settlement and steering
minAmount decimal(16,8), null 1 smallest stake, in pair
maxAmount decimal(16,8), null 10000 largest stake, in pair
isTrending boolean, null false merchandising only
isHot boolean, null false merchandising only
status boolean true tradable

(currency, pair) is a unique key, so a duplicate market is a constraint error rather than a second row. minAmount and maxAmount are DECIMAL, which mysql2 returns as strings — coerce before you do arithmetic.

source is a real column on the row — it is in the table above — but it is invisible from the admin screen: the markets DataTable shows only id, currency, pair, minAmount, maxAmount, isTrending, isHot and status, so there is no source column and no source field on the edit form, and the update route discards it. The create wizard is the only place a human sets it. Full treatment on Binary markets.

Permission keys

Twelve keys are seeded for binary, in backend/seeders/20240402234643-permissions.js.

Key Guards
access.binary.market the Binary Markets page URL and its menu entry
view.binary.market the market list, one market, and the available-markets lookup
create.binary.market creating a market, and Import from Exchange
edit.binary.market editing, the status toggle, and the bulk status route
delete.binary.market deleting a market
access.binary.settings the Binary Settings page, the settings GET and the presets GET
edit.binary.settings saving binary settings
access.binary.order the Binary Orders page URL and its menu entry
view.binary.order reading order rows
edit.binary.order the three order write routes — the full PUT, the single status PUT and the bulk status PUT
delete.binary.order deleting orders
create.binary.order nothing

The nine the audit trail and the API actually enforce are access.binary.settings, edit.binary.settings, the four *.binary.market verbs and the three *.binary.order verbs. Note that access.binary.settings is among them: unlike the other two access.* keys it is a real route permission, on the settings GET and the presets GET, as the settings section above says.

The three keys no route checks are access.binary.market, access.binary.order and create.binary.order. The first two are enforced by the Next.js URL map and the menu — which is why granting access.binary.market alone gives an operator the page and an empty table until view.binary.market goes with it. The third is enforced by nothing at all.

It is seeded, it appears on the Roles multi-select, and the Binary Orders page declares it as its create key — but the page sets canCreate={false} and no backend route asks for it. There is no way to create a binary order from the admin panel. Ticking it grants nothing.

Super Admin short-circuits every one of these by role name. See Roles and permissions for how a key is derived, the four places it is checked, and why a revocation needs a backend restart.

Settings storage

Three rows in the settings table, written together by one PUT, and this screen is the only writer of any of them. None appears on Admin → System → Platform Settings.

Key Shape Holds
binaryStatus "true" / "false" the master switch
binaryPracticeStatus "true" / "false" practice trading
binarySettings one JSON document everything else

The two flat rows are text, as every settings row is — false is the four-character string, not a boolean. Neither row is seeded, and an absent row reads as off. Every enforcement point tests CacheManager.getSetting(key) === "true", and a missing row returns undefined, so on a fresh install POST /api/exchange/binary/order refuses every order with 403 "Binary trading is currently disabled" until this screen has been saved once. The one place an absent row reads as on is the settings GET itself, which is why the console's two switches show green before anything has been written — pressing Save is what creates the rows.

The JSON document carries global, display, cancellation, orderTypes, durations, riskManagement, and the two metadata fields _preset and _lastModified. Its global.enabled and global.practiceEnabled duplicate the two flat rows: the save route reads binaryStatus from the body and falls back to global.enabled only when the body omits it. The order route checks the flat rows. Keep them in agreement.

Binary settings are cached in memory per process, and the settings table has its own Redis layer in front of it. A hand-written SQL update invalidates neither, so every worker keeps paying out on the payout percentages it last read. The Save button publishes an invalidation on the settings bus, every process drops its copy, and the change lands platform-wide at once. If the bus cannot deliver — a Redis outage, since pub/sub has no store-and-forward — the in-memory TTL shortens so convergence is seconds instead of a minute.

Change these from /admin/finance/binary/settings, always.