API para software de gestión externo

Versión v1 — documentación actualizada al 29 de julio de 2026

Esta página documenta las API HTTP que VolleyFriends pone a disposición de los gestores de instalaciones para mantener el calendario de las pistas sincronizado con un software de gestión externo o con un sitio web de reservas online. Son las mismas reglas de dominio del panel del gestor (conflictos, cierres, tarifas): solo cambia la forma de autenticarse.

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.
Ámbito de las claves

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

  1. Una cuenta de gestor de pistas VolleyFriends con al menos una estructura y una pista.
  2. Una suscripción activa con un plan que incluya la función «API para sistemas de gestión externos». Toda la v1incluso las de solo lectura — está reservada para quienes tengan esta función: es una función de pago, no un accesorio del panel.
  3. Una clave API generada desde el panel de gestión: admin.volleyfriends.comConfiguraciónClaves APINueva 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)."
}
Custodia de la clave
  • 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."
}
Cómo mantenerse dentro del límite
  • Para la sincronización periódica, utiliza una sola llamada a /bookings con un intervalo fromto amplio, 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), respetando retry-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

GET/api/manager-api/v1/venues

Muestra 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

CampoTipoDescripción
venues[].iduuidIdentificador de la instalación (venueId en los otros endpoints).
venues[].namestringNombre de la instalación.
venues[].citystring | nullCiudad.
venues[].addressstring | nullDirección.
venues[].verifiedbooleantrue si la instalación tiene la insignia VERIFICADO.
venues[].courts[]arrayPistas de la instalación. Array vacío si no tiene.
courts[].iduuidIdentificador de la pista (courtId en los otros endpoints).
courts[].venueIduuidInstalación a la que pertenece la pista.
courts[].namestringNombre de la pista.
courts[].surfacestring | nullSuperficie, texto libre introducido por el gestor.
courts[].indoorbooleanPista cubierta.
courts[].lightingbooleanIluminació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

GET/api/manager-api/v1/bookings

Devuelve 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)

NombreTipoOblig.Descripción
venueIduuidnoLimita a una instalación. Si no es tuya: 404 not_found.
courtIduuidnoLimita 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.
fromfecha o instante ISOnoLímite inferior incluido. Una fecha sola AAAA-MM-GG equivale a la medianoche local italiana de ese día.
tofecha o instante ISOnoLí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.
limitinteger 1–1000noNúmero máximo de filas (por defecto 500).
offsetinteger ≥ 0noFilas a omitir (por defecto 0): offset=500 es la segunda página con el limit por defecto.
Sobre qué actúa el filtro

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).

CampoTipoDescripción
iduuidIdentificador de la reserva.
venueIduuidEstructura.
courtIduuidPista reservada.
courtNamestringNombre de la pista, para facilitar la visualización.
startsAtinstante ISOInicio, siempre en UTC (sufijo Z).
endsAtinstante ISOFin, límite excluido.
customerNamestringNombre del cliente.
customerPhonestring | nullTeléfono.
customerEmailstring | nullEmail.
priceCentsinteger | nullPrecio acordado en céntimos. null = por definir (nunca 0 para «no definido»).
playersCountinteger | nullNúmero de personas declarado (solo reservas en franjas con precio por persona). null = no aplicable o no indicado.
statusstringpending · confirmed · paid · cancelled — ver estados.
paymentMethodstring | nullcash · bank_transfer · paypal · stripe · other. null = no cobrado.
paymentLinkstring | nullEnlace de pago con tarjeta generado desde el panel, si está presente. En solo lectura.
notesstring | nullNotas libres.
sourcestringOrigen: manual (panel) · api (estas API) · app (solicitud de un jugador desde la app) · tournament (torneo).
recurrenceIduuid | nullPresente e igual en todas las ocurrencias de una serie recurrente creada desde el panel.
createdAtinstante ISOCreación.
updatedAtinstante ISOÚltima modificación.

Errores específicos

CódigoerrorCuándo
400invalid_inputvenueId/courtId no son UUID, o bien from/to no son fechas interpretables.
404not_foundLa instalación ("Struttura non trovata") o la pista ("Campo non trovato") no existe o no te pertenece.

6.3 Nueva reserva

POST/api/manager-api/v1/bookings

Crea 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:

  1. cierre extraordinario de la instalación en la fecha de inicio → 409 venue_closed;
  2. solapamiento con otra reserva no cancelada de la misma pista → 409 overlap;
  3. precio ausente → calculado a partir de la tarifa de la pista (null si la hora de inicio no entra en ninguna franja). Si la franja tiene precio por persona, también se requiere playersCount: de lo contrario, la solicitud se rechaza con 400 players_required (a menos que se indique un priceCents explí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)

NombreTipoOblig.Descripción
courtIduuidCampo a reservar. Debe pertenecer a una de tus instalaciones.
startsAtinstante ISO 8601 con offsetInicio. Se aceptan tanto la forma 2026-08-12T18:00:00Z como 2026-08-12T18:00:00+02:00. Sin offset: 400.
endsAtinstante ISO 8601 con offsetFin, debe ser posterior a startsAt.
customerNamestring (1–160)Nombre del cliente.
customerPhonestring (máx. 40)noTeléfono.
customerEmailemail (máx. 200)noEmail, validado como dirección.
priceCentsinteger ≥ 0noPrecio en céntimos. Si se omite se calcula a partir de la tarifa del campo. Indica 0 solo para una reserva realmente gratuita.
playersCountinteger 1–500noCuá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.
notesstring (máx. 2000)noNotas libres, visibles para el administrador en el panel.
statuspending | confirmed | paid | cancellednoPor 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).
paymentMethodcash | bank_transfer | paypal | stripe | othernoCómo se ha cobrado (o se cobrará). Omitido = no cobrado.
recurrenceobjectnoNo 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ódigoerrorCuándo
400invalid_inputCuerpo 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.
400players_requiredPrecio a calcular a partir de la tarifa en un tramo por persona, pero playersCount ausente.
400recurrence_unsupportedEl cuerpo contiene recurrence: las recurrencias no están disponibles en estas API.
403subscription_requiredNinguna suscripción de administrador activa.
404not_found"Campo non trovato" — el courtId no existe o no es tuyo.
409venue_closedLa instalación tiene un cierre extraordinario en la fecha de inicio.
409overlapLa 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)." }
Idempotencia

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

PATCH/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

NombreTipoOblig.Descripción
iduuidId 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.

NombreTipoNotas
courtIduuidMueve la reserva a otra de tus pistas, incluso de otra de tus instalaciones (venueId se actualiza en consecuencia).
startsAtinstante ISO con offsetNuevo inicio.
endsAtinstante ISO con offsetNuevo fin. El resultado final debe seguir siendo posterior al inicio, incluso combinando un solo extremo con el valor ya guardado.
customerNamestring (1–160)
customerPhonestring (máx. 40)Anulable.
customerEmailemail (máx. 200)Anulable.
priceCentsinteger ≥ 0Anulable (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.
playersCountinteger 1–500Anulable. 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.
notesstring (máx. 2000)Anulable.
statuspending | confirmed | paid | cancelledSe 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.
paymentMethodcash | bank_transfer | paypal | stripe | otherAnulable.
Cuándo se vuelven a comprobar los conflictos

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ódigoerrorCuándo
400invalid_inputCuerpo 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").
403subscription_requiredNinguna suscripción de administrador activa.
404not_found"Prenotazione non trovata" o bien "Campo non trovato" si el nuevo courtId no es tuyo.
409venue_closedDesplazamiento a una fecha de cierre extraordinario.
409overlapEl nuevo horario se solapa con otra reserva no cancelada de la misma pista.

6.5 Disponibilidad de una pista en un día

GET/api/manager-api/v1/availability

Devuelve, 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)

NombreTipoOblig.Descripción
courtIduuidPista de la que leer la disponibilidad. Debe ser tuya.
dateAAAA-MM-GGDí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

CampoTipoDescripción
courtIduuidCampo requerido.
venueIduuidEstructura de la pista.
dateAAAA-MM-GGDía solicitado, repetido.
timezonestringSiempre "Europe/Rome": la zona horaria en la que se deben interpretar date y los minutos de los horarios.
hours.weekdayinteger 0–6Día de la semana: 0 = lunes … 6 = domingo.
hours.openMinuteinteger | nullApertura en minutos desde la medianoche local (540 = 09:00). null si el día se declara cerrado.
hours.closeMinuteinteger | nullCierre en minutos (1380 = 23:00). null si el día se declara cerrado.
hours.closedbooleantrue si el gestor ha declarado ese día de la semana como cerrado.
hours.configuredbooleanfalse 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.
closureobject | nullCierre extraordinario activo en el día solicitado, o bien null.
closure.fromAAAA-MM-GGPrimer día de cierre.
closure.toAAAA-MM-GGÚltimo día de cierre, incluido.
closure.reasonstring | nullMotivo, si lo indica el gestor.
bookings[]arraySlots 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.
Con un cierre en curso

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ódigoerrorCuándo
400invalid_inputcourtId faltante o no UUID, date faltante o no en formato AAAA-MM-GG.
404not_found"Campo non trovato" — el campo no existe o no es tuyo.

6.6 Tarifas de una pista

GET/api/manager-api/v1/rates

Las 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)

NombreTipoOblig.Descripción
courtIduuidPista 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

CampoTipoDescripción
courtIduuidCampo requerido.
rates[].iduuidIdentificador de la franja.
rates[].labelstringEtiqueta elegida por el gestor (ej. «Nocturna»).
rates[].daysMaskinteger 1–127Má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[].fromMinuteinteger 0–1440Inicio de la franja en minutos desde la medianoche local.
rates[].toMinuteinteger 0–1440Fin de la franja, siempre mayor que fromMinute.
rates[].pricePerHourCentsinteger > 0Precio 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[].pricingModecourt | personcourt = precio de la pista (predeterminado). person = precio por persona: el total escala con el número de personas, nunca a la baja.
rates[].minPlayersinteger | nullMínimo garantizado del tramo por persona: por debajo de ese número se paga de todas formas el mínimo. null = sin mínimo.
Cómo se calcula el precio de una reserva

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ódigoerrorCuándo
400invalid_inputcourtId ausente o no es un UUID.
404not_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:00 o 2026-08-12T18:00:00Z. Una cadena sin offset (2026-08-12T18:00:00) se rechaza con 400: 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 Z y milisegundos: "2026-08-12T16:00:00.000Z". Conviértelos al huso local antes de mostrarlos.
  • Las fechas sin hora (date, closure.from, closure.to y from/to cuando los usas en formato de día) son AAAA-MM-GG y 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 medianocheHora local7.3 Minutos desde la medianocheHora local
000:00108018:00
48008:00132022:00
54009:00138023:00
84014:00144024: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

Estado7.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?
pendingSolicitud 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). — el slot ya está bloqueado, para evitar duplicar solicitudes en el mismo horario
confirmedReserva válida, aún no cobrada. Es el estado inicial de cada reserva creada por estas API y desde el panel.
paidReserva válida y cobrada. Normalmente acompañada de paymentMethod.
cancelledReserva cancelada (o solicitud rechazada). Permanece en el historial pero libera la pista: no genera overlap y no aparece en /availability.No
Cómo tratar las reservas «pending»
  • 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 que customerPhone puede ser null.
  • No crees reservas pending desde 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 como confirmed y cámbiala a cancelled si el pago no se realiza.

7.6 Origen y método de pago

CampoValores7.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.`,
sourcemanualIntroducida por el gestor en el panel.
apiCreada por un sistema de gestión externo con estas API. Valor siempre establecido por la POST, no modificable.
appSolicitud de reserva online enviada por un jugador desde la app VolleyFriends: se genera como pending y queda a la espera de tu decisión.
tournamentGenerada por la organización de un torneo en las pistas de las instalaciones.
paymentMethodcash, bank_transfer, paypal, stripe, otherCó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"
    }
  ]
}
HTTPerror7.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
400invalid_inputPará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.
401invalid_keyCabecera 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.
403subscription_requiredNinguna 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.
403feature_not_includedSuscripció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.
404not_foundRecurso 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.
409overlapLa 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.
409venue_closedCierre extraordinario de la instalación en la fecha solicitada.Propón otra fecha. Los cierres se leen desde /availability (closure).
429rate_limitedSuperadas las 120 solicitudes por minuto para la clave.Espera y vuelve a intentarlo con backoff exponencial, respetando retry-after.
4xxerrorCódigo genérico para los errores de solicitud que no entran en los casos anteriores (raro).Lee message y corrige la solicitud.
500internalError 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 message de los errores, manteniendo invariables los códigos error.

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 error y 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.

Cómo escribir un cliente que no se rompa
  • 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 null y campo ausente como equivalentes en lectura.

Registro de cambios

VersiónFechaNotas
v1jul 2026Añ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).
v12026Primera 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 error y mensaje de la respuesta;
  • prefijo de la clave utilizada (ej. vfk_9f2c).
Nunca en el mensaje

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.