API Reference

BotRanks API v1

Report your strategy's orders, positions, and cash events over HTTPS. BotRanks persists the event log, derives positions and NAV, computes performance, and renders the dashboards your subscribers see.

Overview

The BotRanks API is REST over HTTPS. Requests and responses are JSON. All endpoints below live under a common base:

https://botranks.ai/api/v1

Source of truth. Your event log is the truth. BotRanks stores every order and cash event idempotently, then derives current positions, cash balance, and daily NAV history from that log plus cached Polygon prices. You never need to push a position or NAV snapshot separately — though the positions/snapshot endpoint is available for reconciliation.

Authentication

Every API request carries a Bearer token:

Authorization: Bearer fq_user_xxxxxxxx…   or   fq_live_xxxxxxxx…

BotRanks uses two tiers of keys, separated so that compromise of one doesn't bleed into the other:

PrefixScopeUse for
fq_user_AccountCreate, list, update strategies you own (Strategy CRUD)
fq_live_Single strategyReport orders / positions / cash for that one strategy (Ingestion)

User-level keys

Sign in, then from the browser call:

POST https://botranks.ai/account/api-keys
Cookie: access_token=<session cookie>
Content-Type: application/json

{ "name": "deploy-bot" }

Response (plaintext is returned once):

{
  "id": "a8e2…",
  "name": "deploy-bot",
  "key_prefix": "fq_user_7Q",
  "created_at": "2026-04-18T08:00:00Z",
  "plaintext": "fq_user_7QxZw…43char-suffix",
  "warning": "Store this key now; it cannot be retrieved later."
}

Manage with GET /account/api-keys and POST /account/api-keys/{id}/revoke.

Strategy-level keys

Strategy keys are scoped to one strategy — they can ingest to it but cannot create or modify other strategies. Two ways to generate one, pick whichever fits your flow:

A · Over the API (recommended for bots)

Use your user-level key. Lets a strategy provisioned via POST /api/v1/strategies get its ingestion key without ever opening a browser. See POST /strategies/{id}/api-keys for the full reference.

curl -X POST https://botranks.ai/api/v1/strategies/$STRATEGY_ID/api-keys \
  -H "Authorization: Bearer $FQ_USER_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "name": "production-bot" }'

B · From the strategy settings page

Sign in to botranks.ai, open the strategy's settings, and visit the API Keys tab. Under the hood this calls:

POST https://botranks.ai/strategies/{strategy_id}/api-keys
Cookie: access_token=<session cookie>
Content-Type: application/json

{ "name": "my-bot-prod" }

Both paths return the same shape (plaintext once, fq_live_ prefix) and write to the same strategy_api_keys table. List and revoke via GET /strategies/{id}/api-keys and POST /strategies/{id}/api-keys/{kid}/revoke — again, either under /api/v1/ with a user key, or under / with a cookie.

Revoked keys return 401 on every subsequent request. Rotate freely.

Quick Start

Post a filled buy order from your strategy:

curl

curl -X POST https://botranks.ai/api/v1/strategies/$STRATEGY_ID/orders \
  -H "Authorization: Bearer $FQ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "orders": [{
      "symbol": "AAPL",
      "side": "buy",
      "quantity": 100,
      "executed_at": "2026-04-18T14:31:22Z",
      "external_order_id": "tb-20260418-00001"
    }]
  }'

Python

import os, requests
from datetime import datetime, timezone

BASE = "https://botranks.ai/api/v1"
STRATEGY = os.environ["FQ_STRATEGY_ID"]
HEADERS = {"Authorization": f"Bearer {os.environ['FQ_API_KEY']}"}

resp = requests.post(
    f"{BASE}/strategies/{STRATEGY}/orders",
    headers=HEADERS,
    json={
        "orders": [{
            "symbol": "AAPL",
            "side": "buy",
            "quantity": 100,
            "executed_at": datetime.now(timezone.utc).isoformat(),
            "external_order_id": "tb-20260418-00001",
        }]
    },
    timeout=10,
)
resp.raise_for_status()
print(resp.json())

Note: price is optional. When omitted, BotRanks resolves it from the close price on executed_at. Provide it if you have the actual fill price.

POST/strategies

Create a new strategy owned by the calling user. Requires a user-level key (fq_user_).

Request body

FieldTypeDescription
namestringrequiredGlobally unique strategy name
typeenumdefault "manual"manual, live, cron
descriptionstringoptionalUp to 2000 chars
initial_equitynumberdefault 100000Starting cash balance
is_publicbooldefault falseVisible in the public strategy list
statusenumdefault "active"active, paused, stopped, error
domainstringfrom Host headerFor multi-tenant deployments

Example

curl -X POST https://botranks.ai/api/v1/strategies \
  -H "Authorization: Bearer $FQ_USER_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Momentum Alpha v3",
    "description": "Top-decile 12-month momentum, rebalanced weekly",
    "initial_equity": 500000,
    "is_public": true
  }'

Response

{ "strategy": Strategy }

Returns 409 if the name already exists.

GET/strategies

List strategies owned by the calling user. Paginated.

Query parameters

ParamDescription
source_typeoptionalmanual or system
statusoptionalFilter by strategy status
limitdefault 50, max 200
offsetdefault 0For pagination

Response

{
  "strategies": [ Strategy, … ],
  "total":  42,
  "limit":  50,
  "offset": 0
}

GET/strategies/{id}

Fetch a single strategy. Must be owned by the caller (otherwise 403).

Response

{ "strategy": Strategy }

PATCH/strategies/{id}

Partial update. Only fields present in the body are touched. initial_equity and creator_user_id are intentionally immutable.

Updatable fields

FieldType
namestring
descriptionstring
is_publicbool
statusactive | paused | stopped | error
pricing_modelwebsite_membership | independent_pricing
price_monthlynumber ≥ 0
price_annualnumber ≥ 0

Example

curl -X PATCH https://botranks.ai/api/v1/strategies/$STRATEGY_ID \
  -H "Authorization: Bearer $FQ_USER_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "is_public": true, "description": "Updated thesis." }'

Response

{
  "strategy": Strategy,
  "updated_fields": ["description", "is_public", "updated_at"]
}

POST/strategies/{id}/api-keys

Provision an ingestion key (fq_live_*) for a strategy you own, without opening the browser. Use this right after POST /strategies so your bot can start reporting orders immediately. Requires a user-level key.

Request body

{ "name": "production-bot" }

Example — the full bootstrap flow

# 1. Create the strategy with your account key
STRATEGY_ID=$(curl -sX POST https://botranks.ai/api/v1/strategies \
  -H "Authorization: Bearer $FQ_USER_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"Momentum Alpha v3","initial_equity":500000}' \
  | jq -r '.strategy.id')

# 2. Mint an ingestion key for it (still with your account key)
FQ_API_KEY=$(curl -sX POST https://botranks.ai/api/v1/strategies/$STRATEGY_ID/api-keys \
  -H "Authorization: Bearer $FQ_USER_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"production-bot"}' \
  | jq -r '.plaintext')

# 3. Use the ingestion key for orders/positions/cash/etc
curl -sX POST https://botranks.ai/api/v1/strategies/$STRATEGY_ID/orders \
  -H "Authorization: Bearer $FQ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"orders":[{"symbol":"AAPL","side":"buy","quantity":100,
                  "executed_at":"2026-04-18T14:31:22Z",
                  "external_order_id":"bot-00001"}]}'

Response

{
  "id": "a8e2…",
  "name": "production-bot",
  "key_prefix": "fq_live_9Q",
  "created_at": "2026-04-18T08:00:00Z",
  "plaintext": "fq_live_9QxZw…43char-suffix",
  "warning": "Store this key now; it cannot be retrieved later."
}

Returns 403 if the strategy isn't owned by the calling user.

GET/strategies/{id}/api-keys

List non-plaintext metadata for every strategy-level key. POST /strategies/{id}/api-keys/{kid}/revoke revokes one (soft-delete, idempotent).

Response

{
  "keys": [
    { "id": "…", "name": "production-bot", "key_prefix": "fq_live_9Q",
      "created_at": "…", "last_used_at": "…", "revoked_at": null },
    …
  ]
}

POST/strategies/{id}/orders

Submit one or more orders. Batch up to 500 per request.

Request body

{
  "orders": [ Order, Order, … ]
}

Order fields

FieldTypeDescription
symbolstringrequiredTicker, e.g. AAPL, BTC-USD
side"buy" \| "sell"requiredLong buys and short sells both use buy/sell. Short is inferred from resulting position sign.
quantitynumberrequiredPositive; units the order filled for
executed_atISO-8601defaults to nowUTC fill time
pricenumberoptionalFill price. When omitted, resolved from the close on executed_at's date.
feesnumberdefault 0Commission + slippage, in base currency
order_typeenumdefault "market"market, limit, stop, stop_limit
statusenumdefault "filled"pending, filled, partial, cancelled
external_order_idstringrecommendedYour order identifier. Enables Idempotency.
detailsobjectoptionalFree-form JSON for your own metadata

Response

{
  "accepted":   [ Order, … ],        // newly persisted
  "duplicates": [ Order, … ],        // matched an existing external_order_id (not re-applied)
  "counts":     { "accepted": 2, "duplicates": 0 }
}

After a successful ingest, BotRanks re-derives positions and rebuilds NAV history automatically. The next GET /state reflects the new truth.

POST/strategies/{id}/positions/snapshot

Replace the strategy's full position set with the snapshot you send. Every symbol present in the payload is upserted; any symbol not present is deleted. Optionally logs deltas against the prior state into reconciliation_log.

Use this to seed an existing strategy (first-time onboarding without a complete order history), or to reconcile periodically against the order-derived state.

Request body

{
  "positions": [
    { "symbol": "AAPL", "quantity": 100, "cost_basis": 189.42 },
    { "symbol": "MSFT", "quantity":  30, "cost_basis": 420.10 }
  ],
  "log_deltas": true
}

Response

{
  "upserted": 2,
  "removed": 0,
  "reconciliation_entries": 1
}

POST/strategies/{id}/cash

Record a cash event — deposit, withdrawal, dividend received, margin interest, fee, or manual adjustment. Cash events participate in cash_balance and therefore in NAV.

Request body

FieldTypeDescription
event_typeenumrequireddeposit, withdraw, dividend, interest, fee, adjustment
amountnumberrequiredSigned: positive = cash in, negative = cash out
occurred_atISO-8601defaults to now
external_idstringrecommendedYour event identifier; makes the call idempotent
related_symbolstringoptionalFor dividend / stock-related events
notestringoptionalHuman-readable context

Response

{ "status": "created", "event": CashEvent }
// or on duplicate external_id:
{ "status": "duplicate", "event": CashEvent }

GET/strategies/{id}/state

Current derived state. Use this to verify your strategy agrees with BotRanks, or for reconciliation loops.

Response

{
  "strategy_id": "…",
  "positions": [
    { "symbol": "AAPL", "quantity": 100, "cost_basis": 189.42, "updated_at": "…" },
    …
  ],
  "nav": {
    "total_equity":  1021183.79,
    "cash_balance":   796110.50,
    "market_value":   225073.29,
    "captured_at":  "2026-04-17T16:00:00Z"
  },
  "last_order_at": "2026-01-16T14:31:22Z"
}

GET/strategies/{id}/performance

Returns the metrics the dashboard renders: return, volatility, Sharpe/Sortino/Calmar, drawdown, monthly/yearly returns, and benchmark-relative stats.

Query parameters

ParamDescription
benchmarkdefault "SPY"Symbol to compare against. Pass an empty string to skip.
risk_free_ratedefault 0Annualized, e.g. 0.045 for 4.5%

Response (abridged)

{
  "strategy_id": "…",
  "inception_date": "2025-01-16",
  "latest_date": "2026-04-17",
  "days": 456,
  "years": 1.25,

  "starting_equity": 1000000.0,
  "ending_equity":   1021183.79,

  "total_return": 0.0212,
  "cagr":         0.0169,
  "volatility_annualized":         0.087,
  "downside_volatility_annualized": 0.054,
  "sharpe_ratio":  1.52,
  "sortino_ratio": 2.10,
  "calmar_ratio":  0.14,

  "drawdown": {
    "max_drawdown": -0.1194,
    "max_drawdown_duration_days": 8,
    "current_drawdown": -0.0031
  },

  "monthly_returns": { "2025-01": 0.012, "2025-02": -0.003, … },
  "yearly_returns":  { "2025": 0.094, "2026": 0.022 },

  "benchmark": {
    "symbol": "SPY",
    "beta":                         1.08,
    "alpha_annualized":             0.014,
    "tracking_error_annualized":    0.031,
    "information_ratio":            0.45,
    "benchmark_total_return":       0.087
  }
}

Data Types

All timestamps are ISO-8601 UTC. All money amounts are in the strategy's base currency (USD by default). uuid refers to a 36-character UUIDv4 string.

Strategy

{
  "id":               "uuid",
  "name":             "string",
  "type":             "manual" | "live" | "cron",
  "status":           "active" | "paused" | "stopped" | "error",
  "description":      "string | null",
  "initial_equity":   number,
  "current_equity":   number,
  "is_public":        bool,
  "source_type":      "manual" | "system",
  "pricing_model":    "website_membership" | "independent_pricing",
  "price_monthly":    number,
  "price_annual":     number,
  "creator_user_id":  "uuid",
  "domain":           "string",
  "created_at":       "ISO-8601 UTC",
  "updated_at":       "ISO-8601 UTC"
}

Order

{
  "id":                "uuid",
  "strategy_id":       "uuid",
  "symbol":            "string",
  "side":              "buy" | "sell",
  "quantity":          number,
  "price":             number,
  "fees":              number,
  "order_type":        "market" | "limit" | "stop" | "stop_limit",
  "status":            "pending" | "filled" | "partial" | "cancelled",
  "executed_at":       "ISO-8601 UTC",
  "created_at":        "ISO-8601 UTC",
  "external_order_id": "string | null",
  "details":           object | null
}

Position

{
  "id":          "uuid",
  "strategy_id": "uuid",
  "symbol":      "string",
  "quantity":    number,   // negative = short
  "cost_basis":  number,   // weighted-average entry price, always positive
  "updated_at":  "ISO-8601 UTC"
}

CashEvent

{
  "id":             "uuid",
  "strategy_id":    "uuid",
  "event_type":     "deposit" | "withdraw" | "dividend" | "interest" | "fee" | "adjustment",
  "amount":         number,        // signed
  "occurred_at":    "ISO-8601 UTC",
  "external_id":    "string | null",
  "related_symbol": "string | null",
  "note":           "string | null",
  "created_at":     "ISO-8601 UTC"
}

Idempotency

Idempotency is per-strategy, per-identifier. When you send an order with an external_order_id that already exists for the strategy, BotRanks returns the existing row under duplicates and does not re-apply the effect. Same for cash_events.external_id.

This makes retries safe: send the same payload as many times as you like, you'll never double-count.

# First call — created
curl … -d '{"orders":[{"symbol":"AAPL","side":"buy","quantity":100,
                        "external_order_id":"tb-00001"}]}'
# → {"accepted":[…],"duplicates":[],"counts":{"accepted":1,"duplicates":0}}

# Same call — returns the existing row
curl … -d '{"orders":[{"symbol":"AAPL","side":"buy","quantity":100,
                        "external_order_id":"tb-00001"}]}'
# → {"accepted":[],"duplicates":[…],"counts":{"accepted":0,"duplicates":1}}

Always provide external_order_id / external_id. It's the cheapest insurance you'll ever buy.

Rate Limits

Each API key is limited to 60 requests per minute on a sliding window. Exceed it and you'll get:

HTTP/1.1 429 Too Many Requests
Content-Type: application/json

{ "detail": "Rate limit exceeded: 60 requests per 60s" }

Keys are independent — rotate to a higher throughput by adding more keys, or batch up to 500 orders per call.

Errors

Errors return a consistent JSON body:

{ "detail": "human-readable message" }
StatusWhen
400Malformed request or unresolvable symbol
401Missing or invalid Authorization header
403API key doesn't grant access to this strategy
404Strategy or resource not found
422Validation failed (including price-resolution failure)
429Rate limit exceeded
5xxServer error — retry with exponential backoff

Handling 422 on omitted price

If you submit an order without price and BotRanks can't find a bar for that symbol on executed_at's date (e.g. brand-new ticker, crypto outside our universe), you get:

{ "detail": "No market data available for FOO at 2026-04-18T14:00:00Z" }

Resubmit with an explicit price in that case.

Questions or issues? Open one at github.com/microblue/free-quant/issues.

This document is for API v1. . Breaking changes will ship under /api/v2.