# Integration quickstart

From a token to a market price, in five requests.

Every example uses a dummy token. Replace it with yours, and keep it out of
source control.

```
HELIXODDS_TOKEN=LI-EXAMPLE-TOKEN-DO-NOT-USE
```

## 0. Before you start

Your account needs two things, both done once in the panel:

1. an active subscription — `market_engine`, `live_intelligence`, or both
2. your own provider credential, connected and verified

Until the provider is connected, data endpoints return `PROVIDER_NOT_CONNECTED`.
Until the first processing cycle finishes, they return `DATA_NOT_READY`. Both are
expected states, not failures to debug.

Check with one request:

```bash
curl -sS https://api.helixodds.com/api/v1/status \
  -H "Authorization: Bearer $HELIXODDS_TOKEN"
```

Look at `health.components`: `market_engine`, `live_intelligence` and
`provider_connection` each report `HEALTHY`, `DEGRADED`, `DOWN` or
`UNCONFIGURED`. `UNCONFIGURED` means not set up, which is different from broken.

## 1. Page through your fixtures

```bash
curl -sS "https://api.helixodds.com/api/v1/fixtures?limit=50" \
  -H "Authorization: Bearer $HELIXODDS_TOKEN"
```

```json
{
  "data": [
    {
      "fixture_id": "fx_0011bd8f26e2625aae7576f3",
      "home": "Kasem Bundit University U21",
      "away": "Chonburi U21",
      "league": "Thailand U21 League",
      "starts_at": "2026-10-10T08:30:00.000Z",
      "market_count": 22,
      "provenance": { "source_provider": "betsapi", "provider_fixture_id": "202419381" }
    }
  ],
  "pagination": { "limit": 50, "next_cursor": "eyJrIjoiZnhfMDAxMSJ9", "has_more": true },
  "meta": {
    "fixture_count": 1348,
    "provider_fetched_at": "2026-10-08T08:21:33.261Z",
    "processed_at": "2026-10-08T08:22:04.703Z",
    "age_ms": 184000
  }
}
```

Follow `next_cursor` until `has_more` is `false`.

## 2. Read a fixture's markets

```bash
curl -sS "https://api.helixodds.com/api/v1/fixtures/fx_0011bd8f26e2625aae7576f3/markets" \
  -H "Authorization: Bearer $HELIXODDS_TOKEN"
```

```json
{
  "data": [
    {
      "market_id": "mk_3f9a1c4e77b20d58",
      "market": "TOTAL_GOALS",
      "market_name": "Goals Over/Under",
      "period": "FT",
      "origin_type": "MAPPED",
      "derivation": { "version": "canonical_v4" },
      "selections": [
        { "selection_id": "sl_9d1e...", "selection": "Over", "line": "2.5",
          "odds": { "fractional": "11/10", "decimal": 2.1 },
          "quality": { "status": "ok", "publishable": true, "reasons": [] } }
      ]
    }
  ]
}
```

Two fields decide whether you should show a price:

- `origin_type` — `MAPPED` means a canonical market identity was established.
  `RAW` means the market exists upstream and we could not prove a canonical
  identity for it. We report it rather than dropping it; treat it with care.
- `quality.publishable` — `false` means we do not vouch for this selection.
  `quality.reasons` says why.

## 3. Follow changes instead of polling

```bash
# first call: get a cursor
curl -sS "https://api.helixodds.com/api/v1/changes?limit=200" \
  -H "Authorization: Bearer $HELIXODDS_TOKEN"

# then, repeatedly
curl -sS "https://api.helixodds.com/api/v1/changes?cursor=eyJzIjo0MTd9" \
  -H "Authorization: Bearer $HELIXODDS_TOKEN"
```

Each event means "this fixture was refreshed in this cycle". Re-read the
fixtures you care about. An empty page means nothing changed — the normal case
most of the time.

The engine runs hourly, so polling `/changes` once a minute is generous and once
every five minutes is plenty. Polling every second buys nothing and spends your
rate limit.

## 4. Live, if you hold it

```bash
curl -sS "https://api.helixodds.com/api/v1/live" \
  -H "Authorization: Bearer $HELIXODDS_TOKEN"

curl -sS "https://api.helixodds.com/api/v1/live/fx_.../momentum" \
  -H "Authorization: Bearer $HELIXODDS_TOKEN"
```

If `momentum` is `null`, read `unavailable_reason`. A momentum of 50 would be
indistinguishable from "balanced", so we do not fabricate one.

See [Live Intelligence](live-intelligence.md).

---

# In your language

Each example does the same thing: fetch one page of fixtures and print the first
fixture's canonical markets. All of them read the token from the environment.

## Node.js

```js
const BASE = 'https://api.helixodds.com/api/v1';
const TOKEN = process.env.HELIXODDS_TOKEN;

async function get(path) {
  const response = await fetch(BASE + path, {
    headers: { Authorization: 'Bearer ' + TOKEN },
  });
  const body = await response.json();
  if (!response.ok) {
    // Branch on the code, never on the message.
    throw new Error(body.error.code + ' (request ' + body.error.request_id + ')');
  }
  return body;
}

const page = await get('/fixtures?limit=10');
console.log(page.meta.fixture_count, 'fixtures, fetched', page.meta.provider_fetched_at);

const first = page.data[0];
const markets = await get('/fixtures/' + first.fixture_id + '/markets');
for (const market of markets.data) {
  if (market.origin_type !== 'MAPPED') continue;
  for (const selection of market.selections) {
    if (!selection.quality.publishable) continue;
    console.log(market.market, market.period, selection.selection,
      selection.line, selection.odds.decimal);
  }
}
```

Walking every page:

```js
let cursor = null;
do {
  const page = await get('/fixtures?limit=200' + (cursor ? '&cursor=' + encodeURIComponent(cursor) : ''));
  for (const fixture of page.data) handle(fixture);
  cursor = page.pagination.next_cursor;
} while (cursor);
```

## Python

```python
import os
import urllib.parse
import requests

BASE = "https://api.helixodds.com/api/v1"
TOKEN = os.environ["HELIXODDS_TOKEN"]
SESSION = requests.Session()
SESSION.headers["Authorization"] = f"Bearer {TOKEN}"


def get(path):
    response = SESSION.get(BASE + path, timeout=30)
    body = response.json()
    if not response.ok:
        error = body["error"]
        raise RuntimeError(f"{error['code']} (request {error['request_id']})")
    return body


page = get("/fixtures?limit=10")
print(page["meta"]["fixture_count"], "fixtures, fetched", page["meta"]["provider_fetched_at"])

first = page["data"][0]
markets = get(f"/fixtures/{first['fixture_id']}/markets")
for market in markets["data"]:
    if market["origin_type"] != "MAPPED":
        continue
    for selection in market["selections"]:
        if not selection["quality"]["publishable"]:
            continue
        print(market["market"], market["period"], selection["selection"],
              selection["line"], selection["odds"]["decimal"])
```

Following changes:

```python
cursor = None
while True:
    query = f"?cursor={urllib.parse.quote(cursor)}" if cursor else ""
    page = get("/changes" + query)
    for event in page["data"]:
        refresh(event["fixture_id"])
    cursor = page["pagination"]["next_cursor"]
    time.sleep(60)
```

## PHP

```php
<?php
const BASE = 'https://api.helixodds.com/api/v1';

function helixodds_get(string $path): array {
    $handle = curl_init(BASE . $path);
    curl_setopt_array($handle, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 30,
        CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . getenv('HELIXODDS_TOKEN')],
    ]);
    $raw    = curl_exec($handle);
    $status = curl_getinfo($handle, CURLINFO_RESPONSE_CODE);
    curl_close($handle);

    $body = json_decode($raw, true);
    if ($status >= 400) {
        throw new RuntimeException(
            $body['error']['code'] . ' (request ' . $body['error']['request_id'] . ')'
        );
    }
    return $body;
}

$page = helixodds_get('/fixtures?limit=10');
printf("%d fixtures, fetched %s\n",
    $page['meta']['fixture_count'], $page['meta']['provider_fetched_at']);

$first   = $page['data'][0];
$markets = helixodds_get('/fixtures/' . $first['fixture_id'] . '/markets');
foreach ($markets['data'] as $market) {
    if ($market['origin_type'] !== 'MAPPED') { continue; }
    foreach ($market['selections'] as $selection) {
        if (!$selection['quality']['publishable']) { continue; }
        printf("%s %s %s %s %s\n", $market['market'], $market['period'],
            $selection['selection'], $selection['line'],
            $selection['odds']['decimal']);
    }
}
```

## C#

```csharp
using System.Net.Http.Headers;
using System.Text.Json;

const string Base = "https://api.helixodds.com/api/v1";

using var client = new HttpClient { BaseAddress = new Uri(Base + "/") };
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
    "Bearer", Environment.GetEnvironmentVariable("HELIXODDS_TOKEN"));

async Task<JsonDocument> GetAsync(string path)
{
    var response = await client.GetAsync(path);
    var document = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
    if (!response.IsSuccessStatusCode)
    {
        var error = document.RootElement.GetProperty("error");
        throw new InvalidOperationException(
            $"{error.GetProperty("code").GetString()} " +
            $"(request {error.GetProperty("request_id").GetString()})");
    }
    return document;
}

var page = await GetAsync("fixtures?limit=10");
var meta = page.RootElement.GetProperty("meta");
Console.WriteLine($"{meta.GetProperty("fixture_count").GetInt32()} fixtures, " +
                  $"fetched {meta.GetProperty("provider_fetched_at").GetString()}");

var firstId = page.RootElement.GetProperty("data")[0]
    .GetProperty("fixture_id").GetString();
var markets = await GetAsync($"fixtures/{firstId}/markets");

foreach (var market in markets.RootElement.GetProperty("data").EnumerateArray())
{
    if (market.GetProperty("origin_type").GetString() != "MAPPED") continue;
    foreach (var selection in market.GetProperty("selections").EnumerateArray())
    {
        if (!selection.GetProperty("quality").GetProperty("publishable").GetBoolean()) continue;
        Console.WriteLine($"{market.GetProperty("market").GetString()} " +
                          $"{selection.GetProperty("selection").GetString()} " +
                          $"{selection.GetProperty("odds").GetProperty("decimal").GetDouble()}");
    }
}
```

## Four things that will save you a support message

1. **Branch on `error.code`, never on `error.message`.** Codes are frozen;
   messages are prose and may be reworded.
2. **Keep `error.request_id`.** It is the only key that finds your request in our
   logs.
3. **Treat cursors as opaque.** Do not parse one, build one, or move one between
   collections.
4. **Respect `Retry-After`.** A retry inside the window spends the next one.

See also: [Public API](public-api-v1.md) · [Errors](errors.md) ·
[Provider setup](provider-betsapi.md)
