> ## 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.

# Messaging Feed

> Real-time odds delivery via AMQP messaging feed

## Overview

The Ticktock Messaging Feed delivers real-time market updates through **AMQP 0-9-1** (RabbitMQ). Messages are lightweight XML payloads containing only changed data — odds, settlements, and fixture changes. The [REST API](/integration-guide#rest-api-endpoints) complements the feed for recovery and static data.

| Property         | Value                                   |
| ---------------- | --------------------------------------- |
| **Protocol**     | AMQP 0-9-1 (RabbitMQ)                   |
| **Payload**      | XML                                     |
| **Exchange**     | `ttfeed` (topic)                        |
| **Heartbeat**    | `<alive>` every 10 seconds              |
| **Localisation** | English (`textEN`) + Russian (`textRU`) |
| **Product**      | `2` (live)                              |

## Architecture

```mermaid theme={null}
graph LR
    TT["Ticktock<br/>Odds Engine"] --> Exchange["ttfeed<br/>exchange (topic)"]
    Exchange -->|"odds_change"| Q[Your Queue]
    Exchange -->|"bet_settlement"| Q
    Exchange -->|"alive"| Q
    Q --> OP["Your<br/>Integration"]
```

## Connection

Each B2B operator gets an **isolated RabbitMQ vhost** and a dedicated user:

| Property         | Value                                   |
| ---------------- | --------------------------------------- |
| **Server**       | `mq.ticktock.bet`                       |
| **Port**         | `5672` (AMQP) / `5671` (AMQPS)          |
| **Virtual Host** | `/<org-slug>` (e.g. `/acme-sports`)     |
| **Username**     | `op-<org-slug>` (e.g. `op-acme-sports`) |
| **Exchange**     | `ttfeed`                                |

```
amqp://op-<slug>:<password>@mq.ticktock.bet:5672/<slug>
```

Operators cannot access each other's queues or messages.

### Getting Credentials

Contact your account manager or email [b2b@ticktock.bet](mailto:b2b@ticktock.bet) to request AMQP access provisioning.
Credentials are generated via the admin panel and returned once — **store them immediately**.

### Connection Limits

Each B2B account is provisioned with the following resource limits per environment (Production and Integration are counted separately):

| Resource        | Limit | Notes                                                                                |
| --------------- | ----- | ------------------------------------------------------------------------------------ |
| **Connections** | 10    | concurrent AMQP connections across all consumers                                     |
| **Channels**    | 200   | total channels across all your connections (= 20 channels per connection on average) |
| **Queues**      | 10    | total queues declared in your vhost                                                  |

Use **AMQP channels** to multiplex multiple consumers/subscriptions over a single TCP connection — channels are cheap (one Erlang process each) and a well-designed integration typically needs **one or two connections** even at scale. Opening many short-lived connections is an anti-pattern: it strains both client and broker, and you will hit the cap quickly.

<Tip>
  Most integrations run comfortably on 1–2 connections with a handful of channels. If your architecture genuinely needs more (for example, you horizontally scale consumers across many Kubernetes pods, each with its own connection), contact your account manager — we can raise these limits per account after a short review.
</Tip>

## Routing Keys

Messages use an 8-section topic routing key. The layout is **identical for
every message type and every sport** — split on `.` and read fixed positions;
empty fields are the literal placeholder `-` so every key has exactly 8
sections:

```
{priority}.{prematch}.{live}.{msg_type}.{sport}.{source}:{entity}.{event_id}.{node_id}
```

| Section       | Description                                                                                                                                                         | Example values                           |
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------- |
| priority      | Processing priority                                                                                                                                                 | `hi` / `lo` / `-`                        |
| prematch      | Pre-match flag                                                                                                                                                      | `pre` / `-`                              |
| live          | Live flag                                                                                                                                                           | `live` / `-`                             |
| msg\_type     | Message type. **Equals the root XML element name exactly** — e.g. `odds_change` ↔ `<odds_change>` — so a content lookup keyed on the routing type matches the body. | `odds_change`, `bet_settlement`, `alive` |
| sport         | Provider **sport** id — one per feed instance (CyberSport), **not** the CS2 category. CS2 is a category under it and never appears here.                            | `2` (CyberSport); `1` (Soccer)           |
| source:entity | Entity URN — one section containing a colon (`od:match`); it is **not** split on `.`.                                                                               | `od:match`                               |
| event\_id     | Match number                                                                                                                                                        | `2588141`                                |
| node\_id      | Positive integer recovery node, echoed from the initiate request                                                                                                    | `3003` / `-`                             |

### Examples

| Message                                                  | Routing Key                                     |
| -------------------------------------------------------- | ----------------------------------------------- |
| Odds change for match 2588141                            | `hi.-.live.odds_change.2.od:match.2588141.-`    |
| Bet settlement for match 2588141                         | `hi.-.live.bet_settlement.2.od:match.2588141.-` |
| Fixture change                                           | `hi.-.live.fixture_change.2.od:match.2588141.-` |
| Recovery odds replay (priority `lo`, optional `node_id`) | `lo.-.live.odds_change.2.od:match.2588141.3003` |
| Recovery `<snapshot_complete>`                           | `-.-.-.snapshot_complete.-.-.-.3003`            |
| Alive heartbeat                                          | `-.-.-.alive.-.-.-.-`                           |

### Binding Patterns

Use wildcards `#` (multi-section) and `*` (single section) to filter messages:

```
# All live messages (odds + settlements + fixture changes)
hi.-.live.#

# Only odds changes
hi.-.live.odds_change.#

# All messages for a specific match
hi.-.live.*.2.od:match.2588141.-

# Everything including alive heartbeats
#
```

## Message Types

The feed publishes **exactly these 8 message types — no more**. A type outside
the set has no handler on the client and would stall its stream, so the set is
closed.

**Priority is determined by phase, not by message type.** Every **live**
message — of any type — uses priority `hi`; every **recovery** republish uses
`lo` (see [Recovery](#recovery)). So you can bind `hi.…` for live and `lo.…`
for recovery and never split a single message type across both. The `Live
priority` column below is what each type carries during normal live operation.

| XML Element                 | Routing Segment           | Live priority | Description                                                                                                           |
| --------------------------- | ------------------------- | ------------- | --------------------------------------------------------------------------------------------------------------------- |
| `<odds_change>`             | `odds_change`             | hi            | New or updated market odds (also carries suspension — see below)                                                      |
| `<bet_settlement>`          | `bet_settlement`          | hi            | Markets settled — grade bets                                                                                          |
| `<bet_cancel>`              | `bet_cancel`              | hi            | Markets voided — return stakes                                                                                        |
| `<rollback_bet_settlement>` | `rollback_bet_settlement` | hi            | Previous settlement reversed                                                                                          |
| `<rollback_bet_cancel>`     | `rollback_bet_cancel`     | hi            | Previous void reversed (void-reversal)                                                                                |
| `<fixture_change>`          | `fixture_change`          | hi            | Match started, cancelled, or fixture metadata changed (a *finish* is an `odds_change`/`sport_event_status`, not this) |
| `<alive>`                   | `alive`                   | -             | Heartbeat — every 10 seconds                                                                                          |
| `<snapshot_complete>`       | `snapshot_complete`       | -             | Initial state delivery complete after subscription                                                                    |

<Note>
  There is **no `bet_stop` message type**. A full-match suspension at a key moment
  (goal, red card) is expressed as `status="-1"` on the affected `<market>`
  elements inside an `<odds_change>` (one message can carry several `<market>`
  children) — not as a separate message.
</Note>

`<rollback_bet_cancel>` carries the same `{id, specifiers}` market shape as
`<rollback_bet_settlement>`:

```xml theme={null}
<rollback_bet_cancel product="2" timestamp="1711234610000" event_id="od:match:2588141">
  <market id="1013" specifiers="map=1|round=5"/>
</rollback_bet_cancel>
```

### Match Identification

Every match-scoped message carries **two identifiers**:

| Attribute     | Format              | Purpose                                                               |
| ------------- | ------------------- | --------------------------------------------------------------------- |
| `event_id`    | `od:match:{number}` | UOF match ID — stable external identifier for cross-referencing       |
| `tt_match_id` | UUID                | Internal match UUID — use for REST API lookups and settlement queries |

### Odds Change

Published when markets are created or odds update. The message is a **delta** — it contains only changed markets. Markets not present in the message remain unchanged.

Every `<odds_change>` (live and prematch) carries a `<sport_event_status>` element as its **first child** — its `status` attribute is the **single driver of the match lifecycle** (prematch → live → finished/removed; see [Sport-event status](#sport-event-status-bb-4-1)). A message without it would leave the match frozen in prematch.

```xml theme={null}
<odds_change product="2" timestamp="1711234567890"
             event_id="od:match:2588141"
             tt_match_id="98deb7ef-def9-47d4-854c-a13618952f99">
  <sport_event_status status="1"/>
  <odds>
    <market id="1001"
            tt_market_id="a3f1b2c4-5678-4d9e-b012-abcdef123456"
            type="headshot_opening"
            specifiers="map=1|round=5"
            status="1"
            textEN="First kill is a headshot — Round 5"
            textRU="Первое убийство хедшотом — Раунд 5">
      <outcome id="1" active="1" odds="2.10" probabilities="0.41000" />
    </market>
    <market id="1013"
            tt_market_id="b7e2c3d5-6789-4e0f-c123-bcdef2345678"
            type="grenade_kill"
            specifiers="map=1|round=5"
            status="1"
            textEN="Grenade kill in Round 5"
            textRU="Убийство гранатой в раунде 5">
      <outcome id="1" active="1" odds="7.40" probabilities="0.10000" />
    </market>
    <market id="1050"
            tt_market_id="c8f3d4e6-7890-4f1a-d234-cdef34567890"
            type="bomb_planted"
            specifiers="map=1|round=5"
            status="1"
            textEN="Bomb planted — Round 5"
            textRU="Бомба заложена — Раунд 5">
      <outcome id="1" active="1" odds="1.85" probabilities="0.47000" />
    </market>
  </odds>
</odds_change>
```

#### Sport-event status (BB 4.1)

`<sport_event_status>` is the **only** signal you need to drive a match's
lifecycle. It rides every `<odds_change>` (live and prematch). The `status`
attribute is an **integer code** from a fixed four-value set — read it as a
number, not a string. Codes outside the set are not emitted, and legacy
match-level string names (`upcoming`, `finished`, …) never appear here.

| `status` | Meaning     | Client state                      |
| -------- | ----------- | --------------------------------- |
| `0`      | Not started | prematch                          |
| `1`      | Live        | live                              |
| `4`      | Closed      | finished (settlement window done) |
| `5`      | Cancelled   | removed                           |

A mid-match pause (e.g. the break between maps in a best-of series) stays
`1` (live) — the match is in progress, it does not revert to `0`.

**Match finish is `status="4"` here**, carried on a status-only `<odds_change>`
(no `<odds>` child) with the final `home_score` / `away_score` / `winner_id` —
not a `<fixture_change>`. See [Fixture Change](#fixture-change) for why
`change_type="5"` is coverage-only.

Don't rely on `<fixture_change>` alone to advance the match — a match created in
the default prematch state never goes live or finishes without a
`<sport_event_status>` on its `<odds_change>` stream.

<Note>
  **CS2 publishes only `{0, 1, 4, 5}`.** The remaining UOF EventStatus
  codes — `2` suspended, `3` ended, `6` delayed, `7` interrupted, `8` postponed,
  `9` abandoned — are for sports whose lifecycle the four-value set can't express.
  **Football emits the full set today** (see [Football → Match status &
  lifecycle](/football/lifecycle)); each sport's `descriptions/{locale}/match_status`
  enumerates exactly the codes it publishes. A CS2-only integration can ignore the
  extras. The key distinctions they add: a *suspended* / *interrupted* match is
  paused, **not** dropped; a *postponed* / *delayed* match is rescheduled, **not**
  removed; and *ended* (play over) is distinct from *closed* (settled & finalized).
</Note>

**Key attributes:**

| Attribute                         | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `market.id`                       | Stable integer market type code (1001–1091)                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `market.tt_market_id`             | Unique UUID for this specific market instance — use for bet placement, settlement correlation, and audit                                                                                                                                                                                                                                                                                                                                                                 |
| `market.status`                   | Market status code (see table below)                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| `market.type`                     | Market type identifier (e.g. `grenade_kill`, `headshot_opening`)                                                                                                                                                                                                                                                                                                                                                                                                         |
| `market.specifiers`               | Pipe-separated context keys — the market instance's **stable identity**. Keys: `map=N` (integer map number), `round=N` (round number), `player=Name` (player-scoped markets), `competitor=Team` (team-scoped markets — `eco_upset`, `eco_frags`, `force_buy_win`). Example: `map=1\|round=5\|player=s1mple`. **Fixed for the life of the instance** — see the stability note below. Present on every message type: odds\_change, bet\_settlement, bet\_cancel, rollback. |
| `market.textEN` / `market.textRU` | Human-readable market description — display directly to end-users                                                                                                                                                                                                                                                                                                                                                                                                        |
| `outcome.active`                  | `1` = active, `0` = deactivated                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `outcome.odds`                    | Decimal odds for this outcome                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| `outcome.probabilities`           | Implied probability (e.g. `0.41000`)                                                                                                                                                                                                                                                                                                                                                                                                                                     |

<Info>
  **Primary integration contract:** `market.id` (stable integer type code) + `specifiers` uniquely identifies a market type within a match context. `market.tt_market_id` is a unique UUID for each individual market instance — use it for bet placement and settlement correlation. `textEN` / `textRU` is the recommended way to display markets. The `type` and `specifiers` attributes are **optional convenience fields** for operators who want to build custom UIs or map markets programmatically. The `specifiers` attribute always includes `map=N` and `round=N`; it adds `player=Name` for player-scoped markets, and `competitor=Team` for team-scoped economy markets. You can safely ignore them.
</Info>

<Note>
  **Instance identity is stable (BB 3.5).** For the life of a market instance — across price moves, suspension/resume, and recovery replays — both the **outcome id** (always `1` for CS2 single-outcome markets) and the **specifier set** are fixed. They identify the instance, so they never change once first published; only odds/status move. The in-game **map name is deliberately not a specifier** (it is nullable and would otherwise appear mid-life and mutate the identity) — read it from the fixture/summary per-map data instead. So at T=0 and T=10s the same market is `id=1042 specifiers="map=1\|round=5"` with `outcome id="1"`, only the odds differing.
</Note>

<Note>
  **Specifier value form (BB 3.6).** For CS2, the `player` and `competitor` specifier values are the **raw display name** (e.g. `player=s1mple`, `competitor=Navi`) — `variable_text` in the catalog. The client renders them as-is; for CS2 this is acceptable (CS2 entities aren't translated or renamed). For a future football integration, team/player specifiers would instead carry a **stable id** (a competitor/player URN) that the client resolves to a per-locale, rename-stable name — or stay raw strings, accepting no localization and stale names on rename. That choice is part of the football build (see follow-up F-6); CS2 stays raw.
</Note>

### Market Status Codes

| Code | Status      | Description                                                     |
| ---- | ----------- | --------------------------------------------------------------- |
| `1`  | Active      | Bets can be accepted, odds are flowing                          |
| `0`  | Deactivated | Odds not shown, bets not accepted                               |
| `-1` | Suspended   | **Stop accepting bets immediately**                             |
| `-3` | Settled     | Result known — only in `<bet_settlement>` messages              |
| `-4` | Cancelled   | Market voided — return stakes — only in `<bet_cancel>` messages |

<Warning>
  When a market status changes to `-1` (suspended), you **must** immediately stop accepting bets. Do not wait for settlement.
</Warning>

### Bet Settlement

Published when markets are resulted.

```xml theme={null}
<bet_settlement product="2" timestamp="1711234590123"
                event_id="od:match:2588141"
                tt_match_id="98deb7ef-def9-47d4-854c-a13618952f99">
  <outcomes>
    <market id="1013"
            tt_market_id="b7e2c3d5-6789-4e0f-c123-bcdef2345678"
            type="grenade_kill"
            specifiers="map=1|round=5"
            status="-3"
            textEN="Grenade kill in Round 5"
            textRU="Убийство гранатой в раунде 5">
      <outcome id="1" result="1" void_factor="0"/>
    </market>
  </outcomes>
</bet_settlement>
```

Grade each bet from the **`(result, void_factor)` pair** — both are present on
every settled outcome, so you never assume a default:

| `void_factor`  | Settlement               | Payout                                                               |
| -------------- | ------------------------ | -------------------------------------------------------------------- |
| `0`            | Graded fully by `result` | `result="1"` → win the stake; `result="0"` → lose it                 |
| `0 < vf < 1`   | Half-result              | refund `vf` of the stake; grade the remaining `(1 − vf)` by `result` |
| `>= 1` (`1.0`) | Full void                | return the whole stake regardless of `result`                        |

```xml theme={null}
<!-- full void — whole stake returned: -->
<outcome id="1" result="0" void_factor="1.0"/>

<!-- half win — half the stake refunded, half graded as a win: -->
<outcome id="1" result="1" void_factor="0.5"/>
```

CS2 markets settle all-or-nothing, so today every outcome is either a clean
win/loss (`void_factor="0"`) or a full void (`"1.0"`). The fractional form is
part of the contract for partial voids (e.g. a future football half-result):
the exact payout formula is fixed and per-market, so a `0 < void_factor < 1`
value is unambiguous when it appears.

**A settled market lists every one of its active outcomes** — the winner and
each loser — so the losing bets actually receive a status and settle. CS2
markets are single-outcome propositions (one `<outcome id="1">`); a
multi-outcome market lists them all:

```xml theme={null}
<!-- a 1X2 market: every outcome is present, one wins, the rest lose -->
<market id="1" specifiers="…" status="-3">
  <outcome id="1" result="1" void_factor="0"/>  <!-- home -->
  <outcome id="2" result="0" void_factor="0"/>  <!-- draw -->
  <outcome id="3" result="0" void_factor="0"/>  <!-- away -->
</market>
```

<Note>
  When a market is settled, it is automatically removed from all subsequent `<odds_change>` messages.
</Note>

### Bet Cancel

Published when a market is voided — return all stakes.

```xml theme={null}
<bet_cancel product="2" timestamp="1711234600000"
            event_id="od:match:2588141"
            tt_match_id="98deb7ef-def9-47d4-854c-a13618952f99">
  <market id="1013"
          tt_market_id="b7e2c3d5-6789-4e0f-c123-bcdef2345678"
          type="grenade_kill"
          specifiers="map=1|round=5"
          void_reason_id="4"
          void_reason_params="custom=not_played"
          void_factor="1.0"
          textEN="Grenade kill in Round 5"
          textRU="Убийство гранатой в раунде 5" />
</bet_cancel>
```

Each market carries `void_factor` so the same `(result, void_factor)` payout
rule as `<bet_settlement>` applies: a cancel is a **full void** (`void_factor="1.0"`
→ return all stakes); a partial cancel carries a fraction `0 < void_factor < 1`.

Each market also carries a numeric `void_reason_id` — decode it into a
human-readable name and description from the catalog at
[`GET /v1/descriptions/{locale}/void_reasons`](/api-reference/uof-rest-compatibility#resource-paths)
(pull it once and store the map). The reason reflects the real cause (e.g. an
admin match cancel, an odds error), not a single default. (`void_reason_params`
is supplementary free text and need not be parsed.)

`<bet_cancel>` may also carry an **optional `start_time` / `end_time` refund
window** (unix-ms, on the root). When present, refund only bets placed inside
the window — a way to void a period of bad odds without cancelling legitimate
bets accepted outside it. When **absent**, refund every bet on the market (the
default; CS2 micro-markets always void whole-market).

```xml theme={null}
<bet_cancel product="2" event_id="od:match:2588141"
            start_time="1711234500000" end_time="1711234560000">
  <market id="1013" specifiers="map=1|round=5" void_reason_id="14"/>
</bet_cancel>
```

### Rollback Bet Settlement

Reverses a previous settlement due to an error. Rare — you will also be notified via your support channel.

```xml theme={null}
<rollback_bet_settlement product="2" timestamp="1711234610000"
                         event_id="od:match:2588141"
                         tt_match_id="98deb7ef-def9-47d4-854c-a13618952f99">
  <market id="1013"
          tt_market_id="b7e2c3d5-6789-4e0f-c123-bcdef2345678"
          type="grenade_kill"
          specifiers="map=1|round=5" />
</rollback_bet_settlement>
```

A rollback only reverses the **result** on our side; the original settlement
already pulled the market from the player-facing offer. So **every rollback is
followed by an `<odds_change>`** that re-lists the affected market with live
odds (`status="1"`) — that follow-up message is what puts it back in front of
bettors:

```xml theme={null}
<rollback_bet_settlement … event_id="od:match:2588141"/>
<odds_change product="2" event_id="od:match:2588141">
  <sport_event_status status="1"/>
  <odds>
    <market id="1013" specifiers="map=1|round=5" status="1">
      <outcome id="1" odds="2.10"/>
    </market>
  </odds>
</odds_change>
```

### Rollback Bet Cancel

Reverses a previous **`<bet_cancel>`** — the market was voided in error, so the
refund is undone and the market becomes valid again. Carries the same
`{id, specifiers}` market shape as `<rollback_bet_settlement>`. Rare; you will
also be notified via your support channel.

```xml theme={null}
<rollback_bet_cancel product="2" timestamp="1711234620000"
                     event_id="od:match:2588141"
                     tt_match_id="98deb7ef-def9-47d4-854c-a13618952f99">
  <market id="1013"
          tt_market_id="b7e2c3d5-6789-4e0f-c123-bcdef2345678"
          type="grenade_kill"
          specifiers="map=1|round=5" />
</rollback_bet_cancel>
```

A void can **only** be reversed by `<rollback_bet_cancel>` — `<rollback_bet_settlement>`
reverses a prior *settlement*, never a cancel, so without this message a
mistaken void would be permanent. As with a settlement rollback, the market is
put back in front of bettors by the **follow-up `<odds_change>`** that re-lists
it with live odds (the rollback only reverses the result on our side).

If the original `<bet_cancel>` carried a `start_time` / `end_time` refund
window, the `<rollback_bet_cancel>` carries the **same** window, so the reversal
applies to exactly the bets that were voided.

### Fixture Change

Notifies about match lifecycle events. Re-fetch the fixture from the REST API when you receive this message.

```xml theme={null}
<fixture_change product="2" timestamp="1711234500000"
                event_id="od:match:2588141"
                tt_match_id="98deb7ef-def9-47d4-854c-a13618952f99"
                change_type="1"
                status="live">
  <tournament id="tt:tournament:7438" hltv_event_id="7438" name="ESL Pro League S20">
    <sport id="3" name="Counter-Strike 2" abbreviation="CS2"/>
  </tournament>
  <competitors>
    <competitor id="tt:competitor:7187" hltv_id="7187" qualifier="home" name="Sashi"
                icon_path="https://images.ticktock.bet/teams/sashi.png"/>
    <competitor id="tt:competitor:5995" hltv_id="5995" qualifier="away" name="OG"/>
  </competitors>
</fixture_change>
```

| Attribute / element | Meaning                                                                                                                                                                                                                                                                                                                                            |
| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `change_type`       | UOF-compatible event code (see table below)                                                                                                                                                                                                                                                                                                        |
| `status`            | High-level hint for the new/cancelled transitions — `upcoming` \| `live` \| `cancelled`. Use it to distinguish a fresh schedule entry (`status="upcoming"`, `change_type="1"`) from a match that just kicked off (`status="live"`, `change_type="1"`). It is **not** the lifecycle driver — that is `<sport_event_status>` (see below).            |
| `<tournament>`      | Nested element carrying the tournament reference: `id` (`tt:tournament:{hltv_event_id}`, the same key `GET /sports/{locale}/tournaments/{id}/info` resolves), `hltv_event_id`, `name`, and a nested `<sport>`. Read `tournament.id` verbatim to fetch the tournament. Omitted when the match has no tournament name.                               |
| `<competitors>`     | The two teams as nested `<competitor>` elements, each with a `qualifier` (`home` / `away`), an `id` (`tt:competitor:{hltv_id}`, the same key `GET /sports/{locale}/competitors/{id}/profile` resolves), `hltv_id`, `name`, and an optional `icon_path` (team logo). Resolve home/away from the qualifier; enrich name/logo by the competitor `id`. |

| `change_type` | Meaning                                                     |
| ------------- | ----------------------------------------------------------- |
| `1`           | New fixture with markets                                    |
| `2`           | Start time changed                                          |
| `3`           | Match cancelled — **remove the match**                      |
| `5`           | Coverage changed — **coverage only**, never a finish signal |
| `106`         | Presentation / stream URL changed                           |

<Warning>
  **`change_type="5"` does not mean "finished".** A match ending arrives as an
  `<odds_change>` carrying `<sport_event_status status="4">` (see [Sport-event
  status](#sport-event-status-bb-4-1)), **not** as a fixture change. Treating a
  `change_type="5"` coverage update as the end of a match — or expecting a finish
  on `fixture_change` — leaves finished matches open indefinitely. The only
  `fixture_change` that removes a match is `change_type="3"` (cancelled);
  everything else is metadata you can apply by re-fetching the fixture.
</Warning>

```xml theme={null}
<!-- match finished — final score on the status element: -->
<odds_change product="2" timestamp="1711234600000" event_id="od:match:2588141">
  <sport_event_status status="4" home_score="2" away_score="1" winner_id="home"/>
</odds_change>
```

### Alive

Heartbeat sent every 10 seconds per producer. Use it to validate feed connectivity.

Discover producers through `GET /v1/descriptions/producers` before starting
monitoring. The catalog's `producer.id` is identical to the `product`
attribute on every message from that producer, including recovery messages.

```xml theme={null}
<alive product="2" timestamp="1711234570000" subscribed="1"/>
```

| `subscribed` | Meaning                                                               |
| ------------ | --------------------------------------------------------------------- |
| `1`          | Normal — feed is healthy                                              |
| `0`          | Error or producer restart — suspend all markets and initiate recovery |

<Warning>
  If you do not receive an `<alive>` message within 20 seconds, consider the connection stale. Suspend all markets and reconnect.
</Warning>

## Market Ordering

Each `<odds_change>` message delivers up to **3 markets per round**, already sorted in the recommended display order.

The feed applies two selection constraints:

1. **Diversity** — each market comes from a different category (e.g. opening kills, bomb events, round dynamics) to give bettors variety.
2. **Timing order** — markets are ordered earliest-resolving first. The first market in the message resolves soonest in the round, the last resolves latest. This minimises risk from feed-delivery delays.

| Position in message | Resolves       | Typical categories                           |
| ------------------- | -------------- | -------------------------------------------- |
| First               | Early in round | Opening kills, weapon kills                  |
| Second              | Mid-round      | Multi-kills, round dynamics, highlight kills |
| Third               | Late in round  | Bomb events, economy, flawless               |

<Tip>
  **Recommended display pattern:** show the first market in the message prominently and reveal subsequent markets as the bettor interacts. The feed's ordering already reflects what to surface first — no custom sorting logic needed.
</Tip>

## Localisation

Every `<market>` element carries both English and Russian descriptions:

| Attribute | Language | Example                        |
| --------- | -------- | ------------------------------ |
| `textEN`  | English  | "Grenade kill in Round 4"      |
| `textRU`  | Russian  | "Убийство гранатой в раунде 4" |

Outcome elements carry `active`, `odds`, and `probabilities` attributes — localised text is on the parent `<market>` element only.

<Tip>
  **Simplest integration path:** take `textEN` (or `textRU`) directly from the message and display it to the end-user. No template engine, no catalogue look-up, no string interpolation — the text is ready to show as-is.

  **Template path:** for operators who prefer to build market names locally, fetch the catalogue once at integration time from [`GET /cs2/v1/markets/descriptions`](/api-reference/markets/descriptions) (or `descriptions.xml` for UOF-style XML) — every entry carries `template` and `template_ru` strings such as `"{player} gets opening kill — Round {round}"`. Substitute the placeholders with the matching keys from the `specifiers` attribute on each live message:

  | Placeholder    | Specifier    | Notes                                                                          |
  | -------------- | ------------ | ------------------------------------------------------------------------------ |
  | `{round}`      | `round`      | always present                                                                 |
  | `{map}`        | `map`        | always present — BO map number                                                 |
  | `{player}`     | `player`     | player-scoped markets (`player_opening`, `awp_player_kill`, `star_multi_kill`) |
  | `{competitor}` | `competitor` | team-scoped economy markets (`eco_upset`, `eco_frags`, `force_buy_win`)        |
</Tip>

## Reconnection and recovery

RabbitMQ persistent queues retain messages while your consumer is disconnected.
Messages delivered to a durable queue are not lost if your service restarts — they will be re-delivered when you reconnect.

Use `aio_pika.connect_robust` (or equivalent in your AMQP client) for automatic reconnection with exponential backoff.

After reconnecting, call [`POST /cs2/v1/recovery/initiate_request`](/api-reference/recovery) with the timestamp of the last message you processed and a positive integer `request_id`. The server republishes the current state of every active market plus settlements and voids that happened since `after`. Every recovery XML root carries that exact `request_id`, including the terminating `<snapshot_complete>`. Use the XML attribute for live/recovery classification and correlation. The `recovery=1` header remains available as supplementary metadata. Recovery routing keys use priority `lo` and echo the optional positive integer `node_id` in their final section.

## Code Example

The following example uses [`aio-pika`](https://aio-pika.readthedocs.io/) (Python asyncio AMQP client):

```python theme={null}
import asyncio
import aio_pika
from xml.etree import ElementTree


AMQP_URL = "amqp://op-acme-sports:<password>@mq.ticktock.bet:5672/acme-sports"
EXCHANGE = "ttfeed"


async def main():
    connection = await aio_pika.connect_robust(AMQP_URL)
    channel = await connection.channel()

    exchange = await channel.declare_exchange(
        EXCHANGE, aio_pika.ExchangeType.TOPIC, durable=True, passive=True,
    )

    queue = await channel.declare_queue("ticktock-all", durable=True)
    await queue.bind(exchange, routing_key="hi.-.live.#")
    await queue.bind(exchange, routing_key="-.-.-.alive.-.-.-.-")

    async with queue.iterator() as q:
        async for message in q:
            async with message.process():
                xml_body = message.body.decode()
                routing_key = message.routing_key
                # Extract message type from 4th section of routing key
                msg_type = routing_key.split(".")[3] if routing_key else "unknown"

                if msg_type == "odds_change":
                    root = ElementTree.fromstring(xml_body)
                    event_id = root.get("event_id")
                    for market in root.findall(".//market"):
                        status = market.get("status")
                        text = market.get("textEN")
                        if status == "-1":
                            suspend_market(market.get("id"))
                        elif status == "1":
                            for outcome in market.findall("outcome"):
                                update_odds(market.get("id"), text, outcome.get("odds"))

                elif msg_type == "bet_settlement":
                    root = ElementTree.fromstring(xml_body)
                    for market in root.findall(".//market"):
                        for outcome in market.findall("outcome"):
                            grade_bet(market.get("id"), outcome.get("result"))

                elif msg_type == "alive":
                    pass  # heartbeat — feed is healthy


asyncio.run(main())
```

## Support

For credentials, connectivity issues, or integration questions: [b2b@ticktock.bet](mailto:b2b@ticktock.bet)
