Every result, read before it lands. Judged after.
Sign in

API for developers Simulated

Everything you can do with scanners, strategies, backtests, sweeps and paper accounts on this site, from your own code - and events sent to your server as they happen. JSON over HTTPS, one key per program. Results are simulated with virtual money; nothing is sent to a broker.

Start

  1. Make a key on your API page. Choose what it may do: read, run (start scanner runs, backtests and tests; manage webhooks), trade (simulated paper orders). The key is shown once.
  2. Send it with every call: Authorization: Bearer hotw_…
  3. Everything answers JSON with "ok": true, or "ok": false and an error with a code and a message.
curl -s https://finance.brandingbrandz.com/api/v1/me \
  -H "Authorization: Bearer $HOTW_KEY"

curl -s -X POST https://finance.brandingbrandz.com/api/v1/backtests \
  -H "Authorization: Bearer $HOTW_KEY" -H "Content-Type: application/json" \
  -d '{"version_id": 12, "from": "2023-01-02", "to": "2026-06-30"}'
import os, time, requests
H = {"Authorization": "Bearer " + os.environ["HOTW_KEY"]}
job = requests.post("https://finance.brandingbrandz.com/api/v1/research", headers=H, json={
    "kind": "sweep", "version_id": 12, "from": "2022-01-03", "to": "2026-06-30",
    "axes": [{"path": "stop.value", "from": 1, "to": 3, "steps": 5}]}).json()
while True:
    r = requests.get("https://finance.brandingbrandz.com/api/v1/research/%d" % job["id"], headers=H).json()
    if r["status"] in ("done", "failed"):
        break
    time.sleep(20)
print(r["result"]["verdict"])

Endpoints

all under https://finance.brandingbrandz.com/api/v1
MethodPathNeedsWhat it does
GET/mereadYour account, plan, this key and every plan limit.
GET/regimes/currentreadThe current NIFTY 50 market regime.
GET/scannersreadYour scanners and the example scanners.
GET/scanners/{id}readOne scanner with its versions and recent runs.
POST/scanners/{id}/runrunRun its latest version (or {"version_id"}) on the latest close; optional {"asof": "YYYY-MM-DD"}.
GET/scanner-runs/{id}readOne run: every match and why it matched.
GET/strategiesreadYour strategies and the examples.
GET/strategies/{id}readOne strategy with its versions.
GET/strategy-versions/{id}/knobsreadThe numbers in a version a sweep can vary, with their paths.
GET/backtestsreadYour backtests.
POST/backtestsrunQueue one: {"version_id", "from", "to", "participation_pct"?, "slippage"?}.
GET/backtests/{id}readStatus, metrics, checks.
GET/backtests/{id}/tradesreadThe trade ledger with each trade's evidence.
GET/researchreadYour sweeps and walk-forward tests.
POST/researchrunQueue one: {"kind": "sweep"|"walkforward", "version_id", "from", "to", "axes": [{"path", "from", "to", "steps"}], "objective"?, "train_months"?, "test_months"?, "anchored"?}.
GET/research/{id}readStatus and the whole result.
GET/paper/accountsreadYour paper accounts with balances.
GET/paper/accounts/{id}readPositions, orders, fills, ledger, performance.
POST/paper/accounts/{id}/orderstradeA simulated order: {"symbol", "side", "qty", "order_type"?, "limit_price"?, "stop_price"?, "tif"?, "trail_pct"?, "bracket_stop"?, "bracket_target"?}. Send an Idempotency-Key header to make retries safe.
POST/paper/orders/{id}/canceltradeCancel an open simulated order.
GET/webhooksreadYour webhooks and their last deliveries.
POST/webhooksrunAdd one: {"url", "events": ["paper.fill", ...] or "*"}. The answer carries its signing secret, once.
DELETE/webhooks/{id}runRemove one.
POST/webhooks/{id}/testrunQueue a ping (and switch a stopped webhook back on).

Limits

PlanKeysCalls a dayWebhooks
Free15001
Plus35,0005
Pro1050,00020

And at most 60 calls a minute per key. Every answer carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (the day's reset, Unix time). A refused call answers 429, with Retry-After for the minute limit. Backtests, tests and scanner runs also count against the plan's own daily limits, exactly as on the site.

Errors: 400 invalid input · 401 no key, or an unknown or revoked one · 403 the key lacks the scope · 404 not found or not yours · 429 a limit · 503 try again shortly.

No price or index feed is offered: exchange data is not ours to redistribute. What the API returns is your own work - your runs, your backtests' results, your accounts.

Webhooks

Add a webhook on your API page or with POST /webhooks. Each event is a POST of JSON to your address:

{"id": 4812, "event": "paper.fill", "created": "2026-09-26 04:10:02",
 "data": {"account_id": 3, "order_id": 91, "symbol": "INFY", "side": "buy", "qty": 20,
          "price": "1512.40", "charges": "4.87", "simulated": true, "url": "…"}}
EventSent when
backtest.donea backtest finished or failed
research.donea sweep or walk-forward finished or failed
paper.filla simulated order filled
paper.ordera simulated order was rejected, cancelled, expired or its stop triggered
paper.alerta paper account alert
alert.patternone of your pattern alerts fired
regime.changedNIFTY 50 moved into a different market regime
pinga test you asked for

Answer with any 2xx within 5 seconds. Anything else is retried after 1, 5, 30, 120, 360 and 720 minutes; the delivery id stays the same, so use it to ignore repeats. After 20 failed deliveries in a row a webhook is stopped; "Send a test" switches it back on.

Check the signature before trusting a delivery. Headers: X-Hotw-Event, X-Hotw-Delivery, X-Hotw-Timestamp, X-Hotw-Signature: sha256=… - the HMAC-SHA256 of timestamp + "." + raw body with your webhook's secret. Refuse timestamps more than five minutes old.

import hashlib, hmac, time

def verified(secret: str, headers, raw_body: bytes) -> bool:
    ts = headers["X-Hotw-Timestamp"]
    if abs(time.time() - int(ts)) > 300:
        return False
    want = "sha256=" + hmac.new(secret.encode(), ts.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(want, headers["X-Hotw-Signature"])

Addresses must be https:// on port 443 at a public host; private and internal addresses are refused, and redirects are not followed.