← Developer blog

Schedule the Live Tennis API with Kestra

Run the Live Tennis API on a schedule with Kestra. Two open-source blueprints on the free tier: a daily fixtures pull and a windowed live-scores poll.

· · By the Live Tennis API team

You can run the Live Tennis API on a schedule with Kestra, an open-source orchestrator. No always-on server, no cron-plus-curl glue. Two ready-made Kestra blueprints do this: one pulls the fixture slate once a day, the other polls live scores on a windowed schedule. Both run on the free Live Tennis API tier.

This is the ingestion side. Fetch tennis data on a timer, reshape it, and hand a clean file to the rest of your pipeline (a warehouse load or a dashboard). Kestra gives you the trigger, retries, a reshape step, internal storage and full run history in one reviewable YAML file.

What the free tier gives you

Base URL: https://api.livetennisapi.com/api/public/v1. Every request needs the header X-API-Key: <your-key>. Grab a free key at livetennisapi.com/subscribe/free. Self-serve, no card, 30 req/min and 100 req/day.

Endpoint Returns
GET /matches?status=live every match in play right now
GET /fixtures the upcoming scheduled slate
GET /matches/{id}/score current score for one match
GET /usage your own rate/quota counters

The free tier serves the current state: the live board and the fixture slate. Completed-match history and point-by-point tape are paid tiers. Scheduled ingestion of the live state needs none of them.

Blueprint 1: pull the daily fixtures on a schedule

The pattern: a Schedule trigger fires once a day, an http.Request task calls GET /fixtures, a JSONata TransformValue reshapes the response, and a Write task persists it to internal storage. The API key lives in the LIVETENNISAPI_KEY secret, never hardcoded. Following Kestra's guidance, the auth header sits directly on the task and not in pluginDefaults.

id: tennis-fixtures-daily
namespace: company.team

inputs:
  - id: tour
    type: SELECT
    defaults: ""
    required: false
    values: ["", atp, wta, challenger, itf, juniors]

tasks:
  - id: fetch_fixtures
    type: io.kestra.plugin.core.http.Request
    uri: "https://api.livetennisapi.com/api/public/v1/fixtures{{ inputs.tour != '' ? '?tour=' ~ inputs.tour : '' }}"
    method: GET
    headers:
      X-API-Key: "{{ secret('LIVETENNISAPI_KEY') }}"
      Accept: "application/json"
    retry:
      type: exponential
      interval: PT2S
      maxAttempts: 3

  - id: build_fixtures
    type: io.kestra.plugin.transform.jsonata.TransformValue
    from: "{{ outputs.fetch_fixtures.body }}"
    expression: |
      (
        $fixtures := $exists(fixtures) ? fixtures : ($exists(data) ? data : []);
        { "count": $count($fixtures), "fixtures": $fixtures.{
            "id": id, "tour": tour, "status": status,
            "p1": players.p1.name, "p2": players.p2.name } }
      )

  - id: store_fixtures
    type: io.kestra.plugin.core.storage.Write
    content: "{{ outputs.build_fixtures.value | json }}"
    extension: ".json"

triggers:
  - id: every_morning
    type: io.kestra.plugin.core.trigger.Schedule
    cron: "0 7 * * *"

One call a day sits far inside the 100/day budget, leaving nearly the whole quota for a live-scores poll alongside it.

Blueprint 2: poll live scores during match hours only

The live board changes constantly, but you don't need to poll it 24/7.

A windowed schedule (every 15 minutes, 09:00 to 22:45) reads GET /matches?status=live, and a RemoveDuplicates-style dedup keeps only matches newly in progress. The response envelope is { "data": [ ... ] }. Each match carries players.p1.name / players.p2.name, status and a nested score object.

  - id: fetch_live
    type: io.kestra.plugin.core.http.Request
    uri: "https://api.livetennisapi.com/api/public/v1/matches?status=live"
    method: GET
    headers:
      X-API-Key: "{{ secret('LIVETENNISAPI_KEY') }}"

triggers:
  - id: match_hours
    type: io.kestra.plugin.core.trigger.Schedule
    cron: "*/15 9-22 * * *"

*/15 9-22 * * * is roughly 56 calls a day, comfortably inside 30 req/min and 100 req/day. Continuous 60-second polling (~1,440/day) does not fit a free key. For per-second freshness, use the WebSocket stream on the Ultra tier, not a tighter cron. Read GET /usage to see your remaining budget instead of guessing.

Why orchestrate this with Kestra

A cron curl writing a file has no retry policy, no reshape step, and no memory of past runs. Kestra supplies the Schedule trigger, per-task retries, a declarative JSONata reshape, internal storage to hand the result downstream, and full lineage for every run. All of it sits in one YAML file you can review and version.

Get the blueprints

Both flows are in the official Kestra Blueprints library. Import them, add your LIVETENNISAPI_KEY namespace secret, and run. Kestra itself is open-source and self-hostable. Start at kestra.io.

Links

Frequently asked questions

Can I run the Live Tennis API on a schedule without a server?

Yes. Kestra (open-source orchestrator) runs it on a timer via a Schedule trigger and an HTTP Request task. Two ready blueprints exist: a daily fixtures pull and a windowed live-scores poll, both on the free tier (30 req/min, 100 req/day). Import them from kestra.io/blueprints and add your LIVETENNISAPI_KEY secret.

Does the free tier fit a Kestra polling schedule?

Yes, if the schedule is windowed. One daily fixtures call plus a live-scores poll every 15 minutes during match hours (roughly 56 calls/day) fits inside 100 req/day. Continuous 60-second polling (~1,440/day) does not fit a free key. Use the WebSocket stream on the Ultra tier for per-second freshness.

How do I pass the API key to a Kestra HTTP task?

Store it as a Kestra namespace secret (e.g. LIVETENNISAPI_KEY) and reference it in the task's headers: X-API-Key: "{{ secret('LIVETENNISAPI_KEY') }}". Put the header directly on the io.kestra.plugin.core.http.Request task.

What endpoints do the Kestra tennis blueprints use?

GET /fixtures for the daily scheduled slate and GET /matches?status=live for the live board, against base https://api.livetennisapi.com/api/public/v1 and the auth header X-API-Key. A JSONata TransformValue reshapes the {data:[...]} envelope (players.p1.name / players.p2.name, status) and a Write task stores the result for downstream tasks.

Built with the Live Tennis API, which serves real-time scores, players, odds and model win-probability for ATP, WTA, Challenger and ITF.

API reference SDKs on GitHub Plans from $9.99/mo