Skip to main content

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. whoami succeeds; 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 whoami still shows status: "pending" after your onboarding call, tell us and we'll switch it on.