Partner API
Fbenz Partner API · справочник разработчика

Наличие топлива на АЗС России одним GET-запросом

Водители по всей России отмечают на карте Fbenz, где есть бензин и очереди. Partner API отдаёт эти статусы вашему приложению. Или сразу целую карту под вашим брендом.

28 000+АЗС в базе
~5 минутот отметки водителя до API
Индивидуальноусловия и лимиты под партнёра

Два способа подключиться

Если у вас своя карта, берите чистый JSON. Если карты нет, встройте готовый виджет.

Вариант А: у вас своя карта

Чистый JSON-API

Забирайте координаты АЗС и статусы топлива, рисуйте на своей карте в своём стиле. Подходит банкам с готовым приложением.

  • Секретный ключ fb_live_…, запросы сервер-сервер
  • Снапшот по городу или bbox + дешёвый опрос изменений
  • ETag / 304: повторные запросы почти бесплатны
Вариант Б: карты нет

White-label виджет

Готовая карта Fbenz без нашего брендинга: кластеры, фильтр по топливу, карточки АЗС. Встраивается одним iframe за вечер.

  • Публикуемый ключ pk_live_…, привязан к вашим доменам
  • Светлая и тёмная тема, старт с нужного города
  • Обновляется сам, ничего поддерживать не нужно

Первый запрос за 10 минут

Инструкция рассчитана на человека, который никогда не работал с API. Если что-то не получается на любом шаге, просто напишите нам и мы поможем.

0

Что понадобится

  • API-ключ. Это строка вида fb_live_k3J9…, ваш пропуск в API. Чтобы получить ключ, напишите нам: контакт в самом низу страницы. Условия подключения обсудим в переписке. Ключ показывается один раз при выдаче, поэтому сразу сохраните его в надёжное место и обращайтесь с ним как с паролем.
  • Терминал. Это окно, куда вставляют команды из примеров ниже. Windows: нажмите Win+R, введите powershell и нажмите Enter. macOS: откройте Spotlight (Cmd+Пробел) и наберите «Терминал». Ставить ничего не нужно, всё уже есть в системе.
1

Получите ключ

Ключ выдаёт команда Fbenz при подключении: напишите нам (контакт внизу страницы), обсудим условия и аудиторию вашего продукта. Ключ показывается один раз: скопируйте его сразу и храните как пароль.

2

Запросите АЗС города

Подставьте свой ключ в заголовок Authorization.

Запрос
curl --get "https://fbenz.ru/api/v1/partner/stations" \
  --data-urlencode "city=Москва" \
  -H "Authorization: Bearer fb_live_ВАШ_КЛЮЧ"

Флаг --data-urlencode обязателен: он кодирует кириллицу в названии города. Без него curl отправит битые байты, город не совпадёт и придёт 404 city-not-found (кодов 400 в этом API нет, ошибки валидации это всегда 422).

То же самое в Windows PowerShell
Invoke-RestMethod -Uri "https://fbenz.ru/api/v1/partner/stations?city=Москва" -Headers @{ Authorization = "Bearer fb_live_ВАШ_КЛЮЧ" }

Проверьте себя. В ответе должно быть поле count с числом станций и список stations. Если пришла ошибка 401 invalid-key, ключ вставлен не целиком: скопируйте его заново без пробелов. Если пришло 404 city-not-found, название города ушло без кодировки: возьмите команду из примера полностью, не перенабирайте её руками.

3

Покажите статусы клиенту

У каждой станции есть координаты, бренд, адрес и блок fuel с текущим статусом.

Ответ 200 OK
{
  "stations": [{
    "id": "n1234567890",
    "lat": 55.7558,
    "lon": 37.6173,
    "brand": "Лукойл",
    "name": "Лукойл",
    "addr": "Ленинградский пр-т, 63",
    "fuel": {
      "status": "yes",
      // что есть и чего нет
      "fuels_now": "92,95,ДТ",
      "fuels_no": "100",
      // цены за литр, если водители их указали
      "prices": "92=58.4,95=63.2",
      // очередь машин: "", "0-5", "5-10", "10-20", "20+"
      "queue": "5-10",
      // лимит литров в одни руки (null = нет)
      "limit_l": 20,
      // особенности оплаты
      "no_cash": false,
      "fuel_cards_only": false,
      // подтверждений от водителей
      "confirmations": 3,
      // независимых живых устройств за этим статусом
      "real_count": 3,
      // true = статус держится на импортированных данных, а не на отметках людей
      "seeded": false,
      "updated": "2026-07-16 09:41:07",
      // отметка есть, но подтверждений ей пока не хватает
      "pending_status": "no",
      "pending_count": 1,
      // published · unconfirmed · conflict · no_data
      "resolution_reason": "unconfirmed",
      // не null только при resolution_reason = conflict
      "conflict": null
    }
  }],
  "count": 412,
  "as_of": "2026-07-16 09:45:00",
  "stale_after_hours": 24
}
yes · есть queue · очередь low · мало no · нет nodata · нет данных

В поле fuel.status тот же статус, который видят водители на карте Fbenz. Показывайте его как есть: этих пяти значений хватит для любого интерфейса.

4

Дальше обновляйте данные

Простой путь: качайте всю страну целиком из /stations/all раз в 10-15 минут с заголовком If-None-Match. Экономный путь: передавайте as_of из прошлого ответа в /stations/delta?updated_since= раз в 1-5 минут. Подробности в разделе «Свежесть и лимиты».

Восемь методов закрывают весь сценарий

Все методы живут под /api/v1/partner, ключ передаётся в заголовке Authorization: Bearer <ключ>. Полная спецификация: openapi.json.

ЗадачаМетодЧто вернёт
Получить АЗС города или области карты GET/stations?city= или ?bbox= До 4000 станций со статусами. Радиус города задаёт параметр radius_km (до 60 км). Понимает поиск и фильтры. Если область больше и список обрезан, в ответе придёт "truncated": true: берите область меньше или /stations/all.
Выгрузить всю Россию разом GET/stations/all Все станции страны со статусами: страницы по 10 000, курсор в next_cursor, параметром limit можно поднять страницу до 30 000 и забрать всё одним запросом.
Следить за изменениями GET/stations/delta?updated_since= Только станции, у которых статус менялся с указанного момента. Курсор берите из поля as_of.
Открыть карточку одной АЗС GET/stations/{id} Станция и её текущий статус.
Список городов для меню GET/cities?q= По умолчанию все города России разом (~1200, отсортированы по населению) с координатами и зумом: удобно один раз забрать под своё меню. ?q= ищет по названию, ?limit= ограничивает выдачу топ-N.
Посмотреть свой расход GET/usage Запросы за сегодня, лимиты, разбивка по дням и методам. Глубина истории задаётся параметром ?days= (по умолчанию 7, до 365).
Отправить отметку вашего юзера POST/stations/{id}/report Принимает статус от конечного пользователя вашего приложения: есть, мало, нет, очередь, марки, цены. Отметка идёт по тем же правилам, что у водителей на fbenz.ru. Детали в разделе «Отчёты юзеров».
Запросить новые лимиты POST/limit-requests Создаёт заявку команде Fbenz. Тело: {"requested_rpm": 600, "requested_daily": 2000000, "comment": "..."}. Статус смотрите в кабинете партнёра или GET-запросом по тому же пути.
В API те же статусы, что видят водители на карте Fbenz: одна база, одни правила свежести. Ситуации «сайт показывает одно, API другое» не бывает.

Поиск и фильтры прямо в API

Не хотите фильтровать на своей стороне: попросите сервер. Все параметры работают на /stations и /stations/all, считаются в базе до отдачи, поэтому ответ никогда не теряет подходящие станции из-за лимита страницы.

ПараметрПримерЧто делает
qq=лукойл ленина Поиск по названию, бренду и адресу. Каждое слово должно найтись. Слова «95», «дт», «дизель» внутри запроса сами включают фильтр по топливу, как в поиске на fbenz.ru.
statusstatus=yes,low Только станции с этими видимыми статусами. Значения: yes, low, no, queue, nodata. Протухший статус честно считается за nodata, старое значение не подставляется.
fuelfuel=95 Станции, где сейчас отмечена эта марка. Значения: 92, 95, 98, 100, ДТ (можно писать dt или дизель). Несколько через запятую: станция должна иметь все.
brandbrand=Лукойл Точное имя бренда, регистр не важен. Для нестрогого поиска бренда используйте q.
has_pricehas_price=true Только станции со свежими ценами. Честное предупреждение: цены водители указывают редко, выборка будет маленькой.
queuequeue=0-5,5-10 Фильтр по длине очереди. Бакеты: 0-5, 5-10, 10-20, 20+.
fresh_hfresh_h=3 Статус не старше N часов (от 1 до 24). Жёстче стандартного суточного окна.
sortsort=distance Ближние к центру города или области первыми. Только на /stations.
Пример: где в Москве сейчас есть 95-й у Лукойла
curl --get "https://fbenz.ru/api/v1/partner/stations" \
  --data-urlencode "city=Москва" \
  --data-urlencode "fuel=95" \
  --data-urlencode "brand=Лукойл" \
  --data-urlencode "status=yes,low" \
  -H "Authorization: Bearer fb_live_ВАШ_КЛЮЧ"
ETag и 304 работают и с фильтрами: у каждого набора фильтров свой ETag. На /stations/delta фильтров нет сознательно: отфильтрованная дельта скрыла бы момент, когда станция перестала подходить под фильтр, и ваша копия зависла бы на старом статусе.

Ваши юзеры тоже могут ставить статусы

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

Отметка «бензин есть» от вашего юзера
curl -X POST "https://fbenz.ru/api/v1/partner/stations/n1234567890/report" \
  -H "Authorization: Bearer fb_live_ВАШ_КЛЮЧ" \
  -H "Content-Type: application/json" \
  -d '{
    "status": "yes",
    "user_ref": "user-42",
    "fuels": ["92", "95"],
    "prices": {"95": 61.5},
    "lat": 55.7558, "lon": 37.6173,
    "external_id": "report-8f1c"
  }'
ПолеОбязательноЧто значит
statusда yes есть, low мало, no нет, queue очередь.
user_refда Постоянный id юзера на вашей стороне (любая строка до 128 символов). Мы храним только его хеш. Один юзер = один голос, поэтому id должен быть стабильным, а не случайным на каждый запрос.
lat + lonнет GPS юзера в момент отметки (плюс опционально acc_m, точность в метрах). С координатами рядом со станцией голос идёт с полным весом. Без координат отметка принимается, но с пониженным весом, и для публикации нужно подтверждение второго юзера.
fuels, fuels_noнет Какие марки есть и каких нет: ["92","95","ДТ"]. Допустимы только 92, 95, 98, 100, ДТ (можно dt/дизель), иначе 422.
queue, prices, limit_l, no_cash, fuel_cards_only, textнет Детали как в форме на fbenz.ru: очередь (0-520+), цены, лимит литров, «не берут наличные», «только топливные карты», комментарий до 200 символов.
external_idнет Ваш id отметки для повторов. Повторный POST с тем же external_id не создаст дубль, в ответе будет "duplicate": true. Тот же флаг придёт, если один и тот же пользователь повторит СВОЁ ЖЕ мнение по той же АЗС в пределах 5 минут: это не ошибка, отметка просто не удваивается.
observed_atнет Когда юзер реально нажал кнопку (UTC), если отметка ехала к вам с задержкой.
expressнет «Топливо кончилось прямо сейчас»: тот же пользователь может переотметить эту АЗС уже через 2 минуты вместо обычных 20. Для случая, когда человек стоит на заправке и бензин закончился у него на глазах.

Что вернётся: "accepted": true, поле "remote", блок published с итогом пересчёта и блок fuel — та же самая витрина станции, что приходит в чтении. Перечитывать станцию после записи не нужно: обновляйте карточку у себя прямо из ответа. "remote": true значит, что голос принят без гео-подтверждения (координат нет, они далеко от станции или движение юзера физически невозможно): вес такого голоса ниже, и для публикации нужно подтверждение второго юзера. Отметка не обязана менять статус мгновенно: у станции те же пороги доверия, что и для водителей.

Отметка принята, но на карту не попала: подтверждения

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

ОтметкаСколько устройств нужноПочему так
yes, low, queue с геопозицией у АЗС одно человек стоит на заправке и видит топливо своими глазами
no (топлива нет) два, если рядом есть свежие отметки других людей ложное «нет» уводит водителя от живой заправки, это самая дорогая ошибка на карте
любая отметка без геопозиции ("remote": true) два без координат отметку нельзя проверить физически

Пока устройств не хватило, статус станции остаётся прежним, а сама отметка живёт в полях pending_status и pending_count. Они приходят в блоке fuel в каждом чтении: карточка АЗС, выборка по прямоугольнику и дельта. По ним рисуется плашка «ждёт подтверждения» и кнопка «подтверждаю», как это сделано в нашем виджете.

Отдельного метода для подтверждения нет и не нужно. Подтверждение это обычная отметка тем же статусом от другого пользователя: тот же POST /stations/{id}/report, тот же status, другой user_ref. Как только устройств хватило, статус публикуется и pending_count обнуляется. Один и тот же user_ref подтвердить сам себя не может: у устройства одно мнение о станции, повторная отметка заменяет предыдущую, а не удваивает её.

Два только что созданных пользователя не подтверждают друг друга. Устройства, впервые увиденные в пределах 10 минут, считаются одним человеком: это защита от накрутки, когда «толпу» заводят одной пачкой. На практике это заметно только в первые минуты жизни новой интеграции; дальше у ваших пользователей разный возраст и подтверждение проходит. Если ждать не хочется, порог снимается настройкой ниже.

Правило настраивается под вас. Для вашего ключа порог можно снять совсем (публиковать с первой отметки) или, наоборот, поднять до трёх и больше устройств. Настройка действует только на отметки, пришедшие через ваш ключ, и никогда не блокирует отметки обычных водителей в приложении. Текущее значение приходит в GET /widget/config полем confirm_min: 0 это общее правило из таблицы выше. Поменять порог можно по запросу, напишите нам.

ОшибкаПочемуЧто делать
403 reports-disabledприём отметок для вас не включён Напишите нам, включим тумблером без редеплоя.
429 station-cooldownэтот юзер уже отмечал эту станцию недавно Штатно. Одна отметка на станцию раз в 20 минут, покажите юзеру «уже учли».
429 user-rate-limitedчасовой или суточный кап отметок юзера Кап на юзера, не на вас. Повторите позже.
429 new-users-limitсуточный лимит НОВЫХ отмечающих юзеров Известные юзеры продолжают отмечать. Если лимит мал для вашего запуска, напишите нам, поднимем.
422 too-farкоординаты далеко, а приём без GPS для вас выключен По умолчанию далёкие координаты принимаются как "remote": true с пониженным весом, и этой ошибки вы не увидите. Получили её: отправьте без lat/lon или напишите нам.
Только секретный ключ. Анти-накрутка общая с fbenz.ru: новые юзеры первые сутки считаются осторожно, массовые «нет» с одного источника видим и разбираем. Честные отметки ваших юзеров делают карту точнее для всех.

Вы всегда знаете возраст данных

Данные собирают сами водители: отметка появляется в API через несколько минут. У каждого статуса есть updated, а у каждого ответа as_of.

Как устроена свежесть

Нужна вся Россия без дельт

Качайте /stations/all раз в 10-15 минут с заголовком If-None-Match. Ничего не менялось: придёт 304, почти бесплатный. Менялось: забирайте страницы по next_cursor (или всё одним запросом с limit=30000). ETag тут общий на весь дамп, поэтому протухания и пересчёты он ловит сам, дельты и часовые снапшоты в этой схеме не нужны.

Рекомендуемый цикл для дельт

ЧтоКак частоЗачем
/stations/deltaраз в 1-5 минут Свежие изменения. Курсор берите из as_of прошлого ответа.
/stations полный, с If-None-Matchраз в час Ловит устаревания и пересчёты. Без изменений вернётся 304, почти бесплатный.

Лимиты

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

Заголовки ответа
// лимит и остаток в текущей минуте
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 118
// unix-время сброса минуты
X-RateLimit-Reset: 1784192460
// суточная квота и остаток
X-RateLimit-Daily-Limit: 50000
X-RateLimit-Daily-Remaining: 49212

При превышении придёт 429 с заголовком Retry-After: подождите указанное число секунд и повторите. Квота сбрасывается в 00:00 UTC.

Если нужны лимиты выше, отправьте заявку с новыми значениями из кабинета партнёра. Ответим в течение рабочего дня, одобренные лимиты применятся сами.

Кеш выборки по прямоугольнику

Запросы виджет-ключом по bbox отдаются из общего кеша: одинаковый прямоугольник считается один раз на всех ваших зрителей. В ответе есть заголовок X-Fbenz-Cache: hit|miss. Принятая отметка сразу выбрасывает из кеша все прямоугольники, накрывающие эту АЗС, поэтому после записи следующее чтение уже свежее. Секретный ключ читает мимо кеша и получает точный прямоугольник без квантования, поэтому ETag у секретного и виджет-ключа на один и тот же bbox разные — это нормально.

Кеш настраивается под вас. Его можно выключить совсем (тогда каждый запрос считается заново и всегда отдаёт последнее состояние) или задать свою длительность в секундах, от нуля до часа. Дольше кеш — быстрее карта и меньше нагрузка, короче кеш — быстрее видно чужие свежие отметки; свои собственные отметки видны сразу в любом случае, потому что запись сбрасывает кеш по своей АЗС. По умолчанию кеш включён с общей длительностью сервиса. Поменять — напишите нам.

Тонкости, о которые спотыкаются

Карта под вашим брендом за один вечер

Кластеры, фирменные маркеры брендов АЗС, фильтр по топливу, карточки станций. Логотипа Fbenz нигде нет. Один тег HTML.

HTML для вставки
<iframe
  src="https://fbenz.ru/widget/?key=pk_live_ВАШ_КЛЮЧ&city=Москва"
  width="100%" height="480" style="border:0"
  allow="geolocation; fullscreen" title="Наличие топлива на АЗС"></iframe>

Куда вставить этот тег. Свой сайт: в HTML нужной страницы, например перед закрывающим </body>. Tilda: блок T123 «HTML-код». WordPress: блок «Пользовательский HTML». Bitrix: компонент «HTML-код». После вставки откройте страницу: карта должна появиться сразу.

ПараметрЗначенияЧто делает
keypk_live_… Ваш публикуемый ключ. Обязательный.
cityназвание города С какого города открыть карту.
lat + lonкоординаты Открыть карту с точки (вместо city). Опционально zoom (по умолчанию 12).
themelight · dark · auto Стартовая тема, auto следует за системной. На карте есть кнопка переключения: выбор пользователя запоминается в его браузере. Рядом кнопка геолокации, для неё нужен атрибут allow="geolocation" на iframe, и кнопка «во весь экран» — она появляется, только если в теге iframe есть allow="fullscreen" и браузер поддерживает полноэкранный режим (на iOS Safari его для карты нет, там кнопка сама прячется).
fuel92 · 95 · 98 · 100 · ДТ Сразу включить фильтр по топливу.
nofilter1 Спрятать панель фильтров.
nosearch1 Спрятать поиск. По умолчанию в карте живёт поиск как на fbenz.ru: город, АЗС по названию или бренду, слова «95» и «дт» включают фильтр топлива.
themelock1 Тема берётся только из адреса iframe: кнопка темы внутри карты прячется, сохранённый выбор зрителя не участвует. Для витрин и конфигураторов, где темой управляет ваша страница.
gesturescooperative · greedy Как карта делит жесты со страницей. Внутри iframe по умолчанию cooperative: страница скроллится одним пальцем сквозь карту, карта двигается двумя, колесо зумит с Ctrl (с подсказками). При прямом открытии и в вебвью по умолчанию greedy: карта забирает все жесты. Параметр фиксирует режим вручную.
Публикуемый ключ привязан к вашим доменам: на чужом сайте он не заработает. Секретный ключ fb_live_… во фронтенд не вставляйте никогда: он только для запросов с сервера.

Как именно проверяется домен: разрешив bank.ru, вы автоматически разрешаете и его поддомены (www.bank.ru, lk.bank.ru). Похожий чужой домен вроде bank.ru.evil.com не проходит. Запрос вообще без ссылающейся страницы (обращение с сервера, не из браузера) проходит: привязка защищает от копирования ключа на чужой сайт, это не криптография.

Карточка АЗС — та же, что в приложении fbenz

В виджете карточка станции собрана один в один с приложением: цветная плашка статуса с иконкой, крупный заголовок, строка «сколько подтверждений и когда обновлено», значок «данные устарели», блок «Это всё ещё актуально?» с кнопками «Да, подтверждаю» и «Изменилась», плашки «ждёт подтверждения» и «данные расходятся», марки топлива, цены за литр, лимит литров и способы оплаты. Ничего рисовать самому не нужно.

Если вы делаете свою карту на JSON API, тот же экран собирается из полей блока fuel: status и queue дают заголовок, confirmations и updated — строку доверия, seeded отвечает на вопрос «это отметки людей или импорт», real_count показывает, сколько независимых устройств за статусом, pending_status с pending_count рисуют ожидание подтверждения, а conflict (приходит только при resolution_reason: "conflict") даёт обе спорящие версии с их числом устройств.

Любой размер врезки

Карта подстраивается не под «телефон или десктоп», а под то, сколько места ей дал ваш контейнер. Проверено на пятнадцати размерах от 240×160 (крошечная карточка в списке) до 1600×900: ничего не наезжает друг на друга, полосы прокрутки внутри виджета нет, карточка АЗС и форма отметки всегда помещаются целиком.

Одна практическая мелочь: у тега iframe обязательно задайте высоту. Без неё браузер поставит свои 150px, и карта окажется в узкой полоске. Ширину можно смело ставить в 100%, а высоту — хоть в пикселях, хоть в aspect-ratio, хоть на весь экран: раскладка пересоберётся сама, ваши стили для этого не нужны.

Пользователи ставят статусы прямо в виджете

В карточке каждой АЗС есть кнопка «Обновить статус»: зритель отмечает есть топливо, мало, очередь или нет, и какие марки в наличии. Отметка уходит через ваш публикуемый ключ и проходит те же проверки, что и отметки в приложении fbenz.ru: близость к АЗС (виджет спросит геолокацию, для неё нужен allow="geolocation" на iframe), защита от накрутки, кулдаун. Отметка без точной геопозиции принимается с меньшим весом и ждёт подтверждения рядом с АЗС. Кнопку можно выключить для вашего ключа: приём отметок это тумблер в вашем тарифе, напишите нам. Устройство зрителя виджет помнит анонимно, без входа и регистрации.

Если по АЗС уже есть отметка, которой не хватает подтверждений, виджет показывает её отдельной плашкой с кнопкой «Подтверждаю»: один тап, и статус выходит на карту. Автору его собственной отметки кнопка не показывается, подтвердить себя нельзя. Подробнее про пороги написано в разделе про отметки выше.

Внедрение с нуля: рабочий клиент за 15 минут

Скопируйте пример на своём языке: он забирает справочники, держит данные свежими через дельты и правильно реагирует на 304 и 429. Это весь код, который нужен для боевой интеграции.

1

Один раз: справочник городов и полный снапшот

Города меняются редко: заберите список один раз и обновляйте, например, раз в сутки. По умолчанию приходят все города России разом. Единственная зависимость: pip install requests. В обоих блоках поменяйте ключ на свой, больше менять ничего не нужно.

Если Python ещё не установлен: скачайте его с python.org, при установке отметьте галочку «Add Python to PATH». Потом сохраните код ниже в файл app.py и запустите в терминале команду python app.py.

Python
import requests

BASE = "https://fbenz.ru/api/v1/partner"
KEY = "fb_live_ВАШ_КЛЮЧ"          # храните в секретах, не в коде
H = {"Authorization": f"Bearer {KEY}"}

# все города страны одним запросом (для меню, поиска, стартового экрана)
cities = requests.get(f"{BASE}/cities", headers=H, timeout=15).json()["cities"]

# полный снапшот всех АЗС страны (страницы по 10 000)
stations, cursor = [], ""
while True:
    r = requests.get(f"{BASE}/stations/all",
                     params={"cursor": cursor}, headers=H, timeout=60).json()
    stations += r["stations"]
    cursor = r.get("next_cursor") or ""
    if not cursor:
        break
as_of = r["as_of"]                # курсор для дельт, шаг 2
print("станций загружено:", len(stations))
2

Дальше по кругу: дельты каждые пару минут

Опрашивайте /stations/delta и накладывайте изменения на свою копию. При 429 подождите Retry-After секунд, это штатная ситуация, данные не теряются.

Python
import time

by_id = {st["id"]: st for st in stations}

while True:
    r = requests.get(f"{BASE}/stations/delta",
                     params={"updated_since": as_of}, headers=H, timeout=30)
    if r.status_code == 429:      # упёрлись в лимит: ждём и повторяем
        time.sleep(int(r.headers.get("Retry-After", "30")))
        continue
    r.raise_for_status()
    data = r.json()
    for st in data["stations"]:   # обновляем только изменившиеся
        by_id[st["id"]] = st
    as_of = data["as_of"]
    print("обновлено станций:", len(data["stations"]))
    time.sleep(120)               # раз в 2 минуты достаточно

То же самое на Node.js, без единой зависимости. Этот блок самодостаточный: скопируйте в app.js, поменяйте ключ, запустите node app.js.

Node.js 18+
const BASE = "https://fbenz.ru/api/v1/partner";
const KEY = "fb_live_ВАШ_КЛЮЧ";              // поменяйте на свой
const H = { Authorization: "Bearer " + KEY };
const byId = new Map();                       // ваша копия данных
const sleep = (ms) => new Promise((res) => setTimeout(res, ms));
// таймаут на каждый запрос: сбой сети не подвесит ваш сервис (аналог timeout= в requests)
const get = (path) => {
  const c = new AbortController();
  const t = setTimeout(() => c.abort(), 30_000);
  return fetch(BASE + path, { headers: H, signal: c.signal }).finally(() => clearTimeout(t));
};

async function main() {
  // старт: полный снапшот всей страны, страницы по 10 000
  let cursor = "", asOf = "";
  do {
    const r = await get("/stations/all?cursor=" + encodeURIComponent(cursor));
    const data = await r.json();
    for (const st of data.stations) byId.set(st.id, st);
    asOf = data.as_of;
    cursor = data.next_cursor || "";
  } while (cursor);
  console.log("станций загружено:", byId.size);

  // дальше по кругу: только изменения
  while (true) {
    await sleep(120_000);                     // раз в 2 минуты достаточно
    const r = await get("/stations/delta?updated_since=" + encodeURIComponent(asOf));
    if (r.status === 429) {                   // лимит: подождать и повторить
      await sleep(1000 * (+r.headers.get("retry-after") || 30));
      continue;
    }
    const data = await r.json();
    for (const st of data.stations) byId.set(st.id, st);
    asOf = data.as_of;
    console.log("обновлено станций:", data.stations.length);
  }
}
main();
3

Чеклист перед боевым запуском

  • Ключ лежит в секретах (переменная окружения, vault), не в коде и не в репозитории.
  • Кириллица в параметрах кодируется: --data-urlencode в curl, params= в requests, encodeURIComponent в JS делают это сами.
  • Обработан 429: пауза на Retry-After секунд и повтор. Не ретраить в цикле без паузы.
  • Повторные снапшоты ходят с If-None-Match: ответ 304 не тратит трафик.
  • nodata показывается как «нет данных», не как «топлива нет»: это разные вещи.
  • Таймауты на HTTP-запросы стоят (15-60 секунд), сбой сети не вешает ваш сервис.
  • Расход виден в кабинете партнёра: загляните после первого дня.
  • Если планируете большой трафик, запросите лимиты заранее заявкой из кабинета: одобряем в рабочий день.

Частые вопросы

ВопросОтвет
Есть ли тестовая среда или тестовый ключ? Отдельной песочницы нет и не нужно: ваш боевой ключ и есть тестовый. Чтение данных ничего не ломает, а отчёты проходят те же проверки, что и отметки в приложении.
Нужно ли настраивать CORS? Нет. Секретный ключ ходит только сервер-сервер, браузер в этой схеме к нам не обращается. Из браузера работает только виджет со своим pk_live_ ключом.
Потеряли ключ, что делать? Восстановить нельзя (мы храним только хэш), но это не страшно: попросите новый, переключитесь, старый отзовём. Простоя не будет, оба работают одновременно.
Почему у станции статус nodata? Свежих отметок нет: статус живёт 24 часа. Это нормально для маленьких городов ночью. Показывайте «нет данных» и серый цвет.
Виджет на сайте пишет «не разрешён для этого сайта» Домен сайта не привязан к ключу. Напишите менеджеру, какие домены добавить (включая поддомены, если они разные).
Кнопка геолокации в виджете ничего не делает На вашем теге iframe нет атрибута allow="geolocation": браузер блокирует доступ к геопозиции внутри iframe без него.
Как заранее понять, что мы упираемся в лимиты? Следите за X-RateLimit-Remaining и X-RateLimit-Daily-Remaining в ответах или за графиком в кабинете. Заявку на новые лимиты можно отправить в любой момент.
Сколько это стоит? Условия обсуждаем с каждым партнёром отдельно, под продукт и аудиторию. Напишите нам, контакт внизу страницы.

Ошибки, которые можно обработать кодом

Формат один: RFC 9457 (application/problem+json). В type код для программы, в detail объяснение для человека. В каждой ошибке есть request_id: назовите его нам, и мы найдём ваш запрос в логах за минуту.

Ошибка 429
{
  "type": "https://fbenz.ru/problems/rate-limit-exceeded",
  "title": "Rate limit exceeded",
  "status": 429,
  "detail": "Minute quota of 120 requests exhausted; retry after 37s.",
  "limit": 120,
  "window": "60s",
  // назовите этот id в саппорте
  "request_id": "1a2b3c4d5e6f"
}
КодТипЧто делать
401invalid-key Ключ не подошёл или отозван. Проверьте, что скопировали ключ целиком. Если потеряли, запросите новый.
403domain-not-allowed · ip-not-allowed · secret-key-required · partner-disabled Ключ используется не оттуда: виджет-ключ работает только с ваших доменов, секретный только с ваших IP (если включена привязка). partner-disabled значит, что доступ партнёра выключен, напишите вашему менеджеру Fbenz.
422bad-bbox · bad-timestamp · missing-area · window-too-old Параметры не прочитались, формат указан в detail. window-too-old значит, что окно дельты больше 48 часов: заберите полный снапшот /stations.
429rate-limit-exceeded · daily-quota-exceeded Упёрлись в лимит. Подождите Retry-After секунд. Если растёте, запросите новые лимиты в кабинете.
503upstream-unavailable Короткий сбой на нашей стороне. Повторите через несколько секунд, данные не потеряются.

Два ключа: секретный и публикуемый

Какой для чего

  • fb_live_… секретный: все методы API, только сервер-сервер. Храните его как пароль от базы.
  • pk_live_… публикуемый: только виджет и только ваши домены. Можно спокойно вставлять в HTML.

Ротация без простоя

  • Запросите второй секретный ключ, старый продолжит работать.
  • Переключите интеграцию на новый ключ.
  • Попросите отозвать старый. Отзыв срабатывает в пределах минуты.

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