Your first 10 minutes
You just subscribed and received a signals key. This page takes you from that key to a
validated, backfilled feed in about ten minutes — store → verify → pull → backfill → validate →
support. Every step has a curl line and a copy-paste Python block (requests + pandas).
:::note This is the keyed path, not the free quickstart
The Quickstart uses the free, keyless historical API
(api.btcalpha.com.au). This page is for the paid live-signals API at
https://signals.btcalpha.com.au, which is gated by your X-API-Key. Different host, different
product.
:::
Base URL for everything below:
https://signals.btcalpha.com.au
1 · Store your key safely
Your key arrives as a one-time link in your welcome pack — open it once, copy the key, and the link expires. Treat it like a password: it authorises the live signal feed.
- Never commit it, inline it in a URL, or ship it in a browser bundle — it's a server-side
header only (
X-API-Key). - Put it in an environment variable for local work, or a secrets manager (AWS Secrets Manager, GCP Secret Manager, Vault, Doppler) for anything deployed.
- If the one-time link is ever exposed, or you lose the key, ask us to rotate it (see step 6).
Export it once in your shell — every curl below reads it from the environment, so your key never
appears on the command line:
export BTCALPHA_API_KEY="btca_live_xxxxxxxxxxxxxxxxxxxxxxxx"
In Python, read it from the environment (or your secrets manager) — never hard-code it:
import os, requests
BASE = "https://signals.btcalpha.com.au"
HEADERS = {"X-API-Key": os.environ["BTCALPHA_API_KEY"]} # KeyError early if it isn't set
2 · Verify the key — GET /v1/whoami
whoami is the safe first call: it works for pending and active keys, so you can confirm the
key is good the moment you receive it — even before your account is switched on.
curl https://signals.btcalpha.com.au/v1/whoami \
-H "X-API-Key: $BTCALPHA_API_KEY"
{
"client_id": "cli_7f3a9c2e",
"tier": "signals",
"status": "pending",
"entitlements": { "aliases": ["helios"], "gaia": false },
"key_id": "key_01HZX8..."
}
r = requests.get(f"{BASE}/v1/whoami", headers=HEADERS, timeout=10)
r.raise_for_status() # 401 here means a bad / revoked key
me = r.json()
print("status :", me["status"]) # "pending" or "active"
print("tier :", me["tier"])
print("entitlements:", me["entitlements"]) # {"aliases": [...], "gaia": <bool>}
Read the status field:
pending— the key is valid, but your account isn't switched on yet.whoamisucceeds; the actual pull endpoints don't yet.active— fully switched on. Everything below works.
:::warning 403 account_not_active before activation
While status is pending, whoami returns 200, but the pull endpoints
(/v1/signals/latest, /v1/signals/history, /v1/signals/schema) return 403 with code
account_not_active. That is expected — it means "valid key, account not switched on yet," not a
bad key. A 401 on any endpoint is different: it means the key itself is missing, wrong, or
revoked. Once whoami reports status: "active", re-run steps 3–5.
:::
3 · Your first pull — GET /v1/signals/latest
The most recent signal — a quick "am I current?" check. Every response is the canonical
{ data, meta, links } envelope; data is a single
signal object.
curl https://signals.btcalpha.com.au/v1/signals/latest \
-H "X-API-Key: $BTCALPHA_API_KEY"
{
"data": {
"schema_version": "1.0", "signal_id": "sig_helios_000421", "seq": 421,
"alias": "helios", "mode": "LIVE", "is_test": false,
"action": "long", "signal_price": 64000.0, "stop": 61800.0,
"base_risk_pct": 0.025, "timestamp": "2026-06-28T12:00:00Z"
},
"meta": { "generated_utc": "2026-06-28T12:00:01Z", "timing_ms": 1.2 },
"links": { "self": "/v1/signals/latest", "history": "/v1/signals/history?since=0" }
}
r = requests.get(f"{BASE}/v1/signals/latest", headers=HEADERS, timeout=10)
r.raise_for_status()
sig = r.json()["data"] # the signal lives under "data"
if sig["is_test"]:
print("test fire — do nothing") # HARD skip, never route to an OMS
else:
print(sig["signal_id"], sig["action"], sig["signal_price"])
:::danger Never act on is_test: true
is_test: true is an occasional connectivity / test fire — never a tradeable signal, and it can
carry any action. Gate on it first, before sizing or routing. A real, actionable signal is
always is_test: false and mode: "LIVE". See the payload.
:::
4 · Backfill history — GET /v1/signals/history?since=0
since=0 returns every signal from the start (ascending by seq) — your one-shot backfill.
For incremental catch-up later, pass the highest seq you've already processed (the
pull backstop pattern).
# JSON (enveloped)
curl "https://signals.btcalpha.com.au/v1/signals/history?since=0" \
-H "X-API-Key: $BTCALPHA_API_KEY"
# CSV (raw tabular body — the one non-enveloped response)
curl "https://signals.btcalpha.com.au/v1/signals/history?since=0&format=csv" \
-H "X-API-Key: $BTCALPHA_API_KEY"
Load it straight into a pandas DataFrame — either format:
import io
import pandas as pd
# Option A — JSON: rows live under "data"
r = requests.get(f"{BASE}/v1/signals/history",
params={"since": 0}, headers=HEADERS, timeout=30)
r.raise_for_status()
df = pd.DataFrame(r.json()["data"])
# Option B — CSV: raw body, not the envelope
r = requests.get(f"{BASE}/v1/signals/history",
params={"since": 0, "format": "csv"}, headers=HEADERS, timeout=30)
r.raise_for_status()
df = pd.read_csv(io.StringIO(r.text))
df = df.sort_values("seq")
print(f"{len(df)} signals, seq {df['seq'].min()}–{df['seq'].max()}")
last_seq = int(df["seq"].max()) # persist this; pass it as `since` next time
Drop test fires before you analyse or trade anything derived from the history:
live = df[~df["is_test"]] # same HARD rule as step 3
5 · Validate — GET /v1/signals/schema
Pull the JSON Schema for the signal object and validate against it, so a contract change surfaces in
your tests rather than in production. (pip install jsonschema.)
curl https://signals.btcalpha.com.au/v1/signals/schema \
-H "X-API-Key: $BTCALPHA_API_KEY"
import jsonschema
resp = requests.get(f"{BASE}/v1/signals/schema", headers=HEADERS, timeout=10).json()
schema = resp.get("data", resp) # schema document (unwrap the envelope if present)
jsonschema.validate(instance=sig, schema=schema) # raises ValidationError on mismatch
print("signal validates against v1.0 schema ✓")
Wire this into CI against a captured sample: if the feed ever drifts from the schema you pinned to,
the check fails loudly instead of your OMS silently mis-reading a field. Pin schema_version
("1.0" today) and treat a bump as a review trigger — see versioning.
6 · Support & rotation
- Reach a human — reply directly to your welcome-pack email; it goes to the operator. General enquiries: contact.
- Key rotation — we rotate keys on request (delivered the same one-time-link way). Ask us any time, and immediately if a key is exposed or a team member leaves. Your old key stays live until you confirm the new one works, so there's no gap in the feed.
- Activation — if
whoamistill showsstatus: "pending"after your onboarding call, tell us and we'll switch it on.
Every field of the v1.0 signal object, with types and meaning.
Webhooks + the pull backstop, idempotency, and tracking last_seq.
Turn a signal into a position size on your own capital.
The two APIs, the branded hosts, and the CORS allowlist.