BULLINV DATA
API down

bullinv-quant-data API

REST API for the internal quant market-data platform: aggregate bars, raw ticks, symbol metadata, pipeline status, bar coverage and admin backfills. All timestamps are Asia/Taipei (+08:00). Every response (except /health) is wrapped in a standard envelope:

{ "code": 200, "message": "ok", "data": { ... } }

Authentication

Authenticate by passing your key in the X-API-Key header on every request. Keys are issued per service by the platform team.

curl -H "X-API-Key: $BULLINV_API_KEY" \
  "http://localhost:8080/v1/symbols"
Base URLhttp://localhost:8080Auth headerX-API-Key: $BULLINV_API_KEYTimezoneAsia/Taipei (+08:00)

The “Try it” panels below send requests through this console’s server-side proxy, which attaches the API key for you — the key never reaches the browser.

GET/v1/bars/{symbol}

Aggregate Bars

OHLCV bars for a symbol over a date range, aggregated from raw ticks. Supports 1m to 1d timeframes and day/night/full session filters. Timestamps are Asia/Taipei.

Parameters

NameInTypeDescription
symbol*pathstringContract symbol, e.g. the continuous front-month TXF contract.
timeframequerystringBar aggregation window.one of: 1m | 5m | 15m | 30m | 1h | 1ddefault: 1m
startquerystring (ISO 8601)Inclusive range start. Defaults to the current trading day's open.
endquerystring (ISO 8601)Exclusive range end. Defaults to now.
sessionquerystringSession filter. day = 08:45-13:45, night = 15:00-05:00 (crosses midnight), full = both.one of: day | night | fulldefault: full
limitqueryintegerMaximum number of bars returned (max 5000). Use next_start to paginate.default: 1500

Request

curl -H "X-API-Key: $BULLINV_API_KEY" \
  "http://localhost:8080/v1/bars/TXFR1?timeframe=1m&start=2026-06-30T08%3A45%3A00%2B08%3A00&end=2026-06-30T13%3A45%3A00%2B08%3A00&session=day&limit=500"

Response

{
  "code": 200,
  "message": "ok",
  "data": {
    "symbol": "TXFR1",
    "timeframe": "1m",
    "session": "day",
    "tz": "Asia/Taipei",
    "count": 300,
    "results": [
      {
        "ts": "2026-06-30T08:45:00+08:00",
        "open": 23012,
        "high": 23021,
        "low": 23008,
        "close": 23017,
        "volume": 412,
        "amount": 9482804,
        "tick_count": 287,
        "buy_volume": 221,
        "sell_volume": 191
      }
    ],
    "next_start": null
  }
}
  • Bars are keyed by their opening minute; a bar covering 08:45:00-08:45:59 has ts 08:45:00.
  • When more bars exist than limit allows, next_start holds the ts to pass as start on the next call.
Try it
GET/v1/ticks/{symbol}

Raw Ticks

Raw trade ticks for a symbol. Ascending (oldest first) by default; pass order=desc for a newest-first tape. tick_type 1 = outer (traded at ask), 2 = inner (traded at bid), 0 = indeterminate.

Parameters

NameInTypeDescription
symbol*pathstringContract symbol.
startquerystring (ISO 8601)Inclusive range start.
endquerystring (ISO 8601)Exclusive range end. Defaults to now.
limitqueryintegerMaximum number of ticks returned (max 1000).default: 200
include_simtradequerybooleanInclude simulated pre-open matching ticks (simtrade=true).one of: 0 | 1default: 0
orderquerystringSort order. desc returns newest first; next_start pagination is only supported for asc (desc always returns next_start=null).one of: asc | descdefault: asc

Request

curl -H "X-API-Key: $BULLINV_API_KEY" \
  "http://localhost:8080/v1/ticks/TXFR1?start=2026-06-30T09%3A00%3A00%2B08%3A00&end=2026-06-30T09%3A05%3A00%2B08%3A00&limit=50&include_simtrade=0&order=desc"

Response

{
  "code": 200,
  "message": "ok",
  "data": {
    "count": 50,
    "results": [
      {
        "ts": "2026-06-30T09:04:59.812+08:00",
        "recv_ts": "2026-06-30T09:04:59.819+08:00",
        "contract": "TXFR1",
        "price": 23015,
        "volume": 3,
        "total_volume": 48211,
        "tick_type": 1,
        "bid_side_total_vol": 22903,
        "ask_side_total_vol": 25308,
        "simtrade": false
      }
    ],
    "next_start": "2026-06-30T09:03:12.044+08:00"
  }
}
  • next_start holds the ts to pass as start on the next call; it is null when order=desc.
Try it
GET/v1/live/{symbol}

Live Snapshot

Single-call snapshot for a live trading view: running day stats, the level-5 order book and the most recent trades (newest first). Poll this instead of stitching /v1/ticks and /v1/bidask together client-side.

Parameters

NameInTypeDescription
symbol*pathstringContract symbol.
tape_limitqueryintegerNumber of tape entries returned, newest first (max 200).default: 40

Request

curl -H "X-API-Key: $BULLINV_API_KEY" \
  "http://localhost:8080/v1/live/TXFR1?tape_limit=60"

Response

{
  "code": 200,
  "message": "ok",
  "data": {
    "symbol": "TXFR1",
    "tz": "Asia/Taipei",
    "market_open": true,
    "stats": {
      "last_price": 46231.0,
      "price_chg": 35.0,
      "pct_chg": 0.08,
      "open": 46200.0,
      "high": 46280.0,
      "low": 46150.0,
      "avg_price": 46210.5,
      "total_volume": 41231,
      "total_amount": 1904000000000,
      "buy_volume_today": 21031,
      "sell_volume_today": 19822,
      "underlying_price": 46210.11,
      "basis": 20.89,
      "last_tick_ts": "2026-06-30T09:04:59.812+08:00",
      "last_tick_age_s": 1.2
    },
    "book": {
      "ts": "2026-06-30T09:05:00.104+08:00",
      "bids": [{ "price": 46230.0, "volume": 12 }],
      "asks": [{ "price": 46231.0, "volume": 9 }],
      "bid_total_vol": 123,
      "ask_total_vol": 140
    },
    "tape": [
      {
        "ts": "2026-06-30T09:04:59.812331+08:00",
        "price": 46231.0,
        "volume": 2,
        "tick_type": 1,
        "total_volume": 41231
      }
    ]
  }
}
  • stats come from the latest non-simtrade tick; buy/sell_volume_today are summed from 1m bars since Taipei midnight.
  • basis = last_price - underlying_price; both are null when the feed has no underlying price.
  • book is null until bidask snapshots exist for the symbol; levels with price or volume <= 0 are dropped.
  • tape is newest first and excludes simtrade ticks.
Try it
GET/v1/bidask/{symbol}

Order Book Snapshots

Raw level-5 bid/ask snapshots as captured from the feed, keyset-paginated in the same style as /v1/ticks. Arrays are ordered L1 (best) to L5.

Parameters

NameInTypeDescription
symbol*pathstringContract symbol.
startquerystring (ISO 8601)Inclusive range start.
endquerystring (ISO 8601)Exclusive range end. Defaults to now.
limitqueryintegerMaximum number of snapshots returned (max 1000).default: 200
orderquerystringSort order. desc returns newest first; next_start pagination is only supported for asc (desc always returns next_start=null).one of: asc | descdefault: asc

Request

curl -H "X-API-Key: $BULLINV_API_KEY" \
  "http://localhost:8080/v1/bidask/TXFR1?start=2026-06-30T09%3A00%3A00%2B08%3A00&end=2026-06-30T09%3A05%3A00%2B08%3A00&limit=50&order=desc"

Response

{
  "code": 200,
  "message": "ok",
  "data": {
    "count": 1,
    "results": [
      {
        "ts": "2026-06-30T09:04:59.900+08:00",
        "recv_ts": "2026-06-30T09:04:59.904+08:00",
        "contract": "TXFR1",
        "bid_prices": [46230, 46229, 46228, 46227, 46226],
        "bid_volumes": [12, 8, 21, 17, 30],
        "ask_prices": [46231, 46232, 46233, 46234, 46235],
        "ask_volumes": [9, 14, 25, 11, 19],
        "bid_total_vol": 123,
        "ask_total_vol": 140,
        "underlying_price": 46210.11,
        "simtrade": false
      }
    ],
    "next_start": null
  }
}
  • Snapshots dedupe on (ts, symbol, level-1 fields); identical Kafka redeliveries collapse to one row.
Try it
GET/v1/symbols

List Symbols

All symbols tracked by the platform, with their session schedule and stored-bar extent.

Request

curl -H "X-API-Key: $BULLINV_API_KEY" \
  "http://localhost:8080/v1/symbols"

Response

{
  "code": 200,
  "message": "ok",
  "data": {
    "results": [
      {
        "symbol": "TXFR1",
        "source": "shioaji",
        "display_name": "台指期近月",
        "sessions": {
          "day": ["08:45", "13:45"],
          "night": ["15:00", "05:00"]
        },
        "first_bar_ts": "2026-01-05T08:45:00+08:00",
        "last_bar_ts": "2026-06-30T13:44:00+08:00",
        "bar_count": 137940
      }
    ]
  }
}
Try it
GET/v1/internal/bars/{symbol}

Internal Bulk Bars

Unpaginated bulk 1m bars for internal batch consumers (e.g. the backtest engine). Streams QuestDB's raw /exec JSON straight through — no envelope, no per-row transformation — so multi-hundred-thousand-row fetches run at near-direct speed while still being authenticated and visible in /usage.

Parameters

NameInTypeDescription
symbol*pathstringContract symbol.
startquerystring (ISO 8601)Inclusive range start (naive = Asia/Taipei). Defaults to end − 7d.
endquerystring (ISO 8601)Inclusive range end. Defaults to now.

Request

curl -H "X-API-Key: $BULLINV_API_KEY" \
  "http://localhost:8080/v1/internal/bars/TXFR1?start=2026-06-20T08%3A45%3A00%2B08%3A00&end=2026-06-30T13%3A45%3A00%2B08%3A00"

Response

{
  "query": "SELECT ts, open, high, low, close, volume, amount FROM bars_1m ...",
  "columns": [
    { "name": "ts", "type": "TIMESTAMP" },
    { "name": "open", "type": "DOUBLE" },
    { "name": "high", "type": "DOUBLE" },
    { "name": "low", "type": "DOUBLE" },
    { "name": "close", "type": "DOUBLE" },
    { "name": "volume", "type": "LONG" },
    { "name": "amount", "type": "DOUBLE" }
  ],
  "dataset": [
    ["2026-06-30T00:45:00.000000Z", 46100.0, 46120.0, 46081.0, 46095.0, 2362, 108870000.0]
  ],
  "count": 7122
}
  • Requires an admin key (DATA_API_ADMIN_KEYS).
  • Raw QuestDB /exec passthrough: NOT wrapped in the {code,message,data} envelope; ts values are UTC ISO — convert timezone client-side (vectorized).
  • Hard safety cap 5,000,000 rows (≈15 years of 1m bars) — a guardrail, not pagination.
Try it
GET/v1/analytics/latency/{symbol}

Ingest Latency

Pipeline latency analytics over a rolling window: tick and bidask feed latency (recv_ts − ts, ms) with percentiles, a fixed-bucket histogram and a per-minute series, plus bar-write latency (minute close → ingested_at) for realtime bars.

Parameters

NameInTypeDescription
symbol*pathstringContract symbol.
windowquerystringRolling window the stats are computed over.one of: 15m | 1h | 4h | 1ddefault: 1h

Request

curl -H "X-API-Key: $BULLINV_API_KEY" \
  "http://localhost:8080/v1/analytics/latency/TXFR1?window=1h"

Response

{
  "code": 200,
  "message": "ok",
  "data": {
    "symbol": "TXFR1",
    "window": "1h",
    "tick": {
      "count": 8123,
      "avg_ms": 142.1,
      "p50_ms": 120.0,
      "p90_ms": 210.0,
      "p99_ms": 480.0,
      "max_ms": 2210.0,
      "negative_count": 0,
      "histogram": [
        { "le_ms": 50, "count": 10 },
        { "le_ms": 100, "count": 900 },
        { "le_ms": null, "count": 2 }
      ],
      "series": [
        { "t": "2026-07-02T10:04:00+08:00", "count": 210, "avg_ms": 130.5, "p99_ms": 320.0 }
      ]
    },
    "bidask": null,
    "bar_write": {
      "count": 120,
      "avg_s": 6.8,
      "p50_s": 6.5,
      "p99_s": 9.1,
      "max_s": 22.0
    }
  }
}
  • Latency = recv_ts − ts in milliseconds; simtrade ticks are excluded.
  • Negative latencies (local vs exchange clock skew) are excluded from the percentiles and reported via negative_count.
  • Histogram bucket upper bounds are fixed: 25, 50, 75, 100, 150, 200, 300, 500, 1000, 2000, 5000 and null (= above); series is SAMPLE BY 1m.
  • bidask is null when the bidask table has no rows for the symbol.
  • bar_write measures src='rt' bars as ingested_at − (ts + 60s); its window is widened to at least 4h so there are enough samples.
Try it
GET/v1/analytics/quality/{symbol}

Data Quality

Per-trading-day bar provenance for a symbol: how many 1m bars came from the realtime stream (rt), backfill or the vendor seed, plus volume-check failures and per-day activity aggregates.

Parameters

NameInTypeDescription
symbol*pathstringContract symbol.
daysqueryintegerNumber of trading days to return (max 60), ending at the current trading day.default: 14

Request

curl -H "X-API-Key: $BULLINV_API_KEY" \
  "http://localhost:8080/v1/analytics/quality/TXFR1?days=14"

Response

{
  "code": 200,
  "message": "ok",
  "data": {
    "days": [
      {
        "trading_day": "2026-07-02",
        "total_bars": 1108,
        "rt_bars": 420,
        "backfill_bars": 10,
        "seed_bars": 678,
        "vol_check_fail_bars": 0,
        "avg_tick_count": 38.2,
        "total_volume": 183422
      }
    ]
  }
}
  • Bars are grouped by the src column of bars_1m; rt_bars + backfill_bars + seed_bars = total_bars.
  • trading_day attribution uses the 05:01 cut: night-session bars before 05:01 belong to the previous trading day.
  • vol_check_fail_bars counts bars whose volume disagrees with the vendor kbar during the nightly coverage check.
Try it
GET/v1/analytics/profile/{symbol}

Intraday Profile

Average intraday structure over the last N trading days: mean volume and tick count per Taipei minute-of-day, session-filtered. Useful for spotting the open/close U-shape and the night-session US-open bump.

Parameters

NameInTypeDescription
symbol*pathstringContract symbol.
daysqueryintegerNumber of recent trading days to average over (max 60).default: 20
sessionquerystringSession filter. day = 08:45-13:45, night = 15:00-05:00, full = both.one of: day | night | fulldefault: day

Request

curl -H "X-API-Key: $BULLINV_API_KEY" \
  "http://localhost:8080/v1/analytics/profile/TXFR1?days=20&session=day"

Response

{
  "code": 200,
  "message": "ok",
  "data": {
    "session": "day",
    "days_sampled": 20,
    "minutes": [
      { "minute": "08:45", "avg_volume": 812.3, "avg_tick_count": 95.1 },
      { "minute": "08:46", "avg_volume": 590.8, "avg_tick_count": 71.4 }
    ]
  }
}
  • minute is a Taipei minute-of-day (HH:mm); values are means over the sampled trading days.
Try it
GET/v1/analytics/basis/{symbol}

Futures Basis

Futures-vs-underlying basis series over a rolling window, sampled per minute from ticks that carry an underlying price. basis = price − underlying.

Parameters

NameInTypeDescription
symbol*pathstringContract symbol.
windowquerystringRolling window of the series.one of: 1h | 4h | 1ddefault: 1d

Request

curl -H "X-API-Key: $BULLINV_API_KEY" \
  "http://localhost:8080/v1/analytics/basis/TXFR1?window=1d"

Response

{
  "code": 200,
  "message": "ok",
  "data": {
    "series": [
      {
        "t": "2026-07-02T10:04:00+08:00",
        "price": 46231.0,
        "underlying": 46720.1,
        "basis": -489.1
      }
    ]
  }
}
  • SAMPLE BY 1m average over ticks whose underlying_price is not null; minutes without such ticks are omitted.
Try it
GET/v1/analytics/spread/{symbol}

Bid/Ask Spread

Level-1 spread analytics from bidask snapshots over a rolling window: per-minute average spread (ask L1 − bid L1) and average bid/ask total queue volumes.

Parameters

NameInTypeDescription
symbol*pathstringContract symbol.
windowquerystringRolling window of the series.one of: 1h | 4h | 1ddefault: 1d

Request

curl -H "X-API-Key: $BULLINV_API_KEY" \
  "http://localhost:8080/v1/analytics/spread/TXFR1?window=1d"

Response

{
  "code": 200,
  "message": "ok",
  "data": {
    "series": [
      {
        "t": "2026-07-02T10:04:00+08:00",
        "avg_spread": 1.2,
        "avg_bid_total": 123.1,
        "avg_ask_total": 140.2
      }
    ],
    "count": 421
  }
}
  • SAMPLE BY 1m over bidask snapshots; a snapshot only counts when both L1 prices are > 0.
  • When the bidask table has no rows for the symbol the series is empty ([]), not an error.
Try it
GET/v1/status

Platform Status

Live health of every pipeline service (ingestor, writer, backfill, data-api), per-symbol tick freshness with a 60-minute rate history, and recent bar gaps.

Request

curl -H "X-API-Key: $BULLINV_API_KEY" \
  "http://localhost:8080/v1/status"

Response

{
  "code": 200,
  "message": "ok",
  "data": {
    "services": [
      {
        "service": "ingestor",
        "symbol": "TXFR1",
        "status": "ok",
        "lag_ms": 12,
        "ticks_1m": 431,
        "heartbeat_age_s": 1,
        "detail": "shioaji stream connected"
      }
    ],
    "symbols": [
      {
        "symbol": "TXFR1",
        "last_tick_ts": "2026-06-30T09:04:59.812+08:00",
        "last_tick_age_s": 2,
        "last_price": 23015,
        "ticks_today": 48211,
        "simtrade_ticks_today": 193,
        "ticks_per_min": [{ "t": "2026-06-30T08:05:00+08:00", "v": 388 }]
      }
    ],
    "gaps": {
      "detected": 2,
      "filling": 1,
      "unfillable": 1,
      "recent": [
        {
          "gap_id": "gap-txfr1-20260610-1031",
          "symbol": "TXFR1",
          "gap_start": "2026-06-10T10:31:00+08:00",
          "gap_end": "2026-06-10T10:34:00+08:00",
          "expected_bars": 3,
          "actual_bars": 0,
          "status": "unfillable"
        }
      ]
    },
    "market_open": true
  }
}
Try it
GET/v1/coverage/{symbol}

Bar Coverage

Per-trading-day 1m-bar coverage for a symbol: expected vs actual bar counts and the resulting percentage, session-aware.

Parameters

NameInTypeDescription
symbol*pathstringContract symbol.
fromquerystring (date)First trading day, yyyy-MM-dd. Defaults to 90 days ago.
toquerystring (date)Last trading day, yyyy-MM-dd. Defaults to today.
sessionquerystringSession filter used for the expected-bar count.one of: day | night | fulldefault: full

Request

curl -H "X-API-Key: $BULLINV_API_KEY" \
  "http://localhost:8080/v1/coverage/TXFR1?from=2026-04-01&to=2026-06-30&session=full"

Response

{
  "code": 200,
  "message": "ok",
  "data": {
    "results": [
      {
        "trading_day": "2026-06-10",
        "expected": 1140,
        "actual": 1071,
        "coverage_pct": 93.95,
        "is_trading_day": true,
        "verified": true,
        "vendor_bars": 1071,
        "checked_at": "2026-06-11T05:20:00+08:00"
      },
      {
        "trading_day": "2026-06-13",
        "expected": 0,
        "actual": 0,
        "coverage_pct": 0,
        "is_trading_day": false,
        "verified": false,
        "vendor_bars": null,
        "checked_at": null
      }
    ]
  }
}
Try it
GET/v1/usage/summary

API Usage Summary

Aggregated API-usage analytics over a rolling window, computed from the api_requests log: totals by status class, latency (avg / p99), a time series, the top endpoints and per-key counts.

Parameters

NameInTypeDescription
windowquerystringRolling window the summary is computed over.one of: 1h | 4h | 1d | 7ddefault: 1d
exclude_keysquerystringComma-separated key labels to exclude from ALL stats (totals, series, endpoints, keys) — e.g. hide the dashboard's own traffic by its named key.

Request

curl -H "X-API-Key: $BULLINV_API_KEY" \
  "http://localhost:8080/v1/usage/summary?window=1d&exclude_keys=dash"

Response

{
  "code": 200,
  "message": "ok",
  "data": {
    "window": "1d",
    "total": 8213,
    "count_2xx": 8100,
    "count_4xx": 100,
    "count_5xx": 13,
    "avg_ms": 11.2,
    "p99_ms": 88.0,
    "series": [
      { "t": "2026-07-02T10:05:00+08:00", "count": 140, "errors": 2, "avg_ms": 10.1 }
    ],
    "endpoints": [
      { "route": "/v1/status", "count": 5200, "avg_ms": 8.9, "p99_ms": 40.1, "errors": 0 }
    ],
    "keys": [
      { "api_key": "key-1", "count": 8100, "last_seen": "2026-07-02T10:09:41+08:00" }
    ]
  }
}
  • series granularity follows the window: 1h/4h → SAMPLE BY 1m, 1d → 5m, 7d → 1h.
  • endpoints are the top 20 routes by count; route is the FastAPI route template (e.g. /v1/bars/{symbol}).
  • api_key is an identity label — "key-N" / "admin-N" for configured keys, "invalid" for a bad key, "anonymous" when no key was sent (dev mode); raw keys are never stored.
  • The api_requests table keeps 30 days of data (PARTITION BY DAY TTL 30 DAYS); /health and OPTIONS requests are not logged.
Try it
GET/v1/usage/requests

API Request Log

The raw api_requests tail, newest first: one row per API call with the matched route template, actual path, truncated query string, key label, status code, duration and client IP.

Parameters

NameInTypeDescription
limitqueryintegerMaximum number of rows returned (1–500), newest first.default: 100
statusquerystringFilter by status-code class (range match on status_code).one of: all | 2xx | 4xx | 5xxdefault: all
routequerystringExact-match filter on the route template; use values from the summary's endpoints list.
exclude_routesquerystringComma-separated route templates to hide (e.g. the dashboard's own polling). Rows with route=null (unmatched 404s) are always kept.
exclude_keysquerystringComma-separated key labels to hide — e.g. exclude_keys=dash hides the dashboard's own traffic. Keys are named in DATA_API_KEYS as "name:secret".
qquerystringCase-insensitive substring search over path and query string (max 64 chars; letters, digits and /_{}.,=&%?:- only).

Request

curl -H "X-API-Key: $BULLINV_API_KEY" \
  "http://localhost:8080/v1/usage/requests?limit=100&status=4xx&route=%2Fv1%2Fbars%2F%7Bsymbol%7D&exclude_routes=%2Fv1%2Fstatus%2C%2Fv1%2Flive%2F%7Bsymbol%7D&exclude_keys=dash&q=timeframe%3D5m"

Response

{
  "code": 200,
  "message": "ok",
  "data": {
    "count": 100,
    "results": [
      {
        "ts": "2026-07-02T10:09:41.213+08:00",
        "method": "GET",
        "route": "/v1/bars/{symbol}",
        "path": "/v1/bars/TXFR1",
        "query": "timeframe=5m&session=day",
        "api_key": "key-1",
        "status_code": 200,
        "duration_ms": 12.4,
        "client_ip": "127.0.0.1"
      }
    ]
  }
}
  • route is null when the request matched no FastAPI route (e.g. a scanner probing /admin/login); path always holds the actual request path.
  • query is truncated to 512 characters; ts is the request start time.
  • api_requests is an append-only log with a 30-day TTL; /health and OPTIONS are excluded as probe noise.
Try it
POST/v1/admin/backfill

Trigger Backfill

Queue a backfill run that re-downloads ticks for a symbol over a time range and rebuilds the affected bars. Returns the run id.

Parameters

NameInTypeDescription
symbol*bodystringContract symbol to backfill.
start*bodystring (ISO 8601)Inclusive range start.
end*bodystring (ISO 8601)Exclusive range end.

Request

curl -X POST \
  -H "X-API-Key: $BULLINV_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"symbol":"TXFR1","start":"2026-06-10T08:45:00+08:00","end":"2026-06-11T05:00:00+08:00"}' \
  "http://localhost:8080/v1/admin/backfill"

Response

{
  "code": 200,
  "message": "ok",
  "data": {
    "run_id": "bf-20260630-0912-a41c"
  }
}
  • Runs are idempotent per (symbol, start, end); re-posting the same range returns a new run that no-ops on already-filled bars.
Try it
GET/v1/backfill-runs

Backfill Run History

Execution history of backfill runs (one row per run, latest status). Runs are created by the admin trigger, the seed CLI, or the automatic gap scanner, and progress requested → running → done | failed.

Parameters

NameInTypeDescription
limitqueryintMax runs to return (1–200), newest first.default: 20
statusqueryenumFilter by current status.one of: requested | running | done | failed

Request

curl -H "X-API-Key: $BULLINV_API_KEY" \
  "http://localhost:8080/v1/backfill-runs?limit=20&status=done"

Response

{
  "code": 200,
  "message": "ok",
  "data": {
    "count": 1,
    "results": [
      {
        "run_id": "9c2f1c40e2a34d0f8b1d6c1c8f0a2b7e",
        "symbol": "TXFR1",
        "range_start": "2026-06-10T08:45:00+08:00",
        "range_end": "2026-06-11T05:00:00+08:00",
        "status": "done",
        "rows_written": 1108,
        "requested_by": "api",
        "error": null,
        "ts": "2026-07-02T18:40:11+08:00"
      }
    ]
  }
}
  • ts is the run's latest status-change time; a run stuck in running for >30 min is reaped and re-executed by the backfill loop.
Try it
GET/health

Health Check

Liveness probe for the data-api process and its QuestDB connection. This is the only endpoint that is not wrapped in the standard envelope and requires no API key.

Request

curl -H "X-API-Key: $BULLINV_API_KEY" \
  "http://localhost:8080/health"

Response

{
  "status": "ok",
  "questdb": "ok"
}
  • No X-API-Key required.
Try it

Questions? Ping #quant-data. Method badges: GET read, POST mutate.