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
- Kestra: https://kestra.io
- Kestra Blueprints: https://kestra.io/blueprints
- Live Tennis API docs and free key: https://livetennisapi.com
- HTTP Request task: https://kestra.io/plugins/plugin-core/tasks/http/io.kestra.plugin.core.http.request
- JSONata TransformValue: https://kestra.io/plugins/plugin-transform/tasks/jsonata/io.kestra.plugin.transform.jsonata.transformvalue