На главную

API для разработчиков

Доступ к календарю соревнований и реестру участников e-phan в формате JSON, только на чтение. Ключ не нужен — это те же записи, которые платформа показывает пользователям, и они обновляются сразу после изменения.

Базовый адрес

https://api.e-phan.com/api/v1

Как подключиться

  1. 1

    Отправьте GET

    Без ключа, без регистрации, без заголовков. GET /competitions?period=upcoming возвращает JSON по HTTPS.

  2. 2

    Ограничьте запрос

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

  3. 3

    Проверяйте, а не перезагружайте

    Сохраните ETag и верните его в If-None-Match. Неизменённые данные отвечают 304 без тела — это экономит трафик, но не лимит запросов.

Доступ

Без аутентификации. /api/v1 принимает анонимные GET по HTTPS: нечего получать и нечего отправлять. Эндпоинты отдают опубликованный календарь и реестр, которые сайт и так показывает анонимным посетителям, так что здесь нет ни персональных данных, ни учётных данных, которые нужно защищать.

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

Примеры кода

curl "https://api.e-phan.com/api/v1/competitions?period=upcoming&country=UA&limit=10"

Реестр: состояния и статусы

У каждого танцора, тренера, судьи и клуба есть два независимых поля. registryState показывает, где находится запись; status — оплачены ли взносы. Они меняются независимо друг от друга, и если читать только одно, вывод окажется неверным.

Чтобы узнать, кто является участником сегодня, читайте registryState, а не status. Танцор с просроченным взносом остаётся в реестре весь льготный период, поэтому сочетание status: "INACTIVE" и registryState: "REGISTRY" — это норма, а не противоречие.

registryState — где находится запись

Есть у каждого танцора, тренера, судьи и клуба; принимается как фильтр во всех четырёх списках.

  • REGISTRY

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

  • ARCHIVE

    Отдельный раздел реестра, а не удаление. Архивные записи сохраняют свои идентификаторы и остаются доступными, но никогда не попадают в список, если их не запросить явно.

status — оплачены ли взносы

Хранится отдельно для каждой роли, а не для человека: один и тот же человек может быть ACTIVE как судья и INACTIVE как танцор, и каждый эндпоинт отдаёт статус именно той роли, которую показывает. Срок членства идёт заданное число месяцев со дня оплаты, поэтому membershipExpiresAt не приходится на какую-то фиксированную дату — читайте его, а не вычисляйте. Тренеры и клубы ничего не покупают и собственного статуса не имеют вовсе: /coaches и /clubs не отдают поле status и не принимают ?status= — в реестре ли они, говорит registryState.

  • ACTIVE

    Взнос оплачен, срок ещё идёт.

  • LIMITED

    Тоже оплаченное членство, дешевле ACTIVE. Разница в допуске: LIMITED выступает на уровнях 1–3, ACTIVE — на любом. Это отдельное значение, а не оттенок ACTIVE или INACTIVE.

  • INACTIVE

    Оплаченный срок исчерпан. Запись остаётся в реестре до конца льготного периода; registryUntil — это день, когда её заберёт архив.

Как читать архив

Архивирование — это то, благодаря чему реестр остаётся списком нынешних участников, не теряя истории. Записи исчезают из основного списка, но сохраняют всё остальное.

  • По умолчанию списки отдают registryState=REGISTRY. Архив никогда не подмешивается — передайте registryState=ARCHIVE, чтобы прочитать его как отдельный раздел, или перечислите оба состояния, чтобы объединить их.
  • Счётчики из /registry/filters ограничены так же, как и списки, поэтому фасет никогда не пообещает записей, которых список не вернёт. Ответ прямо указывает эту границу в countsScope. Чтобы оценить размер архива, листайте список с registryState=ARCHIVE.
  • Архивирование роли закрывает её связи с клубом и тренером. Строки остаются как история, но dancersCount и coachesCount клуба перестают их считать со дня закрытия.
  • Возврат из архива требует платного восстановления, так что не ждите, что запись вернётся сама. archivedAt содержит дату, когда она туда попала.

Как запросить архив

# Listings return the registry. The archive is not in them.
curl "https://api.e-phan.com/api/v1/dancers?limit=20"

# Ask for the archive by name to read it as its own section.
curl "https://api.e-phan.com/api/v1/dancers?registryState=ARCHIVE&limit=20"

# Or merge the two, if you are reconciling a full history.
curl "https://api.e-phan.com/api/v1/dancers?registryState=REGISTRY,ARCHIVE&limit=20"

Справочник эндпоинтов

Все эндпоинты работают только на чтение и возвращают JSON. Фильтры можно сочетать как угодно: они применяются через AND.

Соревнования

GET/api/v1/competitions

Постраничный список соревнований. Черновики организаторов никогда не возвращаются.

  • searchstring

    Ищет по названию, городу, стране, месту проведения и описанию. Не менее 2 символов.

    Пример: search=kyiv open

  • countrystring

    Название страны или код ISO 3166-1 alpha-2.

    Пример: country=UA

  • countryIdinteger

    Внутренний id страны, см. /filters.

    Пример: countryId=804

  • citystring

    Название города, совпадение по началу строки, без учёта регистра.

    Пример: city=Kyiv

  • statusopen | completed

    Статус соревнования. Несколько значений через запятую.

    Пример: status=open

  • typestring

    Ранг соревнования по коду. Несколько значений через запятую; актуальный перечень см. в /filters.

    Пример: type=ranking_tournament

  • registrationStatusopen | closed

    Состояние регистрации, заданное организатором.

    Пример: registrationStatus=open

  • registrationOpenboolean

    true оставляет только соревнования, принимающие заявки прямо сейчас, — то же самое, что registration.state == "open". И день открытия, и день дедлайна считаются открытыми.

    Пример: registrationOpen=true

  • periodupcoming | ongoing | past

    Период по календарному дню. Соревнование, идущее сегодня, считается текущим весь день.

    Пример: period=upcoming

  • dateFromdate

    Календарный день включительно. Оставляет соревнования, пересекающиеся с диапазоном.

    Пример: dateFrom=2026-09-01

  • dateTodate

    Календарный день включительно. Оставляет соревнования, пересекающиеся с диапазоном.

    Пример: dateTo=2026-12-31

  • updatedSincedate-time

    Только соревнования, изменённые после этой отметки времени. Для инкрементной синхронизации; такие ответы не кешируются.

    Пример: updatedSince=2026-07-01T00:00:00Z

  • includecategories, entriesCount

    Дополнительные данные в ответе. Несколько значений через запятую.

    Пример: include=categories

  • sortstartDate | endDate | name | createdAt | updatedAt

    Поле сортировки.

    Пример: sort=startDate

  • orderasc | desc

    Направление сортировки.

    Пример: order=asc

  • pageinteger

    Номер страницы, начиная с 1.

    Пример: page=1

  • limitinteger, 1-100

    Записей на страницу, от 1 до 100. Значение вне диапазона даёт 400, а не молчаливое обрезание.

    Пример: limit=20

GET/api/v1/competitions/{idOrSlug}

Одно соревнование с полным списком категорий, текущими счётчиками и данными организатора. Принимает как id, так и slug.

GET/api/v1/competitions/{idOrSlug}/categories

Категории одного соревнования: возрастные группы, классы, программы, танцы в каждой и количество выходов. Принимает как id, так и slug.

  • entryTypecouple | solo | duo-synchro | mix | show

    Тип выступления. Несколько значений через запятую.

    Пример: entryType=couple

  • stylenone | latin | standard | combined

    Программа категории.

    Пример: style=latin

  • levelinteger, 1-10

    Класс (уровень) категории.

    Пример: level=4

  • searchstring

    Ищет по названию, городу, стране, месту проведения и описанию. Не менее 2 символов.

    Пример: search=kyiv open

GET/api/v1/filters

Значения для фильтров: страны и города, где сейчас проходят соревнования, а также допустимые значения перечислений.

Реестр

GET/api/v1/dancers

Постраничный список танцоров, находящихся сейчас в реестре. Архивные не попадают в выдачу, если их не запросить через registryState.

  • searchstring

    Поиск по имени и фамилии без учёта регистра (оба варианта написания). Не менее 2 символов.

    Пример: search=Bohdan

  • sexMALE | FEMALE

    Пол: MALE или FEMALE.

    Пример: sex=FEMALE

  • countryIdinteger

    Внутренний id страны, см. /filters.

    Пример: countryId=804

  • countrystring

    Название страны или код ISO 3166-1 alpha-2.

    Пример: country=UA

  • clubIduuid

    UUID клуба. Учитывает только действующее членство — связи, закрытые архивированием, не считаются.

    Пример: clubId=0f1c7a2e-...

  • coachIduuid

    UUID тренера. Учитывает только действующее членство.

    Пример: coachId=0f1c7a2e-...

  • danceClassinteger

    Танцевальный класс.

    Пример: danceClass=3

  • hasPartnerboolean

    Фильтр по наличию партнёра: true или false.

    Пример: hasPartner=true

  • registryStateREGISTRY | ARCHIVE

    Где находится запись: REGISTRY или ARCHIVE, несколько значений через запятую. По умолчанию REGISTRY — архив возвращается, только если указать его здесь явно. Любое другое значение отклоняется.

    Пример: registryState=ARCHIVE

  • statusACTIVE | LIMITED | INACTIVE

    Статус членства именно этой роли. Несколько значений через запятую. Сверяется по роли, поэтому человек с просроченным взносом в другой роли всё равно попадёт в выдачу. Не связан с registryState.

    Пример: status=ACTIVE

  • sortname | approvedAt

    Поле сортировки реестра: name или approvedAt.

    Пример: sort=name

  • orderasc | desc

    Направление сортировки.

    Пример: order=asc

  • pageinteger

    Номер страницы, начиная с 1.

    Пример: page=1

  • limitinteger, 1-100

    Записей на страницу, от 1 до 100. Значение вне диапазона даёт 400, а не молчаливое обрезание.

    Пример: limit=20

GET/api/v1/coaches

Постраничный список тренеров, находящихся сейчас в реестре: тренер остаётся в списке, пока у него есть хотя бы один танцор из реестра. Архивные не попадают в выдачу, если их не запросить через registryState.

  • searchstring

    Поиск по имени и фамилии без учёта регистра (оба варианта написания). Не менее 2 символов.

    Пример: search=Bohdan

  • sexMALE | FEMALE

    Пол: MALE или FEMALE.

    Пример: sex=FEMALE

  • countryIdinteger

    Внутренний id страны, см. /filters.

    Пример: countryId=804

  • countrystring

    Название страны или код ISO 3166-1 alpha-2.

    Пример: country=UA

  • clubIduuid

    UUID клуба. Учитывает только действующее членство — связи, закрытые архивированием, не считаются.

    Пример: clubId=0f1c7a2e-...

  • highestClassinteger

    Наивысший танцевальный класс.

    Пример: highestClass=5

  • registryStateREGISTRY | ARCHIVE

    Где находится запись: REGISTRY или ARCHIVE, несколько значений через запятую. По умолчанию REGISTRY — архив возвращается, только если указать его здесь явно. Любое другое значение отклоняется.

    Пример: registryState=ARCHIVE

  • sortname | approvedAt

    Поле сортировки реестра: name или approvedAt.

    Пример: sort=name

  • orderasc | desc

    Направление сортировки.

    Пример: order=asc

  • pageinteger

    Номер страницы, начиная с 1.

    Пример: page=1

  • limitinteger, 1-100

    Записей на страницу, от 1 до 100. Значение вне диапазона даёт 400, а не молчаливое обрезание.

    Пример: limit=20

GET/api/v1/judges

Постраничный список судей, находящихся сейчас в реестре. Судейское положение не зависит от танцевальной записи того же человека. Архивные не попадают в выдачу, если их не запросить через registryState.

  • searchstring

    Поиск по имени и фамилии без учёта регистра (оба варианта написания). Не менее 2 символов.

    Пример: search=Bohdan

  • sexMALE | FEMALE

    Пол: MALE или FEMALE.

    Пример: sex=FEMALE

  • countryIdinteger

    Внутренний id страны, см. /filters.

    Пример: countryId=804

  • countrystring

    Название страны или код ISO 3166-1 alpha-2.

    Пример: country=UA

  • judgeCategoryinteger

    Квалификационная категория судьи.

    Пример: judgeCategory=2

  • positionjudge | headJudge | sportsInspector

    Позиция: judge, headJudge или sportsInspector.

    Пример: position=judge

  • registryStateREGISTRY | ARCHIVE

    Где находится запись: REGISTRY или ARCHIVE, несколько значений через запятую. По умолчанию REGISTRY — архив возвращается, только если указать его здесь явно. Любое другое значение отклоняется.

    Пример: registryState=ARCHIVE

  • statusACTIVE | LIMITED | INACTIVE

    Статус членства именно этой роли. Несколько значений через запятую. Сверяется по роли, поэтому человек с просроченным взносом в другой роли всё равно попадёт в выдачу. Не связан с registryState.

    Пример: status=ACTIVE

  • sortname | approvedAt

    Поле сортировки реестра: name или approvedAt.

    Пример: sort=name

  • orderasc | desc

    Направление сортировки.

    Пример: order=asc

  • pageinteger

    Номер страницы, начиная с 1.

    Пример: page=1

  • limitinteger, 1-100

    Записей на страницу, от 1 до 100. Значение вне диапазона даёт 400, а не молчаливое обрезание.

    Пример: limit=20

GET/api/v1/clubs

Постраничный список клубов, находящихся сейчас в реестре: клуб остаётся в списке, пока у него есть хотя бы один участник из реестра. dancersCount и coachesCount показывают текущий состав.

  • searchstring

    Поиск по названию клуба без учёта регистра. Не менее 2 символов.

    Пример: search=Diamant

  • countryIdinteger

    Внутренний id страны, см. /filters.

    Пример: countryId=804

  • countrystring

    Название страны или код ISO 3166-1 alpha-2.

    Пример: country=UA

  • citystring

    Название города, совпадение по началу строки, без учёта регистра.

    Пример: city=Kyiv

  • registryStateREGISTRY | ARCHIVE

    Где находится запись: REGISTRY или ARCHIVE, несколько значений через запятую. По умолчанию REGISTRY — архив возвращается, только если указать его здесь явно. Любое другое значение отклоняется.

    Пример: registryState=ARCHIVE

  • sortname | approvedAt

    Поле сортировки реестра: name или approvedAt.

    Пример: sort=name

  • orderasc | desc

    Направление сортировки.

    Пример: order=asc

  • pageinteger

    Номер страницы, начиная с 1.

    Пример: page=1

  • limitinteger, 1-100

    Записей на страницу, от 1 до 100. Значение вне диапазона даёт 400, а не молчаливое обрезание.

    Пример: limit=20

GET/api/v1/clubs/{id}

Один клуб целиком: дата основания, публичные контакты, все адреса и тренерский состав. Сделано под страницу клуба. Архивные клубы здесь тоже читаются — архив это раздел реестра, а не удаление, — поэтому проверяйте `registryState`, а не рассчитывайте на 404. Танцоров в ответе нет, потому что их могут быть сотни: за ними идите по `links.dancers`.

GET/api/v1/registry/filters

Значения для фильтров реестра: страны со счётчиками, классы, судейские категории и позиции, а также допустимые значения status и registryState. Счётчики ограничены так же, как и списки.

Формат ответа

Списки приходят в data, рядом с meta с состоянием пагинации и links с готовыми адресами следующей и предыдущей страниц.

  • У каждого соревнования есть и id, и slug. Slug назначается один раз и фиксируется в момент публикации, поэтому он переживает переименование — стройте свои адреса на нём, а в /competitions/{idOrSlug} можно передать любой из двух.
  • registration.state принимает одно из значений not_yet_open, open или closed. Оно заменило булево isOpen, которое помечало как открытое и то соревнование, регистрация на которое ещё не началась, потому что булево значение не способно выразить три состояния.
  • entriesCount и startsCount считают разные вещи. Одна заявка может выбрать несколько категорий, поэтому сумма startsCount по категориям превышает entriesCount соревнования. Используйте entriesCount для «сколько приняло участие», а startsCount — для «насколько заполнена категория».
  • banner.horizontal и banner.vertical содержат либо адаптивный srcset по ширине в пикселях, либо одну запись default для баннера, загруженного до появления адаптивных размеров, либо null, если для этой ориентации ничего не загружали.
  • Записи реестра содержат registryState рядом со status, а также registryUntil — ближайшую дату на отсчёте этой записи, каким бы этот отсчёт ни был, — и archivedAt, равный null для всего, что сейчас не в архиве.
  • У клуба может быть несколько залов. В `locations` есть все адреса — сначала главный, затем каждый филиал со своим городом, страной и идентификатором места Google. Поля `address` и `country` на верхнем уровне описывают только главный адрес и оставлены плоскими для тех, кто подключился до появления филиалов. Фильтры `city`, `country` и `countryId` тоже смотрят только на главный адрес, поэтому клуб, у которого в городе лишь филиал, в выдачу не попадёт.
  • Даты соревнования и дедлайн регистрации — это календарные дни в формате YYYY-MM-DD, без времени и часового пояса, потому что именно так их задаёт организатор. createdAt и updatedAt — полные отметки времени.
  • Новые поля могут появляться без предупреждения, поэтому ваш клиент должен игнорировать незнакомые ключи.

GET /api/v1/competitions

{
  "data": [
    {
      "id": "0f1c7a2e-9d64-4a1b-bb02-7e9f8c3d5a11",
      "slug": "kyiv-open-2026",
      "name": "Kyiv Open Championship 2026",
      "shortName": "Kyiv Open 2026",
      "status": "open",
      "type": { "code": "ranking_tournament", "label": "Ranking tournament" },
      "organizer": {
        "name": "Ukrainian Dance Federation",
        "shortName": "UDF",
        "country": "Ukraine",
        "countryCode": "UA",
        "website": "https://example.org"
      },
      "resultsUrl": null,
      "dates": { "start": "2026-10-17", "end": "2026-10-18" },
      "location": {
        "country": "Ukraine",
        "countryCode": "UA",
        "city": "Kyiv",
        "venueName": "Palace of Sports",
        "venueAddress": "1 Sportyvna Sq."
      },
      "registration": {
        "status": "open",
        "state": "open",
        "opensAt": "2026-03-01",
        "deadline": "2026-10-10",
        "maxEntries": 800,
        "entriesCount": 317,
        "startsCount": 894
      },
      "banner": {
        "horizontal": {
          "640": "https://api.e-phan.com/uploads/competitions/kyiv-open-640.avif",
          "1280": "https://api.e-phan.com/uploads/competitions/kyiv-open-1280.avif",
          "1920": "https://api.e-phan.com/uploads/competitions/kyiv-open-1920.avif"
        },
        "vertical": null
      },
      "media": {
        "entryForm": null
      },
      "categories": [
        {
          "id": "6b8d1f04-2c77-4f1e-9a3b-5d2e0c8a4477",
          "name": "Juvenile I Latin, class E",
          "entryType": "couple",
          "style": "latin",
          "ageGroup": "Juvenile I",
          "level": 5,
          "dances": ["samba", "cha-cha-cha", "jive"],
          "startsCount": 42
        }
      ],
      "links": {
        "self": "…",
        "categories": "…",
        "web": "…",
        "registration": "…"
      },
      "updatedAt": "2026-07-21T10:12:44.907Z"
    }
  ],
  "meta": { "page": 1, "limit": 20, "total": 34, "totalPages": 2, "hasNextPage": true },
  "links": { "self": "…", "next": "…", "previous": null }
}

GET /api/v1/dancers

{
  "data": [
    {
      "id": "a3f2c1d8-7e4b-49a1-bc06-3d5e9f1a2b77",
      "name": "Олена",
      "surname": "Петренко",
      "nameEn": "Olena",
      "surnameEn": "Petrenko",
      "avatar": "https://api.e-phan.com/uploads/users/avatar.jpg",
      "danceClass": 3,
      "country": { "id": 1, "name": "Ukraine", "code": "UA" },

      "status": "INACTIVE",
      "membershipExpiresAt": "2025-12-31",

      "registryState": "REGISTRY",
      "registryUntil": "2026-06-30",
      "archivedAt": null
    }
  ],
  "meta": {
    "page": 1,
    "limit": 20,
    "total": 142,
    "totalPages": 8,
    "hasNextPage": true,
    "hasPreviousPage": false
  }
}

GET /api/v1/clubs

{
  "data": [
    {
      "id": "7c2b9e14-5a83-4d20-91ff-0e6a4b8c1d33",
      "name": "Diamant Elite",
      "logo": "https://api.e-phan.com/uploads/clubs/diamant.png",

      "address": "вул. Хрещатик, 1, Київ (2 поверх)",
      "country": { "id": 1, "name": "Ukraine", "code": "UA" },

      "locations": [
        {
          "type": "main",
          "address": "вул. Хрещатик, 1, Київ (2 поверх)",
          "city": "Київ",
          "country": { "id": 1, "name": "Ukraine", "code": "UA" },
          "placeId": "ChIJBUVa4U7P1EAR_kYBF9IxSXY"
        },
        {
          "type": "branch",
          "address": "вул. М. Гришка, 6А, Київ",
          "city": "Київ",
          "country": { "id": 1, "name": "Ukraine", "code": "UA" },
          "placeId": "ChIJk3Zt1XjP1EARa4LmC2mQhZ0"
        }
      ],

      "dancersCount": 34,
      "coachesCount": 4,
      "registryState": "REGISTRY",
      "registryUntil": null,
      "archivedAt": null
    }
  ],
  "meta": { "page": 1, "limit": 20, "total": 12, "totalPages": 1, "hasNextPage": false }
}

GET /api/v1/clubs/{id}

{
  "data": {
    "id": "7c2b9e14-5a83-4d20-91ff-0e6a4b8c1d33",
    "name": "Diamant Elite",
    "logo": "https://api.e-phan.com/uploads/clubs/diamant.png",

    "address": "вул. Хрещатик, 1, Київ (2 поверх)",
    "country": { "id": 1, "name": "Ukraine", "code": "UA" },
    "locations": [ "…main and every branch…" ],

    "foundedOn": "2003",
    "approvedAt": "2026-04-06T09:44:41.666Z",
    "contacts": {
      "phone": "+380441234567",
      "email": "info@diamant.example",
      "website": "https://diamant.example",
      "instagram": "https://instagram.com/diamant",
      "facebook": null
    },

    "dancersCount": 34,
    "coachesCount": 2,
    "coaches": [
      {
        "id": "b81f0a55-2c19-4f77-9d3e-6a0c5e2b7f41",
        "name": "Ігор", "surname": "Ковальчук",
        "nameEn": "Ihor", "surnameEn": "Kovalchuk",
        "avatar": null,
        "highestClass": 8,
        "country": { "id": 1, "name": "Ukraine", "code": "UA" },
        "registryState": "REGISTRY",
        "registryUntil": null,
        "archivedAt": null
      }
    ],

    "registryState": "REGISTRY",
    "registryUntil": null,
    "archivedAt": null,

    "links": {
      "self": "…",
      "dancers": "…/dancers?clubId=7c2b9e14-…",
      "coaches": "…/coaches?clubId=7c2b9e14-…",
      "web": "…"
    }
  }
}

Кеширование

Успешные чтения запоминаются в процессе бэкенда по ключу «путь плюс упорядоченная строка запроса», поэтому повторный вызов не доходит ни до планировщика запросов, ни до сериализатора. Вам не обязательно держать копию календаря, а нашу можно кешировать: ответы несут Cache-Control: public, max-age=<TTL>, который CDN или ваш собственный прокси волен учитывать.

  • Любая запись в соревнование или категорию очищает хранилище через хуки Sequelize, поэтому правка становится видна на следующем запросе, а не после истечения TTL.
  • TTL по эндпоинтам: 60 с для списка соревнований и отдельного соревнования, 120 с для списков реестра, 300 с для категорий и значений фильтров. Запросы с updatedSince полностью обходят кеш и отвечают Cache-Control: no-store.
  • X-Cache сообщает HIT, MISS или BYPASS на каждом чтении. ETag и If-None-Match обрабатываются и на пути попадания, и на пути промаха, поэтому условные запросы работают независимо от состояния кеша.
  • entriesCount и startsCount могут отставать на время TTL: заявки поступают непрерывно, и сброс кеша на каждую держал бы его постоянно пустым. Состояние реестра меняется и по таймеру — когда срок просто достигает своей даты, — так что и там запись может отставать на время TTL.
  • Кешируйте эти ответы и у себя и продолжайте отдавать последний удачный, если мы недоступны. Гарантии доступности здесь нет — календарь, который пустеет из-за сбоя, хуже календаря, устаревшего на час.

Условный запрос: 200, затем 304

# 1. Read the data and keep the validator
curl -i "https://api.e-phan.com/api/v1/competitions?period=upcoming"

# HTTP/1.1 200 OK
# ETag: W/"a3f-9Kd1ynQ+Xc0Vv2Bf1lPqE7dZs"
# Cache-Control: public, max-age=60
# X-Cache: MISS

# 2. Ask again, passing the validator back
curl -i -H 'If-None-Match: W/"a3f-9Kd1ynQ+Xc0Vv2Bf1lPqE7dZs"' \
  "https://api.e-phan.com/api/v1/competitions?period=upcoming"

# HTTP/1.1 304 Not Modified
# X-Cache: HIT

Лимиты запросов

300 запросов в минуту с одного IP-адреса, в 60-секундном скользящем окне. Напишите нам, если вашей интеграции нужно больше.

  • Каждый ответ несёт X-RateLimit-Limit, X-RateLimit-Remaining и X-RateLimit-Window.
  • Превышение бюджета даёт 429 с Retry-After в секундах и конвертом ошибки типа RATE_LIMIT_EXCEEDED.
  • Бюджет расходует каждый запрос, включая 304 и попадание в кеш: счётчик срабатывает до обращения к кешу, поэтому лимит ограничивает, как часто вы к нам обращаетесь, а не сколько работы мы выполняем.

Ошибки

Ошибки всегда приходят в одинаковом конверте, с машиночитаемым полем type.

  • 400
    INVALID_REQUEST

    Параметр запроса содержит неподдерживаемое значение. Поле details перечисляет их.

  • 404
    NOT_FOUND

    Публичного ресурса с таким id не существует.

  • 429
    RATE_LIMIT_EXCEEDED

    Минутный бюджет исчерпан.

Пример ошибки

{
  "error": {
    "type": "INVALID_REQUEST",
    "message": "One or more query parameters are invalid.",
    "details": ["limit: expected an integer between 1 and 100"]
  }
}

Каждый ответ несёт X-Request-Id. Укажите его в обращении — и мы найдём запрос в своих логах. Можете прислать собственный, и мы вернём его обратно, если он соответствует [A-Za-z0-9_.:-] и не длиннее 64 символов; всё остальное заменяется сгенерированным.

Условия использования

  • Публикуя эти данные, указывайте e-phan и давайте ссылку на страницу соревнования.
  • API отдаёт только опубликованные соревнования: черновики организаторов в ленту не попадают.
  • Новые поля могут появляться без предупреждения, поэтому ваш клиент должен игнорировать незнакомые ключи. Об изменениях, ломающих совместимость, сообщаем заранее и выпускаем их под новой версией пути.

Вопросы по интеграции: support@e-phan.com