1. Возможности API
С помощью API-ключа вы можете из своего программного обеспечения читать и записывать данные в календарь ваших объектов:
- получать информацию об объектах и площадках с их идентификаторами, необходимыми для всех остальных запросов;
- получать список бронирований за определенный период, по объекту или отдельной площадке для синхронизации с вашей базой данных;
- создавать бронирования из внешних систем (например, когда клиент бронирует на вашем сайте) с теми же проверками на наложение броней и закрытие площадок;
- изменять или отменять существующие бронирования (перенос времени или площадки, оплата, примечания, отмена);
- получать информацию о доступности площадки на определенный день: уже занятые слоты, часы работы и внеплановые закрытия, чтобы предлагать свободные слоты без необходимости дублировать правила на вашей стороне;
- получить прайс-лист площадки для отображения цен конечному клиенту.
Каждый ключ привязан к аккаунту управляющего, который его создал, и видит только объекты, площадки и бронирования этого аккаунта. Ресурс, принадлежащий другому управляющему, возвращает не 403, а 404 not_found: его существование не раскрывается.
В этот API не входят запросы организаторов на использование площадок в турнирах (они одобряются в приложении), создание объектов и площадок, сохранение расписания, закрытий и прайс-листа: это операции панели управляющего.
2. Требования и активация
- Аккаунт управляющего площадками VolleyFriends как минимум с одним объектом и одной площадкой.
- Действующая подписка с тарифом, включающим функцию «API для внешних систем управления». Вся
v1— даже операции только для чтения — зарезервирована для тех, у кого есть эта функция: это платная функция, а не бесплатное дополнение к панели. - 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с широким интерваломfrom–toвместо одного вызова в день. - Кешируйте список объектов и площадок: он меняется редко.
- В случае ошибки
429подождите и повторите попытку с экспоненциальной задержкой (например, 2, 4, 8 секунд), соблюдаяretry-after.
6. Эндпоинты
Шесть эндпоинтов, все по пути /manager-api/v1. Каждый вызов проходит через одну и ту же цепочку проверок в следующем порядке: валидный API-ключ → функция «API для внешних систем управления» включена в тариф → rate limit → валидация параметров → проверка принадлежности ресурса вам.
6.1 Объекты и площадки
/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[].id | uuid | Идентификатор объекта (venueId в других эндпоинтах). |
venues[].name | string | Название объекта. |
venues[].city | string | null | Город. |
venues[].address | string | null | Адрес. |
venues[].verified | boolean | true, если у площадки есть значок ПРОВЕРЕНО. |
venues[].courts[] | array | Игровые поля площадки. Пустой массив, если поля отсутствуют. |
courts[].id | uuid | Идентификатор игрового поля (courtId в других эндпоинтах). |
courts[].venueId | uuid | Площадка, к которой относится игровое поле. |
courts[].name | string | Название игрового поля. |
courts[].surface | string | null | Покрытие, произвольный текст, введенный менеджером. |
courts[].indoor | boolean | Крытое игровое поле. |
courts[].lighting | boolean | Наличие освещения. |
Если у аккаунта еще нет площадок, ответ будет {"venues": []}.
Специфические ошибки
Отсутствуют, кроме общих.
6.2 Список бронирований
/api/manager-api/v1/bookingsВозвращает бронирования ваших площадок, отсортированные по возрастанию времени начала. Список является постраничным: максимум limit строк на запрос (по умолчанию 500, лимит 1000) — если вы получаете ровно limit строк, сделайте повторный запрос с увеличенным значением offset для перехода на следующую страницу. В рабочей среде всегда фильтруйте данные как минимум по диапазону дат.
Параметры (строка запроса)
| Имя | Тип | Обяз. | Описание |
|---|---|---|---|
venueId | uuid | нет | Ограничить одной площадкой. Если она вам не принадлежит: 404 not_found. |
courtId | uuid | нет | Ограничивает одной площадкой. Если она не ваша: 404 not_found. В сочетании с venueId другого вашего объекта результатом будет пустой список. |
from | дата или момент времени ISO | нет | Нижняя граница включена. Простая дата в формате AAAA-MM-GG означает местную итальянскую полночь этого дня. |
to | дата или момент времени ISO | нет | Верхняя граница исключена. Простая дата означает местную полночь следующего дня: to=2026-08-31 включает весь день 31 августа. |
limit | integer 1–1000 | нет | Максимальное количество строк (по умолчанию 500). |
offset | integer ≥ 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).
| Поле | Тип | Описание |
|---|---|---|
id | uuid | Идентификатор бронирования. |
venueId | uuid | Объект. |
courtId | uuid | Забронированная площадка. |
courtName | string | Название площадки для удобства отображения. |
startsAt | момент времени ISO | Начало, всегда в UTC (суффикс Z). |
endsAt | момент времени ISO | Конец, граница исключена. |
customerName | string | Имя клиента. |
customerPhone | string | null | Телефон. |
customerEmail | string | null | Email. |
priceCents | integer | null | Согласованная цена в центах. null = не определено (никогда не 0 для «не определено»). |
playersCount | integer | null | Заявленное количество человек (только для бронирований во временных интервалах с ценой за человека). null = не применимо или не указано. |
status | string | pending · confirmed · paid · cancelled — см. статусы. |
paymentMethod | string | null | cash · bank_transfer · paypal · stripe · other. null = не получено. |
paymentLink | string | null | Ссылка на оплату картой, сгенерированная панелью управления, если есть. Только для чтения. |
notes | string | null | Примечания. |
source | string | Источник: manual (панель управления) · api (данный API) · app (запрос игрока из приложения) · tournament (турнир). |
recurrenceId | uuid | null | Присутствует и совпадает во всех повторениях повторяющейся серии, созданной в панели управления. |
createdAt | момент времени ISO | Создание. |
updatedAt | момент времени ISO | Последнее изменение. |
Специфические ошибки
| Код | error | Когда |
|---|---|---|
| 400 | invalid_input | venueId/courtId не являются UUID, либо from/to не являются распознаваемыми датами. |
| 404 | not_found | Спортивный объект ("Struttura non trovata") или площадка ("Campo non trovato") не существует или вам не принадлежит. |
6.3 Новое бронирование
/api/manager-api/v1/bookingsСоздает бронирование на вашей площадке. Бронирование создается с source: "api", чтобы в панели управления его можно было отличить от введенных вручную, а управляющий получает push-уведомление «Новое бронирование» в приложении.
Применяются те же правила, что и в панели управления, по порядку:
- внеплановое закрытие спортивного объекта на дату начала →
409 venue_closed; - наложение на другое неотмененное бронирование той же площадки →
409 overlap; - цена отсутствует → рассчитывается по тарифной сетке площадки (
null, если время начала не попадает ни в один интервал). Если для интервала установлена цена за человека, также требуетсяplayersCount: без него запрос отклоняется с ошибкой400 players_required(если только не указана явная ценаpriceCents).
Часы работы не блокируют создание: они носят информационный характер, управляющий остается полновластным хозяином своей площадки. Если вы хотите предотвратить бронирование во внерабочее время, самостоятельно проверьте hours, возвращаемые /availability, перед вызовом POST.
Тело запроса (JSON)
| Имя | Тип | Обяз. | Описание |
|---|---|---|---|
courtId | uuid | да | Площадка для бронирования. Должна принадлежать вашей структуре. |
startsAt | момент времени ISO 8601 с офсетом | да | Начало. Принимаются форматы как 2026-08-12T18:00:00Z, так и 2026-08-12T18:00:00+02:00. Без офсета: 400. |
endsAt | момент времени ISO 8601 с офсетом | да | Окончание, должно быть позже startsAt. |
customerName | string (1–160) | да | Имя клиента. |
customerPhone | string (макс. 40) | нет | Телефон. |
customerEmail | email (макс. 200) | нет | Email, валидированный как адрес. |
priceCents | integer ≥ 0 | нет | Цена в центах. Если опущено, рассчитывается по прайс-листу площадки. Указывайте 0 только для действительно бесплатного бронирования. |
playersCount | integer 1–500 | нет | Количество человек. Обязательно, если цена рассчитывается по прайс-листу, а тарифный интервал времени начала — за человека; к общей сумме применяется возможный гарантированный минимум для этого интервала. |
notes | string (макс. 2000) | нет | Произвольные заметки, видимые менеджеру в панели управления. |
status | pending | confirmed | paid | cancelled | нет | По умолчанию confirmed. Из внешней системы управления используйте confirmed или paid: статус pending зарезервирован для запросов, поступающих из приложения игроков (см. статусы). |
paymentMethod | cash | bank_transfer | paypal | stripe | other | нет | Как была (или будет) получена оплата. Опущено = не оплачено. |
recurrence | object | нет | Не поддерживается в этом 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 | Когда |
|---|---|---|
| 400 | invalid_input | Некорректное тело запроса: отсутствует обязательное поле, время указано без офсета, значение endsAt не позже startsAt, невалидный email, превышена допустимая длина. |
| 400 | players_required | Цена должна рассчитываться по прайс-листу для интервала за человека, но параметр playersCount отсутствует. |
| 400 | recurrence_unsupported | Тело запроса содержит recurrence: повторяющиеся бронирования недоступны через это API. |
| 403 | subscription_required | Нет действующей подписки менеджера. |
| 404 | not_found | "Campo non trovato" — courtId не существует или не принадлежит вам. |
| 409 | venue_closed | У объекта внеплановое закрытие на дату начала. |
| 409 | overlap | На этой площадке уже есть неотмененное бронирование, которое пересекается с указанным интервалом. |
// 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 Изменение бронирования
/api/manager-api/v1/bookings/{id}Обновляет существующее бронирование. Это частичное обновление: отправляйте только изменяемые поля, остальные останутся без изменений. Тело запроса должно содержать как минимум одно поле.
Эндпоинт для удаления отсутствует: чтобы отменить бронирование, установите значение status: "cancelled". История сохраняется в панели управления, а слот снова становится свободным (отмененные бронирования не занимают площадку и не вызывают ошибку overlap).
Параметр пути
| Имя | Тип | Обяз. | Описание |
|---|---|---|---|
id | uuid | да | ID бронирования, полученный из GET /bookings или из ответа на POST-запрос. |
Тело запроса (JSON)
Все поля являются необязательными. Поля с пометкой «с возможностью сброса» принимают значение null для очистки значения.
| Имя | Тип | Примечания |
|---|---|---|
courtId | uuid | Переносит бронирование на другую вашу площадку, в том числе на другом вашем объекте (значение venueId обновляется соответствующим образом). |
startsAt | время в формате ISO со смещением | Новое время начала. |
endsAt | время в формате ISO со смещением | Новое время окончания. Конечный результат должен быть позже времени начала, даже если изменяется только одна из границ по сравнению с уже сохраненным значением. |
customerName | string (1–160) | — |
customerPhone | string (макс. 40) | С возможностью сброса. |
customerEmail | email (макс. 200) | С возможностью сброса. |
priceCents | integer ≥ 0 | С возможностью сброса (null = цена подлежит уточнению). В запросе PATCH цена не пересчитывается автоматически по прайс-листу: если вы переносите время и хотите установить новую цену, укажите ее. |
playersCount | integer 1–500 | С возможностью сброса. Обновляет только данные: цена не пересчитывается автоматически — если изменение количества человек влияет на общую стоимость, отправьте также новое значение priceCents. |
notes | string (макс. 2000) | С возможностью сброса. |
status | pending | confirmed | paid | cancelled | Допускается любой переход статуса. Это также способ ответить на запрос из приложения (status: "pending" с source: "app"): переведите его в статус confirmed, чтобы принять, или в cancelled, чтобы отклонить — игрок получит уведомление. |
paymentMethod | cash | 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 | Когда |
|---|---|---|
| 400 | invalid_input | Пустое тело ("Nessun campo da aggiornare"), невалидные значения или время окончания не позже времени начала ("L'orario di fine deve essere successivo a quello di inizio"). |
| 403 | subscription_required | Нет действующей подписки менеджера. |
| 404 | not_found | "Prenotazione non trovata" или "Campo non trovato", если новый courtId вам не принадлежит. |
| 409 | venue_closed | Перенос на дату внепланового закрытия. |
| 409 | overlap | Новое время накладывается на другое неотмененное бронирование того же корта. |
6.5 Доступность корта на день
/api/manager-api/v1/availabilityВозвращает для корта и дня занятые слоты, время работы в этот день недели и возможное внеплановое закрытие. Этот эндпоинт следует использовать для расчета свободных слотов на внешнем сайте без дублирования правил системы управления: вычтите бронирования из окна работы.
Перечислены все слоты, занимающие корт, в любом статусе, кроме cancelled: то есть включая запросы на онлайн-бронирование, которые все еще находятся в статусе pending. День определяется по местному итальянскому времени, а интервалы являются полуоткрытыми: бронирование, заканчивающееся ровно в полночь, относится к предыдущему дню. Бронирование, переходящее через полночь, отображается в обоих пересекаемых днях с его реальным временем (которое может выходить за пределы запрашиваемого дня).
Параметры (строка запроса)
| Имя | Тип | Обяз. | Описание |
|---|---|---|---|
courtId | uuid | да | Корт, доступность которого необходимо проверить. Он должен принадлежать вам. |
date | AAAA-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"
}
]
}
Поля ответа
| Поле | Тип | Описание |
|---|---|---|
courtId | uuid | Запрашиваемый корт. |
venueId | uuid | Структура корта. |
date | AAAA-MM-GG | Запрашиваемый день, повторно. |
timezone | string | Всегда "Europe/Rome": часовой пояс, в котором следует интерпретировать date и минуты времени. |
hours.weekday | integer 0–6 | День недели: 0 = понедельник … 6 = воскресенье. |
hours.openMinute | integer | null | Открытие в минутах от местной полуночи (540 = 09:00). null, если день объявлен закрытым. |
hours.closeMinute | integer | null | Закрытие в минутах (1380 = 23:00). null, если день объявлен закрытым. |
hours.closed | boolean | true, если менеджер объявил этот день недели закрытым. |
hours.configured | boolean | false, если менеджер не настроил время для этого дня: в этом случае openMinute/closeMinute принимают значения по умолчанию 480 (08:00) – 1380 (23:00), что является резервным вариантом отображения, а не объявленным временем работы. |
closure | object | null | Внеочередное закрытие, действующее в запрошенный день, или null. |
closure.from | AAAA-MM-GG | Первый день закрытия. |
closure.to | AAAA-MM-GG | Последний день закрытия, включительно. |
closure.reason | string | null | Причина, если указана менеджером. |
bookings[] | array | Занятые слоты, отсортированные по времени начала. Только startsAt, endsAt и status (pending, confirmed или paid): никаких данных клиента, чтобы ответ можно было использовать для создания публичного календаря. |
Если closure не равен null, день считается недоступным для бронирования, независимо от hours: запрос POST на эту дату вернет 409 venue_closed.
Специфические ошибки
| Код | error | Когда |
|---|---|---|
| 400 | invalid_input | courtId отсутствует или не является UUID, date отсутствует или не в формате AAAA-MM-GG. |
| 404 | not_found | "Campo non trovato" — площадка не существует или вам не принадлежит. |
6.6 Тарифная сетка площадки
/api/manager-api/v1/ratesТарифная сетка площадки в режиме только для чтения: используется для отображения цен клиенту перед бронированием, с теми же интервалами, которые система управления использует для автоматического расчета цены. Интервалы можно изменить только в панели управления. Они отсортированы по возрастанию fromMinute.
Параметры (строка запроса)
| Имя | Тип | Обяз. | Описание |
|---|---|---|---|
courtId | uuid | да | Площадка, тарифную сетку которой нужно прочитать. Должна вам принадлежать. |
Пример запроса
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
}
]
}
Поля ответа
| Поле | Тип | Описание |
|---|---|---|
courtId | uuid | Запрашиваемый корт. |
rates[].id | uuid | Идентификатор интервала. |
rates[].label | string | Метка, выбранная менеджером (например, «Вечерний»). |
rates[].daysMask | integer 1–127 | Битовая маска дней: пн 1, вт 2, ср 4, чт 8, пт 16, сб 32, вс 64. 127 = все дни, 31 = с понедельника по пятницу. |
rates[].fromMinute | integer 0–1440 | Начало интервала в минутах от местной полуночи. |
rates[].toMinute | integer 0–1440 | Конец интервала, всегда больше, чем fromMinute. |
rates[].pricePerHourCents | integer > 0 | Цена за час в центах (2000 = 20,00 €). При pricingMode: "person" это почасовая цена с человека (600 = 6,00 €/человека в час). |
rates[].pricingMode | court | person | court = цена за площадку (по умолчанию). person = цена с человека: общая сумма масштабируется в зависимости от количества человек, но никогда не уменьшается. |
rates[].minPlayers | integer | 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 | Когда |
|---|---|---|
| 400 | invalid_input | courtId отсутствует или не является UUID. |
| 404 | not_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.
| Минуты | Местное время | Минуты | Местное время |
|---|---|---|---|
| 0 | 00:00 | 1080 | 18:00 |
| 480 | 08:00 | 1320 | 22:00 |
| 540 | 09:00 | 1380 | 23:00 |
| 840 | 14:00 | 1440 | 24: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. | Нет |
- Вы найдете их в GET /bookings и среди занятых слотов в /availability: относитесь к слоту как к недоступному, точно так же, как к статусу
confirmed. - Вы можете ответить на запрос из своей системы управления с помощью PATCH:
{"status": "confirmed"}, чтобы принять его, или{"status": "cancelled"}, чтобы отклонить. Игрок получит уведомление с результатом. - Их можно распознать по сочетанию
status: "pending"+source: "app". Данные клиента поступают из профиля игрока в VolleyFriends (имя и email), поэтомуcustomerPhoneможет иметь значениеnull. - Не создавайте бронирования со статусом
pendingчерез эти API: валидация пропустит это значение, но данный статус описывает запрос, ожидающий ответа от менеджера, и на вашей стороне его никто не сможет обработать. Если в вашем сценарии есть корзина или незавершенная оплата, удерживайте ожидание в своей системе и вызывайте POST, когда бронирование станет окончательным — либо создавайте его со статусомconfirmedи переводите вcancelled, если оплата не поступит.
7.6 Источник и способ оплаты
| Поле | Значения | Значение |
|---|---|---|
source | manual | Внесено менеджером в панели управления. |
api | Создано внешней системой управления через эти API. Значение всегда задается при POST-запросе и не подлежит изменению. | |
app | Онлайн-запрос на бронирование, отправленный игроком из приложения VolleyFriends: создается со статусом pending и ожидает вашего решения. | |
tournament | Создано в рамках организации турнира на кортах спортивного объекта. | |
paymentMethod | cash, 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"
}
]
}
| HTTP | error | Значение | Что делать |
|---|---|---|---|
| 400 | invalid_input | Недопустимые параметры или тело запроса. Подробности в issues. | Исправьте запрос. Не повторяйте его в идентичном виде: результат не изменится. |
| 401 | invalid_key | Заголовок X-Api-Key отсутствует, неверно сформирован, неизвестен или отозван. | Убедитесь, что вы отправляете заголовок и что ключ активен в панели. Если он был отозван, сгенерируйте новый. |
| 403 | subscription_required | Нет действующей подписки «Менеджер площадок» на аккаунте владельца ключа. | Реактивируйте подписку в приложении (Профиль → раздел менеджера → Подписка). До этого момента ни один запрос не будет работать. |
| 403 | feature_not_included | Подписка активна, но тарифный план не включает «API для внешних систем управления». В сообщении указывается текущий тарифный план. | Перейдите на тарифный план, включающий эту функцию. Это относится ко всей v1, включая чтение. |
| 404 | not_found | Ресурс не существует или принадлежит другому менеджеру: эти две ситуации не разграничиваются, чтобы не раскрывать чужие данные. | Перепроверьте id в /venues и /bookings: почти всегда это старый id или скопированный из другого аккаунта. |
| 409 | overlap | Площадка уже занята в запрошенном интервале бронированием, которое не было отменено. | Предложите другой слот: перепроверьте /availability. Не повторяйте попытку без изменения времени. |
| 409 | venue_closed | Внеплановое закрытие спортивного объекта на запрошенную дату. | Предложите другую дату. Информацию о закрытиях можно получить из /availability (closure). |
| 429 | rate_limited | Превышен лимит в 120 запросов в минуту для данного ключа. | Подождите и повторите попытку с экспоненциальной задержкой, соблюдая заголовок retry-after. |
| 4xx | error | Общий код для ошибок запроса, которые не подпадают под вышеуказанные случаи (встречается редко). | Прочитайте message и исправьте запрос. |
| 500 | internal | Непредвиденная ошибка на нашей стороне. | Повторите попытку позже. Если проблема повторится, напишите нам, указав время, эндпоинт и префикс ключа (не сам ключ). |
Несуществующий путь под базовым 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 (ранее молча игнорировался). |
| v1 | 2026 | Первая публичная версия: площадки и поля, бронирования (список, создание, изменение), ежедневная доступность, прейскурант. Аутентификация с помощью X-Api-Key, 120 запросов/минуту на ключ. |
10. Поддержка интеграции
По техническим вопросам, при неожиданном поведении или запросах эндпоинтов, которых здесь нет, пишите нам со страницы Контакты. Чтобы сразу перейти к делу, укажите в сообщении:
- endpoint и метод, который вы вызываете;
- дата и время вызова (с часовым поясом) и полученный HTTP-код;
- код
errorи сообщение ответа; - префикс используемого ключа (например,
vfk_9f2c).
Не отправляйте нам полный API-ключ ни по каким каналам связи, даже для проверки проблемы: нам достаточно префикса для его идентификации. Если вы уже поделились им с кем-то, отзовите его и создайте новый.
Если вам нужно протестировать интеграцию, не затрагивая реальный календарь, создайте тестовую площадку с выделенным полем и используйте её: все вызовы ограничены площадками владельца ключа, поэтому остальная часть вашего аккаунта не будет затронута.