Foreline/Документация /Руководство по интеграции

API · v1 · интеграция

Три продукта, один ключ, один идентификатор

Это практическое дополнение к референсу: что отправлять, в каком порядке и какие ошибки стоят рабочего дня. Предполагается, что ключ у вас уже есть и обзор вы прочитали.

Начать с каталога → Каталог — первым делом. Все остальные вызовы принимают наш event_id, и фолбэка по названиям команд нет.
Каталог событий и идентификаторы

Наши идентификаторы, сопоставленные один раз, на вашей стороне

Мы не угадываем, какое из ваших событий соответствует нашему. Вы забираете каталог, один раз сопоставляете его со своими id — и дальше каждый вызов несёт наш event_id. Это осознанный выбор: молчаливый мэтчинг по именам, верный в 98% случаев, — это дефект, который находят при расчёте, а не на тестах.

GET/v1/catalog/events скоуп surface · bulk · 4/мин

Все события, которые мы сейчас отдаём, со всеми полями, нужными для сопоставления: наш id, спорт, лига, обе команды, время начала и список рынков, которые мы по этому событию считаем. По умолчанию JSON, по запросу CSV.

Параметры

ПараметрТипОписание
sportстрока Ограничить одним спортом из вашего ключа. Спорт вне ключа даёт 403 со списком доступных вам спортов — а не пустую страницу, которую легко принять за «нет событий».
hoursчисло Только события, стартующие в ближайшие N часов. Именно на этом параметре и стоит строить ежедневную задачу сопоставления.
limitцелое Максимум строк в ответе. По умолчанию 2000.
formatстрока json (по умолчанию) или csv. CSV приходит с шапкой столбцов и именем файла в Content-Disposition — его можно сразу открыть в таблице или скормить скрипту мэтчинга.

Ответ — events[]

ПолеТипОписание
event_idцелое Наш идентификатор. Стабилен на всю жизнь события. Это единственный ключ, который принимают остальные эндпоинты.
sport, leagueстрока Спорт и лига в нашем справочнике.
home, awayстрока Названия команд так, как их называет наш референс, — подспорье для сопоставления, но не идентификатор.
startsвремя Начало матча, ISO-8601 UTC с суффиксом Z.
marketsмассив Что мы по этому событию действительно считаем: moneyline, spread, totals. Каталог никогда не шире выдачи: если рынок указан здесь — GET /v1/surface?event_id= его отдаст.
updated_tsчисло Когда мы последний раз видели тик по этому событию.
n_unnamedцелое Мета ответа. Сколько строк на этой странице имеют id, но пока не имеют имён команд, — см. примечание ниже. На обычном окне до старта здесь ноль.
# задача сопоставления: всё, что стартует в ближайшие 48 часов, в CSV
curl -s "https://api.foreline.io/v1/catalog/events?hours=48&format=csv" \
  -H "X-Foreline-Key: $FORELINE_KEY"

event_id,sport,league,home,away,starts,markets,updated_ts
1632737351,football,UEFA - Champions League Qualifiers,Shamrock Rovers,Ararat-Armenia,2026-07-28T19:00:00Z,moneyline|spread|totals,1785262578.971

# тот же срез в JSON
curl -s "https://api.foreline.io/v1/catalog/events?hours=48" \
  -H "X-Foreline-Key: $FORELINE_KEY"

В CSV поле markets склеено вертикальной чертой (moneyline|spread|totals), чтобы запятая внутри названия лиги не заставляла вас разбирать правила экранирования. Значения в примерах — иллюстративные.

GET/v1/catalog/leagues скоуп surface · bulk · 4/мин

Тот же срез, свёрнутый до уникальных лиг: сколько в каждой событий и когда ближайший старт. Принимает те же sport, hours и format. Удобно сверить покрытие со своим списком турниров до того, как сопоставлять тысячи матчей.

league,sport,events,next_start
Argentina - Liga Pro,football,4,2026-07-28T22:00:00Z
USA - Major League Soccer,football,15,2026-07-29T00:00:00Z

Что происходит, если идентификатор не наш

Ничего не додумывается и ничего не теряется молча. Каждый эндпоинт, принимающий событие, отбивает строку явно и говорит, какую именно, — так кривое сопоставление всплывает на первом же вызове, а не при сверке через несколько недель.

ГдеЧто вы получите
POST /v1/score rejected[] с кодом unknown_event и самим event_id. Число принятых строк возвращается отдельно, поэтому «принято + отбито» всегда равно тому, что вы прислали.
POST /v1/book-quotes rejected[] с кодом unknown_event и индексом строки в вашем батче. Остальной батч обрабатывается как обычно.
GET /v1/surface?event_id= 404, если такого события у нас нет, и 403, если есть, но его спорт вне вашего ключа.
  • Эндпоинта resolve нет. В этом релизе мы не предлагаем хелпер «пришлите названия команд, а мы найдём событие». Хелпер, который иногда ошибается, переносит нашу ошибку в ваш расчёт, и публиковать его без измеренных цифр точности рядом мы не готовы.
  • Забирайте каталог до старта, а не после. Названия команд и лига приходят из карточки матча, а карточка может выпасть из живого окна, когда матч уже идёт. Строка при этом сохраняет свой event_id и полностью работоспособна, но имена могут оказаться пустыми. Счётчик n_unnamed в ответе показывает, сколько таких строк на странице, — ставьте задачу сопоставления на ?hours=, и она их вообще не увидит.
  • Наш event_id — целое число, и он не меняется ни при движении линии, ни при появлении нового рынка, ни при переносе матча.
Радар игроков

Скоринг счетов: POST /v1/score

Вы присылаете ставки счёта, адресованные нашим event_id; мы пересчитываем каждую против нашего честного клоза на той самой линии, которую взял игрок, и ведём текущий скор счёта.

POST/v1/score скоуп p1 · point · 60/мин

Тело запроса

ПолеТипОписание
account_idстрока Ваш обезличенный ключ счёта. Для нас непрозрачен — нужен только чтобы группировать ставки в один скор. Персональные данные не принимаются и не требуются.
sportстрока Необязательно; по умолчанию football.
bets[].event_idцелое Обязательно. Наш id из каталога.
bets[].marketстрока spread (синонимы ah, handicap), totals (ou, total) или moneyline (1x2, ml).
bets[].lineчисло Взятая линия — любая ступень, не только главная. Конвенция знака та же, что в поверхности; см. разобранные примеры.
bets[].sideстрока home, away, over, under.
bets[].odds, bets[].stakeчисло Десятичный коэффициент, который получил клиент, и сумма ставки в вашей учётной валюте.
bets[].placed_atвремя Когда вы приняли ставку. Только прематч — ставка со временем после начала матча отбивается кодом in_play.
curl -s -X POST "https://api.foreline.io/v1/score" \
  -H "X-Foreline-Key: $FORELINE_KEY" \
  -H "Content-Type: application/json" \
  -d '{"account_id": "a_7731",
       "bets": [{"event_id": 1632737351, "market": "ah", "line": -0.25,
                 "side": "home", "odds": 1.95, "stake": 120,
                 "placed_at": "2026-07-28T14:02:11Z"}]}'

Ответ

ПолеТипОписание
acceptedцелое Сколько ставок принято в счёт.
rejected[]массив По записи на каждую непринятую строку: индекс i, code и человекочитаемое пояснение note. Коды — в таблице ниже.
n_bets / n_scored / n_pending целое Сколько ставок увидели, сколько уже посчитали и сколько ждут закрытия своего события.
scoreчисло Текущий скор счёта — нормированный CLV по посчитанным ставкам.
tstat, sign, sign_p Две компоненты за скором: величина (tstat) и устойчивость (sign в виде плюсовых/всего и p-значение).
flag, flag_k, flag_src Поднят ли флаг, на каком числе посчитанных ставок и какой компонентой (clv, sign или both).
bet_signals[]массив По каждой ставке, если по этому событию и рынку у нас есть живой прогноз: ожидаемый CLV ставки и то, идёт ли она в сторону нашего прогноза. Это сигнал для контроля лимита — он существует в момент приёма, когда у счёта ещё нет истории.

Коды отказа

КодЧто значитЧто делать
unknown_eventНет event_id или он не наш. Поправить сопоставление по каталогу.
event_not_scorableИдентификатор наш, но событие ещё не закрылось — считать против клоза пока не от чего. Прислать ставку повторно после расчёта события. Для ставки, отправленной в момент приёма, это нормальный ответ.
unknown_marketРынок вне spread|totals|moneyline. Отбросить или смэтчить на своей стороне.
in_playplaced_at не раньше начала матча. Так и задумано — скор считается по прематчу.
bad_rowКривая строка или время. Починить и переслать эту строку; остальной батч уже обработан.

Чекпоинты: когда может появиться флаг

Счёт не пересуживается на каждой ставке. Две компоненты проверяются на фиксированных чекпоинтах, считаемых в посчитанных ставках, и флаг поднимается в первый момент, когда любая из них переходит свой порог:

КомпонентаЧекпоинты (посчитанных ставок)Что меряет
CLV (величина)5, 10, 20 Насколько счёт обыгрывает наш честный клоз — с учётом собственного разброса, чтобы один крупный выброс не вытянул картину.
Устойчивость знака10, 20, 30, 50 Насколько часто счёт оказывается на верной стороне клоза — против случайности.
  • До 5 посчитанных ставок скор информативен, но флага не будет. Он возвращается и он настоящий, однако автоматическое урезание лимитов на него вешать не нужно.
  • Флаг «липкий». flag_k фиксирует чекпоинт, на котором он сработал, — счёт, прошедший порог на 10 ставках, не теряет флаг на 20-й, и у вас остаётся след, почему решение было принято.
  • Пороги настраиваются по договору на онбординге, поэтому строгость скора меняется без единой правки в вашей интеграции. Бюджет ложных срабатываний, из которого они выведены, указан в референсе.

Батч или live — по задаче

Батч

Отправьте рассчитанную историю счёта одним вызовом. Каждая ставка, чьё событие уже закрылось, считается сразу, и вы получаете полную картину — скор, чекпоинты, флаг — в том же ответе. Это форма для онбординга существующей базы клиентов и для ретро-аудита.

Live

Отправляйте каждую ставку в момент приёма. Состояние счёта обновляется сразу, запись bet_signals доступна тут же; CLV-компонента появляется после закрытия события. Ставки, присланные до клоза, возвращаются с кодом event_not_scorable — поставьте их в очередь и перешлите либо гоняйте ночную досылку батчем.

Обе формы приводят к одному и тому же состоянию по одним и тем же ставкам. Дважды присланная ставка засчитывается дважды — дедуплицируйте на своей стороне.

Радар линий

Алерты и опциональный поток ваших цен

Радар линий работает сразу на нашей собственной детекции: движения главных линий и алерты из них. Присылать нам свои цены — отдельный необязательный шаг, именно он превращает «рынок сдвинулся» в «ваша цена отличается от честной на столько-то».

GET/v1/alerts скоуп p2 · events · 12/мин

Журнал алертов, свежие сверху, с фильтрами since и status. Алерты, рождённые из ваших цен, привязаны к вашему ключу и видны только вам — другой подписчик Радара вашу линию не увидит. Опрашивайте журнал в своём темпе или получайте те же события потоком.

POST/v1/book-quotes опция · скоуп p2 · quotes · 4/мин

Это выбор, а не требование. Вы присылаете цены, которые сейчас показываете; мы снимаем с них вашу маржу, сравниваем каждую с честной на той же ступени и кладём всё, что вышло за ваш порог, в тот самый журнал, который вы уже читаете через GET /v1/alerts. Ничего о вашей линии не выводится из сторонних источников: не прислали цены — значит, их у нас нет.

Тело запроса

{ "quotes": [
    { "event_id": 1632737351, "market": "spread", "line": -0.25,
      "price_home": 1.95, "price_away": 1.95 },
    { "event_id": 1632737351, "market": "totals", "line": 2.75,
      "price_over": 1.90, "price_under": 1.98 },
    { "event_id": 1632737351, "market": "1x2",
      "price_home": 2.10, "price_draw": 3.40, "price_away": 3.60 }
] }
  • Нужны все стороны рынка. Пока с ваших цен не снята маржа, сравнивать не с чем, а для этого нужны обе (или все три) цены. Односторонняя строка отбивается.
  • Цены — десятичные коэффициенты ровно в том виде, в каком вы их показываете. Мы никогда не просим вашу честную цену или размер маржи — маржу мы выводим из самих цен.
  • Линия обязана быть ступенью нашей лестницы по этому событию и рынку: главная линия плюс ступени, которые мы вокруг неё публикуем. Линию, которую мы не считаем, мы отбиваем, а не приближаем.

Конвенция знака форы — прочитайте дважды

Поле line на рынке spread — это фора ХОЗЯЕВ, и p_home растёт вместе с ростом line, ровно как в surface.markets.spread.rungs[].line. Если у вас фора хранится от гостей или как положительное число рядом с той стороной, которая её получает, — переведите её перед отправкой. Это самый частый дефект интеграции на этом эндпоинте.

lineЧитается какЧестная p_homeПочему так
-0.50Хозяева дают полмяча — нужна чистая победа 0.4246Самый тяжёлый из трёх вариантов для хозяев, отсюда и самая низкая вероятность.
0.00Без форы — ничья возвращает ставку 0.5670Снятие полумяча поднимает хозяев примерно на 14 пп на этой лестнице.
+0.50Хозяева получают полмяча — ничья выигрывает 0.7015Выше line — выше p_home. Всегда.

Разбор перевода. Ваш трейдер выставил «Арарат-Армения −0,5» — фору даёт гостевая команда. Это значит, что полмяча получают хозяева, то есть вы присылаете line: +0.5, где price_home — цена на «Шемрок Роверс», а price_away — на «Арарат-Армению». Пришлёте -0.5 — и мы честно сравним вашу цену со ступенью, отстоящей примерно на 28 пп, и выдадим алерт на расхождение, которого нет.

Проверка перед боем: возьмите GET /v1/surface?event_id= по одному событию, отправьте честные цены обратно как котировки и убедитесь, что в ответе нет ни одного алерта. Правильно подключённая интеграция даёт нулевой гэп на собственных числах. Значения лестницы выше — иллюстративные.

Ответ и коды отказа

ПолеОписание
accepted / rejected[] Сколько строк сверили и какие сверять не стали — с индексом i в батче, кодом и пояснением. Одна кривая строка никогда не роняет батч целиком.
alerts[] / alerts_n Алерты, рождённые этим батчем и уже записанные в журнал. В каждом — gap_pp, direction и exposed_side.
next_recommended_s Через сколько секунд слать следующий батч. Сейчас 30.
alert_threshold_pp / alert_threshold_note Порог, применённый к вашему ключу, и его дисклеймер — в каждом ответе.
snapshot_ts / server_ts Когда сохранён ваш последний принятый батч и сколько на наших часах сейчас.
Код отказаЧто значит
unknown_eventНет event_id, он не наш или по событию сейчас нет посчитанной поверхности.
unknown_marketРынок вне spread|totals|moneyline либо у этого события такого рынка нет.
unknown_lineЛинии нет на нашей лестнице. В пояснении — главная линия и диапазон ступеней, которые мы публикуем: сразу видно, насколько вы промахнулись.
bad_priceЦена отсутствует или не число, цена вне разумных границ либо букcумма, которой не бывает у настоящего рынка.
sport_not_in_subscriptionСпорт этого события не входит в ваш ключ.
bad_rowСтрока не является объектом.

Каденс и порог

  • Батч раз в 30–60 секунд. Данные под нами меняются в лучшем случае раз в минуту, поэтому более частый цикл сравнивает ваши цены с теми же честными числами. Класс квоты разрешает четыре батча в минуту — вдвое щедрее рекомендации, чтобы один ретрай после сетевого сбоя не выбивал вас из окна.
  • Один батч, много строк. Присылайте всю активную линию одним POST, а не запросом на котировку; потолок батча большой, а построчные отказы позволяют продолжать работу, пока вы чините сопоставление.
  • Закрытие гэпа не теряется. Присылайте каждый цикл все активные цены, а не только изменившиеся: алерт закрывается статусом withdrawn только тогда, когда мы видим исправленную цену.
  • alert_threshold_pp — настройка, а не статистика. Значение по умолчанию 2,0 пп не выведено статистически: оно калибруется на вашем собственном фиде при онбординге, и ответ повторяет этот дисклеймер каждый раз, чтобы ниже по течению его никто не принял за измеренную константу. Попросите нас изменить порог — он применится без перезапуска на вашей стороне.
Защита экспрессов

Две проверки: одна на выдаче цены, одна по истории

Риск комбинаций — это не фид, на который подписался и забыл. Это проверка, которую вы делаете перед тем, как вернуть цену, и периодический взгляд назад: во сколько вам обошлись те цены, которые вы уже вернули.

Проверка на котировке

В момент, когда клиент собирает комбинацию, возьмите маргиналы её ног через GET /v1/surface?event_id= — класс point, 60 в минуту, он рассчитан ровно на это. Сравните цену, которую собирается вернуть ваш билдер, с честными маргиналами и их полосами. Ноги из одного события — там, где течёт наивный билдер «перемножь ноги»: исход и результативность коррелированы в любой лиге, а совместная цена, снятая с одной матрицы, — это то, что держит вашу комбинацию согласованной с одиночниками рядом на вашем же сайте.

Ретро-батч

Пришлите период рассчитанной истории комбинаций и получите обратно, где реально сидела утечка — по семействам, по числу ног, по лигам, — а не агрегатную маржу, которая её прячет. Это тот же движок, что и ретро-аудит, и именно с него большинство и начинает: сначала измерение, потом интеграция.

  • Ноги адресуются нашим event_id, ровно как везде. Комбинация настолько хорошо сопоставлена, насколько сопоставлена её худшая нога.
  • Полосы проходят насквозь. Комбинация, собранная на ступенях src: "table", заметно менее уверенная, чем собранная на котировочных, — читайте совместное число против его полосы, а не как точечную оценку.
  • Известное ограничение опубликовано, а не спрятано: на семействе «хозяева и тотал больше» модель недооценивает совместную вероятность на 4–5 пп в средних корзинах, и полоса расширена там, где живёт этот промах. Загляните на страницу продукта прежде, чем принимать число на этом семействе за чистую монету.
  • Поставка определяется договором. Совместный прайсинг приходит сначала аудитом, потом фидом; спецификация полей ретро-батча выдаётся под NDA, а не публикуется здесь.
Синхронизация

Цикл по курсору и пустая страница, которая не конец

Держать локальную копию — это один вызов в цикле: GET /v1/surface?since= возвращает все поверхности, изменившиеся после вашего курсора, целиком, и отдаёт next_cursor. Верните его без изменений в следующем вызове и, пока more равно true, идите сразу за следующей страницей, не дожидаясь интервала опроса.

Пустая страница с more: true означает «продолжайте»
Ваш ключ покрывает часть спортов, которые мы ведём, а спорт-фильтр применяется после того, как страница набрана. Страница, все события которой относятся к спорту вне вашего ключа, приходит пустой — с валидным next_cursor и more: true.
Условием цикла должен быть more, а не число строк. Остановка на пустой странице оставляет вашу копию на этом курсоре, и обновления перестают приходить. Курсор всегда проходит отфильтрованную страницу насквозь — именно это и не даёт вам на ней застрять.
  • Никогда не собирайте курсор сами. Он составной, и часами на вашей стороне его не воспроизвести. Возвращайте ровно то, что вернули мы.
  • Планируйте вызовы по data_cadence_s, который приходит в каждом объекте, а не по фиксированному таймеру: он сам сжимается по мере приближения к началу матча.
  • Каталог обновляйте по своему расписанию. Раз в сутки покрывает обычную линию; гоняйте его с ?hours=, чтобы новые матчи попадали в ваше сопоставление раньше, чем на них можно поставить.
  • На 429 смотрите Retry-After, а не отступайте вслепую. Каталог и bulk-дельта делят класс bulk, поэтому обновление каталога посреди цикла синхронизации конкурирует с ним — разведите их по минутам.

Полный референс API →

Доступ

Застряли на интеграции?

Пришлите запрос, который вы отправили, и ответ, который получили. Коды отказа сделаны так, чтобы ответить можно было с первого письма.

Напишите нам

Обычно отвечаем в течение одного рабочего дня.

Задать вопрос