Skip to content

REST API

The UBI REST API is the whole point of this project. It is a Flask application that serves one combined view of ten broker accounts. You log in to it once a day with an API key and secret. After that, every request carries one access-token header, whichever broker ends up doing the work.

This page is the map. It lists every route, explains the conventions that all of them share, and links to one page per group.

All endpoints

The table below lists all 29 routes in 36 rows, because the seven instrument routes that also take a list of instruments by POST appear once for each method. The badge shows the HTTP method, and the lock column shows which routes need the access-token header.

Method Endpoint What it does Token
GET /api/ Says hello, so a client can tell the API is up
POST /api/session/connect Exchanges the API key and secret for the day's access token
DELETE /api/session/disconnect Revokes the access token
GET /api/session/status Says whether the token is valid and when it expires
GET /api/users/details The account holder's profile and each broker's profile
GET /api/brokers/details Reference details for every broker
GET /api/exchanges/details Exchanges, trading hours and holidays
GET /api/instruments/segments Every exchange segment and the fields that name an instrument in it
GET /api/instruments/master The whole instrument list of a segment, streamed
GET /api/instruments/search Searches one segment by symbol or name
GET /api/instruments/details One instrument and how each broker names it
POST /api/instruments/details The same for a list of instruments in one request
GET /api/instruments/additional_details Extra broker-specific attributes of one instrument
POST /api/instruments/additional_details The same for a list of instruments in one request
GET /api/instruments/ltp Last traded price
POST /api/instruments/ltp The same for a list of instruments in one request
GET /api/instruments/ohlc The day's open, high, low, close and volume
POST /api/instruments/ohlc The same for a list of instruments in one request
GET /api/instruments/quote The full quote, including market depth
POST /api/instruments/quote The same for a list of instruments in one request
GET /api/instruments/prices Historical candles, adjusted for corporate actions if asked
POST /api/instruments/prices The same for a list of instruments in one request
GET /api/instruments/ticks Every recorded tick between two times, streamed
POST /api/instruments/ticks The same for a list of instruments in one request
GET /api/portfolio/funds Cash and margin, summed across brokers
GET /api/portfolio/holdings Long-term holdings, combined and priced
GET /api/portfolio/positions Open positions, combined
GET /api/orders/details Today's orders at every broker
GET /api/orders/trades Today's trades at every broker
POST /api/orders/place Places an order at a broker the API chooses, or each order of a list
GET /api/orders/intents/<intent_id> The order engine's answer for one order, read after the place request stopped waiting
GET /api/orders/parents One of the order engine's parents, or every one still open
DELETE /api/orders/parents Cancels one of the engine's parents with its resting legs, or each of a list
PUT /api/orders/modify Changes an open order, or each order of a list
DELETE /api/orders/cancel Cancels an open order, an engine parent or one part of a plan, or each of a list
POST /api/orders/flatten Cancels every open order and closes every position, everywhere

Four routes move real money

place, modify, cancel and flatten send real requests to real broker accounts. Every one of them accepts "dry_run": true, which builds and returns the broker request without sending it. Use that first.

How the groups relate

The diagram below shows the order in which a client normally uses the groups. It logs in first, looks an instrument up, and only then reads prices or places orders.

flowchart LR
    S["Session<br/>connect"] --> I["Instruments<br/>search / details"]
    I --> Q["Market quotes<br/>ltp / ohlc / quote"]
    I --> H["Historical data<br/>prices / ticks"]
    I --> O["Orders<br/>place / modify / cancel"]
    S --> P["Portfolio<br/>funds / holdings / positions"]
    S --> D["Details<br/>users / brokers / exchanges"]
    O -.-> F["Flatten<br/>(emergency)"]

Base URL

The API listens on 127.0.0.1:8080 by default, and every route lives under /api. The host and port come from UNIFIED_BROKER_INTERFACE_API_HOST and UNIFIED_BROKER_INTERFACE_API_PORT (see Configuration).

http://127.0.0.1:8080/api

Start it with bin/rest-api, which runs gunicorn with two worker processes and four threads each. bin/rest-api --dev runs Flask's development server instead.

Authentication

Authentication happens in two steps. The key and secret are exchanged for a token once a day, and the token goes on every other request.

sequenceDiagram
    autonumber
    participant C as Your program
    participant A as UBI API
    participant M as MongoDB
    participant R as Redis
    C->>A: POST /api/session/connect<br/>api-key, api-secret headers
    A->>M: read settings for unified_broker_interface
    A->>A: compare key and secret in constant time
    alt stored token was issued after 07:00 today and is unexpired
        A-->>C: 200 the same token
    else otherwise
        A->>M: store a new token
        A->>R: copy it to the last_login hash
        A-->>C: 200 a new token
    end
    C->>A: GET /api/portfolio/positions<br/>access-token header
    A->>R: read last_login
    A-->>C: 200 positions
Header Sent to Value
api-key POST /api/session/connect only The key stored in MongoDB settings for unified_broker_interface
api-secret POST /api/session/connect only The matching secret
access-token Every other route except GET /api/ The token returned by connect

There is only one token for the whole application. Issuing a new one replaces the old one, so any other client using the old token starts getting 401. A token issued before 07:00 IST is replaced by the first connect after 07:00.

Request format

GET and DELETE routes take their parameters in the query string. POST and PUT routes take a JSON body with Content-Type: application/json. The order routes modify and cancel also accept order_id, broker and dry_run in the query string, as a convenience.

Response format

Every response is JSON. A successful call returns the data directly, with no envelope around it. A failed call returns an object with an error key, sometimes with extra fields that help you act on the failure.

{
  "status": "connected",
  "expires_at": "2026-09-27 07:00:00.000000"
}
{
  "error": "Access token has expired"
}
{
  "error": "no broker can take this order",
  "instrument_id": "11111111-1111-5111-8111-000000000001",
  "skipped": [
    {"broker": "groww", "reason": "has no mapping for the instrument"}
  ]
}

Two routes, /api/instruments/master and /api/instruments/ticks, can return very large arrays. They stream the JSON array in chunks of about 64 KB rather than building it in memory. If something fails after streaming has started, the array simply ends early and the status stays 200, so check the count you received.

Status codes

The API uses a small set of HTTP status codes, and each one means the same thing on every route. Errors and status codes lists which route returns which.

Status Meaning
200 Done. For orders, the broker accepted the request.
202 Order engine only: a synthetic order is armed and waiting for its trigger.
207 Flatten only: some cancels or closes failed.
400 The request is malformed or breaks a rule, such as a quantity that is not a multiple of the lot size.
401 The token or the key and secret are missing, wrong or expired.
403 Order engine only: the daily loss limit is reached.
404 The instrument, order or document does not exist.
409 The request conflicts with the current state, for example the order is already complete.
422 The broker rejected the order.
429 Order engine only: the broker's daily order cap, from unified.broker_order_costs or UNIFIED_BROKER_INTERFACE_API_ORDER_DAILY_CAPS, has no room left for this kind of order.
500 The API itself is not configured.
501 The broker cannot modify that field.
502 The stored order book, trade book or portfolio document exists, but no broker in it has usable data.
503 A store or a background process that the route depends on is missing or stale.
504 The broker or the order engine did not answer in time, so the outcome is unknown.

504 on an order means unknown, not failed

When an order route returns 504, the order may well have reached the broker. Read GET /api/orders/details before sending it again, or you may end up with two orders.

Where the answers come from

Most routes never call a broker. They read a document that a background script keeps up to date in Redis. The table below shows which routes read stored data and which ones talk to a broker while you wait.

Route group Reads Calls a broker while you wait?
Session MongoDB settings, last_login No
Details Redis unified:details:*, falling back to MongoDB No
Instruments Redis instrument cache, falling back to TimescaleDB No
Market quotes Redis live quotes Only when no live quote is fresh
Historical data Redis candle cache and TimescaleDB No
Portfolio Redis unified:portfolio:* No
Orders (read) Redis unified:orders:* No
Orders (place, modify, cancel, flatten) Redis, then the broker Yes

Because of this, a read route can fail with 503 if the script that fills its Redis key has stopped. The message names the key, so you know which service to restart.

Checking the API is up

GET /api/no token needed

This route needs no token, so it is the quickest way to see whether the API is running.

curl http://127.0.0.1:8080/api/
{"message": "Welcome to the Unified Broker Interface API"}

Trying it by hand

bin/rest-api-app opens a Streamlit page that calls every read route with a click. See Test page.