API для внешних систем управления

Версия v1 — документация обновлена 29 июля 2026

Эта страница содержит документацию по HTTP API, которые VolleyFriends предоставляет владельцам площадок для синхронизации календаря с внешней системой управления или сайтом онлайн-бронирования. Здесь действуют те же бизнес-правила, что и в панели администратора (конфликты, закрытие дат, прайс-лист): меняется только способ авторизации.

1. Возможности API

С помощью API-ключа вы можете из своего программного обеспечения читать и записывать данные в календарь ваших объектов:

  • получать информацию об объектах и площадках с их идентификаторами, необходимыми для всех остальных запросов;
  • получать список бронирований за определенный период, по объекту или отдельной площадке для синхронизации с вашей базой данных;
  • создавать бронирования из внешних систем (например, когда клиент бронирует на вашем сайте) с теми же проверками на наложение броней и закрытие площадок;
  • изменять или отменять существующие бронирования (перенос времени или площадки, оплата, примечания, отмена);
  • получать информацию о доступности площадки на определенный день: уже занятые слоты, часы работы и внеплановые закрытия, чтобы предлагать свободные слоты без необходимости дублировать правила на вашей стороне;
  • получить прайс-лист площадки для отображения цен конечному клиенту.
Область действия ключей

Каждый ключ привязан к аккаунту управляющего, который его создал, и видит только объекты, площадки и бронирования этого аккаунта. Ресурс, принадлежащий другому управляющему, возвращает не 403, а 404 not_found: его существование не раскрывается.

В этот API не входят запросы организаторов на использование площадок в турнирах (они одобряются в приложении), создание объектов и площадок, сохранение расписания, закрытий и прайс-листа: это операции панели управляющего.

2. Требования и активация

  1. Аккаунт управляющего площадками VolleyFriends как минимум с одним объектом и одной площадкой.
  2. Действующая подписка с тарифом, включающим функцию «API для внешних систем управления». Вся v1даже операции только для чтения — зарезервирована для тех, у кого есть эта функция: это платная функция, а не бесплатное дополнение к панели.
  3. API-ключ, сгенерированный в панели управления: admin.volleyfriends.comНастройкиAPI-ключиНовый ключ.

Ключ имеет формат vfk_, за которым следуют 40 шестнадцатеричных символов (всего 44 символа), и отображается только один раз, сразу после создания: в базе данных сохраняется только его хэш sha256, поэтому восстановить его невозможно. Если вы его потеряете, отзовите его и создайте новый. В списке на панели управления видны только префикс (например, vfk_9f2c), дата создания и дата последнего использования.

3. Аутентификация

Каждый запрос должен быть аутентифицирован с помощью заголовка X-Api-Key. Токены, логины, сроки действия или продления не требуются: ключ остается действительным, пока вы его не отзовете.

# Ключи на этой странице приведены для примера: замените их своим собственным.
curl -s https://www.volleyfriends.com/api/manager-api/v1/venues   -H "X-Api-Key: vfk_9f2c41a7b83d05e6c19af4728b60d35e1c9a7f42"

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

Заголовок отсутствует, короче 12 символов, неизвестен или относится к отозванному ключу: возвращается ответ 401 со следующим телом.

{
  "error": "invalid_key",
  "message": "Chiave API mancante o non valida (header X-Api-Key)."
}
Хранение ключа
  • Ключ действует как пароль с полными правами доступа к календарю ваших объектов: не вставляйте его на веб-страницы, в приложения или в JavaScript на стороне браузера, где его сможет прочитать кто угодно. Используйте его только для взаимодействия между серверами.
  • Не добавляйте его в исходный код или в репозиторий: используйте переменную окружения или хранилище секретов.
  • Используйте один ключ для каждой подключенной системы (сайт, система управления, скрипт синхронизации): так вы сможете отозвать один ключ, не отключая остальные.
  • Если вы подозреваете, что ключ был скомпрометирован, немедленно отзовите его в панели управления: отзыв происходит мгновенно и никогда не блокируется статусом подписки. С этого момента любой вызов с этим ключом будет возвращать ошибку 401 invalid_key.
  • Трафик передается исключительно по протоколу HTTPS; поле «последнее использование» в панели управления поможет вам заметить подозрительную активность.

4. Базовый URL

https://www.volleyfriends.com/api

Пути, описанные ниже, должны добавляться к базовому URL. Префикс /api относится к обратному прокси-серверу, поэтому полный адрес конечной точки выглядит, например, так:

GET https://www.volleyfriends.com/api/manager-api/v1/availability

Все ответы возвращаются в формате application/json с кодировкой UTF-8. Запросы с телом (POST, PATCH) должны содержать заголовок Content-Type: application/json.

5. Ограничение частоты запросов

Лимит составляет 120 запросов в минуту на один API-ключ (скользящее окно в 1 минуту). Подсчет ведется по ключу, а не по IP-адресу: системы управления часто находятся за одним и тем же адресом провайдера, и лимит по IP нанесет ущерб разным операторам, размещенным на одной инфраструктуре. Если заголовок X-Api-Key полностью отсутствует, подсчет переносится на IP-адрес.

Каждый ответ содержит состояние счетчика в заголовках x-ratelimit-limit, x-ratelimit-remaining и x-ratelimit-reset (секунд до сброса). При превышении лимита также добавляется заголовок retry-after.

Ответ при превышении лимита — 429

{
  "error": "rate_limited",
  "message": "Limite di 120 richieste al minuto superato per questa chiave API."
}
Как оставаться в пределах лимита
  • Для периодической синхронизации используйте только один вызов /bookings с широким интервалом fromto вместо одного вызова в день.
  • Кешируйте список объектов и площадок: он меняется редко.
  • В случае ошибки 429 подождите и повторите попытку с экспоненциальной задержкой (например, 2, 4, 8 секунд), соблюдая retry-after.

6. Эндпоинты

Шесть эндпоинтов, все по пути /manager-api/v1. Каждый вызов проходит через одну и ту же цепочку проверок в следующем порядке: валидный API-ключ → функция «API для внешних систем управления» включена в тариф → rate limit → валидация параметров → проверка принадлежности ресурса вам.

6.1 Объекты и площадки

GET/api/manager-api/v1/venues

Возвращает список ваших объектов, каждый со своими вложенными площадками. Это базовый вызов: идентификаторы id площадок (courtId) требуются для всех остальных эндпоинтов. Объекты отсортированы по названию, площадки — по названию.

Параметры

Нет.

Пример запроса

curl -s https://www.volleyfriends.com/api/manager-api/v1/venues   -H "X-Api-Key: $VF_API_KEY"

Пример ответа — 200

{
  "venues": [
    {
      "id": "b3f8e5c1-2d47-4a90-8f31-6c0d9a7e4512",
      "name": "Centro Sportivo Aurora",
      "city": "Verona",
      "address": "Via delle Palestre 12",
      "verified": true,
      "courts": [
        {
          "id": "7c94a0d2-58b1-4e6f-9a3c-1d20f8b47e05",
          "venueId": "b3f8e5c1-2d47-4a90-8f31-6c0d9a7e4512",
          "name": "Campo 1",
          "surface": "parquet",
          "indoor": true,
          "lighting": true
        },
        {
          "id": "e2a71f64-9c03-4bd8-b512-38f6cd90a7e1",
          "venueId": "b3f8e5c1-2d47-4a90-8f31-6c0d9a7e4512",
          "name": "Campo 2 beach",
          "surface": "sabbia",
          "indoor": false,
          "lighting": true
        }
      ]
    }
  ]
}

Поля ответа

ПолеТипОписание
venues[].iduuidИдентификатор объекта (venueId в других эндпоинтах).
venues[].namestringНазвание объекта.
venues[].citystring | nullГород.
venues[].addressstring | nullАдрес.
venues[].verifiedbooleantrue, если у площадки есть значок ПРОВЕРЕНО.
venues[].courts[]arrayИгровые поля площадки. Пустой массив, если поля отсутствуют.
courts[].iduuidИдентификатор игрового поля (courtId в других эндпоинтах).
courts[].venueIduuidПлощадка, к которой относится игровое поле.
courts[].namestringНазвание игрового поля.
courts[].surfacestring | nullПокрытие, произвольный текст, введенный менеджером.
courts[].indoorbooleanКрытое игровое поле.
courts[].lightingbooleanНаличие освещения.

Если у аккаунта еще нет площадок, ответ будет {"venues": []}.

Специфические ошибки

Отсутствуют, кроме общих.

6.2 Список бронирований

GET/api/manager-api/v1/bookings

Возвращает бронирования ваших площадок, отсортированные по возрастанию времени начала. Список является постраничным: максимум limit строк на запрос (по умолчанию 500, лимит 1000) — если вы получаете ровно limit строк, сделайте повторный запрос с увеличенным значением offset для перехода на следующую страницу. В рабочей среде всегда фильтруйте данные как минимум по диапазону дат.

Параметры (строка запроса)

ИмяТипОбяз.Описание
venueIduuidнетОграничить одной площадкой. Если она вам не принадлежит: 404 not_found.
courtIduuidнетОграничивает одной площадкой. Если она не ваша: 404 not_found. В сочетании с venueId другого вашего объекта результатом будет пустой список.
fromдата или момент времени ISOнетНижняя граница включена. Простая дата в формате AAAA-MM-GG означает местную итальянскую полночь этого дня.
toдата или момент времени ISOнетВерхняя граница исключена. Простая дата означает местную полночь следующего дня: to=2026-08-31 включает весь день 31 августа.
limitinteger 1–1000нетМаксимальное количество строк (по умолчанию 500).
offsetinteger ≥ 0нетКоличество пропускаемых строк (по умолчанию 0): offset=500 — это вторая страница при значении limit по умолчанию.
На что действует фильтр

from e to si applicano all'istante di inizio della prenotazione. Una prenotazione iniziata prima di from e ancora in corso dopo non compare: se ti serve, allarga l'estremo inferiore di qualche ora. Il filtro non esclude le prenotazioni annullate: l'elenco contiene anche quelle con status: "cancelled".

Пример запроса

curl -s -G https://www.volleyfriends.com/api/manager-api/v1/bookings   -H "X-Api-Key: $VF_API_KEY"   --data-urlencode "courtId=7c94a0d2-58b1-4e6f-9a3c-1d20f8b47e05"   --data-urlencode "from=2026-08-01"   --data-urlencode "to=2026-08-31"

Пример ответа — 200

{
  "bookings": [
    {
      "id": "9d51c7b4-0e28-4f3a-a6d9-72b48e105c3f",
      "venueId": "b3f8e5c1-2d47-4a90-8f31-6c0d9a7e4512",
      "courtId": "7c94a0d2-58b1-4e6f-9a3c-1d20f8b47e05",
      "courtName": "Campo 1",
      "startsAt": "2026-08-12T16:00:00.000Z",
      "endsAt": "2026-08-12T17:30:00.000Z",
      "customerName": "Marta Bellini",
      "customerPhone": "+39 340 1122334",
      "customerEmail": "marta.bellini@example.com",
      "priceCents": 3000,
      "playersCount": null,
      "status": "confirmed",
      "paymentMethod": null,
      "paymentLink": null,
      "notes": "Prenotato dal sito della struttura",
      "source": "api",
      "recurrenceId": null,
      "createdAt": "2026-07-28T09:14:02.517Z",
      "updatedAt": "2026-07-28T09:14:02.517Z"
    }
  ],
  "limit": 500,
  "offset": 0
}

Поля бронирования

Эта же структура возвращается методами POST и PATCH (внутри ключа booking).

ПолеТипОписание
iduuidИдентификатор бронирования.
venueIduuidОбъект.
courtIduuidЗабронированная площадка.
courtNamestringНазвание площадки для удобства отображения.
startsAtмомент времени ISOНачало, всегда в UTC (суффикс Z).
endsAtмомент времени ISOКонец, граница исключена.
customerNamestringИмя клиента.
customerPhonestring | nullТелефон.
customerEmailstring | nullEmail.
priceCentsinteger | nullСогласованная цена в центах. null = не определено (никогда не 0 для «не определено»).
playersCountinteger | nullЗаявленное количество человек (только для бронирований во временных интервалах с ценой за человека). null = не применимо или не указано.
statusstringpending · confirmed · paid · cancelled — см. статусы.
paymentMethodstring | nullcash · bank_transfer · paypal · stripe · other. null = не получено.
paymentLinkstring | nullСсылка на оплату картой, сгенерированная панелью управления, если есть. Только для чтения.
notesstring | nullПримечания.
sourcestringИсточник: manual (панель управления) · api (данный API) · app (запрос игрока из приложения) · tournament (турнир).
recurrenceIduuid | nullПрисутствует и совпадает во всех повторениях повторяющейся серии, созданной в панели управления.
createdAtмомент времени ISOСоздание.
updatedAtмомент времени ISOПоследнее изменение.

Специфические ошибки

КодerrorКогда
400invalid_inputvenueId/courtId не являются UUID, либо from/to не являются распознаваемыми датами.
404not_foundСпортивный объект ("Struttura non trovata") или площадка ("Campo non trovato") не существует или вам не принадлежит.

6.3 Новое бронирование

POST/api/manager-api/v1/bookings

Создает бронирование на вашей площадке. Бронирование создается с source: "api", чтобы в панели управления его можно было отличить от введенных вручную, а управляющий получает push-уведомление «Новое бронирование» в приложении.

Применяются те же правила, что и в панели управления, по порядку:

  1. внеплановое закрытие спортивного объекта на дату начала → 409 venue_closed;
  2. наложение на другое неотмененное бронирование той же площадки → 409 overlap;
  3. цена отсутствует → рассчитывается по тарифной сетке площадки (null, если время начала не попадает ни в один интервал). Если для интервала установлена цена за человека, также требуется playersCount: без него запрос отклоняется с ошибкой 400 players_required (если только не указана явная цена priceCents).

Часы работы не блокируют создание: они носят информационный характер, управляющий остается полновластным хозяином своей площадки. Если вы хотите предотвратить бронирование во внерабочее время, самостоятельно проверьте hours, возвращаемые /availability, перед вызовом POST.

Тело запроса (JSON)

ИмяТипОбяз.Описание
courtIduuidдаПлощадка для бронирования. Должна принадлежать вашей структуре.
startsAtмомент времени ISO 8601 с офсетомдаНачало. Принимаются форматы как 2026-08-12T18:00:00Z, так и 2026-08-12T18:00:00+02:00. Без офсета: 400.
endsAtмомент времени ISO 8601 с офсетомдаОкончание, должно быть позже startsAt.
customerNamestring (1–160)даИмя клиента.
customerPhonestring (макс. 40)нетТелефон.
customerEmailemail (макс. 200)нетEmail, валидированный как адрес.
priceCentsinteger ≥ 0нетЦена в центах. Если опущено, рассчитывается по прайс-листу площадки. Указывайте 0 только для действительно бесплатного бронирования.
playersCountinteger 1–500нетКоличество человек. Обязательно, если цена рассчитывается по прайс-листу, а тарифный интервал времени начала — за человека; к общей сумме применяется возможный гарантированный минимум для этого интервала.
notesstring (макс. 2000)нетПроизвольные заметки, видимые менеджеру в панели управления.
statuspending | confirmed | paid | cancelledнетПо умолчанию confirmed. Из внешней системы управления используйте confirmed или paid: статус pending зарезервирован для запросов, поступающих из приложения игроков (см. статусы).
paymentMethodcash | bank_transfer | paypal | stripe | otherнетКак была (или будет) получена оплата. Опущено = не оплачено.
recurrenceobjectнетНе поддерживается в этом API: при наличии этого поля запрос отклоняется с ошибкой 400 recurrence_unsupported (бронирование не создается). Повторяющиеся серии остаются функцией панели управления: из внешней системы управления отправляйте отдельный запрос POST для каждого случая.

Пример запроса

curl -s -X POST https://www.volleyfriends.com/api/manager-api/v1/bookings   -H "X-Api-Key: $VF_API_KEY"   -H "Content-Type: application/json"   -d '{
    "courtId": "7c94a0d2-58b1-4e6f-9a3c-1d20f8b47e05",
    "startsAt": "2026-08-12T18:00:00+02:00",
    "endsAt": "2026-08-12T19:30:00+02:00",
    "customerName": "Marta Bellini",
    "customerPhone": "+39 340 1122334",
    "customerEmail": "marta.bellini@example.com",
    "notes": "Prenotato dal sito della struttura"
  }'

Пример ответа — 201

{
  "booking": {
    "id": "9d51c7b4-0e28-4f3a-a6d9-72b48e105c3f",
    "venueId": "b3f8e5c1-2d47-4a90-8f31-6c0d9a7e4512",
    "courtId": "7c94a0d2-58b1-4e6f-9a3c-1d20f8b47e05",
    "courtName": "Campo 1",
    "startsAt": "2026-08-12T16:00:00.000Z",
    "endsAt": "2026-08-12T17:30:00.000Z",
    "customerName": "Marta Bellini",
    "customerPhone": "+39 340 1122334",
    "customerEmail": "marta.bellini@example.com",
    "priceCents": 3000,
    "playersCount": null,
    "status": "confirmed",
    "paymentMethod": null,
    "paymentLink": null,
    "notes": "Prenotato dal sito della struttura",
    "source": "api",
    "recurrenceId": null,
    "createdAt": "2026-07-28T09:14:02.517Z",
    "updatedAt": "2026-07-28T09:14:02.517Z"
  }
}

В примере параметр priceCents не был указан: 90 минут в интервале с тарифом 20,00 €/час дают 3000 центов (расчет пропорционален фактическим минутам по тарифу того интервала, в котором начинается бронирование).

Специфические ошибки

КодerrorКогда
400invalid_inputНекорректное тело запроса: отсутствует обязательное поле, время указано без офсета, значение endsAt не позже startsAt, невалидный email, превышена допустимая длина.
400players_requiredЦена должна рассчитываться по прайс-листу для интервала за человека, но параметр playersCount отсутствует.
400recurrence_unsupportedТело запроса содержит recurrence: повторяющиеся бронирования недоступны через это API.
403subscription_requiredНет действующей подписки менеджера.
404not_found"Campo non trovato"courtId не существует или не принадлежит вам.
409venue_closedУ объекта внеплановое закрытие на дату начала.
409overlapНа этой площадке уже есть неотмененное бронирование, которое пересекается с указанным интервалом.
// 409 — пересечение
{ "error": "overlap", "message": "Il campo è già prenotato in quell'orario." }

// 409 — внеплановое закрытие (причина, если она указана администратором, содержится в сообщении)
{ "error": "venue_closed", "message": "Struttura chiusa in quella data (manutenzione)." }
Идемпотентность

Ключ идемпотентности отсутствует: повторный запрос POST после тайм-аута может создать дубликат или, чаще всего, вернуть ошибку 409 overlap, поскольку первая попытка прошла успешно. В случае неопределенного ответа перед повторной попыткой выполните проверку с помощью GET /bookings для интересующего интервала.

6.4 Изменение бронирования

PATCH/api/manager-api/v1/bookings/{id}

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

Эндпоинт для удаления отсутствует: чтобы отменить бронирование, установите значение status: "cancelled". История сохраняется в панели управления, а слот снова становится свободным (отмененные бронирования не занимают площадку и не вызывают ошибку overlap).

Параметр пути

ИмяТипОбяз.Описание
iduuidдаID бронирования, полученный из GET /bookings или из ответа на POST-запрос.

Тело запроса (JSON)

Все поля являются необязательными. Поля с пометкой «с возможностью сброса» принимают значение null для очистки значения.

ИмяТипПримечания
courtIduuidПереносит бронирование на другую вашу площадку, в том числе на другом вашем объекте (значение venueId обновляется соответствующим образом).
startsAtвремя в формате ISO со смещениемНовое время начала.
endsAtвремя в формате ISO со смещениемНовое время окончания. Конечный результат должен быть позже времени начала, даже если изменяется только одна из границ по сравнению с уже сохраненным значением.
customerNamestring (1–160)
customerPhonestring (макс. 40)С возможностью сброса.
customerEmailemail (макс. 200)С возможностью сброса.
priceCentsinteger ≥ 0С возможностью сброса (null = цена подлежит уточнению). В запросе PATCH цена не пересчитывается автоматически по прайс-листу: если вы переносите время и хотите установить новую цену, укажите ее.
playersCountinteger 1–500С возможностью сброса. Обновляет только данные: цена не пересчитывается автоматически — если изменение количества человек влияет на общую стоимость, отправьте также новое значение priceCents.
notesstring (макс. 2000)С возможностью сброса.
statuspending | confirmed | paid | cancelledДопускается любой переход статуса. Это также способ ответить на запрос из приложения (status: "pending" с source: "app"): переведите его в статус confirmed, чтобы принять, или в cancelled, чтобы отклонить — игрок получит уведомление.
paymentMethodcash | bank_transfer | paypal | stripe | otherС возможностью сброса.
Когда проверяются конфликты

Проверки закрытия и наложения выполняются повторно в двух случаях: когда запрос перемещает слот (startsAt, endsAt или courtId) и когда он активирует заново отмененное бронирование (status с cancelled на любой другой статус) — в отмененном состоянии оно не занимало корт, и за это время слот мог быть занят: в этом случае PATCH возвращает ответ 409 overlap или 409 venue_closed. PATCH, который изменяет только другие поля (цена, примечания, выручка), не повторяет эти проверки.

Пример запроса — перенос и выручка

curl -s -X PATCH   https://www.volleyfriends.com/api/manager-api/v1/bookings/9d51c7b4-0e28-4f3a-a6d9-72b48e105c3f   -H "X-Api-Key: $VF_API_KEY"   -H "Content-Type: application/json"   -d '{
    "startsAt": "2026-08-12T19:00:00+02:00",
    "endsAt": "2026-08-12T20:30:00+02:00",
    "status": "paid",
    "paymentMethod": "cash",
    "priceCents": 3000
  }'

Пример запроса — отмена

curl -s -X PATCH   https://www.volleyfriends.com/api/manager-api/v1/bookings/9d51c7b4-0e28-4f3a-a6d9-72b48e105c3f   -H "X-Api-Key: $VF_API_KEY"   -H "Content-Type: application/json"   -d '{ "status": "cancelled" }'

Пример ответа — 200

Обновленное бронирование в том же формате, что и POST:

{
  "booking": {
    "id": "9d51c7b4-0e28-4f3a-a6d9-72b48e105c3f",
    "venueId": "b3f8e5c1-2d47-4a90-8f31-6c0d9a7e4512",
    "courtId": "7c94a0d2-58b1-4e6f-9a3c-1d20f8b47e05",
    "courtName": "Campo 1",
    "startsAt": "2026-08-12T17:00:00.000Z",
    "endsAt": "2026-08-12T18:30:00.000Z",
    "customerName": "Marta Bellini",
    "customerPhone": "+39 340 1122334",
    "customerEmail": "marta.bellini@example.com",
    "priceCents": 3000,
    "playersCount": null,
    "status": "paid",
    "paymentMethod": "cash",
    "paymentLink": null,
    "notes": "Prenotato dal sito della struttura",
    "source": "api",
    "recurrenceId": null,
    "createdAt": "2026-07-28T09:14:02.517Z",
    "updatedAt": "2026-07-28T11:02:47.883Z"
  }
}

Специфические ошибки

КодerrorКогда
400invalid_inputПустое тело ("Nessun campo da aggiornare"), невалидные значения или время окончания не позже времени начала ("L'orario di fine deve essere successivo a quello di inizio").
403subscription_requiredНет действующей подписки менеджера.
404not_found"Prenotazione non trovata" или "Campo non trovato", если новый courtId вам не принадлежит.
409venue_closedПеренос на дату внепланового закрытия.
409overlapНовое время накладывается на другое неотмененное бронирование того же корта.

6.5 Доступность корта на день

GET/api/manager-api/v1/availability

Возвращает для корта и дня занятые слоты, время работы в этот день недели и возможное внеплановое закрытие. Этот эндпоинт следует использовать для расчета свободных слотов на внешнем сайте без дублирования правил системы управления: вычтите бронирования из окна работы.

Перечислены все слоты, занимающие корт, в любом статусе, кроме cancelled: то есть включая запросы на онлайн-бронирование, которые все еще находятся в статусе pending. День определяется по местному итальянскому времени, а интервалы являются полуоткрытыми: бронирование, заканчивающееся ровно в полночь, относится к предыдущему дню. Бронирование, переходящее через полночь, отображается в обоих пересекаемых днях с его реальным временем (которое может выходить за пределы запрашиваемого дня).

Параметры (строка запроса)

ИмяТипОбяз.Описание
courtIduuidдаКорт, доступность которого необходимо проверить. Он должен принадлежать вам.
dateAAAA-MM-GGдаДень по местному итальянскому времени. Другой формат: 400 с сообщением "Data non valida (AAAA-MM-GG)".

Пример запроса

curl -s -G https://www.volleyfriends.com/api/manager-api/v1/availability   -H "X-Api-Key: $VF_API_KEY"   --data-urlencode "courtId=7c94a0d2-58b1-4e6f-9a3c-1d20f8b47e05"   --data-urlencode "date=2026-08-12"

Пример ответа — 200

{
  "courtId": "7c94a0d2-58b1-4e6f-9a3c-1d20f8b47e05",
  "venueId": "b3f8e5c1-2d47-4a90-8f31-6c0d9a7e4512",
  "date": "2026-08-12",
  "timezone": "Europe/Rome",
  "hours": {
    "weekday": 2,
    "openMinute": 540,
    "closeMinute": 1380,
    "closed": false,
    "configured": true
  },
  "closure": null,
  "bookings": [
    {
      "startsAt": "2026-08-12T16:00:00.000Z",
      "endsAt": "2026-08-12T17:30:00.000Z",
      "status": "confirmed"
    },
    {
      "startsAt": "2026-08-12T18:00:00.000Z",
      "endsAt": "2026-08-12T19:00:00.000Z",
      "status": "paid"
    },
    {
      "startsAt": "2026-08-12T19:00:00.000Z",
      "endsAt": "2026-08-12T20:30:00.000Z",
      "status": "pending"
    }
  ]
}

Поля ответа

ПолеТипОписание
courtIduuidЗапрашиваемый корт.
venueIduuidСтруктура корта.
dateAAAA-MM-GGЗапрашиваемый день, повторно.
timezonestringВсегда "Europe/Rome": часовой пояс, в котором следует интерпретировать date и минуты времени.
hours.weekdayinteger 0–6День недели: 0 = понедельник … 6 = воскресенье.
hours.openMinuteinteger | nullОткрытие в минутах от местной полуночи (540 = 09:00). null, если день объявлен закрытым.
hours.closeMinuteinteger | nullЗакрытие в минутах (1380 = 23:00). null, если день объявлен закрытым.
hours.closedbooleantrue, если менеджер объявил этот день недели закрытым.
hours.configuredbooleanfalse, если менеджер не настроил время для этого дня: в этом случае openMinute/closeMinute принимают значения по умолчанию 480 (08:00) – 1380 (23:00), что является резервным вариантом отображения, а не объявленным временем работы.
closureobject | nullВнеочередное закрытие, действующее в запрошенный день, или null.
closure.fromAAAA-MM-GGПервый день закрытия.
closure.toAAAA-MM-GGПоследний день закрытия, включительно.
closure.reasonstring | nullПричина, если указана менеджером.
bookings[]arrayЗанятые слоты, отсортированные по времени начала. Только startsAt, endsAt и status (pending, confirmed или paid): никаких данных клиента, чтобы ответ можно было использовать для создания публичного календаря.
При действующем закрытии

Если closure не равен null, день считается недоступным для бронирования, независимо от hours: запрос POST на эту дату вернет 409 venue_closed.

Специфические ошибки

КодerrorКогда
400invalid_inputcourtId отсутствует или не является UUID, date отсутствует или не в формате AAAA-MM-GG.
404not_found"Campo non trovato" — площадка не существует или вам не принадлежит.

6.6 Тарифная сетка площадки

GET/api/manager-api/v1/rates

Тарифная сетка площадки в режиме только для чтения: используется для отображения цен клиенту перед бронированием, с теми же интервалами, которые система управления использует для автоматического расчета цены. Интервалы можно изменить только в панели управления. Они отсортированы по возрастанию fromMinute.

Параметры (строка запроса)

ИмяТипОбяз.Описание
courtIduuidдаПлощадка, тарифную сетку которой нужно прочитать. Должна вам принадлежать.

Пример запроса

curl -s -G https://www.volleyfriends.com/api/manager-api/v1/rates   -H "X-Api-Key: $VF_API_KEY"   --data-urlencode "courtId=7c94a0d2-58b1-4e6f-9a3c-1d20f8b47e05"

Пример ответа — 200

{
  "courtId": "7c94a0d2-58b1-4e6f-9a3c-1d20f8b47e05",
  "rates": [
    {
      "id": "4a1e0b72-6d35-4c81-93af-0b7e2d5814c6",
      "label": "Mattina infrasettimanale",
      "daysMask": 31,
      "fromMinute": 480,
      "toMinute": 840,
      "pricePerHourCents": 1500,
      "pricingMode": "court",
      "minPlayers": null
    },
    {
      "id": "c07d9b31-8f42-4a05-b6e9-15d3c8017a2b",
      "label": "Serale",
      "daysMask": 127,
      "fromMinute": 1080,
      "toMinute": 1380,
      "pricePerHourCents": 600,
      "pricingMode": "person",
      "minPlayers": 10
    }
  ]
}

Поля ответа

ПолеТипОписание
courtIduuidЗапрашиваемый корт.
rates[].iduuidИдентификатор интервала.
rates[].labelstringМетка, выбранная менеджером (например, «Вечерний»).
rates[].daysMaskinteger 1–127Битовая маска дней: пн 1, вт 2, ср 4, чт 8, пт 16, сб 32, вс 64. 127 = все дни, 31 = с понедельника по пятницу.
rates[].fromMinuteinteger 0–1440Начало интервала в минутах от местной полуночи.
rates[].toMinuteinteger 0–1440Конец интервала, всегда больше, чем fromMinute.
rates[].pricePerHourCentsinteger > 0Цена за час в центах (2000 = 20,00 €). При pricingMode: "person" это почасовая цена с человека (600 = 6,00 €/человека в час).
rates[].pricingModecourt | personcourt = цена за площадку (по умолчанию). person = цена с человека: общая сумма масштабируется в зависимости от количества человек, но никогда не уменьшается.
rates[].minPlayersinteger | nullГарантированный минимум для тарифа за человека: если количество меньше этого числа, оплата все равно взимается за минимум. null = без минимума.
Как рассчитывается стоимость бронирования

Учитывается момент начала: среди временных интервалов площадки выбирается первый (в порядке fromMinute), день которого активен в битовой маске и который содержит минуту начала (fromMinute ≤ inizio < toMinute). Цена пропорциональна фактическим минутам: arrotonda(pricePerHourCents × minuti / 60) — 90 минут по 20,00 €/час составляют 30,00 €. Бронирование, переходящее из одного интервала в другой, тарифицируется по стоимости интервала начала. Если подходящий интервал отсутствует: priceCents принимает значение null (цена не определена), но никогда не 0.

При тарифе за человека (pricingMode: "person") почасовой результат умножается на количество человек, для которых выставляется счет: max(playersCount, minPlayers). Пример с вечерним тарифом выше (6,00 €/чел. в час, минимум 10): 60 минут на 12 человек → 72,00 €; на 16 → 96,00 € (цена за человека не снижается); на 8 → 60,00 € (срабатывает гарантированный минимум). Без playersCount цену невозможно рассчитать: null при чтении, 400 players_required в запросе POST, который опирается на прайс-лист.

Специфические ошибки

КодerrorКогда
400invalid_inputcourtId отсутствует или не является UUID.
404not_found"Campo non trovato" — площадка не существует или вам не принадлежит.

Площадка без прайс-листа возвращает 200 с "rates": [].

7. Соглашения о данных

7.1 Даты и время

  • На входе значения времени представляют собой строки ISO 8601 с обязательным смещением: 2026-08-12T18:00:00+02:00 или 2026-08-12T18:00:00Z. Строка без смещения (2026-08-12T18:00:00) отклоняется с ошибкой 400: она была бы неоднозначной, а для календаря такая неоднозначность приводит к ошибкам в бронировании на один час.
  • На выходе значения времени всегда нормализованы в UTC с суффиксом Z и миллисекундами: "2026-08-12T16:00:00.000Z". Преобразуйте их в местный часовой пояс перед отображением.
  • Простые даты (date, closure.from, closure.to и from/to при использовании в формате дня) имеют формат AAAA-MM-GG и относятся к местному итальянскому времени.
  • Интервалы являются полуоткрытыми [inizio, fine): 18:00–19:00 и 19:00–20:00 не конфликтуют друг с другом.

7.2 Часовой пояс

Бронирования хранятся как абсолютные моменты времени, но все правила календаря — день недели, интервалы прайс-листа, часы работы, даты закрытия, границы дня — оцениваются в часовом поясе Europe/Rome с автоматическим переходом на летнее время. Поле timezone в ответе /availability явно указывает на это. Если ваш сервер работает в UTC или другом часовом поясе, не конвертируйте минуты вручную: они уже выражены по местному итальянскому времени.

7.3 Минуты от полуночи

Время «по календарю» (openMinute, closeMinute, fromMinute, toMinute) представляет собой целые числа от 0 до 1440, обозначающие количество минут от местной полуночи. Преобразование: ore = ⌊m / 60⌋, minuti = m mod 60.

МинутыМестное времяМинутыМестное время
000:00108018:00
48008:00132022:00
54009:00138023:00
84014:00144024:00 (конец дня)

7.4 Суммы

Все суммы представляют собой целые числа в евроцентах (priceCents, pricePerHourCents): никаких значений с плавающей запятой, никаких символов валюты. 2000 = 20,00 €. В бронировании priceCents: null означает «цена не определена» и отличается от 0, что означает «бесплатно».

7.5 Статусы бронирования

СтатусЗначение
pendingОжидающий подтверждения онлайн-запрос на бронирование, требующий решения менеджера. Создается только на основе запросов, отправленных игроками из приложения VolleyFriends (source: "app"): менеджер принимает его (confirmed) или отклоняет (cancelled).Да — слот уже заблокирован во избежание повторных запросов на то же время
confirmedДействительное бронирование, оплата по которому еще не получена. Это начальный статус любого бронирования, созданного через эти API и панель управления.Да
paidДействительное и оплаченное бронирование. Обычно сопровождается полем paymentMethod.Да
cancelledБронирование отменено (или запрос отклонен). Остается в истории, но освобождает корт: не создает overlap и не отображается в /availability.Нет
Как обрабатывать бронирования со статусом «pending»
  • Вы найдете их в GET /bookings и среди занятых слотов в /availability: относитесь к слоту как к недоступному, точно так же, как к статусу confirmed.
  • Вы можете ответить на запрос из своей системы управления с помощью PATCH: {"status": "confirmed"}, чтобы принять его, или {"status": "cancelled"}, чтобы отклонить. Игрок получит уведомление с результатом.
  • Их можно распознать по сочетанию status: "pending" + source: "app". Данные клиента поступают из профиля игрока в VolleyFriends (имя и email), поэтому customerPhone может иметь значение null.
  • Не создавайте бронирования со статусом pending через эти API: валидация пропустит это значение, но данный статус описывает запрос, ожидающий ответа от менеджера, и на вашей стороне его никто не сможет обработать. Если в вашем сценарии есть корзина или незавершенная оплата, удерживайте ожидание в своей системе и вызывайте POST, когда бронирование станет окончательным — либо создавайте его со статусом confirmed и переводите в cancelled, если оплата не поступит.

7.6 Источник и способ оплаты

ПолеЗначенияЗначение
sourcemanualВнесено менеджером в панели управления.
apiСоздано внешней системой управления через эти API. Значение всегда задается при POST-запросе и не подлежит изменению.
appОнлайн-запрос на бронирование, отправленный игроком из приложения VolleyFriends: создается со статусом pending и ожидает вашего решения.
tournamentСоздано в рамках организации турнира на кортах спортивного объекта.
paymentMethodcash, bank_transfer, paypal, stripe, otherСпособ получения оплаты. null = не оплачено.

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

7.7 Персональные данные клиентов

Имя, телефон и email, которые вы передаете при бронировании, являются персональными данными ваших клиентов: вы являетесь оператором персональных данных, а VolleyFriends хранит их от вашего имени в качестве обработчика в соответствии со ст. 28 GDPR. Передавайте только те данные, которые необходимы для управления кортами, и информируйте своих клиентов. Подробная информация содержится в privacy policy.

8. Общие ошибки

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

{
  "error": "overlap",
  "message": "Il campo è già prenotato in quell'orario."
}

Ошибки валидации добавляют issues, подробный список проблем по каждому полю:

{
  "error": "invalid_input",
  "message": "Dati non validi",
  "issues": [
    {
      "code": "invalid_string",
      "validation": "datetime",
      "path": ["startsAt"],
      "message": "Invalid datetime"
    }
  ]
}
HTTPerrorЗначениеЧто делать
400invalid_inputНедопустимые параметры или тело запроса. Подробности в issues.Исправьте запрос. Не повторяйте его в идентичном виде: результат не изменится.
401invalid_keyЗаголовок X-Api-Key отсутствует, неверно сформирован, неизвестен или отозван.Убедитесь, что вы отправляете заголовок и что ключ активен в панели. Если он был отозван, сгенерируйте новый.
403subscription_requiredНет действующей подписки «Менеджер площадок» на аккаунте владельца ключа.Реактивируйте подписку в приложении (Профиль → раздел менеджера → Подписка). До этого момента ни один запрос не будет работать.
403feature_not_includedПодписка активна, но тарифный план не включает «API для внешних систем управления». В сообщении указывается текущий тарифный план.Перейдите на тарифный план, включающий эту функцию. Это относится ко всей v1, включая чтение.
404not_foundРесурс не существует или принадлежит другому менеджеру: эти две ситуации не разграничиваются, чтобы не раскрывать чужие данные.Перепроверьте id в /venues и /bookings: почти всегда это старый id или скопированный из другого аккаунта.
409overlapПлощадка уже занята в запрошенном интервале бронированием, которое не было отменено.Предложите другой слот: перепроверьте /availability. Не повторяйте попытку без изменения времени.
409venue_closedВнеплановое закрытие спортивного объекта на запрошенную дату.Предложите другую дату. Информацию о закрытиях можно получить из /availability (closure).
429rate_limitedПревышен лимит в 120 запросов в минуту для данного ключа.Подождите и повторите попытку с экспоненциальной задержкой, соблюдая заголовок retry-after.
4xxerrorОбщий код для ошибок запроса, которые не подпадают под вышеуказанные случаи (встречается редко).Прочитайте message и исправьте запрос.
500internalНепредвиденная ошибка на нашей стороне.Повторите попытку позже. Если проблема повторится, напишите нам, указав время, эндпоинт и префикс ключа (не сам ключ).

Несуществующий путь под базовым URL возвращает 404; то же самое относится к непредусмотренному методу для существующего пути:

{
  "error": "not_found",
  "message": "Risorsa non trovata"
}

Ответы 502, 503 и 504 поступают от обратного прокси-сервера, когда API недоступен (перезапуск или обслуживание): в этих случаях тело ответа не обязательно будет в формате JSON. Рассматривайте их как временные ошибки и повторите попытку с экспоненциальной задержкой (backoff), не пытаясь прочитать содержимое.

9. Версии и совместимость

Версия указана в пути: /manager-api/v1. Нет необходимости указывать заголовки версии или даты релизов: единственная опубликованная на данный момент версия — v1.

Что мы можем изменить без предварительного уведомления

  • Добавлять поля в существующие ответы.
  • Добавлять необязательные параметры в запросы со значением по умолчанию, сохраняющим текущее поведение.
  • Добавлять новые эндпоинты в /v1.
  • Добавлять значения в открытые текстовые поля (source, paymentMethod, surface).
  • Изменять формулировку message в ошибках, сохраняя коды error без изменений.

Что не меняется в рамках v1

  • Ни одно поле не будет удалено или переименовано в ответах.
  • Ни одно поле не изменит свой тип или значение.
  • Ни один параметр, являющийся сейчас необязательным, не станет обязательным.
  • Коды error и HTTP-статусы, документированные здесь, остаются прежними.

Любое изменение, нарушающее эти гарантии, будет выпущено под новым префиксом (/v2), при этом поддержка v1 продолжится, а администраторы, использующие её, будут предупреждены заранее.

Как написать отказоустойчивый клиент
  • Игнорируйте неизвестные поля вместо вызова ошибки (избегайте строгого парсинга «strict», отклоняющего непредвиденные свойства).
  • Обрабатывайте неизвестные значения открытых текстовых полей с помощью резервной ветки.
  • Не полагайтесь на порядок ключей JSON или на текст сообщений.
  • При чтении относитесь к значению null и отсутствию поля как к эквивалентным.

Список изменений

ВерсияДатаПримечания
v1июл 2026Совместимые добавления: тарифы за человека в прейскуранте (pricingMode, minPlayers) и playersCount в бронированиях (с ошибкой players_required); пагинация limit/offset в списке бронирований; повторная проверка конфликтов при повторной активации отмененного бронирования; recurrence теперь явно отклоняется с кодом 400 recurrence_unsupported (ранее молча игнорировался).
v12026Первая публичная версия: площадки и поля, бронирования (список, создание, изменение), ежедневная доступность, прейскурант. Аутентификация с помощью X-Api-Key, 120 запросов/минуту на ключ.

10. Поддержка интеграции

По техническим вопросам, при неожиданном поведении или запросах эндпоинтов, которых здесь нет, пишите нам со страницы Контакты. Чтобы сразу перейти к делу, укажите в сообщении:

  • endpoint и метод, который вы вызываете;
  • дата и время вызова (с часовым поясом) и полученный HTTP-код;
  • код error и сообщение ответа;
  • префикс используемого ключа (например, vfk_9f2c).
Никогда в сообщении

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

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