CLI Usage
Access FormFav racing data from your terminal. Use the reference Python CLI below, or point your AI agent at the OpenAPI spec to generate a client in any language.
Build Your Own Client
The fastest way to get a CLI, Python client, Node script, or shell aliases that fit your exact workflow is to point your AI agent at the OpenAPI spec and ask it to build one:
I need a CLI tool to query the FormFav horse racing API.
Here's the OpenAPI spec: https://api.formfav.com/v1/openapi.json
Auth is via X-API-Key header, key is in FORMFAV_API_KEY env var.
Build me a [Python CLI / Node script / shell wrapper] that covers
meetings, race form, predictions, and jockey stats.Works with any language
Quick Start
If you'd rather start with something ready-made, the reference CLI below wraps every FormFav endpoint in a single Python file. Two dependencies, no config files.
# Install dependencies
pip install httpx typer
# Set your API key
export FORMFAV_API_KEY=your_api_key_here
# Save formfav.py (see below), then:
python formfav.py meetings 2026-04-03
python formfav.py race-card 2026-04-03 flemington 5
python formfav.py predictions 2026-04-03 randwick 3
# Optional: create an alias for convenience
alias formfav="python /path/to/formfav.py"Usage Examples
List today's meetings
python formfav.py meetings 2026-04-03
python formfav.py meetings 2026-04-03 --race-code harnessGet race form
python formfav.py race-card 2026-04-03 flemington 5Predictions and stats (Pro)
python formfav.py predictions 2026-04-03 randwick 3
python formfav.py jockey-stats "james mcdonald" --window 90
python formfav.py trainer-stats "chris waller"
python formfav.py track-bias flemington --distance 1200-1600
python formfav.py search-runner "via sistina"
python formfav.py runner-profile 12345Pipe-friendly JSON output
# Extract runner names with jq
python formfav.py race-card 2026-04-03 flemington 5 | jq '.runners[] | .name'
# Feed into another script
python formfav.py predictions 2026-04-03 randwick 3 | python my_model.pyReference Implementation
A single-file Python CLI covering every FormFav endpoint. Copy it, modify it, or use it as a starting point for your AI agent to extend.
#!/usr/bin/env python3
"""
FormFav CLI — horse racing data from your terminal.
Usage:
export FORMFAV_API_KEY=your_api_key
python formfav.py meetings 2026-04-03
python formfav.py race-card 2026-04-03 flemington 5
python formfav.py predictions 2026-04-03 randwick 3
Output is JSON by default (pipe-friendly).
Get an API key at https://formfav.com/pricing
"""
import json
import os
import sys
import httpx
import typer
app = typer.Typer(
name="formfav",
help="Horse racing data — form, predictions, and statistics.",
no_args_is_help=True,
)
API_BASE = os.environ.get("FORMFAV_API_URL", "https://api.formfav.com/v1")
API_KEY = os.environ.get("FORMFAV_API_KEY", "")
def _request(method: str, path: str, **kwargs) -> dict | list:
if not API_KEY:
typer.echo(
"Error: FORMFAV_API_KEY not set. Get a key at formfav.com/pricing",
err=True,
)
raise typer.Exit(1)
try:
with httpx.Client(
base_url=API_BASE,
headers={"X-API-Key": API_KEY},
timeout=30.0,
) as client:
resp = client.request(method, path, **kwargs)
except httpx.ConnectError:
typer.echo(
f"Error: Cannot connect to FormFav API at {API_BASE}", err=True
)
raise typer.Exit(1)
except httpx.TimeoutException:
typer.echo("Error: Request timed out.", err=True)
raise typer.Exit(1)
if resp.status_code == 200:
return resp.json()
try:
detail = resp.json().get("detail", resp.text)
except Exception:
detail = resp.text
messages = {
401: "Invalid or missing API key. Check FORMFAV_API_KEY.",
403: f"Pro subscription required. {detail}",
404: f"Not found: {detail}",
400: f"Bad request: {detail}",
429: "Rate limit exceeded. Wait before making more requests.",
}
typer.echo(
f"Error: {messages.get(resp.status_code, f'API error ({resp.status_code}): {detail}')}",
err=True,
)
raise typer.Exit(1)
def _output(data: dict | list):
typer.echo(json.dumps(data, indent=2))
@app.command()
def meetings(
date: str = typer.Argument(help="Race date (YYYY-MM-DD)"),
race_code: str = typer.Option("gallops", help="gallops, harness, or greyhounds"),
):
"""List race meetings for a date."""
_output(
_request("GET", "/form/meetings", params={"date": date, "race_code": race_code}),
json_output,
)
@app.command("race-card")
def race_card(
date: str = typer.Argument(help="Race date (YYYY-MM-DD)"),
track: str = typer.Argument(help="Track slug (e.g. flemington)"),
race: int = typer.Argument(help="Race number"),
race_code: str = typer.Option("gallops", help="gallops, harness, or greyhounds"),
country: str = typer.Option("au", help="Country code, e.g. au, nz, hk, gb, us, jp, sg, fr, ie"),
):
"""Get the full race form for a race."""
_output(
_request("GET", "/form", params={
"date": date, "track": track, "race": race,
"race_code": race_code, "country": country,
}),
json_output,
)
@app.command()
def predictions(
date: str = typer.Argument(help="Race date (YYYY-MM-DD)"),
track: str = typer.Argument(help="Track slug"),
race: int = typer.Argument(help="Race number"),
race_code: str = typer.Option("gallops", help="gallops, harness, or greyhounds"),
country: str = typer.Option("au", help="Country code, e.g. au, nz, hk, gb, us, jp, sg, fr, ie"),
):
"""Get ML win/place probability predictions (Pro)."""
_output(
_request("GET", "/predictions", params={
"date": date, "track": track, "race": race,
"race_code": race_code, "country": country,
}),
json_output,
)
@app.command("jockey-stats")
def jockey_stats(
name: str = typer.Argument(help="Jockey name (partial match OK)"),
race_code: str = typer.Option("gallops", help="gallops or harness"),
window: int = typer.Option(None, help="Rolling window in days (14-365)"),
):
"""Get jockey performance stats by track (Pro)."""
params = {"race_code": race_code}
if window is not None:
params["window"] = window
_output(_request("GET", f"/stats/jockey/{name}", params=params))
@app.command("trainer-stats")
def trainer_stats(
name: str = typer.Argument(help="Trainer name (partial match OK)"),
race_code: str = typer.Option("gallops", help="gallops, harness, or greyhounds"),
window: int = typer.Option(None, help="Rolling window in days (14-365)"),
):
"""Get trainer performance stats by track (Pro)."""
params = {"race_code": race_code}
if window is not None:
params["window"] = window
_output(_request("GET", f"/stats/trainer/{name}", params=params))
@app.command("track-bias")
def track_bias(
track: str = typer.Argument(help="Track slug (e.g. flemington)"),
race_code: str = typer.Option("gallops", help="gallops, harness, or greyhounds"),
country: str = typer.Option("au", help="Country code, e.g. au, nz, hk, gb, us, jp, sg, fr, ie"),
window: int = typer.Option(None, help="Rolling window in days (14-365)"),
condition: str = typer.Option(None, help="Track condition: Good, Soft, Heavy"),
distance: str = typer.Option(None, help="Metres: 1200 or a range 1200-1600"),
):
"""Get barrier/box bias for a venue (Pro)."""
params = {"race_code": race_code, "country": country}
if window is not None:
params["window"] = window
if condition is not None:
params["condition"] = condition
if distance is not None:
params["distance"] = distance
_output(_request("GET", f"/stats/track-bias/{track}", params=params))
@app.command("search-runner")
def search_runner(
query: str = typer.Argument(help="Runner name (min 3 characters)"),
race_code: str = typer.Option("gallops", help="gallops, harness, or greyhounds"),
country: str = typer.Option(None, help="Country filter, e.g. au, nz, hk, gb, us, jp, sg, fr, ie"),
):
"""Search for a horse or greyhound by name (Pro)."""
params = {"q": query, "race_code": race_code}
if country is not None:
params["country"] = country
_output(_request("GET", "/stats/runner/search", params=params))
@app.command("runner-profile")
def runner_profile(
runner_id: int = typer.Argument(help="Runner ID (from search-runner)"),
window: int = typer.Option(None, help="Rolling window in days (14-365)"),
):
"""Get full career statistics for a runner (Pro)."""
params = {}
if window is not None:
params["window"] = window
_output(_request("GET", f"/stats/runner/{runner_id}", params=params))
if __name__ == "__main__":
app()Available Commands
Free-tier commands (meetings and race-card) return basic runner data and full form history. With a Pro key, these same commands add form badges, speed maps and full statistical breakdowns — plus all Pro-only commands become available. See Subscription Tiers for full details.
| Command | What it does | Tier |
|---|---|---|
meetings | List race meetings for a date | Free |
race-card | Full race form for a specific race | Free |
predictions | ML win/place probabilities | Pro |
jockey-stats | Jockey performance by track | Pro |
trainer-stats | Trainer performance by track | Pro |
track-bias | Barrier/box bias, per distance band or --distance | Pro |
search-runner | Find a horse or greyhound by name | Pro |
runner-profile | Full career statistics for a runner | Pro |
Output is JSON by default — pipe-friendly and machine-readable. All commands accept --help for full option details.
Next Steps
- MCP Server — connect FormFav to ChatGPT, Claude and Cursor with zero code
- AI Agents guide — Python and JavaScript agent loop examples
- Quickstart — make your first API call
- Subscription Tiers — compare Free, Pro, and Enterprise features