Four read-only routes, no key and no cookie, using the field names that aggregators (CoinGecko, CoinMarketCap) and market libraries (CCXT) expect to find. They exist to be read by robots: whoever builds a dashboard, feeds an index or reconciles a price reads from here. There is no order route in this API — nothing here moves funds or needs an account.
Everything hangs off https://offcode.pro/api/public/, accepts only GET and answers application/json. The ticker_id is BASE_TARGET (BTC_USDT): base before the underscore, quote after it, uppercase letters and digits, 2 to 15 characters on each side. A pair whose symbol does not fit that shape is not published.
Every number travels as a decimal string, never as a floating-point number — an eight-decimal coin loses decimals on its way through a float. Instants are milliseconds since the Unix epoch.
| Route | What it returns | Parameters |
|---|---|---|
GET /pairs | Every pair this feed addresses, each carrying its price source | — |
GET /tickers | Price, 24 h volume, high, low and both sides for the spot pairs | — |
GET /orderbook | Order book for one pair | ticker_id · depth |
GET /trades | Recent trades for one pair, newest first | ticker_id · type · limit |
GET /historical_trades | The same as /trades — two names, one implementation | same |
The list of everything this feed addresses, and the gate for the other routes: a ticker_id that does not come from here answers 404 on the book and on the trades. kind separates the house's spot market (spot) from the perpetual (perp); when the same name serves both, spot keeps it. price_source states where the number comes from, on the item itself.
Price, 24-hour volume, high, low and both sides for every spot pair. perp pairs are not included: this is the slice the aggregator standard describes.
ticker_id | The pair, in BASE_TARGET form. |
base_currency · target_currency | The two legs of the pair, separately. |
last_price | Last price traded on the perpetual. |
base_volume | 24 h volume in base currency, as measured by the venue when it publishes it. Only in its absence does the route divide target_volume by last_price and cut to 8 decimals down — a declared calculation, not a number from the exchange. |
target_volume | 24 h volume in quote currency, raw from the source. |
bid · ask | Equal to each other and equal to the perpetual's mid — they are not the top of a book. See Where the data comes from. |
high · low | High and low of the last 24 hours. |
timestamp | Instant of the reading, in milliseconds since the Unix epoch. |
The order book for one pair. bids and asks are [price, size] pairs, both decimal strings, best price first. The timestamp is the source's clock, not ours: a local one would let a stale book pass for a fresh one.
depth is optional and goes up to 100 levels per side. Omitted, it returns the cap: 100. A larger request is not refused — it is capped at 100.
The recent tape for one pair, newest trade first. The two routes are the same implementation and the same cache: the aggregator standard asks for historical_trades, CCXT usually asks for trades, and that way there is never a day when one answers differently from the other.
type is optional (buy or sell, by the aggressing side) and limit goes up to 100, also capped rather than refused. Omitting type returns both sides, and omitting limit returns the cap: 100. The trade_id is derived — instant, side, price and size — stable for the same trade across two readings, and it is not the venue's own trade identifier.
Price, book, trades and 24-hour volume in this feed come from the perpetual market — including on the pairs labelled spot. OFFCODE's spot has no book of its own: it quotes the mid of the perpetual with the same symbol. That is why every /pairs item carries price_source: whoever consumes the feed reads the source in the feed itself, instead of deducing it from the label — and deducing it wrong.
ask − bid here is zero. A zero spread is impossible in a real book: do not treat those two fields as the sides of one.base_volume is the 24 h volume in base currency of the perpetual, as measured by the venue when it publishes it. Only in its absence does the route divide target_volume by last_price and cut to 8 decimals down — a declared calculation, not a number from the exchange.
A pair is published only where a tradable market with the same symbol exists: publishing is promising to serve, and the ticker_id that leaves /pairs is the one the book and the trades have to answer right afterwards. A custodied coin with no tradable pair stays out of the feed, and so does a pair whose name at the venue carries a factor — the 1000PEPE_USDT perpetual is published as itself, never as PEPE_USDT.
600 requests per minute per IP, on each route. Above that comes a 429.
A 2xx answer carries Cache-Control: public, max-age=5; anything that is not 2xx carries no-store, because a 404 held for five seconds outlives the pair that was just listed, and a held 502 hands back a venue that is down after it has already come back.
Underneath, the book is re-read at most every 350 ms per pair and depth, and the trades every 1 second per pair: the ceiling on calls to the venue is the cache, not the number of people asking. A failed reading is never cached.
Access-Control-Allow-Origin: *, with no credentials — there is no cookie, no key and no authentication header, and nothing in the answer is personalised.
| Code | When it happens | Body |
|---|---|---|
| 400 | ticker_id outside the shape, a type that is neither buy nor sell, a depth or limit that is not an integer | {"code": "VALIDATION_ERROR", "message": "…"} |
| 404 | A well-formed ticker_id that is not in /pairs | {"error": "UNKNOWN_TICKER"} |
| 429 | Above the request limit for this IP | {"statusCode": 429, "code": "RATE_LIMITED", "message": "…"} |
| 502 | The venue did not answer, or answered with a failure | {"code": "UPSTREAM_UNAVAILABLE", "message": "…"} |
502 and not 500, on purpose: a failure at the venue is not a failure of ours, and whoever reads from outside needs that difference to know whether trying again is worth it.
The domain sits behind Cloudflare. A browser gets through; a command-line call with no cookie may get a challenge instead of the JSON — which is not the route refusing, it is the edge asking who arrived. If your robot needs to sweep the feed continuously, ask for it to be allowed before turning the crawler on: state the user-agent, the outbound IP ranges and the intended frequency at Contact.
Fees and trading rules are not in this API: they live at /api/public/fees and on Fees. Service state is on Platform status.