Sport1 API
REST endpoints that back the React Native (Expo) app and the embeddable sport
widgets. Everything is registered under wp-json/ in three namespaces:
| Namespace | Purpose |
|---|---|
app/v2 | Current app API. Flat content[] component list. |
app/v1 | Older app API (nested sections[]) plus article/video/config/widget routes that have no v2 equivalent. |
sport1/v1 | Menu & navigation used by the website and the app. |
All routes are GET and public (permission_callback => '__return_true').
Files live in api/v1/, api/v2/ and are wired up in sport1-api.php.
In the examples below, replace $HOST with the environment host.
Conventions
Common query parameters (supported where noted per endpoint):
| Param | Description |
|---|---|
nocache=1 | Bypass the payload cache and the per-block query caches. |
debug=1 | Append a _debug object (build time, DB queries, component count). Never cached. |
config={JSON} | URL-encoded personalisation contexts. See Request config. |
Caching. Payloads are stored in the app_api object-cache group with the TTL
from config/app-config.json (cache.ttl). app/v2/page/{id} additionally
supports conditional GET and answers 304 Not Modified when the client’sIf-Modified-Since still matches post_modified_gmt.
Component shape. app/v2 responses carry a flat content array. Every entry
is { type, data, contentIndex, contentTarget, contentInSectionIndex }, wheretype is a component name such as NEWSTICKER, NEWSTICKER_GROUP, HEADING,TEASER_POST or LINK_LIST. contentTarget groups entries that came from the
same section; contentInSectionIndex is the position inside that group.
app/v2
GET app/v2/category
Flat list of all top-level categories (channels). Each item carries id, name,slug, permalink, apiUrl, imageUrl, type and numberOfChildren.
Params: nocache.
curl "https://$HOST/wp-json/app/v2/category"
GET app/v2/category/id/{id}
Canonical category payload: term data, parent, children (top-level only) and
the rendered content for the category page. If an editor has set an override
page for the term, responds 302 to app/v2/page/{pageId} instead.
Params: fetchContent (default true — set 0 to skip building content),nocache, debug.
curl "https://$HOST/wp-json/app/v2/category/id/1234"
curl "https://$HOST/wp-json/app/v2/category/id/1234?fetchContent=0"
GET app/v2/category/id/{id}/news
The category’s „Weitere News“ feed on its own — the target of a newsticker’s
„Alle News“ link. Renders the category-page-newsticker-entries-list pattern, socontent holds the same paginated NEWSTICKER_GROUP component the category page
uses (follow its data.loadMoreUrl for the next batch). type isCATEGORY_NEWS.
Params: nocache, debug.
curl "https://$HOST/wp-json/app/v2/category/id/1234/news"
GET app/v2/category/{slug} and GET app/v2/category/{parent}/{slug}
Slug lookups. Both resolve the term and 302 to the canonicalapp/v2/category/id/{id} route, forwarding any query string, so there is exactly
one cacheable URL per category.
curl -iL "https://$HOST/wp-json/app/v2/category/fussball"
curl -iL "https://$HOST/wp-json/app/v2/category/fussball/bundesliga"
GET app/v2/page/{page}
Single dispatcher for every app page. {page} is either a numeric page ID or one
of the virtual pages:
{page} | Result |
|---|---|
123 | That page’s content, plus topChips when it is the configured home page. |
home | 302 to the configured home page ID (preserves config). |
news | 302 to the configured news page ID (preserves config). |
favoriten | Virtual page built from the client’s favourites (config required for personalised output). |
live | Virtual „live“ (Ergebnisse) page; optional live/{sport} narrows it to one sport. |
daten | Virtual stats page. Always takes a sub-path — see daten pages. |
Params: nocache, debug, config. Numeric pages support 304.
curl "https://$HOST/wp-json/app/v2/page/456"
curl -iL "https://$HOST/wp-json/app/v2/page/home"
curl -iL "https://$HOST/wp-json/app/v2/page/news"
curl "https://$HOST/wp-json/app/v2/page/live"
curl "https://$HOST/wp-json/app/v2/page/live/fussball"
daten pages
app/v2/page/daten/... serves the stats pages. Each response is aVIRTUAL_PAGE whose content holds exactly one WIDGET component:
{
"type": "WIDGET",
"data": {
"widget": "standings",
"widgetUrl": "https://$HOST/wp-json/app/v1/sport-widget/standings?config=%7B...%7D",
"widgetHeight": null
}
}
widgetHeight is a fixed pixel height for widgets that have one; today onlyresult-teaser does (94), so every daten view reports null and the client sizes
the WebView itself.
The client renders widgetUrl in a WebView — it is a ready-madeapp/v1/sport-widget/{view} URL with sportId,competitionId, seasonId and friends already resolved from the term’s
competition metadata. title is the upper-cased view, slug/permalink mirror
the public /daten/... path.
Three path shapes are accepted:
| Shape | Used for |
|---|---|
daten/{sport}/{competition}/{view} | The normal case. |
daten/{sport}/{competition}/live-ticker/{matchId} | Ticker for one specific match. |
daten/{sport}/rangliste | Sport-wide ranking; the only 2-segment form. |
Supported {view} values and the widget each maps to:
{view} | Widget | Notes |
|---|---|---|
tabelle | standings | positionType from the competition’s first standing type, else LEAGUE. |
spielplan | gameplan | |
ergebnisse | gameplan | Same widget as spielplan, different viewType label. |
kalender | kalender | Falls back competition → category → sport when resolving the target. |
fahrerwertung | rankings | positionType=PERSON, limit=100. |
teamwertung | rankings | positionType=TEAM, limit=100. |
rangliste | rankings | positionType from the term meta, limit=20. Needs sport_id + position_type; tennis additionally needs category_id. |
live-ticker | live-ticker | Whole competition, or a single match when {matchId} is appended. |
Params: nocache, debug.
# {sport}/{competition}/{view}
curl "https://$HOST/wp-json/app/v2/page/daten/fussball/bundesliga/tabelle"
curl "https://$HOST/wp-json/app/v2/page/daten/fussball/bundesliga/spielplan"
curl "https://$HOST/wp-json/app/v2/page/daten/fussball/bundesliga/ergebnisse"
curl "https://$HOST/wp-json/app/v2/page/daten/fussball/bundesliga/kalender"
curl "https://$HOST/wp-json/app/v2/page/daten/fussball/bundesliga/live-ticker"
# Motorsport standings split by driver / constructor
curl "https://$HOST/wp-json/app/v2/page/daten/motorsport/formel-1/fahrerwertung"
curl "https://$HOST/wp-json/app/v2/page/daten/motorsport/formel-1/teamwertung"
# Single match ticker
curl "https://$HOST/wp-json/app/v2/page/daten/fussball/bundesliga/live-ticker/opta_match_2345678"
# Sport-wide ranking (2-segment form)
curl "https://$HOST/wp-json/app/v2/page/daten/tennis/rangliste"
curl "https://$HOST/wp-json/app/v2/page/daten/fussball/bundesliga/tabelle?nocache=1&debug=1"
Errors, all 404 unless noted:
| Code | When |
|---|---|
invalid_path | The sub-path doesn’t match one of the three shapes above (including a bare page/daten). |
invalid_view | {view} is not in the table; the message lists the supported values. |
not_found | No competition category for {sport}/{competition}, or the season / calendar / ranking target couldn’t be resolved. |
virtual_pages_unavailable | 500 — sport1-virtual-pages is inactive, so the metadata resolver is missing. |
Competition metadata is read via sport1_vp_get_category_meta( $sport, $competition )
from sport1-virtual-pages. The 2-segment rangliste form asks for the sport’s_default competition directly; the retry against _default after an empty
lookup only kicks in for a single-match live-ticker, which needs no competition
of its own. Every other view returns not_found when the{sport}/{competition} pair has no metadata.
GET app/v2/newsticker/more
Next page of a NEWSTICKER_GROUP feed. Normally you don’t build this URL by
hand — take data.loadMoreUrl from the component and follow it.
| Param | Default | Notes |
|---|---|---|
categoryId | — | Required unless allCategories=1. |
offset | 0 | Pass the previous response’s nextOffset. |
count | 13 | 1–50. |
allCategories | false | Query across all categories. |
excludeCategoryIds | — | Comma-separated term IDs to drop. |
Returns { items, hasMore, nextOffset, loadMoreUrl }; loadMoreUrl is null on
the last page. Responds 400 if neither categoryId nor allCategories is set.
curl "https://$HOST/wp-json/app/v2/newsticker/more?categoryId=1234&offset=13&count=13"
curl "https://$HOST/wp-json/app/v2/newsticker/more?allCategories=1&excludeCategoryIds=99,100"
GET app/v2/search
Typeahead over favourite-able entities: sport categories, competition categories
and team tags. Returns a flat array of favourite chips, each with a fuzzy-matchscore (matching tolerates up to 3 edits). Requires sport1-core, otherwise 500.
| Param | Default | Notes |
|---|---|---|
q | — | Required, at least 2 characters. |
limit | 10 | Max 15, applied per entity type. |
curl "https://$HOST/wp-json/app/v2/search?q=bayern&limit=5"
app/v1
GET app/v1/config
Serves config/app-config.json verbatim — cache TTLs, page IDs and other client
settings. Change the JSON file, no deploy of code needed. Supports ETag /
conditional GET keyed on the file mtime.
curl "https://$HOST/wp-json/app/v1/config"
GET app/v1/article/{id}
Full article for the app: { id, title, excerpt, publishedAt, modifiedAt, permalink, featuredImage, meta: { channels, tags, primaryChannel }, topMedia, authorArea, blocks }. Blocks come from parse_blocks(), layout containers are
flattened, and each remaining block is mapped to a typed object. 404 for
anything that isn’t a published post.
curl "https://$HOST/wp-json/app/v1/article/98765"
GET app/v1/video/{id}
Same shape as the article endpoint, for the video post type.
curl "https://$HOST/wp-json/app/v1/video/98765"
GET app/v1/agf/video/{id}
AGF (audience measurement) metadata for a video. Accepts either a numeric post ID
or the video’s top_video_id UUID, which is looked up against postmeta. 400 on
an id that is neither; 404 when no published video matches or it has notop_video_id.
curl "https://$HOST/wp-json/app/v1/agf/video/98765"
curl "https://$HOST/wp-json/app/v1/agf/video/3fa85f64-5717-4562-b3fc-2c963f66afa6"
GET app/v1/page/{id} and GET app/v1/home
Predecessor of app/v2/page. Returns nested sections[], each{ type: 'SECTION', data: { name, patternName, children: [...] } }, instead of a
flat component list. home redirects to the configured home page ID.
curl "https://$HOST/wp-json/app/v1/page/456"
GET app/v1/newsticker/more
v1 equivalent of app/v2/newsticker/more, same parameters.
curl "https://$HOST/wp-json/app/v1/newsticker/more?categoryId=1234&offset=13"
GET app/v1/category/{slug} and GET app/v1/category/{parent}/{slug}
v1 category payload. Instead of rendering components it describes the page:termId, name, slug, permalink, type, parent, children,overridePage and sections[], where each section lists its feeds[] withtype (POSTS | NEWSTICKER | NEWSTICKER_GROUP), the resolved posts andhasMore / nextOffset for paginated ones. Section structure is derived by
parsing the theme’s block patterns, so it tracks pattern edits automatically.
curl "https://$HOST/wp-json/app/v1/category/fussball"
curl "https://$HOST/wp-json/app/v1/category/fussball/bundesliga"
GET app/v1/sport-widget/{view}
The only endpoint that returns HTML, not JSON — a complete, standalone
document intended for an iframe or WebView. It ships SSR markup for fast first
paint, window.Sport1Data for hydration, and the block’s compiled JS/CSS.
{view} is one of gameplan, standings, rankings, result-teaser,ergebnisse, kalender, live-ticker (liveticker still works as a legacy
alias).
| Param | Default | Notes |
|---|---|---|
config | {} | URL-encoded JSON, e.g. {"sportId":"opta:1","competitionId":"opta_22","seasonId":"opta_2025"}. |
header | 0 | 1 renders in-block chrome (the ergebnisse „HEUTE LIVE“ header + CalendarBar). |
# {"sportId":"opta:1","competitionId":"opta_22","seasonId":"opta_2025"}
curl "https://$HOST/wp-json/app/v1/sport-widget/standings?config=%7B%22sportId%22%3A%22opta%3A1%22%2C%22competitionId%22%3A%22opta_22%22%2C%22seasonId%22%3A%22opta_2025%22%7D"
curl "https://$HOST/wp-json/app/v1/sport-widget/ergebnisse?header=1"
Filters: sport1_widget_bypass_cache (skip transient, send no-store),sport1_widget_frame_ancestors (CSP frame-ancestors).
sport1/v1 — menus & navigation
Competition metadata (sport_id, competition_id, …) is owned by
sport1-virtual-pages and read through sport1_vp_get_category_meta(); these
endpoints degrade gracefully if that plugin is inactive. Responses are cached in
transients.
GET sport1/v1/top-menu
Top navigation entries. Also reused server-side as the topChips field of the
home page payload.
curl "https://$HOST/wp-json/sport1/v1/top-menu"
GET sport1/v1/side-menu
Side navigation, built from the competitions GraphQL query.
curl "https://$HOST/wp-json/sport1/v1/side-menu"
GET sport1/v1/bottom-menu
Footer menu, from the bottom-menu WP nav menu location.
curl "https://$HOST/wp-json/sport1/v1/bottom-menu"
GET sport1/v1/subnav
Tab bar for a sport or competition page (newsticker, videos, heute-live,
transfermarkt, wettbewerbe, …).
| Param | Notes |
|---|---|
sport | Sport slug. |
competition | Competition slug. |
curl "https://$HOST/wp-json/sport1/v1/subnav?sport=fussball"
curl "https://$HOST/wp-json/sport1/v1/subnav?sport=fussball&competition=bundesliga"
GET sport1/v1/competition-menu
Menu for one competition. Both params are required.
curl "https://$HOST/wp-json/sport1/v1/competition-menu?sport=fussball&competition=bundesliga"
Request config
config personalises a response. It is URL-encoded JSON with a contexts
object; today only FAVORITEN is understood:
{
"contexts": {
"FAVORITEN": {
"items": [
{ "id": "123", "type": "TEAM" },
{ "id": "456", "type": "CHANNEL" }
],
"postsToShow": 10
}
}
}
type is TEAM, CHANNEL or SUBCHANNEL (SPORT → CHANNEL andCOMPETITION → SUBCHANNEL are accepted as aliases). Items with an empty id
or an unknown type are dropped; if nothing survives, the context is ignored and
the response falls back to its default content.
Because the payload varies per client, requests carrying config are not
served from — nor written to — the shared payload cache, and they skip the 304
shortcut.
curl -G "https://$HOST/wp-json/app/v2/page/favoriten" \
--data-urlencode 'config={"contexts":{"FAVORITEN":{"items":[{"id":"123","type":"TEAM"}]}}}'
Internals
Not endpoints, but shared by them:
| File | Responsibility |
|---|---|
api/helpers.php | Block-name collection, reshape CDN URL helpers. |
api/rest.php | App_Rest — conditional GET, 304 handling, response headers. |
api/post.php | App_Post — post/term image resolution. |
api/tracking.php | App_Tracking — tracking payloads per post/category. |
api/term_image.php | Category image lookup. |
api/graphql_client.php + api/graphql/*.graphql | Sports/competitions GraphQL queries for the menus. |
api/v2/request-config.php | App_Request_Config — parses and validates config. |
api/v2/widget-config.php | Widget config normalisation. |
api/v2/virtual-page.php | Builds favoriten / live / daten; has no routes of its own — reached through the app/v2/page/{page} dispatcher. |
cache/menu.php | Menu cache invalidation. |
config/app-config.json | Cache TTLs, page IDs, limits. Served by app/v1/config. |