FormFav LogoFormFav

    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:

    Prompt for Claude, GPT, or any AI assistant
    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.

    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 and run
    # 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

    bash
    python formfav.py meetings 2026-04-03
    python formfav.py meetings 2026-04-03 --race-code harness

    Get race form

    bash
    python formfav.py race-card 2026-04-03 flemington 5

    Predictions and stats (Pro)

    bash
    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 12345

    Pipe-friendly JSON output

    bash
    # 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.py

    Reference 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.

    formfav.py
    #!/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.

    CommandWhat it doesTier
    meetingsList race meetings for a dateFree
    race-cardFull race form for a specific raceFree
    predictionsML win/place probabilitiesPro
    jockey-statsJockey performance by trackPro
    trainer-statsTrainer performance by trackPro
    track-biasBarrier/box bias, per distance band or --distancePro
    search-runnerFind a horse or greyhound by namePro
    runner-profileFull career statistics for a runnerPro

    Output is JSON by default — pipe-friendly and machine-readable. All commands accept --help for full option details.

    Next Steps