1. Qué permiten las API
Con una clave API puedes, desde tu software, leer y escribir en el calendario de tus instalaciones:
- leer instalaciones y pistas con sus identificadores, necesarios para todas las demás llamadas;
- leer las reservas de un periodo, de una instalación o de una pista individual, para alinearlas con tu archivo;
- crear reservas desde el exterior (por ejemplo, cuando un cliente reserva desde tu sitio web), con los mismos controles de solapamiento y cierre del panel;
- modificar o cancelar una reserva existente (cambio de horario o pista, cobro, notas, cancelación);
- leer la disponibilidad de una pista en un día: franjas ya ocupadas, horarios de apertura y cierres extraordinarios, para así proponer las franjas libres sin replicar las reglas;
- leer la lista de precios de una pista para mostrar los precios al cliente final.
Cada clave está vinculada a la cuenta de gestor que la creó y ve solo las estructuras, las pistas y las reservas de esa cuenta. Un recurso que pertenece a otro gestor no responde 403 sino 404 not_found: no se revela su existencia.
No forman parte de estas API las solicitudes de los organizadores para el uso de las pistas en los torneos (se aprueban desde la app), la creación de estructuras y pistas, el guardado de horarios, cierres y lista de precios: son operaciones del panel del gestor.
2. Requisitos y activación
- Una cuenta de gestor de pistas VolleyFriends con al menos una estructura y una pista.
- Una suscripción activa con un plan que incluya la función «API para sistemas de gestión externos». Toda la
v1— incluso las de solo lectura — está reservada para quienes tengan esta función: es una función de pago, no un accesorio del panel. - Una clave API generada desde el panel de gestión: admin.volleyfriends.com → Configuración → Claves API → Nueva clave.
La clave tiene la forma vfk_ seguido de 40 caracteres hexadecimales (44 caracteres en total) y se muestra una sola vez, inmediatamente después de su creación: en la base de datos solo queda su huella sha256, por lo que no se puede recuperar de ninguna manera. Si la pierdes, revócala y genera una nueva. En la lista del panel solo verás el prefijo (p. ej., vfk_9f2c), la fecha de creación y la del último uso.
3. Autenticación
Cada solicitud debe autenticarse con la cabecera X-Api-Key. No se necesitan tokens, inicios de sesión, caducidades ni renovaciones: la clave sigue siendo válida hasta que la revoques.
# Las claves de esta página son de ejemplo: sustitúyelas por la tuya.
curl -s https://www.volleyfriends.com/api/manager-api/v1/venues -H "X-Api-Key: vfk_9f2c41a7b83d05e6c19af4728b60d35e1c9a7f42"
En los siguientes ejemplos, la clave se lee de la variable de entorno VF_API_KEY, tal como conviene hacer también en producción.
Cabecera ausente, de menos de 12 caracteres, desconocida o perteneciente a una clave revocada: la respuesta es 401 con este cuerpo.
{
"error": "invalid_key",
"message": "Chiave API mancante o non valida (header X-Api-Key)."
}
- La clave funciona como una contraseña con plenos poderes sobre el calendario de tus estructuras: no la introduzcas en páginas, aplicaciones o JavaScript del lado del navegador, donde cualquiera podría leerla. Úsala solo de servidor a servidor.
- No la incluyas en el código fuente ni en un repositorio: utiliza una variable de entorno o un almacén de secretos.
- Usa una clave para cada sistema conectado (sitio web, sistema de gestión, script de sincronización): así podrás revocar una sin desactivar los demás.
- Si sospechas que ha sido expuesta, revócala de inmediato desde el panel: la revocación es inmediata y nunca se ve bloqueada por el estado de la suscripción. A partir de ese momento, cualquier llamada con esa clave recibirá
401 invalid_key. - Tráfico exclusivamente a través de HTTPS; el campo «último uso» en el panel te ayuda a detectar usos inesperados.
4. URL base
https://www.volleyfriends.com/api
Las rutas documentadas a continuación deben añadirse a la base. El prefijo /api pertenece al proxy inverso, por lo que la dirección completa de un endpoint es, por ejemplo:
GET https://www.volleyfriends.com/api/manager-api/v1/availability
Todas las respuestas son application/json con codificación UTF-8. Las solicitudes con cuerpo (POST, PATCH) deben tener Content-Type: application/json.
5. Límite de peticiones
El límite es de 120 solicitudes por minuto por clave API (ventana deslizante de 1 minuto). El recuento es por clave, no por dirección IP: los sistemas de gestión suelen estar detrás de la misma dirección del proveedor y un límite por IP penalizaría a distintos gestores alojados en la misma infraestructura. Si el encabezado X-Api-Key falta por completo, el recuento recae en la dirección IP.
Cada respuesta muestra el estado del contador en los encabezados x-ratelimit-limit, x-ratelimit-remaining y x-ratelimit-reset (segundos para el restablecimiento). Al superarlo, también se añade retry-after.
Respuesta al superar el límite — 429
{
"error": "rate_limited",
"message": "Limite di 120 richieste al minuto superato per questa chiave API."
}
- Para la sincronización periódica, utiliza una sola llamada a
/bookingscon un intervalofrom–toamplio, en lugar de una llamada al día. - Almacena en caché la lista de instalaciones y pistas: cambia raramente.
- En caso de
429, espera e inténtalo de nuevo con un backoff exponencial (por ejemplo, 2, 4, 8 segundos), respetandoretry-after.
6. Endpoints
Seis endpoints, todos bajo /manager-api/v1. Cada llamada pasa por la misma cadena de controles, en este orden: clave API válida → función «API para sistemas de gestión externos» incluida en el plan → rate limit → validación de parámetros → verificación de que el recurso sea tuyo.
6.1 Instalaciones y pistas
/api/manager-api/v1/venuesMuestra tus instalaciones, cada una con sus propias pistas anidadas. Es la llamada de partida: los id de las pistas (courtId) se necesitan para todos los demás endpoints. Las instalaciones están ordenadas por nombre, las pistas por nombre.
Parámetros
Ninguno.
Ejemplo de solicitud
curl -s https://www.volleyfriends.com/api/manager-api/v1/venues -H "X-Api-Key: $VF_API_KEY"
Ejemplo de respuesta — 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
}
]
}
]
}
Campos de la respuesta
| Campo | Tipo | Descripción |
|---|---|---|
venues[].id | uuid | Identificador de la instalación (venueId en los otros endpoints). |
venues[].name | string | Nombre de la instalación. |
venues[].city | string | null | Ciudad. |
venues[].address | string | null | Dirección. |
venues[].verified | boolean | true si la instalación tiene la insignia VERIFICADO. |
venues[].courts[] | array | Pistas de la instalación. Array vacío si no tiene. |
courts[].id | uuid | Identificador de la pista (courtId en los otros endpoints). |
courts[].venueId | uuid | Instalación a la que pertenece la pista. |
courts[].name | string | Nombre de la pista. |
courts[].surface | string | null | Superficie, texto libre introducido por el gestor. |
courts[].indoor | boolean | Pista cubierta. |
courts[].lighting | boolean | Iluminación disponible. |
Si la cuenta aún no tiene instalaciones, la respuesta es {"venues": []}.
Errores específicos
Ninguno además de los generales.
6.2 Lista de reservas
/api/manager-api/v1/bookingsDevuelve las reservas de tus instalaciones, ordenadas por hora de inicio creciente. La lista está paginada: como máximo limit filas por llamada (por defecto 500, límite máximo 1000) — si recibes exactamente limit filas, vuelve a llamar con el offset incrementado para la página siguiente. En producción, filtra siempre, en cualquier caso, al menos por intervalo de fechas.
Parámetros (query string)
| Nombre | Tipo | Oblig. | Descripción |
|---|---|---|---|
venueId | uuid | no | Limita a una instalación. Si no es tuya: 404 not_found. |
courtId | uuid | no | Limita a una pista. Si no es tuya: 404 not_found. Combinado con un venueId de otra de tus estructuras el resultado es una lista vacía. |
from | fecha o instante ISO | no | Límite inferior incluido. Una fecha sola AAAA-MM-GG equivale a la medianoche local italiana de ese día. |
to | fecha o instante ISO | no | Límite superior excluido. Una fecha sola equivale a la medianoche local del día siguiente: to=2026-08-31 incluye todo el 31 de agosto. |
limit | integer 1–1000 | no | Número máximo de filas (por defecto 500). |
offset | integer ≥ 0 | no | Filas a omitir (por defecto 0): offset=500 es la segunda página con el limit por defecto. |
from y to se aplican al instante de inicio de la reserva. Una reserva iniciada antes de from y que continúe en curso después no aparece: si lo necesitas, amplía el límite inferior unas horas. El filtro no excluye las reservas canceladas: la lista también contiene aquellas con status: "cancelled".
Ejemplo de solicitud
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"
Ejemplo de respuesta — 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
}
Campos de una reserva
La misma estructura es devuelta por POST y PATCH (dentro de la clave booking).
| Campo | Tipo | Descripción |
|---|---|---|
id | uuid | Identificador de la reserva. |
venueId | uuid | Estructura. |
courtId | uuid | Pista reservada. |
courtName | string | Nombre de la pista, para facilitar la visualización. |
startsAt | instante ISO | Inicio, siempre en UTC (sufijo Z). |
endsAt | instante ISO | Fin, límite excluido. |
customerName | string | Nombre del cliente. |
customerPhone | string | null | Teléfono. |
customerEmail | string | null | Email. |
priceCents | integer | null | Precio acordado en céntimos. null = por definir (nunca 0 para «no definido»). |
playersCount | integer | null | Número de personas declarado (solo reservas en franjas con precio por persona). null = no aplicable o no indicado. |
status | string | pending · confirmed · paid · cancelled — ver estados. |
paymentMethod | string | null | cash · bank_transfer · paypal · stripe · other. null = no cobrado. |
paymentLink | string | null | Enlace de pago con tarjeta generado desde el panel, si está presente. En solo lectura. |
notes | string | null | Notas libres. |
source | string | Origen: manual (panel) · api (estas API) · app (solicitud de un jugador desde la app) · tournament (torneo). |
recurrenceId | uuid | null | Presente e igual en todas las ocurrencias de una serie recurrente creada desde el panel. |
createdAt | instante ISO | Creación. |
updatedAt | instante ISO | Última modificación. |
Errores específicos
| Código | error | Cuándo |
|---|---|---|
| 400 | invalid_input | venueId/courtId no son UUID, o bien from/to no son fechas interpretables. |
| 404 | not_found | La instalación ("Struttura non trovata") o la pista ("Campo non trovato") no existe o no te pertenece. |
6.3 Nueva reserva
/api/manager-api/v1/bookingsCrea una reserva en una de tus pistas. La reserva se crea con source: "api", de modo que en el panel se distingue de las introducidas a mano, y el gestor recibe una notificación push «Nueva reserva» en la app.
Se aplican las mismas reglas del panel, en el siguiente orden:
- cierre extraordinario de la instalación en la fecha de inicio →
409 venue_closed; - solapamiento con otra reserva no cancelada de la misma pista →
409 overlap; - precio ausente → calculado a partir de la tarifa de la pista (
nullsi la hora de inicio no entra en ninguna franja). Si la franja tiene precio por persona, también se requiereplayersCount: de lo contrario, la solicitud se rechaza con400 players_required(a menos que se indique unpriceCentsexplícito).
Los horarios de apertura no bloquean la creación: son informativos, el gestor sigue siendo soberano sobre su propia pista. Si quieres impedir reservas fuera de horario, comprueba tú mismo hours devuelto por /availability antes de llamar al POST.
Cuerpo de la solicitud (JSON)
| Nombre | Tipo | Oblig. | Descripción |
|---|---|---|---|
courtId | uuid | sí | Campo a reservar. Debe pertenecer a una de tus instalaciones. |
startsAt | instante ISO 8601 con offset | sí | Inicio. Se aceptan tanto la forma 2026-08-12T18:00:00Z como 2026-08-12T18:00:00+02:00. Sin offset: 400. |
endsAt | instante ISO 8601 con offset | sí | Fin, debe ser posterior a startsAt. |
customerName | string (1–160) | sí | Nombre del cliente. |
customerPhone | string (máx. 40) | no | Teléfono. |
customerEmail | email (máx. 200) | no | Email, validado como dirección. |
priceCents | integer ≥ 0 | no | Precio en céntimos. Si se omite se calcula a partir de la tarifa del campo. Indica 0 solo para una reserva realmente gratuita. |
playersCount | integer 1–500 | no | Cuántas personas. Requerido si el precio se debe calcular a partir de la tarifa y el tramo horario de inicio es por persona; el total aplica el posible mínimo garantizado del tramo. |
notes | string (máx. 2000) | no | Notas libres, visibles para el administrador en el panel. |
status | pending | confirmed | paid | cancelled | no | Por defecto confirmed. Desde un sistema de gestión externo usa confirmed o paid: pending está reservado para las solicitudes que llegan desde la app de los jugadores (ver estados). |
paymentMethod | cash | bank_transfer | paypal | stripe | other | no | Cómo se ha cobrado (o se cobrará). Omitido = no cobrado. |
recurrence | object | no | No compatible con estas API: si está presente, la solicitud se rechaza con 400 recurrence_unsupported (no se crea ninguna reserva). Las series recurrentes siguen siendo una función del panel: desde un sistema de gestión externo, repite la POST para cada ocurrencia. |
Ejemplo de solicitud
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"
}'
Ejemplo de respuesta — 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"
}
}
En el ejemplo no se había indicado priceCents: 90 minutos en un tramo de 20,00 €/hora dan 3000 céntimos (el cálculo es proporcional a los minutos reales, con la tarifa del tramo en el que comienza la reserva).
Errores específicos
| Código | error | Cuándo |
|---|---|---|
| 400 | invalid_input | Cuerpo no válido: falta un campo obligatorio, instante sin offset, endsAt no es posterior a startsAt, email no válido, longitudes fuera de límite. |
| 400 | players_required | Precio a calcular a partir de la tarifa en un tramo por persona, pero playersCount ausente. |
| 400 | recurrence_unsupported | El cuerpo contiene recurrence: las recurrencias no están disponibles en estas API. |
| 403 | subscription_required | Ninguna suscripción de administrador activa. |
| 404 | not_found | "Campo non trovato" — el courtId no existe o no es tuyo. |
| 409 | venue_closed | La instalación tiene un cierre extraordinario en la fecha de inicio. |
| 409 | overlap | La pista ya tiene una reserva no cancelada que se solapa con el intervalo. |
// 409 — solapamiento
{ "error": "overlap", "message": "Il campo è già prenotato in quell'orario." }
// 409 — cierre extraordinario (el motivo, si lo indica el administrador, está en el mensaje)
{ "error": "venue_closed", "message": "Struttura chiusa in quella data (manutenzione)." }
No existe una clave de idempotencia: repetir el mismo POST después de un tiempo de espera puede crear un duplicado o, más a menudo, devolver 409 overlap porque el primer intento se había realizado correctamente. En caso de respuesta dudosa, antes de volver a intentarlo verifica con GET /bookings en el intervalo afectado.
6.4 Modificación de reserva
/api/manager-api/v1/bookings/{id}Actualiza una reserva existente. Es una actualización parcial: envía solo los campos que cambian, los demás permanecen sin cambios. El cuerpo debe contener al menos un campo.
No existe un endpoint de cancelación: para cancelar una reserva se establece status: "cancelled". El historial permanece en el panel y el hueco vuelve a quedar libre (las reservas canceladas no ocupan la pista y no generan overlap).
Parámetro de ruta
| Nombre | Tipo | Oblig. | Descripción |
|---|---|---|---|
id | uuid | sí | Id de la reserva, de GET /bookings o de la respuesta del POST. |
Cuerpo de la solicitud (JSON)
Todos los campos son opcionales. Los marcados como «anulable» aceptan null para vaciar el valor.
| Nombre | Tipo | Notas |
|---|---|---|
courtId | uuid | Mueve la reserva a otra de tus pistas, incluso de otra de tus instalaciones (venueId se actualiza en consecuencia). |
startsAt | instante ISO con offset | Nuevo inicio. |
endsAt | instante ISO con offset | Nuevo fin. El resultado final debe seguir siendo posterior al inicio, incluso combinando un solo extremo con el valor ya guardado. |
customerName | string (1–160) | — |
customerPhone | string (máx. 40) | Anulable. |
customerEmail | email (máx. 200) | Anulable. |
priceCents | integer ≥ 0 | Anulable (null = precio por definir). En PATCH el precio no se recalcula a partir de la lista de precios: si cambias el horario y quieres el nuevo precio, indícalo. |
playersCount | integer 1–500 | Anulable. Actualiza solo el dato: el precio no se recalcula de oficio — si el número de personas cambia el total, envía también el nuevo priceCents. |
notes | string (máx. 2000) | Anulable. |
status | pending | confirmed | paid | cancelled | Se permite cualquier transición. También es la forma de responder a una solicitud recibida desde la app (status: "pending" con source: "app"): cámbiala a confirmed para aceptarla o a cancelled para rechazarla — el jugador recibe la notificación. |
paymentMethod | cash | bank_transfer | paypal | stripe | other | Anulable. |
Los controles de cierre y solapamiento se vuelven a realizar en dos casos: cuando la solicitud desplaza el slot (startsAt, endsAt o courtId) y cuando reactiva una reserva cancelada (status de cancelled a cualquier otro estado) — al estar cancelada no ocupaba la pista, y mientras tanto el slot puede haber sido ocupado: en ese caso la PATCH responde 409 overlap o 409 venue_closed. Una PATCH que solo cambia otros campos (precio, notas, cobro) no los repite.
Ejemplo de solicitud — desplazamiento y cobro
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
}'
Ejemplo de solicitud — cancelación
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" }'
Ejemplo de respuesta — 200
La reserva actualizada, en el mismo formato que la 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"
}
}
Errores específicos
| Código | error | Cuándo |
|---|---|---|
| 400 | invalid_input | Cuerpo vacío ("Nessun campo da aggiornare"), valores no válidos, o bien hora de finalización no posterior a la de inicio ("L'orario di fine deve essere successivo a quello di inizio"). |
| 403 | subscription_required | Ninguna suscripción de administrador activa. |
| 404 | not_found | "Prenotazione non trovata" o bien "Campo non trovato" si el nuevo courtId no es tuyo. |
| 409 | venue_closed | Desplazamiento a una fecha de cierre extraordinario. |
| 409 | overlap | El nuevo horario se solapa con otra reserva no cancelada de la misma pista. |
6.5 Disponibilidad de una pista en un día
/api/manager-api/v1/availabilityDevuelve, para una pista y un día, los slots ocupados, los horarios de apertura de ese día de la semana y el posible cierre extraordinario. Es el endpoint que se debe usar para calcular los slots libres en un sitio web externo sin duplicar las reglas del sistema de gestión: resta las reservas de la ventana de apertura.
Se enumeran todos los slots que ocupan la pista, en cualquier estado excepto cancelled: por lo tanto, también las solicitudes de reserva online que aún estén pending. El día es el local italiano y los intervalos son semiabiertos: una reserva que termina exactamente a medianoche pertenece al día anterior. Una reserva que abarca la medianoche aparece en ambos días que interseca, con sus instantes reales (que pueden caer fuera del día solicitado).
Parámetros (query string)
| Nombre | Tipo | Oblig. | Descripción |
|---|---|---|---|
courtId | uuid | sí | Pista de la que leer la disponibilidad. Debe ser tuya. |
date | AAAA-MM-GG | sí | Día local italiano. Formato diferente: 400 con mensaje "Data non valida (AAAA-MM-GG)". |
Ejemplo de solicitud
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"
Ejemplo de respuesta — 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"
}
]
}
Campos de la respuesta
| Campo | Tipo | Descripción |
|---|---|---|
courtId | uuid | Campo requerido. |
venueId | uuid | Estructura de la pista. |
date | AAAA-MM-GG | Día solicitado, repetido. |
timezone | string | Siempre "Europe/Rome": la zona horaria en la que se deben interpretar date y los minutos de los horarios. |
hours.weekday | integer 0–6 | Día de la semana: 0 = lunes … 6 = domingo. |
hours.openMinute | integer | null | Apertura en minutos desde la medianoche local (540 = 09:00). null si el día se declara cerrado. |
hours.closeMinute | integer | null | Cierre en minutos (1380 = 23:00). null si el día se declara cerrado. |
hours.closed | boolean | true si el gestor ha declarado ese día de la semana como cerrado. |
hours.configured | boolean | false cuando el gestor no ha establecido los horarios para ese día: en ese caso openMinute/closeMinute toman el valor de la convención predeterminada 480 (08:00) – 1380 (23:00), que es una solución alternativa de visualización, no un horario declarado. |
closure | object | null | Cierre extraordinario activo en el día solicitado, o bien null. |
closure.from | AAAA-MM-GG | Primer día de cierre. |
closure.to | AAAA-MM-GG | Último día de cierre, incluido. |
closure.reason | string | null | Motivo, si lo indica el gestor. |
bookings[] | array | Slots ocupados, ordenados por inicio. Solo startsAt, endsAt y status (pending, confirmed o paid): ningún dato del cliente, de modo que la respuesta se puede usar para construir un calendario público. |
Cuando closure no es null el día se debe considerar no reservable, independientemente de hours: una POST en esa fecha recibiría 409 venue_closed.
Errores específicos
| Código | error | Cuándo |
|---|---|---|
| 400 | invalid_input | courtId faltante o no UUID, date faltante o no en formato AAAA-MM-GG. |
| 404 | not_found | "Campo non trovato" — el campo no existe o no es tuyo. |
6.6 Tarifas de una pista
/api/manager-api/v1/ratesLas tarifas de la pista en solo lectura: sirve para mostrar los precios al cliente antes de la reserva, con las mismas franjas que el sistema de gestión utiliza para calcular el precio automático. Las franjas solo se modifican desde el panel. Están ordenadas por fromMinute creciente.
Parámetros (query string)
| Nombre | Tipo | Oblig. | Descripción |
|---|---|---|---|
courtId | uuid | sí | Pista de la que leer las tarifas. Debe ser tuya. |
Ejemplo de solicitud
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"
Ejemplo de respuesta — 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
}
]
}
Campos de la respuesta
| Campo | Tipo | Descripción |
|---|---|---|
courtId | uuid | Campo requerido. |
rates[].id | uuid | Identificador de la franja. |
rates[].label | string | Etiqueta elegida por el gestor (ej. «Nocturna»). |
rates[].daysMask | integer 1–127 | Máscara de bits de los días: lun 1, mar 2, mié 4, jue 8, vie 16, sáb 32, dom 64. 127 = todos los días, 31 = de lunes a viernes. |
rates[].fromMinute | integer 0–1440 | Inicio de la franja en minutos desde la medianoche local. |
rates[].toMinute | integer 0–1440 | Fin de la franja, siempre mayor que fromMinute. |
rates[].pricePerHourCents | integer > 0 | Precio por hora en céntimos (2000 = 20,00 €). Con pricingMode: "person" es el precio por hora por persona (600 = 6,00 €/persona por hora). |
rates[].pricingMode | court | person | court = precio de la pista (predeterminado). person = precio por persona: el total escala con el número de personas, nunca a la baja. |
rates[].minPlayers | integer | null | Mínimo garantizado del tramo por persona: por debajo de ese número se paga de todas formas el mínimo. null = sin mínimo. |
Se mira el instante de inicio: entre los tramos de la pista se toma el primero (en orden de fromMinute) cuyo día esté activo en la bitmask y que contenga el minuto de inicio (fromMinute ≤ inizio < toMinute). El precio es proporcional a los minutos reales: arrotonda(pricePerHourCents × minuti / 60) — 90 minutos a 20,00 €/hora son 30,00 €. Una reserva a caballo entre dos tramos se sigue valorando con la tarifa del tramo de inicio. Si no hay ningún tramo correspondiente: priceCents resulta null (precio por definir), nunca 0.
Con una franja por persona (pricingMode: "person") el resultado por hora se multiplica por las personas facturadas: max(playersCount, minPlayers). Ejemplo con la franja nocturna de arriba (6,00 €/persona por hora, mínimo 10): 60 minutos para 12 personas → 72,00 €; para 16 → 96,00 € (el precio por persona no baja); para 8 → 60,00 € (se aplica el mínimo garantizado). Sin playersCount el precio no se puede calcular: null en lectura, 400 players_required en la POST que depende de la lista de precios.
Errores específicos
| Código | error | Cuándo |
|---|---|---|
| 400 | invalid_input | courtId ausente o no es un UUID. |
| 404 | not_found | "Campo non trovato" — el campo no existe o no es tuyo. |
Un campo sin lista de precios responde 200 con "rates": [].
7. Convenciones de datos
7.1 Fechas y horarios
- De entrada los instantes son cadenas ISO 8601 con offset obligatorio:
2026-08-12T18:00:00+02:00o2026-08-12T18:00:00Z. Una cadena sin offset (2026-08-12T18:00:00) se rechaza con400: sería ambigua, y para un calendario es una ambigüedad que se paga con reservas incorrectas de una hora. - De salida los instantes están siempre normalizados en UTC con sufijo
Zy milisegundos:"2026-08-12T16:00:00.000Z". Conviértelos al huso local antes de mostrarlos. - Las fechas sin hora (
date,closure.from,closure.toyfrom/tocuando los usas en formato de día) sonAAAA-MM-GGy se refieren al día local italiano. - Los intervalos son semiabiertos
[inizio, fine): 18:00–19:00 y 19:00–20:00 no están en conflicto.
7.2 Huso horario
Las reservas se conservan como instantes absolutos, pero todas las reglas del calendario — día de la semana, franjas de precios, horarios de apertura, fechas de cierre, límites del día — se evalúan en la zona horaria Europe/Rome, con el horario de verano gestionado automáticamente. El campo timezone de la respuesta de /availability lo declara explícitamente. Si tu servidor funciona en UTC o en otra zona horaria, no conviertas a mano los minutos: ya están expresados en hora local italiana.
7.3 Minutos desde la medianoche
Los horarios «de calendario» (openMinute, closeMinute, fromMinute, toMinute) son enteros de 0 a 1440 que cuentan los minutos desde la medianoche local. Conversiones: ore = ⌊m / 60⌋, minuti = m mod 60.
| 7.3 Minutos desde la medianoche | Hora local | 7.3 Minutos desde la medianoche | Hora local |
|---|---|---|---|
| 0 | 00:00 | 1080 | 18:00 |
| 480 | 08:00 | 1320 | 22:00 |
| 540 | 09:00 | 1380 | 23:00 |
| 840 | 14:00 | 1440 | 24:00 (fin del día) |
7.4 Importes
Todos los importes son enteros en céntimos de euro (priceCents, pricePerHourCents): ningún valor en coma flotante, ningún símbolo de moneda. 2000 = 20,00 €. En una reserva, priceCents: null significa «precio por definir» y es diferente de 0, que significa «gratuito».
7.5 Estados de una reserva
| Estado | 7.5 Estados de una reservaSignalement d'un problème d'accès à l'eau potable à l'école primaire de l'Ermitage. L'eau est trouble et a une odeur suspecte. Les élèves et le personnel se plaignent de maux de ventre. Une intervention rapide est demandée pour analyser l'eau et sécuriser l'approvisionnement. Les autorités sanitaires ont été informées. Une distribution d'eau en bouteille a été mise en place temporairement. Les cours sont maintenus pour le moment. Des prélèvements ont été effectués ce matin par les services techniques de la mairie. Les résultats sont attendus sous 48 heures. En attendant, l'utilisation de l'eau du robinet est strictement interdite pour la consommation et la préparation des repas. Un point de situation sera fait régulièrement. Pour toute question, contacter le secrétariat de la mairie au 02 62 40 00 00. Merci de votre compréhension et de votre vigilance. Le Maire.`, | ¿Ocupa el slot? |
|---|---|---|
pending | Solicitud de reserva online pendiente de la decisión del gestor. Se genera únicamente a partir de las solicitudes enviadas por los jugadores desde la app VolleyFriends (source: "app"): el gestor la acepta (confirmed) o la rechaza (cancelled). | Sí — el slot ya está bloqueado, para evitar duplicar solicitudes en el mismo horario |
confirmed | Reserva válida, aún no cobrada. Es el estado inicial de cada reserva creada por estas API y desde el panel. | Sí |
paid | Reserva válida y cobrada. Normalmente acompañada de paymentMethod. | Sí |
cancelled | Reserva cancelada (o solicitud rechazada). Permanece en el historial pero libera la pista: no genera overlap y no aparece en /availability. | No |
- Las encontrarás en GET /bookings y en los slots ocupados de /availability: trata el slot como no disponible, exactamente igual que una
confirmed. - Puedes responder a la solicitud desde tu sistema de gestión con una PATCH:
{"status": "confirmed"}para aceptarla,{"status": "cancelled"}para rechazarla. El jugador recibirá una notificación con el resultado. - Reconócelas por la combinación
status: "pending"+source: "app". Los datos del cliente provienen del perfil de VolleyFriends del jugador (nombre y email), por lo quecustomerPhonepuede sernull. - No crees reservas
pendingdesde estas API: la validación acepta el valor, pero ese estado describe una solicitud pendiente de respuesta por parte del gestor y nadie la gestionaría desde tu lado. Si en tu flujo hay un carrito o un pago por completar, mantén la espera en tu sistema y llama al POST cuando la reserva sea definitiva — o bien créala comoconfirmedy cámbiala acancelledsi el pago no se realiza.
7.6 Origen y método de pago
| Campo | Valores | 7.5 Estados de una reservaSignalement d'un problème d'accès à l'eau potable à l'école primaire de l'Ermitage. L'eau est trouble et a une odeur suspecte. Les élèves et le personnel se plaignent de maux de ventre. Une intervention rapide est demandée pour analyser l'eau et sécuriser l'approvisionnement. Les autorités sanitaires ont été informées. Une distribution d'eau en bouteille a été mise en place temporairement. Les cours sont maintenus pour le moment. Des prélèvements ont été effectués ce matin par les services techniques de la mairie. Les résultats sont attendus sous 48 heures. En attendant, l'utilisation de l'eau du robinet est strictement interdite pour la consommation et la préparation des repas. Un point de situation sera fait régulièrement. Pour toute question, contacter le secrétariat de la mairie au 02 62 40 00 00. Merci de votre compréhension et de votre vigilance. Le Maire.`, |
|---|---|---|
source | manual | Introducida por el gestor en el panel. |
api | Creada por un sistema de gestión externo con estas API. Valor siempre establecido por la POST, no modificable. | |
app | Solicitud de reserva online enviada por un jugador desde la app VolleyFriends: se genera como pending y queda a la espera de tu decisión. | |
tournament | Generada por la organización de un torneo en las pistas de las instalaciones. | |
paymentMethod | cash, bank_transfer, paypal, stripe, other | Cómo se ha cobrado. null = no cobrado. |
Ambos son dominios de texto: se pueden añadir nuevos valores en el futuro sin cambiar de versión, por lo que tu código no debe fallar ante un valor que no conozca.
7.7 Datos personales de los clientes
El nombre, teléfono y email que envías en las reservas son datos personales de tus clientes: tú eres el responsable del tratamiento, VolleyFriends los conserva por tu cuenta como encargado del tratamiento de acuerdo con el art. 28 GDPR. Envía solo lo necesario para la gestión de la pista e informa a tus clientes. Los detalles se encuentran en la privacy policy.
8. Errores generales
Cada error tiene el mismo cuerpo: un código estable en error, pensado para tu código, y un message en italiano, pensado para las personas. Toma tus decisiones sobre error y sobre el estado HTTP, no sobre el texto del mensaje, que puede cambiar.
{
"error": "overlap",
"message": "Il campo è già prenotato in quell'orario."
}
Los errores de validación añaden issues, la lista detallada de los problemas campo por campo:
{
"error": "invalid_input",
"message": "Dati non validi",
"issues": [
{
"code": "invalid_string",
"validation": "datetime",
"path": ["startsAt"],
"message": "Invalid datetime"
}
]
}
| HTTP | error | 7.5 Estados de una reservaSignalement d'un problème d'accès à l'eau potable à l'école primaire de l'Ermitage. L'eau est trouble et a une odeur suspecte. Les élèves et le personnel se plaignent de maux de ventre. Une intervention rapide est demandée pour analyser l'eau et sécuriser l'approvisionnement. Les autorités sanitaires ont été informées. Une distribution d'eau en bouteille a été mise en place temporairement. Les cours sont maintenus pour le moment. Des prélèvements ont été effectués ce matin par les services techniques de la mairie. Les résultats sont attendus sous 48 heures. En attendant, l'utilisation de l'eau du robinet est strictement interdite pour la consommation et la préparation des repas. Un point de situation sera fait régulièrement. Pour toute question, contacter le secrétariat de la mairie au 02 62 40 00 00. Merci de votre compréhension et de votre vigilance. Le Maire.`, | Qué hacer |
|---|---|---|---|
| 400 | invalid_input | Parámetros o cuerpo no válidos. El detalle está en issues. | Corrige la solicitud. No vuelvas a intentarlo de forma idéntica: el resultado no cambia. |
| 401 | invalid_key | Cabecera X-Api-Key ausente, mal formada, desconocida o revocada. | Verifica que envías la cabecera y que la clave esté activa en el panel. Si ha sido revocada, genera una nueva. |
| 403 | subscription_required | Ninguna suscripción «Gestore campi» en curso en la cuenta propietaria de la clave. | Reactiva la suscripción desde la app (Perfil → área gestor → Suscripción). Ninguna llamada funciona hasta ese momento. |
| 403 | feature_not_included | Suscripción activa, pero el plan no incluye «API para sistemas de gestión externos». El mensaje nombra el plan en curso. | Cambia a un plan que incluya la función. Vale para toda la v1, lecturas incluidas. |
| 404 | not_found | Recurso inexistente o bien perteneciente a otro gestor: las dos situaciones no se distinguen, para no revelar datos ajenos. | Vuelve a leer los id de /venues y de /bookings: casi siempre es un id antiguo o copiado de otra cuenta. |
| 409 | overlap | La pista ya está ocupada en el intervalo solicitado por una reserva no cancelada. | Propón otra franja horaria: vuelve a leer /availability. No vuelvas a intentarlo sin cambiar la hora. |
| 409 | venue_closed | Cierre extraordinario de la instalación en la fecha solicitada. | Propón otra fecha. Los cierres se leen desde /availability (closure). |
| 429 | rate_limited | Superadas las 120 solicitudes por minuto para la clave. | Espera y vuelve a intentarlo con backoff exponencial, respetando retry-after. |
| 4xx | error | Código genérico para los errores de solicitud que no entran en los casos anteriores (raro). | Lee message y corrige la solicitud. |
| 500 | internal | Error imprevisto por nuestra parte. | Inténtalo más tarde. Si persiste, escríbenos indicando la hora, el endpoint y el prefijo de la clave (no la clave). |
Una ruta inexistente bajo la base responde 404; lo mismo se aplica a un método no previsto en una ruta existente:
{
"error": "not_found",
"message": "Risorsa non trovata"
}
Las respuestas 502, 503 y 504 llegan desde el proxy inverso cuando la API no está accesible (reinicio o mantenimiento): en esos casos no se garantiza que el cuerpo esté en formato JSON. Trátalas como errores temporales e inténtalo de nuevo con backoff, sin intentar leer su contenido.
9. Versiones y compatibilidad
La versión está en la ruta: /manager-api/v1. No existen encabezados de versión ni fechas de lanzamiento que especificar: la única versión publicada actualmente es la v1.
Qué podemos cambiar sin avisarte
- Añadir campos a las respuestas existentes.
- Añadir parámetros opzionales a las solicitudes, con un valor predeterminado que conserve el comportamiento actual.
- Añadir nuevos endpoints bajo
/v1. - Añadir valores a los campos de texto abiertos (
source,paymentMethod,surface). - Reformular los
messagede los errores, manteniendo invariables los códigoserror.
Qué no cambia dentro de la v1
- Ningún campo será eliminado o renombrado de las respuestas.
- Ningún campo cambiará de tipo o significado.
- Ningún parámetro hoy opcional pasará a ser obligatorio.
- Los códigos
errory los estados HTTP documentados aquí seguirán siendo los mismos.
Cualquier modificación que rompa estas garantías llegará con un nuevo prefijo (/v2), manteniendo la v1 en servicio y con un aviso previo a los gestores que la estén utilizando.
- Ignora los campos que no conozcas en lugar de dar error con ellos (ningún parsing «strict» que rechace las propiedades inesperadas).
- Gestiona los valores desconocidos de los campos de texto abiertos con una alternativa de respaldo.
- No confíes en el orden de las claves JSON ni en el texto de los mensajes.
- Trata
nully campo ausente como equivalentes en lectura.
Registro de cambios
| Versión | Fecha | Notas |
|---|---|---|
| v1 | jul 2026 | Añadidos compatibles: tarifas por persona en la lista de precios (pricingMode, minPlayers) y playersCount en las reservas (con el error players_required); paginación limit/offset en la lista de reservas; nueva comprobación de conflictos al reactivar una reserva cancelada; recurrence ahora rechazado explícitamente con 400 recurrence_unsupported (antes se ignoraba en silencio). |
| v1 | 2026 | Primera versión pública: instalaciones y pistas, reservas (lista, creación, modificación), disponibilidad diaria, lista de precios. Autenticación con X-Api-Key, 120 solicitudes/minuto por clave. |
10. Soporte para la integración
Para preguntas técnicas, comportamientos inesperados o solicitudes de endpoints que no encuentres aquí, escríbenos desde la página Contacto. Para ir directo al grano, indica en el mensaje:
- endpoint y método que estás llamando;
- fecha y hora de la llamada (con la zona horaria) y el código HTTP recibido;
- código
errory mensaje de la respuesta; - prefijo de la clave utilizada (ej.
vfk_9f2c).
No nos envíes la clave API completa por ningún canal, ni siquiera para que verifiquemos un problema: el prefijo nos basta para identificarla. Si ya la has compartido con alguien, revócala y genera una nueva.
Si necesitas probar la integración sin tocar el calendario real, crea una instalación de prueba con una pista dedicada y usa esa: todas las llamadas están limitadas a las instalaciones del propietario de la clave, por lo que el resto de tu cuenta no está involucrado.