BP_DOCS_NAME_HTML TV Fixtures
API reference · OpenAPI · Plain Markdown
What you get
An API for fixtures, kickoff times, competitions and TV channels across every available sport.
There is no viewing app, video or right to rebroadcast. The backoffice manages keys and billing.
The api plan has a seven-day free trial with no payment details. Prices are sent by invoice or shown by the reselling partner.
It includes every language available, a rolling window from yesterday through ten days ahead,
60 requests per minute, no streams, and up to three live keys. Check /tv-fixtures/v1/plans
for the current scope and /tv-fixtures/v1/status for data freshness before importing.
Availability of a channel listing does not guarantee reception in your region.
The primary API origin is https://dev.feed.sportlive.hr, with product prefix /tv-fixtures/v1.
The old https://dev.feed.sportlive.hr/tv-fixtures/v1 is a deprecated alias, served without redirects.
On a partner account use the API origin returned with your credentials.
BASE=https://dev.feed.sportlive.hr
curl "$BASE/tv-fixtures/v1/plans"
curl "$BASE/tv-fixtures/v1/status"API reference: GET /tv-fixtures/v1/plans GET /tv-fixtures/v1/status
Get a key
You can sign up on the offer page or use HTTP on the
creator's store. For the following two calls set BASE=https://store.dev.tantu.stream.
First find the offer whose slug is tv-fixtures. Read its terms.url, price, plan,
trial length and availability. Set TERMS_VERSION to its exact terms.version;
only accept terms you have read. Replace the example email with your own.
BASE=https://store.dev.tantu.stream
curl "$BASE/api/v1/offers"
curl "$BASE/api/v1/offers/tv-fixtures/signups" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: tv-fixtures-signup-20261021-01" \
-d '{"email":"[email protected]","termsAccepted":true,"termsVersion":"'$TERMS_VERSION'"}'API reference: GET /api/v1/offers POST /api/v1/offers/{slug}/signups
Keep the same Idempotency-Key and body when retrying this signup. Use a fresh key for a
new signup. Do not fill the optional website field. When next.type is check_inbox,
confirm your email first and follow the returned next step. A checkout next step needs
payment in the browser; thanks has no further action. Do not assume every offer starts a trial.
For next.type=welcome, request the returned next.url with Accept: application/json.
The following URL illustrates its shape: set HANDOFF to the last segment of that URL,
and keep its exact host, slug and path. A handoff expires after 30 minutes.
The JSON answer has welcome.state and welcome.revealUrl. POST to that exact reveal URL
with the handoff in the body. The example shows the same creator store host.
curl "$BASE/o/tv-fixtures/start/$HANDOFF" -H "Accept: application/json"
curl "$BASE/o/tv-fixtures/welcome/reveal" \
-H "Content-Type: application/json" -H "Accept: application/json" \
-d '{"handoff":"'$HANDOFF'"}'API reference: GET /o/{slug}/start/{handoff} POST /o/{slug}/welcome/reveal
Save login, password, apiKey, apiBaseUrl and docsUrl securely: credentials are revealed
once. Do not log this response or put the key in source control. A consumed handoff cannot
reveal credentials again. Partner storefronts use the browser handoff; credentials are
revealed on the creator's page, never through a partner frontend.
First call
Set BASE to the origin of the returned apiBaseUrl (normally https://dev.feed.sportlive.hr),
removing a trailing /tv-fixtures product path if present. Set API_KEY to the revealed
apiKey in your local environment. The examples below append the product path themselves.
BASE=https://dev.feed.sportlive.hr
curl "$BASE/tv-fixtures/v1/me" -H "X-API-Key: $API_KEY"API reference: GET /tv-fixtures/v1/me
Then read one fixture:
curl "$BASE/tv-fixtures/v1/fixtures?limit=1" -H "X-API-Key: $API_KEY"API reference: GET /tv-fixtures/v1/fixtures
The answer includes tenant, product, plan, status, trialEndsAt, payUrl, limits
and scope. Use the returned limits rather than assuming the plan will never change.
The keyless /tv-fixtures/v1 index links to this guide, OpenAPI, offer and status.
Fixtures
curl "$BASE/tv-fixtures/v1/fixtures?limit=100&sport=Soccer&lang=de" \
-H "X-API-Key: $API_KEY"API reference: GET /tv-fixtures/v1/fixtures
fixtures is an array; nextCursor is a string or null on the last page. Fixtures are
ordered by kickoff then stable id. Pass the previous nextCursor as cursor, URL-encoded,
with identical filters. Page size is 1..2000 (default 500). Treat the cursor as opaque.
from and to are inclusive kickoff instants; use ISO-8601 UTC values ending in Z.
updatedSince is inclusive and refers to updatedAt, not kickoff. sport is the original
sport label, case-insensitive, and does not follow lang.
lang selects the single names: requested language, else English, else German, else any
known language. homeNames, awayNames, sportNames, competition.names and channel
names retain every known translation. Never join by a translated name.
For incremental sync, fix from and to for an entire paginated pass, keep the prior
watermark unchanged while paging, and upsert each fixture by id. Only after all pages
succeed persist the largest updatedAt seen. Begin the next pass with that inclusive
watermark, so repeats at the boundary are expected. On failure retry the same pass;
never advance the saved watermark after a partial import. Delete locally on status=removed.
Removed fixtures remain available for 24 hours. If offline longer, rebuild the rolling
window; expire local fixtures outside it, including when the window advances.
curl "$BASE/tv-fixtures/v1/fixtures?updatedSince=2026-10-21T00:00:00Z&limit=100" \
-H "X-API-Key: $API_KEY"API reference: GET /tv-fixtures/v1/fixtures
This Python standard-library example replaces the separate demo client. Set FROM and TO
to a fixed window inside the current plan scope, and optional UPDATED_SINCE to your last
completed watermark. It prints pages; replace printing with durable upserts before saving
the watermark. URL encoding also handles opaque cursors safely.
import json
import os
from urllib.parse import urlencode
from urllib.request import Request, urlopen
from datetime import datetime
base = os.environ['BASE'].rstrip('/')
params = {'from': os.environ['FROM'], 'to': os.environ['TO'], 'limit': 500, 'lang': 'en'}
watermark = os.environ.get('UPDATED_SINCE')
if watermark:
params['updatedSince'] = watermark
while True:
url = base + '/tv-fixtures/v1/fixtures?' + urlencode(params)
request = Request(url, headers={'X-API-Key': os.environ['API_KEY']})
with urlopen(request, timeout=30) as response:
page = json.load(response)
for fixture in page['fixtures']:
print(fixture)
# Compare parsed instants if your stored watermark uses a different precision.
stamp = fixture['updatedAt']
if watermark is None or datetime.fromisoformat(stamp) > datetime.fromisoformat(watermark):
watermark = stamp
if page['nextCursor'] is None:
break
params['cursor'] = page['nextCursor']
print('Completed watermark:', watermark)API reference: GET /tv-fixtures/v1/fixtures
Channels
curl "$BASE/tv-fixtures/v1/channels?lang=en" -H "X-API-Key: $API_KEY"API reference: GET /tv-fixtures/v1/channels
channels contains channel names, language maps, regions, types, satellite positions and
receiver references where known. A nullable receiver field is unknown, not an empty string.
Use each fixture's channels for its actual listings; the directory is not a fixture filter.
Ids
Ids are reference data for joining your schedule. A missing id never removes a fixture.
Every fixture has an id: the Betradar match id (sr:match:61301157) when it is known, else
our own id (tvf:fixture:1042). Both stay the same for that fixture; use it as your local key.
Never infer an id from a name or fabricate one.
Every other id is in ids, one map per subject: the match, the home team, the away team and the
competition. Each map is {system: id}. A system is in a map only when the id is known; it is
never null. The keyless GET /tv-fixtures/v1/id-systems lists the systems served now, what they
have ids of, their format, an example and the sports that have ids in them.
{"ids": {
"match": {"betradar": "sr:match:61301157", "soccerdonna": "168515"},
"home": {"betradar": "sr:competitor:42", "soccerdonna": "2061"},
"away": {"betradar": "sr:competitor:30", "soccerdonna": "1150"},
"competition": {"betradar": "sr:tournament:17", "soccerdonna": "WMQR"}
}}| System | Format | Coverage |
|---|---|---|
betradar | sr:match:<n>, sr:competitor:<n>, sr:tournament:<n> | Match, team and competition ids where known across sports |
soccerdonna | Bare: digits (match, team), a code (competition) | Some women's football matches, teams and competitions |
GET /tv-fixtures/v1/id-systems is the authority: a system it does not list is not served, and
other systems may appear there later. Do not assume coverage for every fixture or subject.
An id can arrive after the fixture: the fixture's updatedAt moves, so an updatedSince poll
gets it. An id can also be corrected or removed the same way. These labels do not grant rights
to third-party data.
curl "$BASE/tv-fixtures/v1/id-systems"
curl "$BASE/tv-fixtures/v1/fixtures/by-id/betradar/sr:match:61301157" -H "X-API-Key: $API_KEY"
curl "$BASE/tv-fixtures/v1/fixtures?system=betradar&team=sr:competitor:42" -H "X-API-Key: $API_KEY"API reference: GET /tv-fixtures/v1/id-systems GET /tv-fixtures/v1/fixtures/by-id/{system}/{id} GET /tv-fixtures/v1/fixtures
by-id answers one fixture, or 404 fixture_not_found. system with team (home or away)
and/or competition keeps the fixtures of that team or competition. To look up many at once,
POST /tv-fixtures/v1/fixtures/lookup takes up to 500 {system, id} pairs; the answer's
fixtures is aligned with them, null where no fixture has the id:
curl "$BASE/tv-fixtures/v1/fixtures/lookup" -H "X-API-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{"ids":[{"system":"betradar","id":"sr:match:61301157"}]}'API reference: POST /tv-fixtures/v1/fixtures/lookup
Limits and Retry-After
The plan permits 60 requests/minute shared by your tenant's keys, zero streams and three live keys.
On HTTP 429 read Retry-After (seconds) and retryAfterSeconds, pause at least that long,
then retry with backoff and jitter. A new key does not reset the quota. Bound retries for
503 responses, respecting Retry-After when supplied. There is no streaming endpoint.
Keys over HTTP
Use the API origin with /account/v1; this path has no product prefix. Lists contain metadata
and current, never plaintext. Create and rotate return plaintext once; securely replace
your application's key before discarding that response. Retrying with the same idempotency key
within 24 hours returns the same result. Use a different idempotency key for a different action.
Set KEY_ID to an id from the key list. The delete example is for an unused key, never your last key.
curl "$BASE/account/v1/keys" -H "X-API-Key: $API_KEY"
curl -X POST "$BASE/account/v1/keys/$KEY_ID/rotate" \
-H "X-API-Key: $API_KEY" -H "Idempotency-Key: rotate-key-20261021-01"API reference: GET /account/v1/keys POST /account/v1/keys/{id}/rotate
curl -X POST "$BASE/account/v1/keys" \
-H "X-API-Key: $API_KEY" -H "Idempotency-Key: create-key-20261021-01"
curl -X DELETE "$BASE/account/v1/keys/$KEY_ID" -H "X-API-Key: $API_KEY"API reference: GET /account/v1/keys DELETE /account/v1/keys/{id}
Rotation leaves the old key valid for 24 hours (expiresAt), then it stops working.
An expiring rotated key does not count against the three live keys. Revocation takes effect
on the next request. The last live key cannot be revoked. Account home provides the same controls.
Errors
Product and account errors use {error, code, message, next}; error equals code.
next is an array of actions with an optional href. Follow a supplied URL only after
checking its destination. Product parameter errors also name field where applicable.
For example, limit=0 is outside 1..2000 and answers 400 invalid_parameter with field: limit:
curl "$BASE/tv-fixtures/v1/fixtures?limit=0" -H "X-API-Key: $API_KEY"API reference: GET /tv-fixtures/v1/fixtures
code | Meaning | Fix | next action |
|---|---|---|---|
invalid_key | Missing, expired or invalid key | Supply a valid key | get_key |
invalid_parameter | Invalid query value or lookup body (field, e.g. system, ids[3].system) | Fix field and retry | Inspect next |
fixture_not_found | No fixture served has this id in this system | Check the system and the subject: a team's id is not a match's | None |
invalid_cursor | Cursor cannot be read | Restart pagination with identical filters | Inspect next |
range_too_large | Kickoff range exceeds scope | Narrow from and to | Inspect next |
payment_required | Access needs payment | Open payUrl or the provided link | pay |
suspended | Account suspended | Contact support | contact_support |
not_entitled | Product not on this account | Contact support | contact_support |
rate_limited | Request quota reached | Wait for Retry-After | retry_after |
unavailable | Service temporarily unavailable | Retry with bounded backoff | retry_after |
not_a_tenant | Calling identity is not a tenant | Use a tenant key | None |
permission_required | Key action not permitted | Ask the account owner for permission | contact_support |
tenant_inactive | Account key changes disabled | Contact support | contact_support |
key_not_found | No such key on your account | Refresh key metadata | list_keys |
key_limit | Maximum live keys reached | Revoke an unused key | revoke_key |
last_key | Cannot revoke the last live key | Create a replacement first | create_key |
key_rotating | Key already has a replacement | Use its replacement or create another | list_keys |
key_revoked | Key already revoked | Create a replacement | create_key |
idempotency_key_required | Missing or invalid idempotency key | Send a fresh printable key | send_idempotency_key |
idempotency_key_reused | Key used for a different request | Use a new key | use_new_idempotency_key |
idempotency_in_progress | The first signup with this key still runs | Retry after Retry-After seconds | Inspect response |
not_found | Offer or handoff not found | Check the returned URL and offer slug | Inspect response |
validation_failed | Signup fields invalid | Fix all reported fields | Inspect response |
full | Trial capacity reached | Try later or contact support | Inspect response |
origin_not_allowed | Browser origin refused | Use the creator storefront | Inspect response |
Storefront errors currently use {error, fields, retryAt} rather than the product error body.
For signup 429, use Retry-After or retryAt. A reused signup idempotency key with a changed
body returns 409; a spent reveal cannot disclose the credentials again. Do not retry a
consumed reveal in a loop. Contact the offer's support address to recover account access.
Trial, payment and pause
The seven-day trial starts on the server without payment details. We request payment before it ends, by invoice
or card through payUrl. Starting the trial does not authorize a charge; no automatic charge is made without
your action and consent. Prices are shown or sent with your invoice or by the reselling partner.
If unpaid at trial end, access is suspended. While paused,
requests return 402 with next: [{action: "pay", href: "…"}]; payment restores access.
A complimentary grant needs no payment while active and keeps the same plan limits.
It may end on a stated date; after that, normal trial/payment rules apply again.
An unlimited grant has no trial end. Account home shows billing, grants, limits and keys;
there are no other product screens. Support is listed on the offer and in your welcome message.
Network
If your code runs in a hosted sandbox with an outbound allowlist, allow api.example.test,
dev.feed.sportlive.hr, store.dev.tantu.stream and docs.dev.tantu.stream.
Keep HTTPS certificate validation enabled. The primary API is served directly over HTTPS.
Changelog
- 2026-10-21: First complete HTTP setup guide, account key operations and shared API origin.
- 0.3: one
idsmap per match, team and competition replaces thebetradar,transfermarktandsoccerdonnablocks; our owntvf:fixture:<n>id when no Betradar match id is known;by-id,lookup, team/competition filters and the keylessid-systems.
