> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ticktock.bet/llms.txt
> Use this file to discover all available pages before exploring further.

# UOF REST Compatibility

> UOF-compatible REST paths for existing UOF-style client integrations

The B2B API exposes an UOF-compatible REST facade for integrations whose
request classes resolve fixed UOF resource paths against a configured base
URL. The facade uses the same CS2 data, tenant permissions, market catalogue,
and AMQP recovery worker as the canonical `/cs2/v1/*` API.

Use a base URL ending in `/v1/`, for example:

```text theme={null}
https://ticktock.bet/v1/
```

<Warning>
  Set `Accept: application/xml` on every consumed request. The canonical
  Ticktock API honors content negotiation: `Accept: application/json`, `*/*`,
  or an absent `Accept` header selects JSON on dual-format endpoints. Unlike the
  upstream UOF API, Ticktock does not deliberately ignore a JSON `Accept`
  header. The UOF compatibility paths documented below return their
  hand-tuned XML response, and item 8.8 tracks complete UOF XML coverage for
  every consumed catalog/detail path.
</Warning>

## Sport & category model

The facade follows the UOF hierarchy **Sport → Category → Tournament →
Match**. The **sport** is one per integration instance (`provider_default_sport_id`,
default `"2"` = CyberSport). It is **not** a record in the REST `sports`
catalog — that catalog returns **categories**. The sport id rides only the
**AMQP routing key** (its sport section), where the client scopes match-pairing
to one sport.

| Sport (`provider_default_sport_id`) | Category (REST catalog / tournament) | Where the sport id appears  |
| ----------------------------------- | ------------------------------------ | --------------------------- |
| CyberSport (`2`)                    | **CS2** (`id="3"`)                   | AMQP routing key, section 5 |
| Soccer (`1`)                        | **country** (e.g. Россия)            | *planned* — see below       |

So for CS2: the AMQP routing-key sport section is `2` (CyberSport); the REST
`sports` catalog and the nested `<sport>` a tournament carries are the **CS2
category** (`id="3"`, folded into the client's `category_id`). Both the routing
key and the category id stay stable, so match pairing — which searches
candidates **within one sport** — lines up.

<Note>
  **Football is served now** under the **Soccer** sport (id `1`) with **per-country
  categories** — see the [Football](/football/overview) section. Events classify
  under Soccer with the country as the category (nested in the tournament), matching
  the client's football model so fuzzy match-pairing resolves. Data, the REST
  catalog, the XML feed, and the live lifecycle are live; market generation is the
  remaining piece.
</Note>

## Authentication

These routes accept the tenant key in either `X-API-Key` or the UOF-standard
`x-access-token` header — send `x-access-token: <your-key>`, exactly as against
the upstream UOF REST API. (A legacy `$x$access-token` spelling is also still
accepted for backward compatibility, but prefer `x-access-token`.) All names
enter the same authentication path, so scopes, `sport_allowlist`, key
revocation, and rate limiting behave identically. If more than one is sent,
their values must match.

For the current CS2 integration, issue the `cs2.odds.basic` bundle and include
`cs2` in `sport_allowlist`; that bundle includes match, market, tournament,
team, player, settlement, stream, and recovery access. A future second sport
must add its own scope bundle and sport slug to the same key when that sport's
API is implemented.

## Locales

Every catalogue and detail request consumes the locale from its path. Only
`en` and `ru` are supported; unknown locale values return HTTP `400`.

| Display value                                         | `en`                   | `ru`                   |
| ----------------------------------------------------- | ---------------------- | ---------------------- |
| Market name and template                              | English                | Russian                |
| Market description                                    | English                | English fallback       |
| Match status description                              | English                | Russian                |
| Composed match name                                   | `Alpha vs Bravo`       | `Alpha против Bravo`   |
| Team, player, tournament, map and stream proper names | Canonical source value | Canonical source value |
| Sport name                                            | `Counter-Strike 2`     | `Counter-Strike 2`     |

Entity IDs, qualifiers, status codes and other machine-readable values are
locale-independent. A localized request never returns an undefined or empty
display value merely because a Russian translation is unavailable.

## Resource paths

All paths below are relative to the `/v1/` base URL:

```text theme={null}
GET  sports/:locale/sports
GET  sports/:locale/tournaments
GET  sports/:locale/tournaments/:id/info
GET  sports/:locale/sport_events/:id/summary
GET  sports/:locale/sport_events/:id/fixture
GET  sports/:locale/competitors/:id/profile
GET  sports/:locale/players/:id/profile
GET  sports/:locale/schedules/pre/schedule
GET  sports/:locale/schedules/live/schedule

GET  descriptions/:locale/markets
GET  descriptions/:locale/markets/:market_id/variants/:variant_urn
GET  descriptions/:locale/match_status
GET  descriptions/:locale/void_reasons
GET  descriptions/producers

POST :product/recovery/initiate_request
POST :product/odds/events/:id/initiate_request
POST :product/stateful_messages/events/:id/initiate_request
```

Every successful compatibility response is XML with
`Content-Type: application/xml`. Empty collections return HTTP `200` with an
empty valid XML container. Unknown individual resources return HTTP `404`. For
the catalogue-detail endpoints the `404` body is an XML not-found marker — not
a JSON `{detail}` envelope a strict XML client would crash on:

* Missing tournament → `<tournament_info><response response_code="NOT_FOUND"/></tournament_info>`
* Missing competitor → `<competitor_profile><response response_code="NOT_FOUND"/></competitor_profile>`
* Missing sport-event summary → `<match_summary><response response_code="NOT_FOUND"/></match_summary>`
* Missing sport-event fixture → `<fixtures_fixture><response response_code="NOT_FOUND"/></fixtures_fixture>`
* Missing player → `<player_profile><response response_code="NOT_FOUND"/></player_profile>`
* Missing market variant → `<market_descriptions><response response_code="NOT_FOUND"/></market_descriptions>`

A client that parses every body as XML detects not-found from the in-body
marker on **every** catalogue/detail endpoint — none return a JSON `{detail}`.

`GET sports/:locale/tournaments/:id/info` returns `<tournament_info>` with a
nested `<tournament>` carrying `id`, `name`, `risk_tier`, `scheduled`,
`scheduled_end`, an optional `icon_path`, and a nested `<sport>`.
`GET sports/:locale/competitors/:id/profile` returns `<competitor_profile>`
with a nested `<competitor>` carrying `id`, `name`, an optional `icon_path`,
and `country_code`. The same competitor `id` appears as the home/away
`<competitor>` in event payloads, so the payload reference and the profile
lookup share one key.

`GET sports/:locale/sport_events/:id/fixture` returns `<fixtures_fixture>` and
`GET sports/:locale/sport_events/:id/summary` returns `<match_summary>`. Both
envelopes carry the two teams as nested `<competitor>` elements with a
`qualifier` (`home` / `away`), and an `<extra_info>` block with at least
`<info key="sport_format" value="classic"/>` (the fixture also adds
`best_of`). Clients that admit a match by its competitor qualifiers and read
`sport_format` from `<extra_info>` find both on either envelope.

`GET sports/:locale/sports` returns `<sports>` with one `<sport>` entry per
**category** (today: CS2). Each entry carries the bare integer `id` (`3` for
CS2 — the value the client stores as its `category_id`), `name`, and
`icon_path`. In the client's Sport → Category → Tournament → Match hierarchy
CS2 is the category; the sport itself (e.g. CyberSport) is the client's own
configured default, not an entry here. `icon_path` always points to a
platform-hosted asset. The default is
`https://ticktock.bet/categories/cs2.svg`; deployments on another public
domain can override it with `CS2_CATEGORY_ICON_URL`.

`GET descriptions/:locale/markets` returns `<market_descriptions>` with one
`<market>` per type, each carrying nested `<outcomes>` (every CS2 market has
one `<outcome id="1" name="…">`) and `<specifiers>`
(`<specifier name="…" type="integer|variable_text|decimal"/>`). The response is
**single-locale**: every display string (`<market name>`, `<outcome name>`)
appears once, in the path's `:locale`, with **no `name_ru` sibling** — fetch
the `ru` path for Russian. (`description` stays English until a Russian source
exists.) Those `name` attributes are **parametrized templates**: substitute
`{placeholder}` tokens (e.g. `{round}`, `{player}`) from the live market's
specifiers — they are not pre-rendered static labels. A live `<odds_change>`
outcome is unnamed and its market carries only `id` + `specifiers`, so clients
name the outcome from the catalog's `<outcome>` list and render the template's
placeholders only for the declared `<specifier>` keys.

`GET descriptions/:locale/markets/:market_id/variants/:variant_urn` resolves a
market whose **outcome set varies match-to-match** (BB 3.8) — a live market
flags it with a `variant=<urn>` specifier, and this resource returns that
variant's outcomes (same nested `<market variant="…"><outcomes>` form, stable
ids, per-locale template names). Ticktock has no variable-outcome markets today
(all CS2 outcome sets are fixed), so this currently returns the market's
declared outcomes; the dynamic per-variant roster (e.g. a football
first-goalscorer) lands with the football build.

`GET descriptions/:locale/match_status` returns the match-status dictionary the
client uses to label `<sport_event_status>` (BB 4.3). It enumerates **every
lifecycle code the feed publishes** — `0` not started, `1` live, `4` closed,
`5` cancelled — each with a localized `description`, so a client builds its
status-name lookup entirely from this catalogue:

```xml theme={null}
<match_status_descriptions response_code="OK">
  <match_status id="0" description="Not started"/>
  <match_status id="1" description="Live"/>
  <match_status id="4" description="Closed"/>
  <match_status id="5" description="Cancelled"/>
</match_status_descriptions>
```

The integer `id` values are exactly those carried by `<sport_event_status
status>` on the feed (see [Sport-event status](/odds-feed/amqp#sport-event-status-bb-4-1)),
so the catalogue and the live messages never disagree. `descriptions/ru/match_status`
returns the same ids with Russian descriptions.

The dictionary lists **only the codes the feed currently publishes**, so for
CS2 it is exactly `{0, 1, 4, 5}`. The extended UOF lifecycle codes a
football feed needs (`2` suspended, `3` ended, `6` delayed, `7` interrupted,
`8` postponed, `9` abandoned) are added to this same catalogue when that sport
ships — it always mirrors what is actually emitted.

`GET descriptions/:locale/void_reasons` decodes the numeric `void_reason_id`
each `<bet_cancel>` market carries into a stable id → localized signature
(BB 5.2), so an operator or bettor sees *why* a market was voided rather than
an opaque integer:

```xml theme={null}
<void_reasons_descriptions response_code="OK">
  <void_reason id="4" name="Match abandoned" description="Match abandoned before completion; all bets void."/>
  <void_reason id="14" name="Incorrect odds" description="Odds posted in error; bets settled at corrected odds."/>
  <void_reason id="15" name="Market scratched" description="Market voided due to a data correction."/>
</void_reasons_descriptions>
```

The `bet_cancel` body is unchanged — this is purely the decode catalog. Today
the feed emits `void_reason_id="4"`; the other ids are part of the stable
vocabulary. `descriptions/ru/void_reasons` returns the same ids in Russian.

## Identifiers

* Sport events accept `od:match:{number}` or the Ticktock match UUID.
* Tournament, competitor, and player paths accept a numeric HLTV ID or a URN
  whose final segment is that numeric ID.
* URNs may be URL encoded.
* Responses preserve an existing `od:match:*` identifier when the match is
  linked to UOF. Ticktock-owned catalogue entities (tournaments, competitors,
  players) use stable `tt:*` URNs.
* The sport/category id is the bare UOF integer (`3` for CS2), both in the
  `sports` catalog and where `<sport>` is nested under a tournament — it maps
  to the client's `category_id`, so it is not URN-wrapped.

## Schedules

Prematch schedule pagination uses offset semantics:

| Parameter | Default | Constraint         |
| --------- | ------- | ------------------ |
| `start`   | `0`     | integer, `>= 0`    |
| `limit`   | `1000`  | integer, `1..1000` |

The prematch endpoint returns booked `discovered` and `upcoming` events. The
live endpoint returns only booked live events. Per-tenant unbook overrides are
applied to both.

Prematch events are ordered by scheduled start time and then event ID. Read the
complete catalogue by requesting `limit=1000`, increasing `start` by the same
limit after every full page, and stopping when a page contains fewer than
`limit` events. The live schedule is intentionally not paginated.

```bash theme={null}
curl -H "X-API-Key: $TT_KEY" \
  "https://ticktock.bet/v1/sports/en/schedules/pre/schedule?start=0&limit=100"
```

Clients configured with both supported locales should repeat catalogue and
detail requests with `ru`:

```bash theme={null}
curl -H "X-API-Key: $TT_KEY" \
  "https://ticktock.bet/v1/sports/ru/schedules/pre/schedule?start=0&limit=100"
```

## Producer discovery

Call `GET descriptions/producers` before starting producer monitoring or
recovery. The response contains at least the configured live producer:

```xml theme={null}
<producers response_code="OK">
  <producer
    id="2"
    name="ticktock_live"
    scope="live"
    description="Live in-play markets"
    active="true"
    stateful_recovery_window_in_minutes="4320"/>
</producers>
```

The producer `id` is the exact value carried by the `product` attribute on
`<alive>`, odds, settlement, cancellation, recovery acknowledgement, and
`<snapshot_complete>` messages. The leading recovery path segment accepts the
producer `id`, `scope` (`live`), or `name` (`ticktock_live`). Deployments
overriding `AMQP_PRODUCT_ID` publish the configured value consistently in both
the catalog and messages.

The catalog lists **one `<producer>` per stream the feed publishes**. Today the
live-only volume needs just the live producer. If a separate stream is added —
for example a prematch producer — it appears here as an additional `<producer>`
with its own `id` and `scope` (e.g. `scope="prematch"`), and the feed publishes
that stream's `<alive>` / odds with `product` equal to its `id`; its `id` /
`scope` / `name` are then also accepted in the `:product` path segment. A client
that runs recovery in scope phases (non-`live` first, then `live`) keys off the
`scope` value here.

```xml theme={null}
<producers response_code="OK">
  <producer id="2" name="ticktock_live" scope="live"/>
  <producer id="3" name="ticktock_prematch" scope="prematch"/>  <!-- only if a prematch stream ships -->
</producers>
```

## Recovery paths

Ticktock currently exposes the live producer. Discover its ID and scope from
`GET descriptions/producers`, then use either value in `:product`;
unsupported products return HTTP `422`.

The global recovery endpoint is:

```text theme={null}
POST /v1/:product/recovery/initiate_request
```

Some clients repeat `product` as a query parameter. Ticktock accepts and
ignores that duplicate, so both forms are valid:

```text theme={null}
POST /v1/ticktock_live/recovery/initiate_request?product=ticktock_live&after=...
POST /v1/2/recovery/initiate_request?after=...
```

Global recovery keeps the canonical recovery parameters:

| Parameter    | Required | Description                                                                                                                                              |
| ------------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `after`      | no       | Positive Unix timestamp in milliseconds for incremental settlement/cancel replay. Omit it or pass `0` for a full snapshot of all current active markets. |
| `request_id` | yes      | Positive integer echoed verbatim in every replay envelope's root XML attribute                                                                           |
| `node_id`    | no       | Optional positive integer echoed verbatim as every replay routing key's final section                                                                    |

Full snapshot mode publishes current active odds for every booked visible
match and terminates with `<snapshot_complete>`. It deliberately does not scan
settlement/cancel history and is not constrained by the stateful recovery
window.

For incremental recovery, a positive `after` replays current active odds plus
settlement/cancel transitions after that timestamp. It must not be in the
future or older than the producer's
`stateful_recovery_window_in_minutes`. The default is `4320` minutes (3 days),
but this is a deployment configuration value rather than a hard-coded platform
maximum. Requests outside the configured window return HTTP `422`.

Every replay envelope carries the matching root `request_id`; the AMQP
`recovery=1` header is supplementary. Event odds recovery republishes active
odds only for the selected event.
Event stateful recovery republishes settlements and cancellations only for the
selected event. Event requests accept optional `request_id` and `node_id`.
When `request_id` is omitted on an event request, Ticktock generates a positive
numeric correlation ID and returns it in the acknowledgement.
All three endpoints return HTTP `202`; replay data arrives through the tenant's
AMQP vhost and terminates with `<snapshot_complete>`.

A replayed `<bet_settlement>` / `<bet_cancel>` carries the **original
transition `timestamp`** — the time the market was settled or voided — not the
time of the replay. So a `timestamp`-keyed dedup matches the live message it
already processed and never re-settles; replaying the same window twice is
idempotent. Markets that transitioned together (one settlement batch) share a
`timestamp` and so a single envelope, exactly as in the live feed.

A global replay reconstructs the full state a warm-restart client would
otherwise rebuild from REST, in this order (then `<snapshot_complete>`):

1. `<fixture_change>` for each match whose lifecycle changed since `after` (a
   kickoff → `change_type="1"`, a cancellation → `change_type="3"`). A finish is
   **not** a fixture change — it rides `<sport_event_status status="4">`.
2. `<odds_change>` per match — its current active markets.
3. `<bet_settlement>` / `<bet_cancel>` for transitions since `after`.
4. `<rollback_bet_settlement>` / `<rollback_bet_cancel>` for any settlement or
   void **reversed** since `after`, so a rollback during the gap is not lost.

Every replayed envelope — including the fixture change and the rollbacks —
reuses the original transition `timestamp`, so the same dedup applies.

```bash theme={null}
curl -X POST -H "X-API-Key: $TT_KEY" \
  "https://ticktock.bet/v1/live/odds/events/od%3Amatch%3A258814/initiate_request?request_id=4567&node_id=3003"
```

## Canonical API

The compatibility facade is additive. Existing `/cs2/v1/*` and sport-agnostic
`/v1/health`, `/v1/alive`, `/v1/whoami`, and `/v1/sports` routes remain
available without contract changes.
