← Developer Blog

Schedule the Live Tennis API with Kestra

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

· By the Live Tennis API team

You can run the Live Tennis API on a schedule — no always-on server, no cron-plus-curl glue — with Kestra, an open-source orchestrator. Two ready-made Kestra blueprints do exactly this: one pulls the daily 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, a notification, 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, and (following Kestra's guidance) the auth header sits directly on the task rather than 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–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, that's the WebSocket stream on the Ultra tier, not a tighter cron. Always read GET /usage to see your remaining budget rather than 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 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; never hardcode the key in the flow.

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 with X-API-Key auth. 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 — 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