{"openapi":"3.1.0","info":{"title":"Skalia Partner API","version":"1.3.0","summary":"API server-to-server para el sistema del club: canchas, disponibilidad, ocupación, reservas y eventos de agenda.","description":"API server-to-server para el sistema del CLUB (web propia, POS, proveedor de software). Se autentica con una **API key** de club (`Authorization: Bearer sk_live_…` en producción, `sk_test_…` en el sandbox), nunca con un JWT de usuario.\n\n**Quickstart**\n1. `GET /me` con la key: si responde 200, la integración, el club, los scopes efectivos y las cuotas que ves son los que rigen.\n2. `GET /courts` para mapear ids de canchas; `GET /availability?date=` para saber qué se puede vender (comienzos, duraciones, precio).\n3. `POST /bookings` SIEMPRE con `externalRef` (tu id de orden): 201 al crear, 200 + `Idempotent-Replay: true` al repetir.\n4. `PATCH` / `DELETE /bookings/{id}` para mover, corregir o cancelar (`?force=true` si había partidos armados en el turno).\n5. `GET /events?since=` cada 30–60 s para enterarte de todo cambio de agenda, y `GET /occupancy` (con `If-None-Match`) como fuente de verdad de \"ocupado\".\n\n**Reglas que conviene saber**: `date` es `YYYY-MM-DD` y las horas `HH:MM`, naive en la zona del club (`clubs[].timezone` de `/me`); `createdAt` / `updatedAt` / `server.now` son ISO UTC. Todo error viaja como `ErrorEnvelope` (`{ error, code, details?, requestId }`): ramificá por `code`. Las reservas ajenas se ven siempre anónimas (`SlotDto`). Mandá tu propio `X-Request-Id`: es lo que soporte busca.\n\n**Docs**: [guía para empezar](https://www.skaliasports.com/desarrolladores#docs-start) · [autenticación](https://www.skaliasports.com/desarrolladores#docs-auth) · [errores y `code`](https://www.skaliasports.com/desarrolladores#docs-envelope) · [sincronización](https://www.skaliasports.com/desarrolladores#docs-sync) · [DTOs](https://www.skaliasports.com/desarrolladores#docs-ref-dtos) · todo en [skaliasports.com/desarrolladores](https://www.skaliasports.com/desarrolladores). Acceso y credenciales desde el [panel del club](https://www.skaliasports.com/admin/api).","contact":{"name":"Skalia","url":"https://www.skaliasports.com/admin/api"}},"externalDocs":{"description":"Documentación pública de la Partner API: guías, referencia y tutoriales.","url":"https://www.skaliasports.com/desarrolladores"},"servers":[{"url":"https://skalia-server.vercel.app/api/partner/v1","description":"Producción (keys sk_live_…)"},{"url":"https://api-test.skaliasports.com/api/partner/v1","description":"Sandbox (keys sk_test_…)"}],"tags":[{"name":"Cuenta","description":"Quién soy: integración, key, clubes, cuotas."},{"name":"Agenda","description":"Canchas, qué se puede vender y qué está ocupado."},{"name":"Reservas","description":"ABML de reservas propias (idempotente, con cuota de escritura)."},{"name":"Eventos","description":"Outbox de todo cambio de agenda, por polling."},{"name":"Canchas, precios y horarios","description":"Configuración del club por API: alta / edición / baja de canchas, tarifario por franja y duración, defaults y horario semanal. Scopes de escritura sensibles (sólo el OWNER emite la key)."},{"name":"Bloqueos, fijas y asistencia","description":"Bloqueos de cancha, series de reservas fijas y pasar lista, con el emisor único de eventos."},{"name":"Torneos","description":"Torneos del club: estructura del evento, grupos, cuadro, cronograma y resultados. Las parejas, siempre anónimas; la inscripción no se hace por API."},{"name":"Club","description":"Ficha, configuración de negocio, espacios y categorías de recurso del club."},{"name":"Webhooks","description":"Webhooks salientes: recibí los eventos de la agenda firmados, sin pollear."},{"name":"Partidos","description":"Partidos públicos del club: el cartel \"sumate\" y publicar un turno muerto en Skalia. Sólo contadores, nunca jugadores."},{"name":"Reportes","description":"Totales de horas y venta esperada del club, sin personas."},{"name":"Americanos","description":"Americanos publicados por el club: cupos y estado. Sólo lectura; nunca los inscriptos. El `AmericanoDto` no trae URL: el enlace a Skalia se arma con el `id` (`https://www.skaliasports.com/americanos/<id>`)."},{"name":"Streaming","description":"Qué cancha está en vivo ahora (TV del hall, web del torneo)."},{"name":"Meta","description":"Esta spec."}],"security":[{"apiKey":[]}],"paths":{"/amenities":{"get":{"operationId":"listClubAmenities","tags":["Club"],"summary":"Espacios y servicios del club","description":"Los espacios y servicios (amenities) que el club tiene marcados, del catálogo de la plataforma. Orden: nombre.","x-skalia-scope":"amenities:read","x-skalia-write":false,"parameters":[{"$ref":"#/components/parameters/RequestId"},{"name":"clubId","in":"query","required":false,"description":"Club sobre el que opera la request. Opcional: se infiere cuando la integración tiene un solo grant (kind CLUB, v1).","schema":{"type":"string","pattern":"^[a-f0-9-]{8,64}$"}}],"responses":{"200":{"description":"Los del club.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClubAmenitiesResponse"}}}},"400":{"description":"Request mal formada (credencial con forma rara, body / query inválidos, `clubId` faltante con varios grants).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MALFORMED_API_KEY","VALIDATION","CLUB_REQUIRED"]},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Integración deshabilitada, módulo API apagado en el club, club sin grant o scope insuficiente (`details.scope`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CLIENT_DISABLED","MODULE_DISABLED","CLUB_NOT_GRANTED","SCOPE_DENIED"]},"429":{"$ref":"#/components/responses/RateLimitedRead"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}},"put":{"operationId":"replaceClubAmenities","tags":["Club"],"summary":"Reemplazar los espacios y servicios del club","description":"Reemplazo COMPLETO del conjunto (espejo de `PUT /club-owner/clubs/:id/amenities`): lo que no está en `ids` se quita; `[]` lo vacía. Un id fuera del catálogo rechaza todo sin escribir nada. Scope sensible `amenities:write`. Audit `CLUB_AMENITIES_UPDATED`. Consume cuota de escritura.","x-skalia-scope":"amenities:write","x-skalia-write":true,"parameters":[{"$ref":"#/components/parameters/RequestId"}],"requestBody":{"required":true,"description":"JSON obligatorio (`Content-Type: application/json`; si no, 415).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AmenitiesPutBody"}}}},"responses":{"200":{"description":"El conjunto resultante.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClubAmenitiesResponse"}}}},"400":{"description":"Request mal formada (credencial con forma rara, body / query inválidos, `clubId` faltante con varios grants).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MALFORMED_API_KEY","VALIDATION","CLUB_REQUIRED"]},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Integración deshabilitada, módulo API apagado en el club, club sin grant o scope insuficiente (`details.scope`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CLIENT_DISABLED","MODULE_DISABLED","CLUB_NOT_GRANTED","SCOPE_DENIED"]},"404":{"description":"Algún id no existe en el catálogo (`details.amenityIds`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["AMENITY_NOT_FOUND"]},"415":{"$ref":"#/components/responses/UnsupportedMediaType"},"429":{"$ref":"#/components/responses/RateLimitedWrite"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}}},"/amenities/catalog":{"get":{"operationId":"listAmenityCatalog","tags":["Club"],"summary":"Catálogo de espacios y servicios","description":"Todo el catálogo de la plataforma (lo define Skalia): los ids que acepta `PUT /amenities`. Orden: nombre. Es un recurso global: no lleva `clubId` (el scope `amenities:read` se evalúa sobre la key, como `/me`).","x-skalia-scope":"amenities:read","x-skalia-write":false,"parameters":[{"$ref":"#/components/parameters/RequestId"}],"responses":{"200":{"description":"El catálogo.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AmenityCatalogResponse"}}}},"400":{"description":"Request mal formada (credencial con forma rara, body / query inválidos, `clubId` faltante con varios grants).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MALFORMED_API_KEY"]},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Integración deshabilitada, módulo API apagado en el club, club sin grant o scope insuficiente (`details.scope`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CLIENT_DISABLED","MODULE_DISABLED","SCOPE_DENIED"]},"429":{"$ref":"#/components/responses/RateLimitedRead"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}}},"/americanos":{"get":{"operationId":"listAmericanos","tags":["Americanos"],"summary":"Listar americanos del club","description":"Americanos CLUB_PUBLIC del club (los que la app lista y a los que cualquier jugador puede sumarse), con cupos y estado, para publicarlos en la web del club. No hay campo de enlace: la web del club lo arma con el `id` (`https://www.skaliasports.com/americanos/<id>`: detalle público, sin login; en el celular abre la app). Orden: `startsAt` asc, alta asc. Con el flag `americano_tournaments_enabled` apagado en la plataforma responde `americanos: []`. Nunca datos de personas: ni quién lo creó ni quiénes juegan (`players` no existe en esta API); tampoco rondas, mesas ni tabla.","x-skalia-scope":"americanos:read","x-skalia-write":false,"parameters":[{"$ref":"#/components/parameters/RequestId"},{"name":"limit","in":"query","required":false,"description":"Tamaño de página, 1..200 (default 100).","schema":{"type":"integer","minimum":1,"maximum":200}},{"name":"offset","in":"query","required":false,"description":"Desplazamiento (paginación offset/limit).","schema":{"type":"integer","minimum":0}},{"name":"clubId","in":"query","required":false,"description":"Club sobre el que opera la request. Opcional: se infiere cuando la integración tiene un solo grant (kind CLUB, v1).","schema":{"type":"string","pattern":"^[a-f0-9-]{8,64}$"}},{"name":"status","in":"query","required":false,"description":"Filtra por estado.","schema":{"type":"string","enum":["OPEN","IN_PROGRESS","FINISHED","CANCELLED"]}},{"name":"from","in":"query","required":false,"description":"Americanos cuyo `startsAt` es ≥ `from` (`YYYY-MM-DD` naive del club).","schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"example":"2026-09-18"},{"name":"to","in":"query","required":false,"description":"Americanos cuyo `startsAt` es ≤ `to` (`YYYY-MM-DD` naive del club).","schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"example":"2026-10-18"}],"responses":{"200":{"description":"Página de americanos.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AmericanosListResponse"}}}},"400":{"description":"Request mal formada (credencial con forma rara, body / query inválidos, `clubId` faltante con varios grants).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MALFORMED_API_KEY","VALIDATION","CLUB_REQUIRED"]},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Integración deshabilitada, módulo API apagado en el club, club sin grant o scope insuficiente (`details.scope`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CLIENT_DISABLED","MODULE_DISABLED","CLUB_NOT_GRANTED","SCOPE_DENIED"]},"429":{"$ref":"#/components/responses/RateLimitedRead"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}}},"/americanos/{id}":{"get":{"operationId":"getAmericano","tags":["Americanos"],"summary":"Detalle de un americano","description":"El mismo `AmericanoDto` del listado (cupos, estado, canchas), para refrescar el cartel. Nunca datos de personas: ni quién lo creó ni quiénes juegan (`players` no existe en esta API); tampoco rondas, mesas ni tabla.","x-skalia-scope":"americanos:read","x-skalia-write":false,"parameters":[{"$ref":"#/components/parameters/RequestId"},{"name":"id","in":"path","required":true,"description":"Id del americano.","schema":{"type":"string","pattern":"^[a-f0-9-]{8,64}$"}}],"responses":{"200":{"description":"El americano.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AmericanoResponse"}}}},"400":{"description":"Request mal formada (credencial con forma rara, body / query inválidos, `clubId` faltante con varios grants).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MALFORMED_API_KEY"]},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Integración deshabilitada, módulo API apagado en el club, club sin grant o scope insuficiente (`details.scope`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CLIENT_DISABLED","MODULE_DISABLED","CLUB_NOT_GRANTED","SCOPE_DENIED"]},"404":{"description":"El americano no existe, no es CLUB_PUBLIC, es de un club sin grant, o los americanos están apagados en la plataforma (`americano_tournaments_enabled`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["AMERICANO_NOT_FOUND"]},"429":{"$ref":"#/components/responses/RateLimitedRead"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}}},"/availability":{"get":{"operationId":"getAvailability","tags":["Agenda"],"summary":"Qué se puede vender","description":"Comienzos vendibles por cancha (grilla :00/:30 dentro de las franjas de la cancha y del horario del club), con duraciones y precio. Sólo canchas habilitadas de kind COURT; descuenta las 4 fuentes de ocupación; oculta los comienzos ya pasados si `date` es hoy en el club. Hasta 180 días de anticipación. No es autoritativo: el POST revalida.","x-skalia-scope":"availability:read","x-skalia-write":false,"parameters":[{"$ref":"#/components/parameters/RequestId"},{"name":"clubId","in":"query","required":false,"description":"Club sobre el que opera la request. Opcional: se infiere cuando la integración tiene un solo grant (kind CLUB, v1).","schema":{"type":"string","pattern":"^[a-f0-9-]{8,64}$"}},{"name":"date","in":"query","required":true,"description":"`YYYY-MM-DD`, naive en la zona del club (`clubs[].timezone` de `/me`). Un ISO con offset es VALIDATION.","schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"example":"2026-09-15"},{"name":"sport","in":"query","required":false,"description":"Filtra las canchas por deporte (`courts.sports`, p.ej. `PADEL`).","schema":{"type":"string","minLength":2,"maxLength":32},"example":"PADEL"},{"name":"durationMinutes","in":"query","required":false,"description":"Restringe el cálculo a esa duración (si no, se calculan las cuatro).","schema":{"type":"integer","enum":[30,60,90,120]},"example":90}],"responses":{"200":{"description":"Comienzos por cancha. `courts: []` si el club no abre ese día.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AvailabilityResponse"}}}},"400":{"description":"Request mal formada (credencial con forma rara, body / query inválidos, `clubId` faltante con varios grants). Además: `date` anterior a hoy del club, o más de 180 días adelante.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MALFORMED_API_KEY","VALIDATION","CLUB_REQUIRED","DATE_IN_PAST","MAX_ADVANCE_EXCEEDED"]},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Integración deshabilitada, módulo API apagado en el club, club sin grant o scope insuficiente (`details.scope`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CLIENT_DISABLED","MODULE_DISABLED","CLUB_NOT_GRANTED","SCOPE_DENIED"]},"429":{"$ref":"#/components/responses/RateLimitedRead"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}}},"/blocks":{"get":{"operationId":"listBlocks","tags":["Bloqueos, fijas y asistencia"],"summary":"Listar bloqueos","description":"Bloqueos del club que solapan `[from, to]`, cada uno con los huecos que lo perforan. Todos los orígenes (panel y API): un bloqueo es del club, no de quien lo creó. Sin datos de personas.","x-skalia-scope":"blocks:read","x-skalia-write":false,"parameters":[{"$ref":"#/components/parameters/RequestId"},{"name":"clubId","in":"query","required":false,"description":"Club sobre el que opera la request. Opcional: se infiere cuando la integración tiene un solo grant (kind CLUB, v1).","schema":{"type":"string","pattern":"^[a-f0-9-]{8,64}$"}},{"name":"courtId","in":"query","required":false,"description":"Sólo los bloqueos de esta cancha.","schema":{"type":"string","pattern":"^[a-f0-9-]{8,64}$"}},{"name":"from","in":"query","required":true,"description":"Primer día del rango a consultar (se listan los bloqueos que lo solapan).","schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"example":"2026-09-15"},{"name":"to","in":"query","required":true,"description":"Último día del rango (inclusive); como mucho 366 días.","schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"example":"2026-09-30"}],"responses":{"200":{"description":"Bloqueos en el rango.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BlocksListResponse"}}}},"400":{"description":"Request mal formada (credencial con forma rara, body / query inválidos, `clubId` faltante con varios grants).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MALFORMED_API_KEY","VALIDATION","CLUB_REQUIRED"]},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Integración deshabilitada, módulo API apagado en el club, club sin grant o scope insuficiente (`details.scope`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CLIENT_DISABLED","MODULE_DISABLED","CLUB_NOT_GRANTED","SCOPE_DENIED"]},"429":{"$ref":"#/components/responses/RateLimitedRead"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}},"post":{"operationId":"createBlock","tags":["Bloqueos, fijas y asistencia"],"summary":"Bloquear una cancha (o todo el club)","description":"Crea un bloqueo por cancha (`courtId: null` = todas las habilitadas del club) que ocupa `[startTime, endTime]` en cada día de `[from, to]` (sin horas: el día entero). Si pisa reservas vivas (PENDING / CONFIRMED de hoy en adelante, sin las de hoy que ya terminaron: ésas se pasan lista, no se cancelan) responde 409 con la lista anónima; con `?force=true` las cancela (`cancel_reason = API`, evento `booking.cancelled` con reason `court_block`, partidos desligados, push al titular real, audit por reserva). Emite `occupancy.changed` por el rango. Consume cuota de escritura.","x-skalia-scope":"blocks:write","x-skalia-write":true,"parameters":[{"$ref":"#/components/parameters/RequestId"},{"name":"force","in":"query","required":false,"description":"`true` cancela las reservas vivas que el bloqueo pisa (por el emisor único, reason `court_block`, partidos desligados) en vez de responder 409 BLOCK_CONFLICTS_WITH_BOOKINGS. Las de torneo nunca se cancelan desde acá.","schema":{"type":"boolean"}}],"requestBody":{"required":true,"description":"JSON obligatorio (`Content-Type: application/json`; si no, 415).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BlockCreateBody"}}}},"responses":{"201":{"description":"Bloqueo(s) creado(s).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BlocksCreateResponse"}}}},"400":{"description":"Request mal formada (credencial con forma rara, body / query inválidos, `clubId` faltante con varios grants).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MALFORMED_API_KEY","VALIDATION","CLUB_REQUIRED"]},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Integración deshabilitada, módulo API apagado en el club, club sin grant o scope insuficiente (`details.scope`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CLIENT_DISABLED","MODULE_DISABLED","CLUB_NOT_GRANTED","SCOPE_DENIED"]},"404":{"description":"La cancha no es del club o está deshabilitada (`details.courtId`; `null` si el club no tiene canchas habilitadas).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["COURT_NOT_FOUND"]},"409":{"description":"Pisa reservas vivas sin `force` (`details.bookings[]` anónimas, `count`), o pisa reservas de torneo (`details.uncancellable: true`, ni con `force`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["BLOCK_CONFLICTS_WITH_BOOKINGS"]},"415":{"$ref":"#/components/responses/UnsupportedMediaType"},"429":{"$ref":"#/components/responses/RateLimitedWrite"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}}},"/blocks/{id}":{"delete":{"operationId":"deleteBlock","tags":["Bloqueos, fijas y asistencia"],"summary":"Quitar un bloqueo","description":"Borra el bloqueo (los huecos de esa cancha quedan: no dependen de él). Emite `occupancy.changed` con efecto de liberar: la lista de espera del club se entera sola por el emisor. Consume cuota de escritura.","x-skalia-scope":"blocks:write","x-skalia-write":true,"parameters":[{"$ref":"#/components/parameters/RequestId"},{"name":"id","in":"path","required":true,"description":"Id del bloqueo.","schema":{"type":"string","pattern":"^[a-f0-9-]{8,64}$"}}],"responses":{"200":{"description":"Quitado.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BlockDeleteResponse"}}}},"400":{"description":"Request mal formada (credencial con forma rara, body / query inválidos, `clubId` faltante con varios grants).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MALFORMED_API_KEY"]},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Integración deshabilitada, módulo API apagado en el club, club sin grant o scope insuficiente (`details.scope`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CLIENT_DISABLED","MODULE_DISABLED","CLUB_NOT_GRANTED","SCOPE_DENIED"]},"404":{"description":"El bloqueo no existe o es de un club sin grant.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["BLOCK_NOT_FOUND"]},"429":{"$ref":"#/components/responses/RateLimitedWrite"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}}},"/blocks/{id}/holes":{"post":{"operationId":"createBlockHole","tags":["Bloqueos, fijas y asistencia"],"summary":"Abrir un hueco en un bloqueo","description":"Libera `[startTime, endTime]` de UN día dentro del bloqueo: ahí el turno se vuelve a vender aunque el bloqueo siga. La cancha sale del bloqueo; el día tiene que caer en `[from, to]` y la franja dentro de `[startTime, endTime]` del bloqueo (si no, 400 VALIDATION con `details.reason: outside_block` y `details.block`). Los huecos se guardan FUSIONADOS por cancha y día, con la misma fusión del panel: uno que toque o solape a otro existente se une con él (aunque el otro se salga del bloqueo), por eso la respuesta trae el bloqueo releído y el hueco resultante. Repetir la misma franja es inofensivo (fusión idempotente; los ids de ese día cambian). Emite `occupancy.changed` (`court_block_hole`, libera ese día): la lista de espera del club se entera sola. Consume cuota de escritura.","x-skalia-scope":"blocks:write","x-skalia-write":true,"parameters":[{"$ref":"#/components/parameters/RequestId"},{"name":"id","in":"path","required":true,"description":"Id del bloqueo.","schema":{"type":"string","pattern":"^[a-f0-9-]{8,64}$"}}],"requestBody":{"required":true,"description":"JSON obligatorio (`Content-Type: application/json`; si no, 415).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BlockHoleCreateBody"}}}},"responses":{"201":{"description":"Hueco abierto (fusionado con los de ese día si tocaba alguno).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BlockHoleCreateResponse"}}}},"400":{"description":"Request mal formada (credencial con forma rara, body / query inválidos, `clubId` faltante con varios grants). Además: día fuera del rango del bloqueo o franja fuera de su ventana (`details.reason: outside_block`, `details.block`), o `endTime ≤ startTime`.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MALFORMED_API_KEY","VALIDATION"]},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Integración deshabilitada, módulo API apagado en el club, club sin grant o scope insuficiente (`details.scope`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CLIENT_DISABLED","MODULE_DISABLED","CLUB_NOT_GRANTED","SCOPE_DENIED"]},"404":{"description":"El bloqueo no existe o es de un club sin grant.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["BLOCK_NOT_FOUND"]},"415":{"$ref":"#/components/responses/UnsupportedMediaType"},"429":{"$ref":"#/components/responses/RateLimitedWrite"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}}},"/blocks/{id}/holes/{holeId}":{"delete":{"operationId":"deleteBlockHole","tags":["Bloqueos, fijas y asistencia"],"summary":"Cerrar un hueco de un bloqueo","description":"Quita el hueco: el bloqueo vuelve a ocupar esa franja (lo ya reservado en el hueco queda: una reserva viva no se cancela desde acá). El hueco tiene que perforar ESE bloqueo (misma cancha, día dentro del rango, franja solapada); si no, 404 HOLE_NOT_FOUND. Emite `occupancy.changed` (`court_block_hole`, ocupa). Consume cuota de escritura.","x-skalia-scope":"blocks:write","x-skalia-write":true,"parameters":[{"$ref":"#/components/parameters/RequestId"},{"name":"id","in":"path","required":true,"description":"Id del bloqueo.","schema":{"type":"string","pattern":"^[a-f0-9-]{8,64}$"}},{"name":"holeId","in":"path","required":true,"description":"Id del hueco (`BlockDto.holes[].id`), del bloqueo de la URL.","schema":{"type":"string","pattern":"^[a-f0-9-]{8,64}$"}}],"responses":{"200":{"description":"Hueco cerrado.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BlockHoleDeleteResponse"}}}},"400":{"description":"Request mal formada (credencial con forma rara, body / query inválidos, `clubId` faltante con varios grants).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MALFORMED_API_KEY"]},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Integración deshabilitada, módulo API apagado en el club, club sin grant o scope insuficiente (`details.scope`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CLIENT_DISABLED","MODULE_DISABLED","CLUB_NOT_GRANTED","SCOPE_DENIED"]},"404":{"description":"El bloqueo no existe o es de un club sin grant; o el hueco no existe o no perfora ese bloqueo (`details.blockId`, `details.holeId`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["BLOCK_NOT_FOUND","HOLE_NOT_FOUND"]},"429":{"$ref":"#/components/responses/RateLimitedWrite"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}}},"/bookings":{"get":{"operationId":"listBookings","tags":["Reservas"],"summary":"Listar reservas","description":"Reservas creadas por ESTA integración (`BookingDto`). Con `all=true` suma las demás del club anónimas: `ManagedSlotDto` (con `price` / `paymentStatus`) si la integración tiene `canManageAllBookings`, `SlotDto` si no. Orden: fecha, hora, id. `limit` ≤ 200 (default 100). `updatedSince` como mucho 7 días hacia atrás.","x-skalia-scope":"bookings:read","x-skalia-write":false,"parameters":[{"$ref":"#/components/parameters/RequestId"},{"name":"limit","in":"query","required":false,"description":"Tamaño de página, 1..200 (default 100).","schema":{"type":"integer","minimum":1,"maximum":200}},{"name":"offset","in":"query","required":false,"description":"Desplazamiento (paginación offset/limit).","schema":{"type":"integer","minimum":0}},{"name":"clubId","in":"query","required":false,"description":"Club sobre el que opera la request. Opcional: se infiere cuando la integración tiene un solo grant (kind CLUB, v1).","schema":{"type":"string","pattern":"^[a-f0-9-]{8,64}$"}},{"name":"from","in":"query","required":false,"description":"Desde este día (inclusive), `YYYY-MM-DD` naive del club.","schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"example":"2026-09-15"},{"name":"to","in":"query","required":false,"description":"Hasta este día (inclusive), `YYYY-MM-DD` naive del club.","schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"example":"2026-09-21"},{"name":"courtId","in":"query","required":false,"description":"Id de la cancha (`GET /courts`).","schema":{"type":"string","pattern":"^[a-f0-9-]{8,64}$"}},{"name":"status","in":"query","required":false,"description":"Filtra por status de la reserva.","schema":{"type":"string","enum":["PENDING","CONFIRMED","CANCELLED","ATTENDED","NO_SHOW"]}},{"name":"externalRef","in":"query","required":false,"description":"Filtra por tu ref (sólo propias).","schema":{"type":"string","minLength":1,"maxLength":64,"pattern":"^[\\x21-\\x7E]+$"},"example":"ORD-123"},{"name":"updatedSince","in":"query","required":false,"description":"ISO 8601 con zona; como mucho 7 días hacia atrás. Filtra por `COALESCE(updated_at, created_at)`.","schema":{"type":"string","format":"date-time"},"example":"2026-09-14T12:00:00Z"},{"name":"all","in":"query","required":false,"description":"`true` suma las reservas del club que NO creó esta integración, anónimas (`SlotDto`). Booleano. En query también se aceptan los strings \"true\" / \"false\" / \"1\" / \"0\".","schema":{"type":"boolean"}},{"name":"paymentStatus","in":"query","required":false,"description":"Filtra por estado de pago. Ojo: sólo la Partner API escribe `payment_status` (la app y el panel dejan `PENDING`): sirve si tu sistema es el que registra el cobro con `PATCH`. \"Pendientes de cobro de hoy\" = `?all=true&from=D&to=D&paymentStatus=PENDING&status=CONFIRMED`.","schema":{"type":"string","enum":["PAID","PENDING","UNPAID"]}}],"responses":{"200":{"description":"Página de reservas.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingsListResponse"}}}},"400":{"description":"Request mal formada (credencial con forma rara, body / query inválidos, `clubId` faltante con varios grants).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MALFORMED_API_KEY","VALIDATION","CLUB_REQUIRED"]},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Integración deshabilitada, módulo API apagado en el club, club sin grant o scope insuficiente (`details.scope`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CLIENT_DISABLED","MODULE_DISABLED","CLUB_NOT_GRANTED","SCOPE_DENIED"]},"429":{"$ref":"#/components/responses/RateLimitedRead"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}},"post":{"operationId":"createBooking","tags":["Reservas"],"summary":"Crear una reserva (idempotente)","description":"Alta de una reserva puntual. Nace `CONFIRMED` (el club es el autor), `source = api`, a nombre del usuario de servicio del club (o del jugador vinculado si la integración tiene `linkPlayersByPhone` y el teléfono matchea exactamente un usuario verificado). Validaciones en orden: turno → cancha → fecha/hora en la zona del club → horario → idempotencia → tope de futuras → precio → INSERT atómico contra las 4 fuentes de ocupación. Mandá SIEMPRE `externalRef`: repetir el mismo POST devuelve 200 + `Idempotent-Replay: true` (dos POST idénticos en paralelo → uno 201 y otro 200, una sola fila). Consume cuota de escritura.","x-skalia-scope":"bookings:write","x-skalia-write":true,"parameters":[{"$ref":"#/components/parameters/RequestId"},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"description":"JSON obligatorio (`Content-Type: application/json`; si no, 415).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingCreateBody"}}}},"responses":{"200":{"description":"Replay: el `externalRef` ya existía vivo con el MISMO turno; se devuelve esa reserva.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"},"Idempotent-Replay":{"$ref":"#/components/headers/Idempotent-Replay"},"Location":{"$ref":"#/components/headers/Location"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingResponse"}}}},"201":{"description":"Creada.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"},"Location":{"$ref":"#/components/headers/Location"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingResponse"}}}},"400":{"description":"Request mal formada (credencial con forma rara, body / query inválidos, `clubId` faltante con varios grants). Además: `endTime` que no da 30/60/90/120, fecha u hora ya pasada en la zona del club, más de 180 días, fuera del horario de la cancha / del club, sin tarifario y sin `price`.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MALFORMED_API_KEY","VALIDATION","CLUB_REQUIRED","INVALID_DURATION","DATE_IN_PAST","MAX_ADVANCE_EXCEEDED","OUTSIDE_OPENING_HOURS","PRICE_UNAVAILABLE"]},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Integración deshabilitada, módulo API apagado en el club, club sin grant o scope insuficiente (`details.scope`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CLIENT_DISABLED","MODULE_DISABLED","CLUB_NOT_GRANTED","SCOPE_DENIED"]},"404":{"description":"La cancha no es del club o está deshabilitada (`details.courtId`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["COURT_NOT_FOUND"]},"409":{"description":"El `externalRef` ya se usó en una cancelada (ref de un solo uso), ya existe vivo con OTRO turno, el turno está ocupado (`details.busy`), o `courtId` no es una cancha.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["EXTERNAL_REF_USED","EXTERNAL_REF_CONFLICT","BOOKING_CONFLICT","RESOURCE_NOT_COURT"]},"415":{"$ref":"#/components/responses/UnsupportedMediaType"},"429":{"$ref":"#/components/responses/RateLimitedWrite"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}}},"/bookings/{id}":{"get":{"operationId":"getBooking","tags":["Reservas"],"summary":"Detalle de una reserva","description":"Propia → `BookingDto`. De otro origen / otra integración → 403 BOOKING_NOT_OWNED_BY_CLIENT, o `ManagedSlotDto` anónimo (con `price` / `paymentStatus`) si la integración tiene `canManageAllBookings`.","x-skalia-scope":"bookings:read","x-skalia-write":false,"parameters":[{"$ref":"#/components/parameters/RequestId"},{"$ref":"#/components/parameters/BookingId"}],"responses":{"200":{"description":"La reserva.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingResponse"}}}},"400":{"description":"Request mal formada (credencial con forma rara, body / query inválidos, `clubId` faltante con varios grants).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MALFORMED_API_KEY"]},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Integración deshabilitada, módulo API apagado en el club, club sin grant o scope insuficiente (`details.scope`). O la reserva es del club pero no la creó esta integración (sin `canManageAllBookings`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CLIENT_DISABLED","MODULE_DISABLED","CLUB_NOT_GRANTED","SCOPE_DENIED","BOOKING_NOT_OWNED_BY_CLIENT"]},"404":{"description":"La reserva no existe o es de un club sin grant (no se confirma nada).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["BOOKING_NOT_FOUND"]},"429":{"$ref":"#/components/responses/RateLimitedRead"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}},"patch":{"operationId":"updateBooking","tags":["Reservas"],"summary":"Modificar una reserva","description":"Mover cancha/fecha/horario, corregir el cliente, cambiar precio o estado de pago. Sólo propias (o `canManageAllBookings`, en cuyo caso la respuesta es `ManagedSlotDto`). Mover aplica las validaciones del alta sobre el destino con un UPDATE atómico contra las 4 fuentes; mover sin `price` recalcula con el tarifario del destino y conserva el override. `customer` reemplaza los tres campos; `customer: null` los borra. Una ocurrencia de fija sólo admite customer / price / paymentStatus (mover = DELETE + POST). Consume cuota de escritura.","x-skalia-scope":"bookings:write","x-skalia-write":true,"parameters":[{"$ref":"#/components/parameters/RequestId"},{"$ref":"#/components/parameters/BookingId"}],"requestBody":{"required":true,"description":"JSON obligatorio (`Content-Type: application/json`; si no, 415).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingPatchBody"}}}},"responses":{"200":{"description":"La reserva actualizada.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingResponse"}}}},"400":{"description":"Request mal formada (credencial con forma rara, body / query inválidos, `clubId` faltante con varios grants). Además: el turno ya terminó, o los del alta sobre el destino.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MALFORMED_API_KEY","VALIDATION","BOOKING_IN_PAST","INVALID_DURATION","DATE_IN_PAST","MAX_ADVANCE_EXCEEDED","OUTSIDE_OPENING_HOURS","PRICE_UNAVAILABLE"]},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Integración deshabilitada, módulo API apagado en el club, club sin grant o scope insuficiente (`details.scope`). O la reserva no la creó esta integración (sin `canManageAllBookings`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CLIENT_DISABLED","MODULE_DISABLED","CLUB_NOT_GRANTED","SCOPE_DENIED","BOOKING_NOT_OWNED_BY_CLIENT"]},"404":{"description":"La reserva no existe / es de un club sin grant, o la cancha destino no es del club.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["BOOKING_NOT_FOUND","COURT_NOT_FOUND"]},"409":{"description":"Destino ocupado (`details.busy`, fila intacta), partidos vivos en el turno (`details.matchCount`), ya cancelada, no modificable así (`details.reason`: `recurring_occurrence` / `finished`), o el destino no es una cancha.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["BOOKING_CONFLICT","BOOKING_HAS_MATCH_ATTACHED","BOOKING_ALREADY_CANCELLED","BOOKING_NOT_MODIFIABLE","RESOURCE_NOT_COURT"]},"415":{"$ref":"#/components/responses/UnsupportedMediaType"},"429":{"$ref":"#/components/responses/RateLimitedWrite"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}},"delete":{"operationId":"cancelBooking","tags":["Reservas"],"summary":"Cancelar una reserva","description":"Baja = cancelación por status, idempotente, nunca borrado físico. Sólo propias (o `canManageAllBookings`). Con partidos vivos en el turno responde 409 salvo `force=true`, que los desliga como lo haría el panel (nunca los cancela). El `externalRef` queda de un solo uso. `cancel_reason = API`, salvo sobre un pedido `PENDING` ajeno que esperaba al club (política MANUAL): ahí es el rechazo del club (`CLUB_REJECTED`, push \"rechazada\" al titular). Consume cuota de escritura.","x-skalia-scope":"bookings:write","x-skalia-write":true,"parameters":[{"$ref":"#/components/parameters/RequestId"},{"$ref":"#/components/parameters/BookingId"},{"name":"force","in":"query","required":false,"description":"`true` desliga los partidos vivos del turno en vez de responder 409 BOOKING_HAS_MATCH_ATTACHED (nunca los cancela). Booleano. En query también se aceptan los strings \"true\" / \"false\" / \"1\" / \"0\".","schema":{"type":"boolean"}},{"name":"reason","in":"query","required":false,"description":"Motivo de la cancelación; va al audit del club.","schema":{"type":"string","maxLength":140},"example":"el cliente canceló"}],"responses":{"200":{"description":"Cancelada, o ya lo estaba (`alreadyCancelled: true`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteResponse"}}}},"400":{"description":"Request mal formada (credencial con forma rara, body / query inválidos, `clubId` faltante con varios grants). Además: el turno ya terminó.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MALFORMED_API_KEY","VALIDATION","BOOKING_IN_PAST"]},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Integración deshabilitada, módulo API apagado en el club, club sin grant o scope insuficiente (`details.scope`). O la reserva no la creó esta integración (sin `canManageAllBookings`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CLIENT_DISABLED","MODULE_DISABLED","CLUB_NOT_GRANTED","SCOPE_DENIED","BOOKING_NOT_OWNED_BY_CLIENT"]},"404":{"description":"La reserva no existe o es de un club sin grant.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["BOOKING_NOT_FOUND"]},"409":{"description":"Partidos vivos en el turno sin `force` (`details.matchCount`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["BOOKING_HAS_MATCH_ATTACHED"]},"429":{"$ref":"#/components/responses/RateLimitedWrite"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}}},"/bookings/{id}/attendance":{"post":{"operationId":"markBookingAttendance","tags":["Bloqueos, fijas y asistencia"],"summary":"Pasar lista (asistencia / no-show)","description":"Marca `ATTENDED` / `NO_SHOW` sobre una reserva propia, o sobre cualquiera del club si la integración tiene `canManageAllBookings` (un POS que pasa lista de todo el club lo necesita). Mismos guards que el panel: la cuenta de servicio y el jugador vinculado sin consentimiento nunca cobran ni pagan asistencia / puntos (`scored: false`); el estado de la reserva se marca igual. Ventana: desde 30 minutos antes del comienzo hasta 7 días después del turno. Idempotente por estado. Emite `booking.attended` / `booking.no_show`. Consume cuota de escritura.","x-skalia-scope":"attendance:write","x-skalia-write":true,"parameters":[{"$ref":"#/components/parameters/RequestId"},{"$ref":"#/components/parameters/BookingId"}],"requestBody":{"required":true,"description":"JSON obligatorio (`Content-Type: application/json`; si no, 415).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AttendanceBody"}}}},"responses":{"200":{"description":"Marcada (o ya estaba: `changed: false`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AttendanceResponse"}}}},"400":{"description":"Request mal formada (credencial con forma rara, body / query inválidos, `clubId` faltante con varios grants). Además: fuera de la ventana (`details.reason`: `not_started` con `opensAt`, o `expired` con `closedAt`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MALFORMED_API_KEY","VALIDATION","ATTENDANCE_OUT_OF_WINDOW"]},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Integración deshabilitada, módulo API apagado en el club, club sin grant o scope insuficiente (`details.scope`). O la reserva no la creó esta integración (sin `canManageAllBookings`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CLIENT_DISABLED","MODULE_DISABLED","CLUB_NOT_GRANTED","SCOPE_DENIED","BOOKING_NOT_OWNED_BY_CLIENT"]},"404":{"description":"La reserva no existe o es de un club sin grant.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["BOOKING_NOT_FOUND"]},"409":{"description":"La reserva está cancelada, o cambió de estado entre la lectura y la escritura (`details.reason: state_changed`; releé y decidí de nuevo).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["BOOKING_ALREADY_CANCELLED","BOOKING_NOT_MODIFIABLE"]},"415":{"$ref":"#/components/responses/UnsupportedMediaType"},"429":{"$ref":"#/components/responses/RateLimitedWrite"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}}},"/bookings/{id}/confirm":{"post":{"operationId":"confirmBooking","tags":["Reservas"],"summary":"Confirmar un pedido pendiente","description":"Aprueba un pedido `PENDING` de la app (categoría con política de confirmación MANUAL) como lo haría el panel. Sin body. Sólo integraciones con `canManageAllBookings` (la API crea `CONFIRMED`: una `PENDING` nunca es propia). El UPDATE está condicionado a `PENDING` y a que ninguna otra `CONFIRMED` pise el turno; si el pedido venció (cron), el jugador lo retiró u otro operador lo resolvió en el medio, se responde por el estado final. Efectos: grabación del turno si la cancha tiene cámara, audit `BOOKING_CONFIRMED`, `booking.confirmed` en `/events` y webhooks, push `BOOKING_CONFIRMED` al titular. Respuesta anónima (`SlotDto`). Idempotente: sobre una ya `CONFIRMED` responde 200 con `alreadyConfirmed: true`. Consume cuota de escritura.","x-skalia-scope":"bookings:write","x-skalia-write":true,"parameters":[{"$ref":"#/components/parameters/RequestId"},{"$ref":"#/components/parameters/BookingId"}],"responses":{"200":{"description":"Confirmada, o ya lo estaba (`alreadyConfirmed: true`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfirmResponse"}}}},"400":{"description":"Request mal formada (credencial con forma rara, body / query inválidos, `clubId` faltante con varios grants). Además: el turno ya terminó en la zona del club.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MALFORMED_API_KEY","BOOKING_IN_PAST"]},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Integración deshabilitada, módulo API apagado en el club, club sin grant o scope insuficiente (`details.scope`). O la integración no tiene `canManageAllBookings` (una PENDING nunca es propia).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CLIENT_DISABLED","MODULE_DISABLED","CLUB_NOT_GRANTED","SCOPE_DENIED","BOOKING_NOT_OWNED_BY_CLIENT"]},"404":{"description":"La reserva no existe o es de un club sin grant.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["BOOKING_NOT_FOUND"]},"409":{"description":"Ya cancelada (cron, jugador o club), cerrada (`ATTENDED` / `NO_SHOW`: `details.status`), u otra `CONFIRMED` pisa el turno (`details.busy`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["BOOKING_ALREADY_CANCELLED","BOOKING_NOT_PENDING","BOOKING_CONFLICT"]},"429":{"$ref":"#/components/responses/RateLimitedWrite"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}}},"/club":{"get":{"operationId":"getClub","tags":["Club"],"summary":"Ficha del club","description":"La ficha pública del club, sus banderas de plataforma (`modules`), su configuración de negocio (`settings`, la misma de `GET /settings`) y sus seguidores en Skalia (`followers`: `total` como prueba social, `notifiable` como audiencia real de los avisos; sólo dos enteros). Nunca credenciales (streaming, ingest, tokens) ni datos de personas.","x-skalia-scope":"club:read","x-skalia-write":false,"parameters":[{"$ref":"#/components/parameters/RequestId"},{"name":"clubId","in":"query","required":false,"description":"Club sobre el que opera la request. Opcional: se infiere cuando la integración tiene un solo grant (kind CLUB, v1).","schema":{"type":"string","pattern":"^[a-f0-9-]{8,64}$"}}],"responses":{"200":{"description":"La ficha.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClubDto"}}}},"400":{"description":"Request mal formada (credencial con forma rara, body / query inválidos, `clubId` faltante con varios grants).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MALFORMED_API_KEY","VALIDATION","CLUB_REQUIRED"]},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Integración deshabilitada, módulo API apagado en el club, club sin grant o scope insuficiente (`details.scope`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CLIENT_DISABLED","MODULE_DISABLED","CLUB_NOT_GRANTED","SCOPE_DENIED"]},"429":{"$ref":"#/components/responses/RateLimitedRead"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}},"patch":{"operationId":"updateClub","tags":["Club"],"summary":"Editar la ficha del club","description":"Edición parcial de texto, coordenadas y URL https del logo (sin subir binarios: el logo se aloja fuera del storage de Skalia). Scope sensible `club:write` (sólo el OWNER emite una key que lo incluya). Audit `CLUB_UPDATED` (con los valores enviados). Consume cuota de escritura.","x-skalia-scope":"club:write","x-skalia-write":true,"parameters":[{"$ref":"#/components/parameters/RequestId"}],"requestBody":{"required":true,"description":"JSON obligatorio (`Content-Type: application/json`; si no, 415).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClubPatchBody"}}}},"responses":{"200":{"description":"La ficha actualizada.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClubDto"}}}},"400":{"description":"Request mal formada (credencial con forma rara, body / query inválidos, `clubId` faltante con varios grants). Además: `logoUrl` apunta al storage de Skalia y no es el logo actual del club (`details.field`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MALFORMED_API_KEY","VALIDATION","CLUB_REQUIRED","LOGO_URL_RESERVED"]},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Integración deshabilitada, módulo API apagado en el club, club sin grant o scope insuficiente (`details.scope`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CLIENT_DISABLED","MODULE_DISABLED","CLUB_NOT_GRANTED","SCOPE_DENIED"]},"415":{"$ref":"#/components/responses/UnsupportedMediaType"},"429":{"$ref":"#/components/responses/RateLimitedWrite"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}}},"/courts":{"get":{"operationId":"listCourts","tags":["Agenda"],"summary":"Canchas del club","description":"Para mapear ids del lado del partner. Lista también las deshabilitadas (`enabled: false`) y los recursos que no son cancha con su `kind`. Nunca claves ni ingest de streaming.","x-skalia-scope":"courts:read","x-skalia-write":false,"parameters":[{"$ref":"#/components/parameters/RequestId"},{"name":"clubId","in":"query","required":false,"description":"Club sobre el que opera la request. Opcional: se infiere cuando la integración tiene un solo grant (kind CLUB, v1).","schema":{"type":"string","pattern":"^[a-f0-9-]{8,64}$"}}],"responses":{"200":{"description":"Canchas (y demás recursos) del club con su tarifario.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CourtsResponse"}}}},"400":{"description":"Request mal formada (credencial con forma rara, body / query inválidos, `clubId` faltante con varios grants).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MALFORMED_API_KEY","VALIDATION","CLUB_REQUIRED"]},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Integración deshabilitada, módulo API apagado en el club, club sin grant o scope insuficiente (`details.scope`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CLIENT_DISABLED","MODULE_DISABLED","CLUB_NOT_GRANTED","SCOPE_DENIED"]},"429":{"$ref":"#/components/responses/RateLimitedRead"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}},"post":{"operationId":"createCourt","tags":["Canchas, precios y horarios"],"summary":"Crear una cancha","description":"Alta de una cancha (o de un recurso de otra categoría, F6) con las mismas reglas que el panel: nace habilitada, con `pricePerHour` propio o el default del club y con las franjas del horario por defecto del club (si el club no tiene horario por defecto nace sin franjas: cargalas con `PUT /courts/{id}/pricing`). Devuelve el MISMO `Court` de `GET /courts`. Nunca cámara ni ingest de streaming. Emite `occupancy.changed` si nació con franjas. Consume cuota de escritura.","x-skalia-scope":"courts:write","x-skalia-write":true,"parameters":[{"$ref":"#/components/parameters/RequestId"}],"requestBody":{"required":true,"description":"JSON obligatorio (`Content-Type: application/json`; si no, 415).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CourtCreateBody"}}}},"responses":{"201":{"description":"Creada.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"},"Location":{"$ref":"#/components/headers/Location"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CourtResponse"}}}},"400":{"description":"Request mal formada (credencial con forma rara, body / query inválidos, `clubId` faltante con varios grants). Además: el club tiene categorías y no mandaste `categoryId`.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MALFORMED_API_KEY","VALIDATION","CLUB_REQUIRED","CATEGORY_REQUIRED"]},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Integración deshabilitada, módulo API apagado en el club, club sin grant o scope insuficiente (`details.scope`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CLIENT_DISABLED","MODULE_DISABLED","CLUB_NOT_GRANTED","SCOPE_DENIED"]},"404":{"description":"`categoryId` no existe o no es del club.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CATEGORY_NOT_FOUND"]},"415":{"$ref":"#/components/responses/UnsupportedMediaType"},"429":{"$ref":"#/components/responses/RateLimitedWrite"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}}},"/courts/{id}":{"get":{"operationId":"getCourt","tags":["Canchas, precios y horarios"],"summary":"Una cancha","description":"El MISMO `Court` de `GET /courts` para una cancha (habilitada o no) más sus features. Es adonde apunta el `Location` de `POST /courts`. Scope `courts:read` sobre el club de la cancha.","x-skalia-scope":"courts:read","x-skalia-write":false,"parameters":[{"$ref":"#/components/parameters/RequestId"},{"name":"id","in":"path","required":true,"description":"Id de la cancha (`GET /courts`).","schema":{"type":"string","pattern":"^[a-f0-9-]{8,64}$"}}],"responses":{"200":{"description":"La cancha.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CourtResponse"}}}},"400":{"description":"Request mal formada (credencial con forma rara, body / query inválidos, `clubId` faltante con varios grants).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MALFORMED_API_KEY"]},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Integración deshabilitada, módulo API apagado en el club, club sin grant o scope insuficiente (`details.scope`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CLIENT_DISABLED","MODULE_DISABLED","CLUB_NOT_GRANTED","SCOPE_DENIED"]},"404":{"description":"La cancha no existe o es de un club sin grant (no se confirma nada).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["COURT_NOT_FOUND"]},"429":{"$ref":"#/components/responses/RateLimitedRead"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}},"patch":{"operationId":"updateCourt","tags":["Canchas, precios y horarios"],"summary":"Editar / deshabilitar una cancha","description":"Parcial. Nombre, deportes, superficie, techado, orden, precio/hora base, categoría (F6; `null` = sin categoría), `enabled` y `features` (reemplaza el set; `feat-cancha-interior` / `feat-cancha-exterior` derivan `isIndoor`, como en el panel). Deshabilitar es la baja lógica del panel: la cancha deja de venderse y se sigue listando con `enabled: false`; con reservas vivas (PENDING / CONFIRMED) cuyo turno todavía no terminó responde 409 COURT_HAS_FUTURE_BOOKINGS (`details.bookings`, `from`, `to`) salvo `?force=true`; los turnos de hoy ya terminados o marcados son historial y no cuentan. Las series de fijas quedan activas pero el cron no materializa ocurrencias sobre una cancha deshabilitada. Cambiar `enabled` emite `occupancy.changed`. Consume cuota de escritura.","x-skalia-scope":"courts:write","x-skalia-write":true,"parameters":[{"$ref":"#/components/parameters/RequestId"},{"name":"id","in":"path","required":true,"description":"Id de la cancha (`GET /courts`).","schema":{"type":"string","pattern":"^[a-f0-9-]{8,64}$"}},{"name":"force","in":"query","required":false,"description":"Con `enabled: false` y reservas vivas cuyo turno no terminó: cancelarlas (status CANCELLED, `cancel_reason = API`, partidos desligados —nunca cancelados—, aviso al titular real, evento `booking.cancelled` con `reason = court_disabled`). Un turno de hoy ya terminado o ya marcado (ATTENDED / NO_SHOW) nunca se toca. Sin `force` nunca se toca una reserva.","schema":{"type":"boolean"}},{"name":"reason","in":"query","required":false,"description":"Motivo (≤ 140) que queda en el audit y en el aviso a los titulares.","schema":{"type":"string","maxLength":140},"example":"el cliente canceló"}],"requestBody":{"required":true,"description":"JSON obligatorio (`Content-Type: application/json`; si no, 415).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CourtPatchBody"}}}},"responses":{"200":{"description":"La cancha actualizada.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CourtPatchResponse"}}}},"400":{"description":"Request mal formada (credencial con forma rara, body / query inválidos, `clubId` faltante con varios grants). Además: `categoryId: null` en un club con categorías.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MALFORMED_API_KEY","VALIDATION","CATEGORY_REQUIRED"]},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Integración deshabilitada, módulo API apagado en el club, club sin grant o scope insuficiente (`details.scope`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CLIENT_DISABLED","MODULE_DISABLED","CLUB_NOT_GRANTED","SCOPE_DENIED"]},"404":{"description":"La cancha no existe o es de un club sin grant (no se confirma nada); `categoryId` no es del club; algún id de `features` no existe en el catálogo (`details.featureIds`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["COURT_NOT_FOUND","CATEGORY_NOT_FOUND","FEATURE_NOT_FOUND"]},"409":{"description":"Reservas futuras activas sin `force` (`details.bookings`); cambio de categoría a otro kind con turnos vendidos; `features` sobre un recurso que no es cancha.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["COURT_HAS_FUTURE_BOOKINGS","RESOURCE_KIND_CHANGE_NOT_ALLOWED","RESOURCE_NOT_COURT"]},"415":{"$ref":"#/components/responses/UnsupportedMediaType"},"429":{"$ref":"#/components/responses/RateLimitedWrite"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}},"delete":{"operationId":"deleteCourt","tags":["Canchas, precios y horarios"],"summary":"Borrar una cancha sin historial","description":"Borrado físico SÓLO de una cancha que nunca tuvo reservas, fijas, partidos (jugados o pedidos con `requestedCourtId`), partidos de torneo / americano, broadcasts ni mapeos externos (una creada por error). Con cualquiera de esos responde 409 COURT_HAS_BOOKINGS con el conteo por tabla (`details.bookings` / `recurring` / `matches` / `tournamentMatches` / `americanoMatches` / `broadcasts` / `externalMappings`): la baja es `PATCH { enabled: false }`. Los bloqueos de la cancha y sus huecos se borran con ella. Emite `occupancy.changed`. Consume cuota de escritura.","x-skalia-scope":"courts:write","x-skalia-write":true,"parameters":[{"$ref":"#/components/parameters/RequestId"},{"name":"id","in":"path","required":true,"description":"Id de la cancha (`GET /courts`).","schema":{"type":"string","pattern":"^[a-f0-9-]{8,64}$"}}],"responses":{"200":{"description":"Borrada.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CourtDeleteResponse"}}}},"400":{"description":"Request mal formada (credencial con forma rara, body / query inválidos, `clubId` faltante con varios grants).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MALFORMED_API_KEY"]},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Integración deshabilitada, módulo API apagado en el club, club sin grant o scope insuficiente (`details.scope`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CLIENT_DISABLED","MODULE_DISABLED","CLUB_NOT_GRANTED","SCOPE_DENIED"]},"404":{"description":"La cancha no existe o es de un club sin grant.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["COURT_NOT_FOUND"]},"409":{"description":"La cancha tiene reservas, fijas, partidos, partidos de torneo / americano, broadcasts o mapeos externos (históricos incluidos).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["COURT_HAS_BOOKINGS"]},"429":{"$ref":"#/components/responses/RateLimitedWrite"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}}},"/courts/{id}/pricing":{"get":{"operationId":"getCourtPricing","tags":["Canchas, precios y horarios"],"summary":"Tarifario de una cancha","description":"La grilla de franjas vendibles por día de la semana con su precio/hora base y sus totales por duración (60 / 90 / 120), tal como la edita el panel en Horarios y precios. Las filas de 30' contiguas con la misma tarifa salen fundidas en una franja.","x-skalia-scope":"pricing:read","x-skalia-write":false,"parameters":[{"$ref":"#/components/parameters/RequestId"},{"name":"id","in":"path","required":true,"description":"Id de la cancha (`GET /courts`).","schema":{"type":"string","pattern":"^[a-f0-9-]{8,64}$"}}],"responses":{"200":{"description":"El tarifario.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CourtPricingResponse"}}}},"400":{"description":"Request mal formada (credencial con forma rara, body / query inválidos, `clubId` faltante con varios grants).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MALFORMED_API_KEY"]},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Integración deshabilitada, módulo API apagado en el club, club sin grant o scope insuficiente (`details.scope`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CLIENT_DISABLED","MODULE_DISABLED","CLUB_NOT_GRANTED","SCOPE_DENIED"]},"404":{"description":"La cancha no existe o es de un club sin grant.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["COURT_NOT_FOUND"]},"429":{"$ref":"#/components/responses/RateLimitedRead"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}},"put":{"operationId":"replaceCourtPricing","tags":["Canchas, precios y horarios"],"summary":"Reemplazar el tarifario de una cancha","description":"Reemplazo completo (espejo exacto de `PUT /club-owner/courts/:id/slots`): cada franja se materializa en filas de 30' con la misma tarifa; franjas fuera de la grilla de 30' o solapadas en un mismo día son 400 VALIDATION. Precio ausente o 0 = sin precio (nunca se persiste un 0). Devuelve la grilla resultante. Emite `occupancy.changed` sólo si cambiaron las franjas vendibles (no si cambió sólo el precio). Consume cuota de escritura.","x-skalia-scope":"pricing:write","x-skalia-write":true,"parameters":[{"$ref":"#/components/parameters/RequestId"},{"name":"id","in":"path","required":true,"description":"Id de la cancha (`GET /courts`).","schema":{"type":"string","pattern":"^[a-f0-9-]{8,64}$"}}],"requestBody":{"required":true,"description":"JSON obligatorio (`Content-Type: application/json`; si no, 415).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CourtPricingPutBody"}}}},"responses":{"200":{"description":"El tarifario resultante.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CourtPricingResponse"}}}},"400":{"description":"Request mal formada (credencial con forma rara, body / query inválidos, `clubId` faltante con varios grants).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MALFORMED_API_KEY","VALIDATION"]},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Integración deshabilitada, módulo API apagado en el club, club sin grant o scope insuficiente (`details.scope`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CLIENT_DISABLED","MODULE_DISABLED","CLUB_NOT_GRANTED","SCOPE_DENIED"]},"404":{"description":"La cancha no existe o es de un club sin grant.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["COURT_NOT_FOUND"]},"415":{"$ref":"#/components/responses/UnsupportedMediaType"},"429":{"$ref":"#/components/responses/RateLimitedWrite"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}}},"/events":{"get":{"operationId":"listEvents","tags":["Eventos"],"summary":"Qué cambió en la agenda","description":"Polling de TODO lo que cambió en la agenda del club, venga de donde venga (tus reservas, las de otras integraciones, la app, el panel, QuieroJugar, torneos, crons, bloqueos / huecos / series de fijas). Proyección por lector: lo propio viaja como `BookingDto` hidratado desde la fila actual; lo ajeno como `SlotDto` anónimo. Cursor: `since` = último `id` procesado; sin `since` no se listan eventos (arranque: guardá `nextSince`). Garantías: at-least-once y orden por `id` dentro del club; procesá cada evento idempotentemente. No consume cuota de escritura.","x-skalia-scope":"events:read","x-skalia-write":false,"parameters":[{"$ref":"#/components/parameters/RequestId"},{"name":"clubId","in":"query","required":false,"description":"Club sobre el que opera la request. Opcional: se infiere cuando la integración tiene un solo grant (kind CLUB, v1).","schema":{"type":"string","pattern":"^[a-f0-9-]{8,64}$"}},{"name":"since","in":"query","required":false,"description":"Último `id` de evento procesado: se devuelven `id > since`. Sin `since` no se listan eventos, sólo el cursor actual (`nextSince`).","schema":{"type":"integer","minimum":0},"example":1042},{"name":"limit","in":"query","required":false,"description":"Eventos por página, 1..200 (default y máximo 200).","schema":{"type":"integer","minimum":1,"maximum":200}}],"responses":{"200":{"description":"Eventos `id > since` en orden ascendente, y el cursor para el próximo poll.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EventsResponse"}}}},"400":{"description":"Request mal formada (credencial con forma rara, body / query inválidos, `clubId` faltante con varios grants). Además: `since` negativo o no entero, `limit` fuera de 1..200.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MALFORMED_API_KEY","VALIDATION","CLUB_REQUIRED"]},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Integración deshabilitada, módulo API apagado en el club, club sin grant o scope insuficiente (`details.scope`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CLIENT_DISABLED","MODULE_DISABLED","CLUB_NOT_GRANTED","SCOPE_DENIED"]},"410":{"description":"Tu `since` es anterior a lo que la retención conserva (90 días): pudiste perder eventos. Resincronizá (`/bookings?updatedSince` + `/occupancy`) y seguí desde `details.nextSince`.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["EVENTS_CURSOR_EXPIRED"]},"429":{"$ref":"#/components/responses/RateLimitedRead"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}}},"/hours":{"get":{"operationId":"getClubHours","tags":["Canchas, precios y horarios"],"summary":"Horario semanal del club","description":"`club_hours`: el techo duro de lo vendible por día (`/availability` y las altas no venden fuera de él). Un día ausente está cerrado; `[]` = sin horario cargado (mandan las franjas de cada cancha). Es lo mismo que `clubs[].hours` de `/me`.","x-skalia-scope":"hours:read","x-skalia-write":false,"parameters":[{"$ref":"#/components/parameters/RequestId"},{"name":"clubId","in":"query","required":false,"description":"Club sobre el que opera la request. Opcional: se infiere cuando la integración tiene un solo grant (kind CLUB, v1).","schema":{"type":"string","pattern":"^[a-f0-9-]{8,64}$"}}],"responses":{"200":{"description":"El horario.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClubHoursResponse"}}}},"400":{"description":"Request mal formada (credencial con forma rara, body / query inválidos, `clubId` faltante con varios grants).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MALFORMED_API_KEY","VALIDATION","CLUB_REQUIRED"]},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Integración deshabilitada, módulo API apagado en el club, club sin grant o scope insuficiente (`details.scope`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CLIENT_DISABLED","MODULE_DISABLED","CLUB_NOT_GRANTED","SCOPE_DENIED"]},"429":{"$ref":"#/components/responses/RateLimitedRead"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}},"put":{"operationId":"replaceClubHours","tags":["Canchas, precios y horarios"],"summary":"Reemplazar el horario semanal del club","description":"Reemplazo completo de `club_hours`. Los días que no mandás quedan CERRADOS (con horario cargado nada se vende ese día); `hours: []` borra el horario. `24:00` vale como cierre. No toca las franjas de las canchas: si el horario nuevo es más chico, las franjas fuera de él quedan invendibles hasta que las achiques con `PUT /courts/{id}/pricing`. Emite `occupancy.changed` del club si cambió. Consume cuota de escritura.","x-skalia-scope":"hours:write","x-skalia-write":true,"parameters":[{"$ref":"#/components/parameters/RequestId"}],"requestBody":{"required":true,"description":"JSON obligatorio (`Content-Type: application/json`; si no, 415).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClubHoursPutBody"}}}},"responses":{"200":{"description":"El horario resultante.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClubHoursResponse"}}}},"400":{"description":"Request mal formada (credencial con forma rara, body / query inválidos, `clubId` faltante con varios grants).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MALFORMED_API_KEY","VALIDATION","CLUB_REQUIRED"]},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Integración deshabilitada, módulo API apagado en el club, club sin grant o scope insuficiente (`details.scope`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CLIENT_DISABLED","MODULE_DISABLED","CLUB_NOT_GRANTED","SCOPE_DENIED"]},"415":{"$ref":"#/components/responses/UnsupportedMediaType"},"429":{"$ref":"#/components/responses/RateLimitedWrite"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}}},"/matches":{"get":{"operationId":"listMatches","tags":["Partidos"],"summary":"Listar partidos públicos","description":"Los partidos PÚBLICOS del club (los mismos que ve la app en \"Buscar partido\" y en resultados): nunca privados ni a la espera de aprobación; `COMPLETED` sólo con resultado confirmado. Todos los orígenes (app, panel, API), anónimos: cupo por contadores, nunca jugadores. Orden: fecha y hora, id. `limit` ≤ 200 (default 100).","x-skalia-scope":"matches:read","x-skalia-write":false,"parameters":[{"$ref":"#/components/parameters/RequestId"},{"name":"limit","in":"query","required":false,"description":"Tamaño de página, 1..200 (default 100).","schema":{"type":"integer","minimum":1,"maximum":200}},{"name":"offset","in":"query","required":false,"description":"Desplazamiento (paginación offset/limit).","schema":{"type":"integer","minimum":0}},{"name":"clubId","in":"query","required":false,"description":"Club sobre el que opera la request. Opcional: se infiere cuando la integración tiene un solo grant (kind CLUB, v1).","schema":{"type":"string","pattern":"^[a-f0-9-]{8,64}$"}},{"name":"status","in":"query","required":false,"description":"Filtra por estado público. Sin filtro: todos (`OPEN`, `FULL`, `IN_PROGRESS`, `COMPLETED`, `CANCELLED`).","schema":{"type":"string","enum":["OPEN","FULL","IN_PROGRESS","COMPLETED","CANCELLED"]}},{"name":"from","in":"query","required":false,"description":"Desde este día (inclusive), `YYYY-MM-DD` naive del club.","schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"example":"2026-09-15"},{"name":"to","in":"query","required":false,"description":"Hasta este día (inclusive), `YYYY-MM-DD` naive del club.","schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"example":"2026-09-30"},{"name":"courtId","in":"query","required":false,"description":"Sólo los partidos de esta cancha.","schema":{"type":"string","pattern":"^[a-f0-9-]{8,64}$"}},{"name":"sport","in":"query","required":false,"description":"Sólo los de este deporte (`PADEL`, `TENNIS`, …; sin distinguir mayúsculas).","schema":{"type":"string","minLength":2,"maxLength":32},"example":"PADEL"},{"name":"isCompetitive","in":"query","required":false,"description":"`true` sólo los competitivos, `false` sólo los amistosos.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"Página de partidos.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicMatchesListResponse"}}}},"400":{"description":"Request mal formada (credencial con forma rara, body / query inválidos, `clubId` faltante con varios grants).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MALFORMED_API_KEY","VALIDATION","CLUB_REQUIRED"]},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Integración deshabilitada, módulo API apagado en el club, club sin grant o scope insuficiente (`details.scope`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CLIENT_DISABLED","MODULE_DISABLED","CLUB_NOT_GRANTED","SCOPE_DENIED"]},"429":{"$ref":"#/components/responses/RateLimitedRead"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}},"post":{"operationId":"publishMatch","tags":["Partidos"],"summary":"Publicar un partido sobre una reserva","description":"El sistema del club detecta el turno muerto y lo publica en Skalia: el partido nace `OPEN`, público, del club (no se autocancela a las 3 h como los de un jugador: espera el resultado 48 h), sobre una reserva **propia** (o cualquiera del club con `canManageAllBookings`) que ya existe, `CONFIRMED` y con el turno sin terminar, en una cancha. Fecha, hora, duración y cancha salen de la reserva; una publicación viva por reserva. Skalia hace el push por `notifyCategories` (\"{club} abrió un partido para tu categoría\", en background). Emite `booking.updated` (reason `match`) sobre la reserva. Consume cuota de escritura.","x-skalia-scope":"matches:write","x-skalia-write":true,"parameters":[{"$ref":"#/components/parameters/RequestId"}],"requestBody":{"required":true,"description":"JSON obligatorio (`Content-Type: application/json`; si no, 415).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MatchCreateBody"}}}},"responses":{"201":{"description":"Publicado.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"},"Location":{"$ref":"#/components/headers/Location"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicMatchResponse"}}}},"400":{"description":"Request mal formada (credencial con forma rara, body / query inválidos, `clubId` faltante con varios grants). Además: la reserva es de un recurso que no es cancha (`details.kind`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MALFORMED_API_KEY","VALIDATION","RESOURCE_NOT_COURT"]},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Integración deshabilitada, módulo API apagado en el club, club sin grant o scope insuficiente (`details.scope`). O la reserva no la creó esta integración (sin `canManageAllBookings`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CLIENT_DISABLED","MODULE_DISABLED","CLUB_NOT_GRANTED","SCOPE_DENIED","BOOKING_NOT_OWNED_BY_CLIENT"]},"404":{"description":"La reserva no existe o es de un club sin grant (no se confirma nada).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["BOOKING_NOT_FOUND"]},"409":{"description":"La reserva ya tiene un partido vivo (`details.matchId`), o no está en condiciones (`details.reason`: `not_confirmed` / `finished` / `tournament` / `booking_type` / `club_not_visible`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MATCH_ALREADY_PUBLISHED","BOOKING_NOT_MODIFIABLE"]},"415":{"$ref":"#/components/responses/UnsupportedMediaType"},"429":{"$ref":"#/components/responses/RateLimitedWrite"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}}},"/matches/{id}":{"get":{"operationId":"getMatch","tags":["Partidos"],"summary":"Detalle de un partido","description":"El destino del `Location` del POST y el refresco del cartel. Un `CANCELLED` sí se devuelve (para que el cartel lo saque); un partido privado, a la espera del club o de un club sin grant es 404 (no se confirma nada).","x-skalia-scope":"matches:read","x-skalia-write":false,"parameters":[{"$ref":"#/components/parameters/RequestId"},{"name":"id","in":"path","required":true,"description":"Id del partido.","schema":{"type":"string","pattern":"^[a-f0-9-]{8,64}$"}}],"responses":{"200":{"description":"El partido.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicMatchResponse"}}}},"400":{"description":"Request mal formada (credencial con forma rara, body / query inválidos, `clubId` faltante con varios grants).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MALFORMED_API_KEY"]},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Integración deshabilitada, módulo API apagado en el club, club sin grant o scope insuficiente (`details.scope`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CLIENT_DISABLED","MODULE_DISABLED","CLUB_NOT_GRANTED","SCOPE_DENIED"]},"404":{"description":"El partido no existe, es privado, todavía espera al club o es de un club sin grant.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MATCH_NOT_FOUND"]},"429":{"$ref":"#/components/responses/RateLimitedRead"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}},"delete":{"operationId":"cancelMatch","tags":["Partidos"],"summary":"Dar de baja un partido publicado","description":"Cancela el partido como lo haría el panel (`cancellation_reason` \"Cancelado por el club\", aviso al roster, video del partido cancelado): sólo los que publicó esta integración (o cualquiera del club con `canManageAllBookings`), sólo `OPEN` / `FULL`. Idempotente sobre un `CANCELLED` (`changed: false`). La reserva NO se toca (el club sigue teniendo la cancha vendida): sólo baja su `matchCount` (`booking.updated`). Consume cuota de escritura.","x-skalia-scope":"matches:write","x-skalia-write":true,"parameters":[{"$ref":"#/components/parameters/RequestId"},{"name":"id","in":"path","required":true,"description":"Id del partido.","schema":{"type":"string","pattern":"^[a-f0-9-]{8,64}$"}},{"name":"reason","in":"query","required":false,"description":"Motivo interno (va al audit; el jugador ve \"Cancelado por el club\").","schema":{"type":"string","maxLength":140},"example":"el cliente canceló"}],"responses":{"200":{"description":"Cancelado, o ya lo estaba (`changed: false`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicMatchDeleteResponse"}}}},"400":{"description":"Request mal formada (credencial con forma rara, body / query inválidos, `clubId` faltante con varios grants).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MALFORMED_API_KEY","VALIDATION"]},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Integración deshabilitada, módulo API apagado en el club, club sin grant o scope insuficiente (`details.scope`). O el partido no lo publicó esta integración (sin `canManageAllBookings`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CLIENT_DISABLED","MODULE_DISABLED","CLUB_NOT_GRANTED","SCOPE_DENIED","MATCH_NOT_OWNED_BY_CLIENT"]},"404":{"description":"El partido no existe, es privado, todavía espera al club o es de un club sin grant.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MATCH_NOT_FOUND"]},"409":{"description":"El partido ya no está `OPEN` / `FULL` (`details.status`): se juega, se jugó o ya cerró.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MATCH_NOT_MODIFIABLE"]},"429":{"$ref":"#/components/responses/RateLimitedWrite"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}}},"/me":{"get":{"operationId":"getMe","tags":["Cuenta"],"summary":"Quién soy","description":"Smoke test del onboarding: integración, key, clubes con scopes efectivos y horario informativo, cuotas y hora del server. Sin scope: basta una key válida. Nunca devuelve ids de usuarios ni datos de personas.","x-skalia-scope":null,"x-skalia-write":false,"parameters":[{"$ref":"#/components/parameters/RequestId"}],"responses":{"200":{"description":"Integración, key, clubes, cuotas y hora del server.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MeResponse"}}}},"400":{"description":"Request mal formada (credencial con forma rara, body / query inválidos, `clubId` faltante con varios grants).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MALFORMED_API_KEY"]},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Integración deshabilitada, módulo API apagado en el club, club sin grant o scope insuficiente (`details.scope`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CLIENT_DISABLED","MODULE_DISABLED"]},"429":{"$ref":"#/components/responses/RateLimitedRead"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}}},"/occupancy":{"get":{"operationId":"getOccupancy","tags":["Agenda"],"summary":"Fuente de verdad de \"ocupado\"","description":"Reservas de cualquier origen (app, panel, API, fijas materializadas), fijas no materializadas, bloqueos menos huecos y partidos con cancha sin reserva, por día y recurso, sin PII. Rango de 1 a 31 días (`to >= from`). `bookingId` sólo en los rangos que son reservas de ESTA integración. Con ETag fuerte: mandá `If-None-Match` y recibís 304 sin cuerpo si nada cambió (el `Cache-Control: no-store` sigue: el 304 es un ahorro, no un permiso para cachear).","x-skalia-scope":"availability:read","x-skalia-write":false,"parameters":[{"$ref":"#/components/parameters/RequestId"},{"$ref":"#/components/parameters/IfNoneMatch"},{"name":"clubId","in":"query","required":false,"description":"Club sobre el que opera la request. Opcional: se infiere cuando la integración tiene un solo grant (kind CLUB, v1).","schema":{"type":"string","pattern":"^[a-f0-9-]{8,64}$"}},{"name":"from","in":"query","required":true,"description":"Primer día del rango, `YYYY-MM-DD` naive del club.","schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"example":"2026-09-15"},{"name":"to","in":"query","required":true,"description":"Último día del rango (inclusive), `YYYY-MM-DD` naive del club.","schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"example":"2026-09-21"},{"name":"courtId","in":"query","required":false,"description":"Id de la cancha (`GET /courts`).","schema":{"type":"string","pattern":"^[a-f0-9-]{8,64}$"}}],"responses":{"200":{"description":"Ocupación por día y cancha.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"},"ETag":{"$ref":"#/components/headers/ETag"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OccupancyResponse"}}}},"304":{"description":"`If-None-Match` coincide: nada cambió. Sin cuerpo.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"},"ETag":{"$ref":"#/components/headers/ETag"}}},"400":{"description":"Request mal formada (credencial con forma rara, body / query inválidos, `clubId` faltante con varios grants). Además: rango invertido o de más de 31 días (`details.issues[0].path = \"to\"`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MALFORMED_API_KEY","VALIDATION","CLUB_REQUIRED"]},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Integración deshabilitada, módulo API apagado en el club, club sin grant o scope insuficiente (`details.scope`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CLIENT_DISABLED","MODULE_DISABLED","CLUB_NOT_GRANTED","SCOPE_DENIED"]},"429":{"$ref":"#/components/responses/RateLimitedRead"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}}},"/openapi.json":{"get":{"operationId":"getOpenApi","tags":["Meta"],"summary":"Esta spec","description":"Documento OpenAPI 3.1.0 de la Partner API v1, generado del código en cada deploy. Es una de las DOS rutas del namespace sin API key y con CORS abierto (`Access-Control-Allow-Origin: *`); la otra es `GET /postman.json`, la colección Postman generada de esta misma spec. Cualquier otro método sobre esas rutas sigue el pipeline normal (401 sin key, 404 con key). Importalo en Postman / Insomnia / Swagger UI o generá un cliente. Las horas de FIN (`endTime`, `close`, `end`) admiten `24:00` como medianoche; las de comienzo, no.","x-skalia-scope":null,"x-skalia-write":false,"security":[],"parameters":[{"$ref":"#/components/parameters/RequestId"}],"responses":{"200":{"description":"El documento. `Cache-Control: public, max-age=3600` y `Access-Control-Allow-Origin: *` (se puede cargar desde Swagger UI / Redoc / Postman web en cualquier origen).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"}},"content":{"application/json":{"schema":{"type":"object","description":"Documento OpenAPI 3.1 (este mismo)."}}}},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}}},"/pricing/defaults":{"get":{"operationId":"getPricingDefaults","tags":["Canchas, precios y horarios"],"summary":"Defaults del club (precio, horario, intervalo)","description":"Los tres defaults del club que heredan las canchas nuevas y con los que el panel materializa las franjas: precio/hora, apertura / cierre e intervalo.","x-skalia-scope":"pricing:read","x-skalia-write":false,"parameters":[{"$ref":"#/components/parameters/RequestId"},{"name":"clubId","in":"query","required":false,"description":"Club sobre el que opera la request. Opcional: se infiere cuando la integración tiene un solo grant (kind CLUB, v1).","schema":{"type":"string","pattern":"^[a-f0-9-]{8,64}$"}}],"responses":{"200":{"description":"Los defaults.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PricingDefaults"}}}},"400":{"description":"Request mal formada (credencial con forma rara, body / query inválidos, `clubId` faltante con varios grants).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MALFORMED_API_KEY","VALIDATION","CLUB_REQUIRED"]},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Integración deshabilitada, módulo API apagado en el club, club sin grant o scope insuficiente (`details.scope`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CLIENT_DISABLED","MODULE_DISABLED","CLUB_NOT_GRANTED","SCOPE_DENIED"]},"429":{"$ref":"#/components/responses/RateLimitedRead"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}},"put":{"operationId":"updatePricingDefaults","tags":["Canchas, precios y horarios"],"summary":"Cambiar los defaults del club","description":"Misma semántica que el PATCH del club en el panel: si cambia el horario o el intervalo se REGENERAN las franjas de TODAS las canchas del club (cada hora conserva la tarifa que tenía; las horas nuevas toman el precio resuelto; las que quedan fuera se pierden); si cambia sólo el precio, pisa `pricePerHour` de todas las canchas y el base de sus franjas sin tocar los totales por duración (`durationPriceOverrides` dice cuántas franjas siguen cobrando por duración). Emite `occupancy.changed` del club si regeneró. Consume cuota de escritura.","x-skalia-scope":"pricing:write","x-skalia-write":true,"parameters":[{"$ref":"#/components/parameters/RequestId"}],"requestBody":{"required":true,"description":"JSON obligatorio (`Content-Type: application/json`; si no, 415).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PricingDefaultsPutBody"}}}},"responses":{"200":{"description":"Los defaults resultantes y qué se regeneró.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PricingDefaultsPutResponse"}}}},"400":{"description":"Request mal formada (credencial con forma rara, body / query inválidos, `clubId` faltante con varios grants).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MALFORMED_API_KEY","VALIDATION","CLUB_REQUIRED"]},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Integración deshabilitada, módulo API apagado en el club, club sin grant o scope insuficiente (`details.scope`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CLIENT_DISABLED","MODULE_DISABLED","CLUB_NOT_GRANTED","SCOPE_DENIED"]},"415":{"$ref":"#/components/responses/UnsupportedMediaType"},"429":{"$ref":"#/components/responses/RateLimitedWrite"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}}},"/recurring-bookings":{"get":{"operationId":"listRecurringBookings","tags":["Bloqueos, fijas y asistencia"],"summary":"Listar reservas fijas","description":"Series de fijas del club (`RecurringBookingDto`), propias y ajenas: `customer` y `externalRef` sólo viajan en las que creó esta integración (`owned: true`); del resto nunca el titular. Orden: primer día, hora, id. `limit` ≤ 200.","x-skalia-scope":"recurring:read","x-skalia-write":false,"parameters":[{"$ref":"#/components/parameters/RequestId"},{"name":"limit","in":"query","required":false,"description":"Tamaño de página, 1..200 (default 100).","schema":{"type":"integer","minimum":1,"maximum":200}},{"name":"offset","in":"query","required":false,"description":"Desplazamiento (paginación offset/limit).","schema":{"type":"integer","minimum":0}},{"name":"clubId","in":"query","required":false,"description":"Club sobre el que opera la request. Opcional: se infiere cuando la integración tiene un solo grant (kind CLUB, v1).","schema":{"type":"string","pattern":"^[a-f0-9-]{8,64}$"}},{"name":"courtId","in":"query","required":false,"description":"Sólo las series de esta cancha.","schema":{"type":"string","pattern":"^[a-f0-9-]{8,64}$"}},{"name":"status","in":"query","required":false,"description":"Filtra por estado de la serie. Default `ACTIVE`.","schema":{"type":"string","enum":["ACTIVE","CANCELLED"]}}],"responses":{"200":{"description":"Página de series.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecurringListResponse"}}}},"400":{"description":"Request mal formada (credencial con forma rara, body / query inválidos, `clubId` faltante con varios grants).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MALFORMED_API_KEY","VALIDATION","CLUB_REQUIRED"]},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Integración deshabilitada, módulo API apagado en el club, club sin grant o scope insuficiente (`details.scope`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CLIENT_DISABLED","MODULE_DISABLED","CLUB_NOT_GRANTED","SCOPE_DENIED"]},"429":{"$ref":"#/components/responses/RateLimitedRead"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}},"post":{"operationId":"createRecurringBooking","tags":["Bloqueos, fijas y asistencia"],"summary":"Crear una reserva fija (atómica, idempotente)","description":"Alta de una serie a nombre del usuario de servicio del club. Validaciones en orden: turno → cancha (kind COURT) → comienzo en la zona del club → idempotencia → horario de cada ocurrencia materializable → tope de futuras → pre-chequeo de ocupación de cada ocurrencia materializable (hoy → +30 días, las 4 fuentes) → INSERT de la serie + INSERT atómico de esas ocurrencias (`source = recurring`, propias de la integración). Si una carrera ocupa un turno en el medio se deshace todo y responde 409. Una serie que arranca más allá del horizonte no materializa nada hoy, pero se validan igual (horario + ocupación) sus primeros 30 días desde `startDate`. Las ocurrencias más lejanas las crea el cron cuando entran en el horizonte (si para entonces chocan, el club recibe el aviso de conflicto, como con una fija del panel). Emite `booking.created` por ocurrencia y `occupancy.changed` por la serie. Consume cuota de escritura.","x-skalia-scope":"recurring:write","x-skalia-write":true,"parameters":[{"$ref":"#/components/parameters/RequestId"},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"description":"JSON obligatorio (`Content-Type: application/json`; si no, 415).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecurringCreateBody"}}}},"responses":{"200":{"description":"Replay: el `externalRef` ya existía vivo con la MISMA serie; se devuelve esa serie.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"},"Idempotent-Replay":{"$ref":"#/components/headers/Idempotent-Replay"},"Location":{"$ref":"#/components/headers/Location"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecurringCreateResponse"}}}},"201":{"description":"Serie creada.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"},"Location":{"$ref":"#/components/headers/Location"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecurringCreateResponse"}}}},"400":{"description":"Request mal formada (credencial con forma rara, body / query inválidos, `clubId` faltante con varios grants). Además: `endTime` que no da 30/60/90/120, comienzo ya pasado en la zona del club, más de 180 días, o alguna ocurrencia materializable fuera del horario de la cancha / del club (`details.date`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MALFORMED_API_KEY","VALIDATION","CLUB_REQUIRED","INVALID_DURATION","DATE_IN_PAST","MAX_ADVANCE_EXCEEDED","OUTSIDE_OPENING_HOURS"]},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Integración deshabilitada, módulo API apagado en el club, club sin grant o scope insuficiente (`details.scope`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CLIENT_DISABLED","MODULE_DISABLED","CLUB_NOT_GRANTED","SCOPE_DENIED"]},"404":{"description":"La cancha no es del club o está deshabilitada (`details.courtId`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["COURT_NOT_FOUND"]},"409":{"description":"Alguna ocurrencia materializable choca (`details.occurrences[]`: `date`, `startTime`, `endTime`, `busy[]`), el `externalRef` ya se usó en una serie cancelada, ya existe vivo con OTRA serie, o `courtId` no es una cancha.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["RECURRING_CONFLICT","EXTERNAL_REF_USED","EXTERNAL_REF_CONFLICT","RESOURCE_NOT_COURT"]},"415":{"$ref":"#/components/responses/UnsupportedMediaType"},"429":{"$ref":"#/components/responses/RateLimitedWrite"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}}},"/recurring-bookings/{id}":{"patch":{"operationId":"updateRecurringBooking","tags":["Bloqueos, fijas y asistencia"],"summary":"Acortar una fija o corregir cliente / precio","description":"Sólo `endDate` (acortar o cerrar la serie: las ocurrencias VIVAS que quedan afuera, de hoy en adelante, se cancelan con su evento; lo ya jugado no se toca y `endDate` nunca puede ser anterior a ayer), `customer` y `price` (se propagan a las ocurrencias futuras). Mover cancha / día / horario = DELETE + POST. Sólo series propias (o `canManageAllBookings`). Consume cuota de escritura.","x-skalia-scope":"recurring:write","x-skalia-write":true,"parameters":[{"$ref":"#/components/parameters/RequestId"},{"name":"id","in":"path","required":true,"description":"Id de la serie de fijas.","schema":{"type":"string","pattern":"^[a-f0-9-]{8,64}$"}}],"requestBody":{"required":true,"description":"JSON obligatorio (`Content-Type: application/json`; si no, 415).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecurringPatchBody"}}}},"responses":{"200":{"description":"La serie actualizada.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecurringPatchResponse"}}}},"400":{"description":"Request mal formada (credencial con forma rara, body / query inválidos, `clubId` faltante con varios grants).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MALFORMED_API_KEY","VALIDATION"]},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Integración deshabilitada, módulo API apagado en el club, club sin grant o scope insuficiente (`details.scope`). O la serie no la creó esta integración (sin `canManageAllBookings`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CLIENT_DISABLED","MODULE_DISABLED","CLUB_NOT_GRANTED","SCOPE_DENIED","RECURRING_NOT_OWNED_BY_CLIENT"]},"404":{"description":"La serie no existe o es de un club sin grant.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["RECURRING_NOT_FOUND"]},"409":{"description":"Ya cancelada, o `endDate` que extiende la serie / cae antes del comienzo / cae antes de ayer en la zona del club (`details.reason`: `extend_end_date` / `end_before_start` / `end_in_past`, con `today`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["RECURRING_ALREADY_CANCELLED","RECURRING_NOT_MODIFIABLE"]},"415":{"$ref":"#/components/responses/UnsupportedMediaType"},"429":{"$ref":"#/components/responses/RateLimitedWrite"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}},"delete":{"operationId":"cancelRecurringBooking","tags":["Bloqueos, fijas y asistencia"],"summary":"Cancelar una fija","description":"Cierra la serie (`status = CANCELLED`) y, salvo `keepFuture=true`, cancela las ocurrencias materializadas VIVAS de hoy en adelante (evento `booking.cancelled` por cada una, partidos desligados; lo ya jugado, incluido el turno de hoy que ya terminó, no se toca). Emite `occupancy.changed` liberando la cadencia desde hoy. Idempotente (`alreadyCancelled`). Sólo series propias (o `canManageAllBookings`). Consume cuota de escritura.","x-skalia-scope":"recurring:write","x-skalia-write":true,"parameters":[{"$ref":"#/components/parameters/RequestId"},{"name":"id","in":"path","required":true,"description":"Id de la serie de fijas.","schema":{"type":"string","pattern":"^[a-f0-9-]{8,64}$"}},{"name":"keepFuture","in":"query","required":false,"description":"`true` sólo cierra la serie: las ocurrencias ya materializadas siguen vivas como reservas sueltas.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"Cancelada, o ya lo estaba (`alreadyCancelled: true`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecurringDeleteResponse"}}}},"400":{"description":"Request mal formada (credencial con forma rara, body / query inválidos, `clubId` faltante con varios grants).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MALFORMED_API_KEY","VALIDATION"]},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Integración deshabilitada, módulo API apagado en el club, club sin grant o scope insuficiente (`details.scope`). O la serie no la creó esta integración (sin `canManageAllBookings`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CLIENT_DISABLED","MODULE_DISABLED","CLUB_NOT_GRANTED","SCOPE_DENIED","RECURRING_NOT_OWNED_BY_CLIENT"]},"404":{"description":"La serie no existe o es de un club sin grant.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["RECURRING_NOT_FOUND"]},"429":{"$ref":"#/components/responses/RateLimitedWrite"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}},"get":{"operationId":"getRecurringBooking","tags":["Bloqueos, fijas y asistencia"],"summary":"Una reserva fija","description":"La serie (`RecurringBookingDto`), propia o ajena, con la misma anonimización del listado: `customer` y `externalRef` sólo si la creó esta integración (`owned: true`). Es adonde apunta el `Location` del alta y el `recurringId` de una ocurrencia (`BookingDto` / `SlotDto`).","x-skalia-scope":"recurring:read","x-skalia-write":false,"parameters":[{"$ref":"#/components/parameters/RequestId"},{"name":"id","in":"path","required":true,"description":"Id de la serie de fijas.","schema":{"type":"string","pattern":"^[a-f0-9-]{8,64}$"}}],"responses":{"200":{"description":"La serie.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecurringGetResponse"}}}},"400":{"description":"Request mal formada (credencial con forma rara, body / query inválidos, `clubId` faltante con varios grants).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MALFORMED_API_KEY"]},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Integración deshabilitada, módulo API apagado en el club, club sin grant o scope insuficiente (`details.scope`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CLIENT_DISABLED","MODULE_DISABLED","CLUB_NOT_GRANTED","SCOPE_DENIED"]},"404":{"description":"La serie no existe o es de un club sin grant.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["RECURRING_NOT_FOUND"]},"429":{"$ref":"#/components/responses/RateLimitedRead"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}}},"/recurring-bookings/{id}/exceptions":{"post":{"operationId":"skipRecurringBookingDate","tags":["Bloqueos, fijas y asistencia"],"summary":"Saltar una fecha de la fija","description":"Inserta la excepción (la serie sigue) y, si la ocurrencia ya estaba materializada, la cancela con su evento. Emite `occupancy.changed` liberando ese día. Repetir la misma fecha responde 200 (`alreadySkipped: true`). Sólo series propias (o `canManageAllBookings`). Consume cuota de escritura.","x-skalia-scope":"recurring:write","x-skalia-write":true,"parameters":[{"$ref":"#/components/parameters/RequestId"},{"name":"id","in":"path","required":true,"description":"Id de la serie de fijas.","schema":{"type":"string","pattern":"^[a-f0-9-]{8,64}$"}}],"requestBody":{"required":true,"description":"JSON obligatorio (`Content-Type: application/json`; si no, 415).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecurringExceptionBody"}}}},"responses":{"200":{"description":"Ya estaba salteada (idempotente).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecurringExceptionResponse"}}}},"201":{"description":"Fecha salteada.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecurringExceptionResponse"}}}},"400":{"description":"Request mal formada (credencial con forma rara, body / query inválidos, `clubId` faltante con varios grants). Además: fecha ya pasada en la zona del club, o que no es una ocurrencia de la serie (`details.issues[]`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MALFORMED_API_KEY","VALIDATION","DATE_IN_PAST"]},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Integración deshabilitada, módulo API apagado en el club, club sin grant o scope insuficiente (`details.scope`). O la serie no la creó esta integración (sin `canManageAllBookings`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CLIENT_DISABLED","MODULE_DISABLED","CLUB_NOT_GRANTED","SCOPE_DENIED","RECURRING_NOT_OWNED_BY_CLIENT"]},"404":{"description":"La serie no existe o es de un club sin grant.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["RECURRING_NOT_FOUND"]},"409":{"description":"La serie ya está cancelada.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["RECURRING_ALREADY_CANCELLED"]},"415":{"$ref":"#/components/responses/UnsupportedMediaType"},"429":{"$ref":"#/components/responses/RateLimitedWrite"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}}},"/recurring-bookings/{id}/occurrences":{"get":{"operationId":"listRecurringBookingOccurrences","tags":["Bloqueos, fijas y asistencia"],"summary":"Las fechas de una fija en un rango","description":"Las ocurrencias de la serie en `[from, to]`, pasadas y futuras: la cadencia proyectada (la misma del detector de conflictos: `monthly` anclada al día original, sin fin respetado) cruzada con las fechas salteadas (`SKIPPED`) y con las ocurrencias ya materializadas como reservas, cuyo `status` real (`PENDING` / `CONFIRMED` / `CANCELLED` / `ATTENDED` / `NO_SHOW`) pisa la proyección. `PROJECTED` = fecha de la cadencia sin fila todavía (más allá del horizonte de 30 días del cron; una pasada sin fila es una que el cron no llegó a crear por un conflicto). Una serie cancelada no proyecta nada: quedan sólo sus filas materializadas. `bookingId` sólo en series propias o con `canManageAllBookings` (la misma regla que `GET /bookings/{id}` sobre una reserva ajena; no la de `/occupancy`, que sólo etiqueta lo creado por esta integración). Es lo que necesita un sistema de cuotas (\"las 4 fechas de octubre\"). Sin PII, sin ETag, no emite eventos.","x-skalia-scope":"recurring:read","x-skalia-write":false,"parameters":[{"$ref":"#/components/parameters/RequestId"},{"name":"id","in":"path","required":true,"description":"Id de la serie de fijas.","schema":{"type":"string","pattern":"^[a-f0-9-]{8,64}$"}},{"name":"from","in":"query","required":false,"description":"Primer día del rango (`YYYY-MM-DD` naive del club). Default: hoy en el club.","schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"example":"2026-10-01"},{"name":"to","in":"query","required":false,"description":"Último día del rango (inclusive). Default: `min(endDate, from + 90 días)`. Como mucho 366 días desde `from`.","schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"example":"2026-10-31"}],"responses":{"200":{"description":"Las fechas en el rango.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecurringOccurrencesResponse"}}}},"400":{"description":"Request mal formada (credencial con forma rara, body / query inválidos, `clubId` faltante con varios grants). Además: `to < from` o rango de más de 366 días (también con los defaults aplicados).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MALFORMED_API_KEY","VALIDATION"]},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Integración deshabilitada, módulo API apagado en el club, club sin grant o scope insuficiente (`details.scope`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CLIENT_DISABLED","MODULE_DISABLED","CLUB_NOT_GRANTED","SCOPE_DENIED"]},"404":{"description":"La serie no existe o es de un club sin grant.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["RECURRING_NOT_FOUND"]},"429":{"$ref":"#/components/responses/RateLimitedRead"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}}},"/reports/bookings":{"get":{"operationId":"getBookingsReport","tags":["Reportes"],"summary":"Horas y venta esperada del club","description":"Totales de las reservas JUGADAS en `[from, to]` (días naive del club, ≤ 366 días): puntuales y ocurrencias materializadas de fijas, de todos los orígenes (app, panel, QuieroJugar, esta y otras integraciones), `status != CANCELLED`, sin los placeholders del importador de QJ. Es el mismo dataset que el panel exporta a CSV / XLSX. `price` es la venta ESPERADA con la precedencia del panel (`price_override` total → `price` materializado → pro-rata del precio/hora), nunca lo cobrado (`priceIsExpected: true`). Sólo agregados: `totals` (+ `byType`, `bySource`) y, con `groupBy`, un grupo por cancha / categoría / tipo / origen. Ninguna fila por reserva ni por persona. `bySource` identifica sólo a tu integración (`apiClientId`); las demás van sumadas en una fila `api` anónima. Scope `reports:read` (opt-in y sensible: lo emite el dueño del club). No consume cuota de escritura.","x-skalia-scope":"reports:read","x-skalia-write":false,"parameters":[{"$ref":"#/components/parameters/RequestId"},{"name":"clubId","in":"query","required":false,"description":"Club sobre el que opera la request. Opcional: se infiere cuando la integración tiene un solo grant (kind CLUB, v1).","schema":{"type":"string","pattern":"^[a-f0-9-]{8,64}$"}},{"name":"from","in":"query","required":true,"description":"Primer día JUGADO del rango (`YYYY-MM-DD` naive del club).","schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"example":"2026-09-01"},{"name":"to","in":"query","required":true,"description":"Último día (inclusive); `to ≥ from` y como mucho 366 días.","schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"example":"2026-09-30"},{"name":"courtId","in":"query","required":false,"description":"Sólo esta cancha / recurso del club (404 COURT_NOT_FOUND si no es del club).","schema":{"type":"string","pattern":"^[a-f0-9-]{8,64}$"}},{"name":"categoryId","in":"query","required":false,"description":"Sólo esta categoría de recurso (F6), o `court-implicit` para las canchas sin categoría (404 CATEGORY_NOT_FOUND si no es del club).","schema":{"anyOf":[{"type":"string","pattern":"^[a-f0-9-]{8,64}$"},{"type":"string","const":"court-implicit"}]},"example":"court-implicit"},{"name":"kind","in":"query","required":false,"description":"Sólo los recursos de este kind (`COURT`, `CLINIC`, `RESTAURANT`, `OTHER`).","schema":{"type":"string","enum":["COURT","CLINIC","RESTAURANT","OTHER"]},"example":"COURT"},{"name":"groupBy","in":"query","required":false,"description":"Dimensión de `groups`: `court`, `category`, `type` o `source`. Sin `groupBy`, `groups` es `null`.","schema":{"type":"string","enum":["court","category","type","source"]},"example":"court"}],"responses":{"200":{"description":"Totales del rango.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingsReportDto"}}}},"400":{"description":"Request mal formada (credencial con forma rara, body / query inválidos, `clubId` faltante con varios grants). Además: `to < from`, rango > 366 días, `kind` / `groupBy` fuera del enum.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MALFORMED_API_KEY","VALIDATION","CLUB_REQUIRED"]},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Integración deshabilitada, módulo API apagado en el club, club sin grant o scope insuficiente (`details.scope`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CLIENT_DISABLED","MODULE_DISABLED","CLUB_NOT_GRANTED","SCOPE_DENIED"]},"404":{"description":"`courtId` que no es del club (`details.courtId`) o `categoryId` que no es del club (`details.categoryId`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["COURT_NOT_FOUND","CATEGORY_NOT_FOUND"]},"429":{"$ref":"#/components/responses/RateLimitedRead"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}}},"/resource-categories":{"get":{"operationId":"listResourceCategories","tags":["Club"],"summary":"Categorías de recurso del club","description":"Todas las categorías de recurso (F6) del club, deshabilitadas y default incluidas, más `moduleEnabled` (si el módulo está apagado la lectura igual responde; las escrituras no).","x-skalia-scope":"club:read","x-skalia-write":false,"parameters":[{"$ref":"#/components/parameters/RequestId"},{"name":"clubId","in":"query","required":false,"description":"Club sobre el que opera la request. Opcional: se infiere cuando la integración tiene un solo grant (kind CLUB, v1).","schema":{"type":"string","pattern":"^[a-f0-9-]{8,64}$"}}],"responses":{"200":{"description":"Las categorías.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResourceCategoriesResponse"}}}},"400":{"description":"Request mal formada (credencial con forma rara, body / query inválidos, `clubId` faltante con varios grants).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MALFORMED_API_KEY","VALIDATION","CLUB_REQUIRED"]},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Integración deshabilitada, módulo API apagado en el club, club sin grant o scope insuficiente (`details.scope`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CLIENT_DISABLED","MODULE_DISABLED","CLUB_NOT_GRANTED","SCOPE_DENIED"]},"429":{"$ref":"#/components/responses/RateLimitedRead"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}},"post":{"operationId":"createResourceCategory","tags":["Club"],"summary":"Crear una categoría de recurso","description":"Espejo de `POST /club-owner/clubs/:clubId/resource-categories`, con los mismos defaults por kind y gates. La PRIMERA categoría de un club con canchas sin categoría materializa \"Canchas\" (`defaultCourtCategoryId`). Exige el módulo de categorías del club (lo prende Skalia). Scope sensible `club:write`. Audit `RESOURCE_CATEGORY_CREATED`. Consume cuota de escritura.","x-skalia-scope":"club:write","x-skalia-write":true,"parameters":[{"$ref":"#/components/parameters/RequestId"}],"requestBody":{"required":true,"description":"JSON obligatorio (`Content-Type: application/json`; si no, 415).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResourceCategoryCreateBody"}}}},"responses":{"201":{"description":"Creada.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResourceCategoryCreatedResponse"}}}},"400":{"description":"Request mal formada (credencial con forma rara, body / query inválidos, `clubId` faltante con varios grants). Además: reglas de negocio de la categoría (`details` vacío, `error` dice cuál), MANUAL en kind COURT, o `awardsPoints` en el body.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MALFORMED_API_KEY","VALIDATION","CLUB_REQUIRED","INVALID_CATEGORY_INPUT","RESOURCE_KIND_UNSUPPORTED","AWARDS_POINTS_NOT_ALLOWED"]},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Integración deshabilitada, módulo API apagado en el club, club sin grant o scope insuficiente (`details.scope`). O el club no tiene el módulo de categorías de recurso.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CLIENT_DISABLED","MODULE_DISABLED","CLUB_NOT_GRANTED","SCOPE_DENIED","RESOURCE_CATEGORIES_DISABLED"]},"415":{"$ref":"#/components/responses/UnsupportedMediaType"},"429":{"$ref":"#/components/responses/RateLimitedWrite"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}}},"/resource-categories/{id}":{"patch":{"operationId":"updateResourceCategory","tags":["Club"],"summary":"Editar una categoría de recurso","description":"Espejo de `PATCH /club-owner/resource-categories/:id`: parcial; `kind` inmutable; `resourceNoun` sólo cambia sin recursos; cambiar `allowedDurations` sin `defaultDurationMinutes` la recalcula. Exige el módulo de categorías. Scope sensible `club:write`. Audit `RESOURCE_CATEGORY_UPDATED`. Consume cuota de escritura.","x-skalia-scope":"club:write","x-skalia-write":true,"parameters":[{"$ref":"#/components/parameters/RequestId"},{"name":"id","in":"path","required":true,"description":"Id de la categoría (`GET /resource-categories`).","schema":{"type":"string","pattern":"^[a-f0-9-]{8,64}$"}}],"requestBody":{"required":true,"description":"JSON obligatorio (`Content-Type: application/json`; si no, 415).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResourceCategoryPatchBody"}}}},"responses":{"200":{"description":"La categoría actualizada.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResourceCategoryResponse"}}}},"400":{"description":"Request mal formada (credencial con forma rara, body / query inválidos, `clubId` faltante con varios grants). Además: `kind` distinto al actual, reglas de negocio, MANUAL en COURT, o `awardsPoints`.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MALFORMED_API_KEY","VALIDATION","RESOURCE_KIND_CHANGE_NOT_ALLOWED","INVALID_CATEGORY_INPUT","RESOURCE_KIND_UNSUPPORTED","AWARDS_POINTS_NOT_ALLOWED"]},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Integración deshabilitada, módulo API apagado en el club, club sin grant o scope insuficiente (`details.scope`). O el club no tiene el módulo de categorías de recurso.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CLIENT_DISABLED","MODULE_DISABLED","CLUB_NOT_GRANTED","SCOPE_DENIED","RESOURCE_CATEGORIES_DISABLED"]},"404":{"description":"La categoría no existe o es de un club sin grant (no se confirma nada).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CATEGORY_NOT_FOUND"]},"409":{"description":"Cambiar `resourceNoun` con recursos colgados (`details.resourceCount`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CATEGORY_HAS_RESOURCES"]},"415":{"$ref":"#/components/responses/UnsupportedMediaType"},"429":{"$ref":"#/components/responses/RateLimitedWrite"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}},"delete":{"operationId":"deleteResourceCategory","tags":["Club"],"summary":"Borrar una categoría de recurso","description":"Espejo de `DELETE /club-owner/resource-categories/:id`: sólo una categoría vacía y que no sea la \"Canchas\" materializada. Exige el módulo de categorías. Scope sensible `club:write`. Audit `RESOURCE_CATEGORY_DELETED`. Consume cuota de escritura.","x-skalia-scope":"club:write","x-skalia-write":true,"parameters":[{"$ref":"#/components/parameters/RequestId"},{"name":"id","in":"path","required":true,"description":"Id de la categoría (`GET /resource-categories`).","schema":{"type":"string","pattern":"^[a-f0-9-]{8,64}$"}}],"responses":{"200":{"description":"Borrada.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResourceCategoryDeletedResponse"}}}},"400":{"description":"Request mal formada (credencial con forma rara, body / query inválidos, `clubId` faltante con varios grants).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MALFORMED_API_KEY"]},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Integración deshabilitada, módulo API apagado en el club, club sin grant o scope insuficiente (`details.scope`). O el club no tiene el módulo de categorías de recurso.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CLIENT_DISABLED","MODULE_DISABLED","CLUB_NOT_GRANTED","SCOPE_DENIED","RESOURCE_CATEGORIES_DISABLED"]},"404":{"description":"La categoría no existe o es de un club sin grant.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CATEGORY_NOT_FOUND"]},"409":{"description":"Es la default (`CATEGORY_IS_DEFAULT`) o tiene recursos (`details.resourceCount`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CATEGORY_IS_DEFAULT","CATEGORY_HAS_RESOURCES"]},"429":{"$ref":"#/components/responses/RateLimitedWrite"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}}},"/settings":{"get":{"operationId":"getClubSettings","tags":["Club"],"summary":"Configuración del club","description":"La configuración de NEGOCIO de `club_settings`: política de confirmación, ventanas, topes, moneda, zona, franja pico y cola de espontáneos. Nada de credenciales.","x-skalia-scope":"club:read","x-skalia-write":false,"parameters":[{"$ref":"#/components/parameters/RequestId"},{"name":"clubId","in":"query","required":false,"description":"Club sobre el que opera la request. Opcional: se infiere cuando la integración tiene un solo grant (kind CLUB, v1).","schema":{"type":"string","pattern":"^[a-f0-9-]{8,64}$"}}],"responses":{"200":{"description":"La configuración.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClubSettingsDto"}}}},"400":{"description":"Request mal formada (credencial con forma rara, body / query inválidos, `clubId` faltante con varios grants).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MALFORMED_API_KEY","VALIDATION","CLUB_REQUIRED"]},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Integración deshabilitada, módulo API apagado en el club, club sin grant o scope insuficiente (`details.scope`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CLIENT_DISABLED","MODULE_DISABLED","CLUB_NOT_GRANTED","SCOPE_DENIED"]},"429":{"$ref":"#/components/responses/RateLimitedRead"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}},"patch":{"operationId":"updateClubSettings","tags":["Club"],"summary":"Editar la configuración del club","description":"Parcial, con las MISMAS validaciones del panel (`PATCH /club-owner/clubs/:id/settings`): zona IANA válida, franja pico `HH:MM` con fin posterior al inicio (contra lo guardado si sólo mandás uno), días 0..6, cola de categorías conocida. Scope sensible `club:write`. Audit `SETTINGS_UPDATED`. Consume cuota de escritura.","x-skalia-scope":"club:write","x-skalia-write":true,"parameters":[{"$ref":"#/components/parameters/RequestId"}],"requestBody":{"required":true,"description":"JSON obligatorio (`Content-Type: application/json`; si no, 415).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClubSettingsPatchBody"}}}},"responses":{"200":{"description":"La configuración actualizada.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClubSettingsDto"}}}},"400":{"description":"Request mal formada (credencial con forma rara, body / query inválidos, `clubId` faltante con varios grants). Además: la franja pico termina antes de empezar (`details.peakHoursStart` / `peakHoursEnd`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MALFORMED_API_KEY","VALIDATION","CLUB_REQUIRED","INVALID_PEAK_RANGE"]},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Integración deshabilitada, módulo API apagado en el club, club sin grant o scope insuficiente (`details.scope`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CLIENT_DISABLED","MODULE_DISABLED","CLUB_NOT_GRANTED","SCOPE_DENIED"]},"415":{"$ref":"#/components/responses/UnsupportedMediaType"},"429":{"$ref":"#/components/responses/RateLimitedWrite"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}}},"/streams":{"get":{"operationId":"listStreams","tags":["Streaming"],"summary":"Qué cancha está en vivo","description":"Los vivos del club por cancha: broadcasts armados (`SCHEDULED`) o al aire (`LIVE`), una fila por cancha, sin PII. `url` / `embedUrl` sólo para lo `STANDALONE`, lo `TOURNAMENT` (público por diseño) y lo `BOOKING` de una reserva creada por TU integración; el turno de una reserva ajena sólo dice \"esta cancha está en vivo\" (`url`, `embedUrl` y `bookingId` en `null`), porque el título del video lleva el nombre del titular. Con el streaming del club apagado la lista es `[]`. Con ETag fuerte: mandá `If-None-Match` y recibís 304 sin cuerpo si nada cambió (el `Cache-Control: no-store` sigue). No consume cuota de escritura ni emite eventos; el VOD de un turno terminado es `recording` de la reserva (`recordings:read`) y su momento real de \"ya se puede mirar\" es el evento `booking.recording_ready`.","x-skalia-scope":"recordings:read","x-skalia-write":false,"parameters":[{"$ref":"#/components/parameters/RequestId"},{"$ref":"#/components/parameters/IfNoneMatch"},{"name":"clubId","in":"query","required":false,"description":"Club sobre el que opera la request. Opcional: se infiere cuando la integración tiene un solo grant (kind CLUB, v1).","schema":{"type":"string","pattern":"^[a-f0-9-]{8,64}$"}},{"name":"courtId","in":"query","required":false,"description":"Sólo esa cancha (`GET /courts`). Una cancha de otro club no filtra nada: la lista sale vacía.","schema":{"type":"string","pattern":"^[a-f0-9-]{8,64}$"}}],"responses":{"200":{"description":"Vivos por cancha.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"},"ETag":{"$ref":"#/components/headers/ETag"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StreamsListResponse"}}}},"304":{"description":"`If-None-Match` coincide: nada cambió. Sin cuerpo.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"},"ETag":{"$ref":"#/components/headers/ETag"}}},"400":{"description":"Request mal formada (credencial con forma rara, body / query inválidos, `clubId` faltante con varios grants). `courtId` con forma inválida.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MALFORMED_API_KEY","VALIDATION","CLUB_REQUIRED"]},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Integración deshabilitada, módulo API apagado en el club, club sin grant o scope insuficiente (`details.scope`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CLIENT_DISABLED","MODULE_DISABLED","CLUB_NOT_GRANTED","SCOPE_DENIED"]},"429":{"$ref":"#/components/responses/RateLimitedRead"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}}},"/tournaments":{"get":{"operationId":"listTournaments","tags":["Torneos"],"summary":"Listar torneos","description":"Categorías (filas de `tournaments`) del club; `eventId` agrupa las que forman un mismo evento. Orden: fecha de inicio desc, alta desc. Nunca datos de las personas que integran una pareja.","x-skalia-scope":"tournaments:read","x-skalia-write":false,"parameters":[{"$ref":"#/components/parameters/RequestId"},{"name":"limit","in":"query","required":false,"description":"Tamaño de página, 1..200 (default 100).","schema":{"type":"integer","minimum":1,"maximum":200}},{"name":"offset","in":"query","required":false,"description":"Desplazamiento (paginación offset/limit).","schema":{"type":"integer","minimum":0}},{"name":"clubId","in":"query","required":false,"description":"Club sobre el que opera la request. Opcional: se infiere cuando la integración tiene un solo grant (kind CLUB, v1).","schema":{"type":"string","pattern":"^[a-f0-9-]{8,64}$"}},{"name":"status","in":"query","required":false,"description":"Filtra por status de la categoría.","schema":{"type":"string","enum":["OPEN","CLOSED","SCHEDULE_BUILDING","GROUP_PHASE","ELIMINATION_PHASE","COMPLETED","CANCELLED"]}},{"name":"from","in":"query","required":false,"description":"Torneos cuya fecha de inicio es ≥ `from` (`YYYY-MM-DD`).","schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"example":"2026-10-01"},{"name":"to","in":"query","required":false,"description":"Torneos cuya fecha de inicio es ≤ `to` (`YYYY-MM-DD`).","schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"example":"2026-12-31"},{"name":"eventId","in":"query","required":false,"description":"Sólo las categorías de este evento.","schema":{"type":"string","pattern":"^[a-f0-9-]{8,64}$"}}],"responses":{"200":{"description":"Página de categorías.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TournamentsListResponse"}}}},"400":{"description":"Request mal formada (credencial con forma rara, body / query inválidos, `clubId` faltante con varios grants).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MALFORMED_API_KEY","VALIDATION","CLUB_REQUIRED"]},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Integración deshabilitada, módulo API apagado en el club, club sin grant o scope insuficiente (`details.scope`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CLIENT_DISABLED","MODULE_DISABLED","CLUB_NOT_GRANTED","SCOPE_DENIED"]},"429":{"$ref":"#/components/responses/RateLimitedRead"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}},"post":{"operationId":"createTournament","tags":["Torneos"],"summary":"Crear un evento con sus categorías","description":"Espejo de `POST /admin/tournaments`: una fila por (género, categoría), todas `OPEN` y con el mismo `eventId`. Mismas validaciones del panel (fechas parseables, sin variante repetida → 400 VALIDATION). Audit `TOURNAMENT_CREATED` por categoría. Consume cuota de escritura.","x-skalia-scope":"tournaments:write","x-skalia-write":true,"parameters":[{"$ref":"#/components/parameters/RequestId"}],"requestBody":{"required":true,"description":"JSON obligatorio (`Content-Type: application/json`; si no, 415).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TournamentCreateBody"}}}},"responses":{"201":{"description":"Creado.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TournamentCreateResponse"}}}},"400":{"description":"Request mal formada (credencial con forma rara, body / query inválidos, `clubId` faltante con varios grants).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MALFORMED_API_KEY","VALIDATION","CLUB_REQUIRED"]},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Integración deshabilitada, módulo API apagado en el club, club sin grant o scope insuficiente (`details.scope`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CLIENT_DISABLED","MODULE_DISABLED","CLUB_NOT_GRANTED","SCOPE_DENIED"]},"415":{"$ref":"#/components/responses/UnsupportedMediaType"},"429":{"$ref":"#/components/responses/RateLimitedWrite"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}}},"/tournaments/{id}":{"get":{"operationId":"getTournament","tags":["Torneos"],"summary":"Detalle de un torneo","description":"La categoría con sus hermanas del mismo evento, las parejas anónimas (`TeamDto`), los grupos, todos los partidos (el cuadro son los de `phase != GROUP`) y el podio (`[]` hasta que se finaliza). Nunca datos de las personas que integran una pareja.","x-skalia-scope":"tournaments:read","x-skalia-write":false,"parameters":[{"$ref":"#/components/parameters/RequestId"},{"name":"id","in":"path","required":true,"description":"Id de la categoría (`TournamentDto.id`).","schema":{"type":"string","pattern":"^[a-f0-9-]{8,64}$"}}],"responses":{"200":{"description":"El torneo.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TournamentDetailResponse"}}}},"400":{"description":"Request mal formada (credencial con forma rara, body / query inválidos, `clubId` faltante con varios grants).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MALFORMED_API_KEY"]},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Integración deshabilitada, módulo API apagado en el club, club sin grant o scope insuficiente (`details.scope`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CLIENT_DISABLED","MODULE_DISABLED","CLUB_NOT_GRANTED","SCOPE_DENIED"]},"404":{"description":"El torneo no existe o es de un club sin grant (no se confirma nada).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["TOURNAMENT_NOT_FOUND"]},"429":{"$ref":"#/components/responses/RateLimitedRead"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}},"patch":{"operationId":"updateTournament","tags":["Torneos"],"summary":"Modificar un torneo","description":"Nombre, descripción, lugar, fechas, cupo, precio y `status`. Las transiciones válidas son las de docs/enums.md (OPEN → CLOSED → GROUP_PHASE → ELIMINATION_PHASE → COMPLETED, CANCELLED desde cualquier no terminal, SCHEDULE_BUILDING → GROUP_PHASE, y la única hacia atrás: ELIMINATION_PHASE → GROUP_PHASE, que borra el cuadro y devuelve las parejas eliminadas al pool para poder regenerarlo); repetir el status actual es un no-op. Después aplican los guards del panel: CLOSED exige al menos una pareja, ELIMINATION_PHASE exige el cuadro generado, volver a GROUP_PHASE con eliminatorias jugadas responde 409 TOURNAMENT_ALREADY_PLAYED salvo `force: true` (que las borra y revierte su impacto en el nivel). `status: COMPLETED` es cerrar la categoría: el mismo op que `POST …/finalize` (podio, puntos Skalia, premios; exige la final jugada → si no, 400 TOURNAMENT_RULE_VIOLATION) y la respuesta suma `podium`. Los cambios de fase notifican a los inscriptos como lo hace el panel. Consume cuota de escritura.","x-skalia-scope":"tournaments:write","x-skalia-write":true,"parameters":[{"$ref":"#/components/parameters/RequestId"},{"name":"id","in":"path","required":true,"description":"Id de la categoría (`TournamentDto.id`).","schema":{"type":"string","pattern":"^[a-f0-9-]{8,64}$"}}],"requestBody":{"required":true,"description":"JSON obligatorio (`Content-Type: application/json`; si no, 415).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TournamentPatchBody"}}}},"responses":{"200":{"description":"El torneo actualizado (con `warnings` si arrancó la fase de grupos con parejas incompletas).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TournamentResponse"}}}},"400":{"description":"Request mal formada (credencial con forma rara, body / query inválidos, `clubId` faltante con varios grants). Además: un guard del panel (cerrar sin parejas, pasar a eliminatorias sin cuadro, COMPLETED con la final sin jugar).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MALFORMED_API_KEY","VALIDATION","TOURNAMENT_RULE_VIOLATION"]},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Integración deshabilitada, módulo API apagado en el club, club sin grant o scope insuficiente (`details.scope`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CLIENT_DISABLED","MODULE_DISABLED","CLUB_NOT_GRANTED","SCOPE_DENIED"]},"404":{"description":"El torneo no existe o es de un club sin grant (no se confirma nada).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["TOURNAMENT_NOT_FOUND"]},"409":{"description":"Transición fuera de la máquina de estados (`details.from` / `details.to`), o eliminatorias ya jugadas al retroceder (`details.playedMatches`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["TOURNAMENT_INVALID_TRANSITION","TOURNAMENT_ALREADY_PLAYED"]},"415":{"$ref":"#/components/responses/UnsupportedMediaType"},"429":{"$ref":"#/components/responses/RateLimitedWrite"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}},"delete":{"operationId":"deleteTournament","tags":["Torneos"],"summary":"Borrar o cancelar un torneo","description":"Sin inscriptos: se borra como lo hace el panel (`deleted: true`). Con inscriptos: 409 TOURNAMENT_HAS_REGISTRATIONS salvo `?force=true`, que lo pasa a CANCELLED (`cancelled: true`, idempotente). Consume cuota de escritura.","x-skalia-scope":"tournaments:write","x-skalia-write":true,"parameters":[{"$ref":"#/components/parameters/RequestId"},{"name":"id","in":"path","required":true,"description":"Id de la categoría (`TournamentDto.id`).","schema":{"type":"string","pattern":"^[a-f0-9-]{8,64}$"}},{"name":"force","in":"query","required":false,"description":"`true` cancela el torneo (status CANCELLED, con el aviso a los inscriptos) cuando tiene parejas; sin él, 409.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"Borrado o cancelado.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TournamentDeleteResponse"}}}},"400":{"description":"Request mal formada (credencial con forma rara, body / query inválidos, `clubId` faltante con varios grants).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MALFORMED_API_KEY","VALIDATION"]},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Integración deshabilitada, módulo API apagado en el club, club sin grant o scope insuficiente (`details.scope`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CLIENT_DISABLED","MODULE_DISABLED","CLUB_NOT_GRANTED","SCOPE_DENIED"]},"404":{"description":"El torneo no existe o es de un club sin grant (no se confirma nada).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["TOURNAMENT_NOT_FOUND"]},"409":{"description":"Tiene parejas inscriptas y no mandaste `force` (`details.registeredTeams`), o ya está en un estado terminal distinto de CANCELLED.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["TOURNAMENT_HAS_REGISTRATIONS","TOURNAMENT_INVALID_TRANSITION"]},"429":{"$ref":"#/components/responses/RateLimitedWrite"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}}},"/tournaments/{id}/categories":{"post":{"operationId":"addTournamentCategories","tags":["Torneos"],"summary":"Abrir más categorías en un evento","description":"Suma categorías (filas OPEN) al evento de la categoría de la URL, con su mismo `eventId`: lo que el panel hace al editar la serie. Nombre (`\"<evento> — <categoría>\"`), deporte, descripción, lugar y club se heredan; fechas y precio también cuando la variante no los manda. Una (género, categoría) que ya existe en el evento es 409 TOURNAMENT_CATEGORY_EXISTS; una repetida dentro del body, 400 VALIDATION (mismo criterio que el alta). Un torneo legacy sin evento pasa a ser el evento (se le asigna su propio id). No renombra ni borra categorías (`DELETE /tournaments/{id}`). Audit TOURNAMENT_CREATED por fila. Consume cuota de escritura.","x-skalia-scope":"tournaments:write","x-skalia-write":true,"parameters":[{"$ref":"#/components/parameters/RequestId"},{"name":"id","in":"path","required":true,"description":"Id de la categoría (`TournamentDto.id`).","schema":{"type":"string","pattern":"^[a-f0-9-]{8,64}$"}}],"requestBody":{"required":true,"description":"JSON obligatorio (`Content-Type: application/json`; si no, 415).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TournamentCategoriesBody"}}}},"responses":{"201":{"description":"Creadas: sólo las categorías nuevas.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TournamentCategoriesResponse"}}}},"400":{"description":"Request mal formada (credencial con forma rara, body / query inválidos, `clubId` faltante con varios grants).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MALFORMED_API_KEY","VALIDATION"]},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Integración deshabilitada, módulo API apagado en el club, club sin grant o scope insuficiente (`details.scope`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CLIENT_DISABLED","MODULE_DISABLED","CLUB_NOT_GRANTED","SCOPE_DENIED"]},"404":{"description":"El torneo no existe o es de un club sin grant (no se confirma nada).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["TOURNAMENT_NOT_FOUND"]},"409":{"description":"La (género, categoría) ya existe en el evento (`details.gender` / `category` / `existingTournamentId`), o el evento ya terminó (`details.from` / `details.to = OPEN`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["TOURNAMENT_CATEGORY_EXISTS","TOURNAMENT_INVALID_TRANSITION"]},"415":{"$ref":"#/components/responses/UnsupportedMediaType"},"429":{"$ref":"#/components/responses/RateLimitedWrite"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}}},"/tournaments/{id}/finalize":{"post":{"operationId":"finalizeTournament","tags":["Torneos"],"summary":"Finalizar una categoría","description":"Sin body. El botón \"Finalizar\" del panel: exige ELIMINATION_PHASE con la final jugada; escribe el podio (1 campeón, 2 subcampeón, 3 los dos semifinalistas), acredita los puntos Skalia por posición (idempotentes por (torneo, jugador)), habilita los premios configurados y avisa a los ganadores (en background), cura mirrors faltantes y deja la categoría COMPLETED con las parejas que no ganaron en ELIMINATED. Idempotente: repetirlo sobre una COMPLETED responde 200 con el podio que tiene, sin volver a otorgar nada. Audit TOURNAMENT_FINALIZED. Nunca datos de las personas que integran una pareja. Consume cuota de escritura.","x-skalia-scope":"tournaments:write","x-skalia-write":true,"parameters":[{"$ref":"#/components/parameters/RequestId"},{"name":"id","in":"path","required":true,"description":"Id de la categoría (`TournamentDto.id`).","schema":{"type":"string","pattern":"^[a-f0-9-]{8,64}$"}}],"responses":{"200":{"description":"Cerrada (o ya lo estaba): la categoría y su podio.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TournamentFinalizeResponse"}}}},"400":{"description":"Request mal formada (credencial con forma rara, body / query inválidos, `clubId` faltante con varios grants). No está en ELIMINATION_PHASE, o la final no está COMPLETED (guard del panel).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MALFORMED_API_KEY","TOURNAMENT_RULE_VIOLATION"]},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Integración deshabilitada, módulo API apagado en el club, club sin grant o scope insuficiente (`details.scope`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CLIENT_DISABLED","MODULE_DISABLED","CLUB_NOT_GRANTED","SCOPE_DENIED"]},"404":{"description":"El torneo no existe o es de un club sin grant (no se confirma nada).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["TOURNAMENT_NOT_FOUND"]},"409":{"description":"La categoría está CANCELLED (`details.from` / `details.to = COMPLETED`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["TOURNAMENT_INVALID_TRANSITION"]},"429":{"$ref":"#/components/responses/RateLimitedWrite"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}}},"/tournaments/{id}/generate-bracket":{"post":{"operationId":"generateTournamentBracket","tags":["Torneos"],"summary":"Generar el cuadro de eliminatorias","description":"Clasifican las 2 mejores de cada grupo (o `qualifiedTeamIds`, que tienen que ser parejas de ESTA categoría: un id ajeno es 400 VALIDATION con `details.unknownTeamIds`, nunca se ignora), se siembran mejor contra peor y el torneo pasa a ELIMINATION_PHASE. Sólo desde SCHEDULE_BUILDING / GROUP_PHASE. Para rehacer un cuadro ya generado: `PATCH { status: GROUP_PHASE }` (con `force: true` si hay eliminatorias jugadas) y volver a llamar. Consume cuota de escritura.","x-skalia-scope":"tournaments:write","x-skalia-write":true,"parameters":[{"$ref":"#/components/parameters/RequestId"},{"name":"id","in":"path","required":true,"description":"Id de la categoría (`TournamentDto.id`).","schema":{"type":"string","pattern":"^[a-f0-9-]{8,64}$"}}],"requestBody":{"required":true,"description":"JSON obligatorio (`Content-Type: application/json`; si no, 415).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerateBracketBody"}}}},"responses":{"200":{"description":"Partidos del cuadro.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MatchesResponse"}}}},"400":{"description":"Request mal formada (credencial con forma rara, body / query inválidos, `clubId` faltante con varios grants). Status que no admite generar el cuadro (`details.status`), no hay clasificados suficientes, o `qualifiedTeamIds` con ids que no son parejas de la categoría (VALIDATION, `details.unknownTeamIds`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MALFORMED_API_KEY","VALIDATION","TOURNAMENT_STATUS_INVALID","TOURNAMENT_RULE_VIOLATION"]},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Integración deshabilitada, módulo API apagado en el club, club sin grant o scope insuficiente (`details.scope`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CLIENT_DISABLED","MODULE_DISABLED","CLUB_NOT_GRANTED","SCOPE_DENIED"]},"404":{"description":"El torneo no existe o es de un club sin grant (no se confirma nada).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["TOURNAMENT_NOT_FOUND"]},"409":{"description":"El cuadro ya existe: `PATCH { status: GROUP_PHASE }` antes de regenerarlo.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["BRACKET_ALREADY_GENERATED"]},"415":{"$ref":"#/components/responses/UnsupportedMediaType"},"429":{"$ref":"#/components/responses/RateLimitedWrite"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}}},"/tournaments/{id}/generate-groups":{"post":{"operationId":"generateTournamentGroups","tags":["Torneos"],"summary":"Sortear los grupos","description":"Mismo handler que el panel: una cabeza de serie por grupo (por nivel promedio), el resto por afinidad horaria si `autoFill`; arma los partidos de cada grupo y deja el torneo en SCHEDULE_BUILDING. Regenerar borra grupos, partidos y sus reservas (emisor único). Exige al menos 3 parejas completas. Un torneo COMPLETED / CANCELLED no se regenera ni con `force`. Consume cuota de escritura.","x-skalia-scope":"tournaments:write","x-skalia-write":true,"parameters":[{"$ref":"#/components/parameters/RequestId"},{"name":"id","in":"path","required":true,"description":"Id de la categoría (`TournamentDto.id`).","schema":{"type":"string","pattern":"^[a-f0-9-]{8,64}$"}}],"requestBody":{"required":true,"description":"JSON obligatorio (`Content-Type: application/json`; si no, 415).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerateGroupsBody"}}}},"responses":{"200":{"description":"Grupos generados (con `warnings` si hubo que forzar algo).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GroupsResponse"}}}},"400":{"description":"Request mal formada (credencial con forma rara, body / query inválidos, `clubId` faltante con varios grants). Menos de 3 parejas completas.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MALFORMED_API_KEY","VALIDATION","TOURNAMENT_RULE_VIOLATION"]},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Integración deshabilitada, módulo API apagado en el club, club sin grant o scope insuficiente (`details.scope`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CLIENT_DISABLED","MODULE_DISABLED","CLUB_NOT_GRANTED","SCOPE_DENIED"]},"404":{"description":"El torneo no existe o es de un club sin grant (no se confirma nada).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["TOURNAMENT_NOT_FOUND"]},"409":{"description":"Parejas duplicadas (`details.teamIds`), partidos ya jugados sin `force` (`details.playedMatches`), o torneo terminal (`details.from` / `details.to`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["DUPLICATE_PAIRS","TOURNAMENT_ALREADY_PLAYED","TOURNAMENT_INVALID_TRANSITION"]},"415":{"$ref":"#/components/responses/UnsupportedMediaType"},"429":{"$ref":"#/components/responses/RateLimitedWrite"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}}},"/tournaments/{id}/generate-matches":{"post":{"operationId":"generateTournamentMatches","tags":["Torneos"],"summary":"Regenerar los partidos de grupos","description":"Rehace los partidos de la fase de grupos a partir de los grupos actuales (2 parejas → 1 partido; 3 → cadena \"ganador queda en cancha\"; 4+ → round-robin) y borra los anteriores con sus reservas. Bloqueado si ya hay partidos de grupo jugados o si el torneo es terminal. Consume cuota de escritura.","x-skalia-scope":"tournaments:write","x-skalia-write":true,"parameters":[{"$ref":"#/components/parameters/RequestId"},{"name":"id","in":"path","required":true,"description":"Id de la categoría (`TournamentDto.id`).","schema":{"type":"string","pattern":"^[a-f0-9-]{8,64}$"}}],"responses":{"200":{"description":"Partidos de grupo.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MatchesResponse"}}}},"400":{"description":"Request mal formada (credencial con forma rara, body / query inválidos, `clubId` faltante con varios grants). No hay grupos.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MALFORMED_API_KEY","TOURNAMENT_RULE_VIOLATION"]},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Integración deshabilitada, módulo API apagado en el club, club sin grant o scope insuficiente (`details.scope`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CLIENT_DISABLED","MODULE_DISABLED","CLUB_NOT_GRANTED","SCOPE_DENIED"]},"404":{"description":"El torneo no existe o es de un club sin grant (no se confirma nada).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["TOURNAMENT_NOT_FOUND"]},"409":{"description":"Ya hay partidos de grupo jugados, o torneo terminal (`details.from` / `details.to`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["TOURNAMENT_ALREADY_PLAYED","TOURNAMENT_INVALID_TRANSITION"]},"429":{"$ref":"#/components/responses/RateLimitedWrite"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}}},"/tournaments/{id}/matches":{"get":{"operationId":"listTournamentMatches","tags":["Torneos"],"summary":"Partidos de un torneo","description":"Grupos y cuadro, con las parejas por id y etiqueta anónima, fase, ronda, cancha, horario y sets. Nunca datos de las personas que integran una pareja.","x-skalia-scope":"tournaments:read","x-skalia-write":false,"parameters":[{"$ref":"#/components/parameters/RequestId"},{"name":"id","in":"path","required":true,"description":"Id de la categoría (`TournamentDto.id`).","schema":{"type":"string","pattern":"^[a-f0-9-]{8,64}$"}},{"name":"phase","in":"query","required":false,"description":"Filtra por fase (`GROUP`, `R16`, `QF`, `SF`, `FINAL`).","schema":{"type":"string","enum":["GROUP","R16","QF","SF","FINAL"]}}],"responses":{"200":{"description":"Partidos.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MatchesResponse"}}}},"400":{"description":"Request mal formada (credencial con forma rara, body / query inválidos, `clubId` faltante con varios grants).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MALFORMED_API_KEY","VALIDATION"]},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Integración deshabilitada, módulo API apagado en el club, club sin grant o scope insuficiente (`details.scope`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CLIENT_DISABLED","MODULE_DISABLED","CLUB_NOT_GRANTED","SCOPE_DENIED"]},"404":{"description":"El torneo no existe o es de un club sin grant (no se confirma nada).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["TOURNAMENT_NOT_FOUND"]},"429":{"$ref":"#/components/responses/RateLimitedRead"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}}},"/tournaments/{id}/matches/{matchId}/result":{"patch":{"operationId":"saveTournamentMatchResult","tags":["Torneos"],"summary":"Cargar el resultado de un partido","description":"Mismos guards que el panel: torneo no terminado, parejas completas, sets sin empate, ganador coherente, no pisar partidos dependientes ya jugados. Propaga la cadena \"ganador queda en cancha\", avanza el cuadro, aplica el nivel Skalia de los 4 jugadores (salvo W.O.), notifica y, si cerró la fase de grupos, genera el cuadro solo. Consume cuota de escritura.","x-skalia-scope":"tournaments:write","x-skalia-write":true,"parameters":[{"$ref":"#/components/parameters/RequestId"},{"name":"id","in":"path","required":true,"description":"Id de la categoría (`TournamentDto.id`).","schema":{"type":"string","pattern":"^[a-f0-9-]{8,64}$"}},{"name":"matchId","in":"path","required":true,"description":"Id del partido (`TournamentMatchDto.id`), del torneo de la URL.","schema":{"type":"string","pattern":"^[a-f0-9-]{8,64}$"}}],"requestBody":{"required":true,"description":"JSON obligatorio (`Content-Type: application/json`; si no, 415).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MatchResultBody"}}}},"responses":{"200":{"description":"El partido con su resultado.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MatchResultResponse"}}}},"400":{"description":"Request mal formada (credencial con forma rara, body / query inválidos, `clubId` faltante con varios grants). Además: torneo terminado, sets empatados, ganador que no coincide, W.O. mal indicado.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MALFORMED_API_KEY","VALIDATION","TOURNAMENT_RULE_VIOLATION"]},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Integración deshabilitada, módulo API apagado en el club, club sin grant o scope insuficiente (`details.scope`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CLIENT_DISABLED","MODULE_DISABLED","CLUB_NOT_GRANTED","SCOPE_DENIED"]},"404":{"description":"El torneo no existe / es de un club sin grant, o el partido no es de ese torneo.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["TOURNAMENT_NOT_FOUND","TOURNAMENT_MATCH_NOT_FOUND"]},"409":{"description":"Una pareja del partido sin compañero (`details.teamIds`), o el cambio de ganador pisaría partidos ya jugados (`details.matchIds`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["INCOMPLETE_PAIR_IN_MATCH","CHILD_MATCH_ALREADY_PLAYED"]},"415":{"$ref":"#/components/responses/UnsupportedMediaType"},"429":{"$ref":"#/components/responses/RateLimitedWrite"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}}},"/tournaments/{id}/matches/{matchId}/schedule":{"patch":{"operationId":"scheduleTournamentMatch","tags":["Torneos"],"summary":"Programar cancha y horario","description":"La cancha tiene que ser del club del torneo. Después es el handler del panel: reglas de la fase de grupos (dos partidos del mismo grupo no comparten horario; una pareja descansa al menos 1 h), libera las reservas comunes que pisen el slot, crea o mueve la reserva del partido (emisor único: `booking.*` con actor `tournament` + `occupancy.changed` con actor `api`) y auto-programa los hijos de la cadena en la misma cancha. `null` en los dos desprograma. Consume cuota de escritura.","x-skalia-scope":"tournaments:write","x-skalia-write":true,"parameters":[{"$ref":"#/components/parameters/RequestId"},{"name":"id","in":"path","required":true,"description":"Id de la categoría (`TournamentDto.id`).","schema":{"type":"string","pattern":"^[a-f0-9-]{8,64}$"}},{"name":"matchId","in":"path","required":true,"description":"Id del partido (`TournamentMatchDto.id`), del torneo de la URL.","schema":{"type":"string","pattern":"^[a-f0-9-]{8,64}$"}}],"requestBody":{"required":true,"description":"JSON obligatorio (`Content-Type: application/json`; si no, 415).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MatchScheduleBody"}}}},"responses":{"200":{"description":"El partido programado.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MatchResponse"}}}},"400":{"description":"Request mal formada (credencial con forma rara, body / query inválidos, `clubId` faltante con varios grants). Además: torneo terminado.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MALFORMED_API_KEY","VALIDATION","TOURNAMENT_RULE_VIOLATION"]},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Integración deshabilitada, módulo API apagado en el club, club sin grant o scope insuficiente (`details.scope`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CLIENT_DISABLED","MODULE_DISABLED","CLUB_NOT_GRANTED","SCOPE_DENIED"]},"404":{"description":"El torneo no existe / es de un club sin grant, o el partido no es de ese torneo. O la cancha no es del club del torneo.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["TOURNAMENT_NOT_FOUND","TOURNAMENT_MATCH_NOT_FOUND","COURT_NOT_FOUND"]},"409":{"description":"Otro partido del grupo en ese horario o una pareja sin descanso, un partido de OTRO torneo en la cancha, o el recurso no es una cancha.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["TOURNAMENT_SCHEDULE_CONFLICT","BOOKING_CONFLICT","RESOURCE_NOT_COURT"]},"415":{"$ref":"#/components/responses/UnsupportedMediaType"},"429":{"$ref":"#/components/responses/RateLimitedWrite"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}}},"/tournaments/{id}/standings":{"get":{"operationId":"getTournamentStandings","tags":["Torneos"],"summary":"Tabla de posiciones por grupo","description":"Mismo cálculo que la app (ganados → diferencia de sets → diferencia de games; clasifican las 2 primeras). Nunca datos de las personas que integran una pareja.","x-skalia-scope":"tournaments:read","x-skalia-write":false,"parameters":[{"$ref":"#/components/parameters/RequestId"},{"name":"id","in":"path","required":true,"description":"Id de la categoría (`TournamentDto.id`).","schema":{"type":"string","pattern":"^[a-f0-9-]{8,64}$"}}],"responses":{"200":{"description":"Tablas.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StandingsResponse"}}}},"400":{"description":"Request mal formada (credencial con forma rara, body / query inválidos, `clubId` faltante con varios grants).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MALFORMED_API_KEY"]},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Integración deshabilitada, módulo API apagado en el club, club sin grant o scope insuficiente (`details.scope`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CLIENT_DISABLED","MODULE_DISABLED","CLUB_NOT_GRANTED","SCOPE_DENIED"]},"404":{"description":"El torneo no existe o es de un club sin grant (no se confirma nada).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["TOURNAMENT_NOT_FOUND"]},"429":{"$ref":"#/components/responses/RateLimitedRead"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}}},"/tournaments/{id}/suggest-schedule":{"post":{"operationId":"suggestTournamentSchedule","tags":["Torneos"],"summary":"Sugerir horarios por grupo","description":"Mismo cálculo del panel: grupos en paralelo hasta la cantidad de canchas del club, 3 partidos consecutivos por grupo, por franja de preferencia. No escribe nada (después programás cada partido con `/matches/{matchId}/schedule`). Consume cuota de escritura por ser POST.","x-skalia-scope":"tournaments:write","x-skalia-write":true,"parameters":[{"$ref":"#/components/parameters/RequestId"},{"name":"id","in":"path","required":true,"description":"Id de la categoría (`TournamentDto.id`).","schema":{"type":"string","pattern":"^[a-f0-9-]{8,64}$"}}],"requestBody":{"required":true,"description":"JSON obligatorio (`Content-Type: application/json`; si no, 415).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuggestScheduleBody"}}}},"responses":{"200":{"description":"Sugerencia por grupo.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduleSuggestionResponse"}}}},"400":{"description":"Request mal formada (credencial con forma rara, body / query inválidos, `clubId` faltante con varios grants). No hay grupos generados.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MALFORMED_API_KEY","VALIDATION","TOURNAMENT_RULE_VIOLATION"]},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Integración deshabilitada, módulo API apagado en el club, club sin grant o scope insuficiente (`details.scope`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CLIENT_DISABLED","MODULE_DISABLED","CLUB_NOT_GRANTED","SCOPE_DENIED"]},"404":{"description":"El torneo no existe o es de un club sin grant (no se confirma nada).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["TOURNAMENT_NOT_FOUND"]},"415":{"$ref":"#/components/responses/UnsupportedMediaType"},"429":{"$ref":"#/components/responses/RateLimitedWrite"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}}},"/tournaments/{id}/teams/{teamId}":{"patch":{"operationId":"updateTournamentTeam","tags":["Torneos"],"summary":"Cobro, nombre de equipo o baja de una pareja","description":"Lo que un POS necesita sin abrir el panel: marcar `paid`, poner o quitar el nombre de equipo, dar de baja (`WITHDRAWN`, avisa a los integrantes como lo hace la app) o reingresar (`REGISTERED`, si el cupo lo permite). Una pareja con partidos jugados no se da de baja; en un torneo terminal o sobre una ELIMINATED el estado no cambia (`paid` y `teamName` sí se editan siempre). Repetir el mismo `status` es un no-op sin avisos. Audit TOURNAMENT_TEAM_UPDATED (`details.fields`, sin nombres). Nunca datos de las personas que integran una pareja. Consume cuota de escritura.","x-skalia-scope":"tournaments:write","x-skalia-write":true,"parameters":[{"$ref":"#/components/parameters/RequestId"},{"name":"id","in":"path","required":true,"description":"Id de la categoría (`TournamentDto.id`).","schema":{"type":"string","pattern":"^[a-f0-9-]{8,64}$"}},{"name":"teamId","in":"path","required":true,"description":"Id de la pareja (`TeamDto.id`), de la categoría de la URL.","schema":{"type":"string","pattern":"^[a-f0-9-]{8,64}$"}}],"requestBody":{"required":true,"description":"JSON obligatorio (`Content-Type: application/json`; si no, 415).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TeamPatchBody"}}}},"responses":{"200":{"description":"La pareja actualizada.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TeamResponse"}}}},"400":{"description":"Request mal formada (credencial con forma rara, body / query inválidos, `clubId` faltante con varios grants).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MALFORMED_API_KEY","VALIDATION"]},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Integración deshabilitada, módulo API apagado en el club, club sin grant o scope insuficiente (`details.scope`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CLIENT_DISABLED","MODULE_DISABLED","CLUB_NOT_GRANTED","SCOPE_DENIED"]},"404":{"description":"El torneo no existe o es de un club sin grant (no se confirma nada). O la pareja no es de esa categoría.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["TOURNAMENT_NOT_FOUND","TOURNAMENT_TEAM_NOT_FOUND"]},"409":{"description":"WITHDRAWN con partidos jugados (`details.playedMatches`); REGISTERED sin cupo (`details.maxTeams` / `registeredTeams`); cambio de estado en un torneo terminal o sobre una ELIMINATED (`details.reason`: `tournament_terminal` / `team_eliminated`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["TOURNAMENT_ALREADY_PLAYED","TOURNAMENT_RULE_VIOLATION","TOURNAMENT_INVALID_TRANSITION"]},"415":{"$ref":"#/components/responses/UnsupportedMediaType"},"429":{"$ref":"#/components/responses/RateLimitedWrite"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}}},"/waitlists/demand":{"get":{"operationId":"getWaitlistDemand","tags":["Agenda"],"summary":"Demanda insatisfecha por franja","description":"Cuántos jugadores DISTINTOS esperan que se libere cada franja del club (lista de espera de turnos: día, comienzo, duración, deporte, categoría), en `[from, to]` (1 a 14 días). Es el dato para abrir una cancha más, mover una fija o decidir tarifas; no es operativo (cuando un turno se libera Skalia avisa sola). Nunca quiénes esperan ni qué ocupaba el turno. Con la lista de espera apagada en la plataforma responde `items: []`. Sin ETag.","x-skalia-scope":"availability:read","x-skalia-write":false,"parameters":[{"$ref":"#/components/parameters/RequestId"},{"name":"clubId","in":"query","required":false,"description":"Club sobre el que opera la request. Opcional: se infiere cuando la integración tiene un solo grant (kind CLUB, v1).","schema":{"type":"string","pattern":"^[a-f0-9-]{8,64}$"}},{"name":"from","in":"query","required":false,"description":"Primer día (`YYYY-MM-DD` naive del club). Default: hoy del club.","schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"example":"2026-09-14"},{"name":"to","in":"query","required":false,"description":"Último día (inclusive). Default: `from` + 7 días. Rango de 1 a 14 días inclusive (`to >= from` y `to <= from + 13`).","schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"example":"2026-09-21"},{"name":"categoryId","in":"query","required":false,"description":"Categoría de recurso (F6) cuya demanda mirar. Ausente = canchas.","schema":{"type":"string","pattern":"^[a-f0-9-]{8,64}$"}}],"responses":{"200":{"description":"Demanda por franja.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WaitlistDemandResponse"}}}},"400":{"description":"Request mal formada (credencial con forma rara, body / query inválidos, `clubId` faltante con varios grants). Además: rango invertido o de más de 14 días (`details.issues[0].path = \"to\"`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MALFORMED_API_KEY","VALIDATION","CLUB_REQUIRED"]},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Integración deshabilitada, módulo API apagado en el club, club sin grant o scope insuficiente (`details.scope`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CLIENT_DISABLED","MODULE_DISABLED","CLUB_NOT_GRANTED","SCOPE_DENIED"]},"429":{"$ref":"#/components/responses/RateLimitedRead"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}}},"/webhooks":{"get":{"operationId":"listWebhooks","tags":["Webhooks"],"summary":"Listar webhooks","description":"Los webhooks de la integración (máximo 5). Un webhook recibe los eventos de todos los clubes con grant de la integración. Nunca devuelve el secreto.","x-skalia-scope":"webhooks:manage","x-skalia-write":false,"parameters":[{"$ref":"#/components/parameters/RequestId"},{"name":"clubId","in":"query","required":false,"description":"Club sobre el que opera la request. Opcional: se infiere cuando la integración tiene un solo grant (kind CLUB, v1).","schema":{"type":"string","pattern":"^[a-f0-9-]{8,64}$"}}],"responses":{"200":{"description":"Los webhooks.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhooksListResponse"}}}},"400":{"description":"Request mal formada (credencial con forma rara, body / query inválidos, `clubId` faltante con varios grants).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MALFORMED_API_KEY","VALIDATION","CLUB_REQUIRED"]},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Integración deshabilitada, módulo API apagado en el club, club sin grant o scope insuficiente (`details.scope`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CLIENT_DISABLED","MODULE_DISABLED","CLUB_NOT_GRANTED","SCOPE_DENIED"]},"429":{"$ref":"#/components/responses/RateLimitedRead"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}},"post":{"operationId":"createWebhook","tags":["Webhooks"],"summary":"Registrar un webhook","description":"Alta de un receptor. La URL tiene que ser https y resolver a una dirección pública (se valida al registrar y en CADA entrega; un DNS que después apunte a una red privada hace fallar la entrega, no la desvía). La respuesta trae el secreto `whsec_…` UNA sola vez. Cada entrega es un POST JSON con `X-Skalia-Event`, `X-Skalia-Event-Id`, `X-Skalia-Delivery-Id` y `X-Skalia-Signature: t=<unix>,v1=<hmac-sha256(secret, t + \".\" + body)>`; respondé 2xx en menos de 5 s. Reintentos con backoff 1 min, 5 min, 30 min, 2 h, 12 h; al 6.º fallo la entrega queda DEAD; 25 fallos seguidos apagan el webhook. Consume cuota de escritura.","x-skalia-scope":"webhooks:manage","x-skalia-write":true,"parameters":[{"$ref":"#/components/parameters/RequestId"}],"requestBody":{"required":true,"description":"JSON obligatorio (`Content-Type: application/json`; si no, 415).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookCreateBody"}}}},"responses":{"201":{"description":"Creado. `secret` viaja sólo acá.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookCreatedResponse"}}}},"400":{"description":"Request mal formada (credencial con forma rara, body / query inválidos, `clubId` faltante con varios grants). Además: URL rechazada (`details.reason`: `not_https`, `has_credentials`, `private_address`, `unresolvable`, `local_hostname`, `malformed`, `too_long`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MALFORMED_API_KEY","VALIDATION","CLUB_REQUIRED","WEBHOOK_URL_INVALID"]},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Integración deshabilitada, módulo API apagado en el club, club sin grant o scope insuficiente (`details.scope`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CLIENT_DISABLED","MODULE_DISABLED","CLUB_NOT_GRANTED","SCOPE_DENIED"]},"409":{"description":"Ya hay 5 webhooks (`details.max`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["WEBHOOK_LIMIT"]},"415":{"$ref":"#/components/responses/UnsupportedMediaType"},"429":{"$ref":"#/components/responses/RateLimitedWrite"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"description":"Kill switch global de la Partner API apagado (tarda ≤ 1 min en propagarse), o el ambiente no tiene la clave de cifrado de secretos de webhooks.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["PARTNER_API_DISABLED","WEBHOOKS_UNAVAILABLE"]}}}},"/webhooks/{id}":{"patch":{"operationId":"updateWebhook","tags":["Webhooks"],"summary":"Editar un webhook","description":"Cambiar la URL (se re-valida), los eventos o el estado. `status: DISABLED` deja de entregar y las pendientes esperan; `ACTIVE` lo reactiva, resetea la racha de fallos y el cron retoma lo pendiente. Consume cuota de escritura.","x-skalia-scope":"webhooks:manage","x-skalia-write":true,"parameters":[{"$ref":"#/components/parameters/RequestId"},{"name":"id","in":"path","required":true,"description":"Id del webhook.","schema":{"type":"string","pattern":"^[a-f0-9-]{8,64}$"}}],"requestBody":{"required":true,"description":"JSON obligatorio (`Content-Type: application/json`; si no, 415).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookPatchBody"}}}},"responses":{"200":{"description":"El webhook actualizado y qué cambió.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookPatchResponse"}}}},"400":{"description":"Request mal formada (credencial con forma rara, body / query inválidos, `clubId` faltante con varios grants). Además: URL rechazada (`details.reason`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MALFORMED_API_KEY","VALIDATION","WEBHOOK_URL_INVALID"]},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Integración deshabilitada, módulo API apagado en el club, club sin grant o scope insuficiente (`details.scope`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CLIENT_DISABLED","MODULE_DISABLED","CLUB_NOT_GRANTED","SCOPE_DENIED"]},"404":{"description":"El webhook no existe o es de otra integración (no se confirma nada).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["WEBHOOK_NOT_FOUND"]},"415":{"$ref":"#/components/responses/UnsupportedMediaType"},"429":{"$ref":"#/components/responses/RateLimitedWrite"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}},"delete":{"operationId":"deleteWebhook","tags":["Webhooks"],"summary":"Borrar un webhook","description":"Borra el webhook y su historial de entregas. Consume cuota de escritura.","x-skalia-scope":"webhooks:manage","x-skalia-write":true,"parameters":[{"$ref":"#/components/parameters/RequestId"},{"name":"id","in":"path","required":true,"description":"Id del webhook.","schema":{"type":"string","pattern":"^[a-f0-9-]{8,64}$"}}],"responses":{"200":{"description":"Borrado.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookDeleteResponse"}}}},"400":{"description":"Request mal formada (credencial con forma rara, body / query inválidos, `clubId` faltante con varios grants).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MALFORMED_API_KEY"]},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Integración deshabilitada, módulo API apagado en el club, club sin grant o scope insuficiente (`details.scope`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CLIENT_DISABLED","MODULE_DISABLED","CLUB_NOT_GRANTED","SCOPE_DENIED"]},"404":{"description":"El webhook no existe o es de otra integración (no se confirma nada).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["WEBHOOK_NOT_FOUND"]},"429":{"$ref":"#/components/responses/RateLimitedWrite"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}}},"/webhooks/{id}/deliveries":{"get":{"operationId":"listWebhookDeliveries","tags":["Webhooks"],"summary":"Historial de entregas","description":"Las entregas del webhook, más recientes primero, con el último status HTTP y el último error. Retención 30 días.","x-skalia-scope":"webhooks:manage","x-skalia-write":false,"parameters":[{"$ref":"#/components/parameters/RequestId"},{"name":"id","in":"path","required":true,"description":"Id del webhook.","schema":{"type":"string","pattern":"^[a-f0-9-]{8,64}$"}},{"name":"status","in":"query","required":false,"description":"Filtra por estado de la entrega.","schema":{"type":"string","enum":["PENDING","DELIVERED","FAILED","DEAD"]}},{"name":"limit","in":"query","required":false,"description":"Entregas por página, 1..200 (default 50).","schema":{"type":"integer","minimum":1,"maximum":200}},{"name":"offset","in":"query","required":false,"description":"Desplazamiento (paginación por offset).","schema":{"type":"integer","minimum":0}}],"responses":{"200":{"description":"Página de entregas.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookDeliveriesResponse"}}}},"400":{"description":"Request mal formada (credencial con forma rara, body / query inválidos, `clubId` faltante con varios grants).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MALFORMED_API_KEY","VALIDATION"]},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Integración deshabilitada, módulo API apagado en el club, club sin grant o scope insuficiente (`details.scope`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CLIENT_DISABLED","MODULE_DISABLED","CLUB_NOT_GRANTED","SCOPE_DENIED"]},"404":{"description":"El webhook no existe o es de otra integración (no se confirma nada).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["WEBHOOK_NOT_FOUND"]},"429":{"$ref":"#/components/responses/RateLimitedRead"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}}},"/webhooks/{id}/deliveries/{deliveryId}/redeliver":{"post":{"operationId":"redeliverWebhookDelivery","tags":["Webhooks"],"summary":"Reenviar una entrega","description":"Vuelve a mandar el MISMO cuerpo (firma nueva) en el acto y devuelve el resultado. Sirve para DELIVERED (el receptor lo perdió), FAILED y DEAD; si vuelve a fallar sigue el ciclo normal. Consume cuota de escritura.","x-skalia-scope":"webhooks:manage","x-skalia-write":true,"parameters":[{"$ref":"#/components/parameters/RequestId"},{"name":"id","in":"path","required":true,"description":"Id del webhook.","schema":{"type":"string","pattern":"^[a-f0-9-]{8,64}$"}},{"name":"deliveryId","in":"path","required":true,"description":"Id de la entrega (`X-Skalia-Delivery-Id`).","schema":{"type":"string","pattern":"^[a-f0-9-]{8,64}$"}}],"responses":{"200":{"description":"La entrega tras el reintento.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookDeliveryResponse"}}}},"400":{"description":"Request mal formada (credencial con forma rara, body / query inválidos, `clubId` faltante con varios grants).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MALFORMED_API_KEY"]},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Integración deshabilitada, módulo API apagado en el club, club sin grant o scope insuficiente (`details.scope`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CLIENT_DISABLED","MODULE_DISABLED","CLUB_NOT_GRANTED","SCOPE_DENIED"]},"404":{"description":"El webhook no existe o es de otra integración, o la entrega no es de ese webhook.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["WEBHOOK_NOT_FOUND","WEBHOOK_DELIVERY_NOT_FOUND"]},"409":{"description":"El webhook está DISABLED.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["WEBHOOK_DISABLED"]},"429":{"$ref":"#/components/responses/RateLimitedWrite"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}}},"/webhooks/{id}/rotate-secret":{"post":{"operationId":"rotateWebhookSecret","tags":["Webhooks"],"summary":"Rotar el secreto","description":"Genera un secreto nuevo; el anterior deja de firmar EN EL ACTO (no hay gracia: actualizá el receptor antes de llamar, o aceptá unas entregas con firma inválida y reenvialas). Se devuelve una sola vez. Consume cuota de escritura.","x-skalia-scope":"webhooks:manage","x-skalia-write":true,"parameters":[{"$ref":"#/components/parameters/RequestId"},{"name":"id","in":"path","required":true,"description":"Id del webhook.","schema":{"type":"string","pattern":"^[a-f0-9-]{8,64}$"}}],"responses":{"201":{"description":"Secreto nuevo (sólo acá).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookCreatedResponse"}}}},"400":{"description":"Request mal formada (credencial con forma rara, body / query inválidos, `clubId` faltante con varios grants).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MALFORMED_API_KEY"]},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Integración deshabilitada, módulo API apagado en el club, club sin grant o scope insuficiente (`details.scope`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CLIENT_DISABLED","MODULE_DISABLED","CLUB_NOT_GRANTED","SCOPE_DENIED"]},"404":{"description":"El webhook no existe o es de otra integración (no se confirma nada).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["WEBHOOK_NOT_FOUND"]},"429":{"$ref":"#/components/responses/RateLimitedWrite"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"description":"Kill switch global de la Partner API apagado (tarda ≤ 1 min en propagarse), o el ambiente no tiene la clave de cifrado de secretos de webhooks.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["PARTNER_API_DISABLED","WEBHOOKS_UNAVAILABLE"]}}}},"/webhooks/{id}/test":{"post":{"operationId":"testWebhook","tags":["Webhooks"],"summary":"Mandar un evento de prueba","description":"Manda en el acto un evento `webhook.test` firmado con el secreto vigente y devuelve la entrega con `lastStatus` / `lastError`. No se reintenta ni cuenta para apagar el webhook. Consume cuota de escritura.","x-skalia-scope":"webhooks:manage","x-skalia-write":true,"parameters":[{"$ref":"#/components/parameters/RequestId"},{"name":"id","in":"path","required":true,"description":"Id del webhook.","schema":{"type":"string","pattern":"^[a-f0-9-]{8,64}$"}}],"responses":{"200":{"description":"La entrega de prueba (mirá `status`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookDeliveryResponse"}}}},"400":{"description":"Request mal formada (credencial con forma rara, body / query inválidos, `clubId` faltante con varios grants).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MALFORMED_API_KEY"]},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Integración deshabilitada, módulo API apagado en el club, club sin grant o scope insuficiente (`details.scope`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["CLIENT_DISABLED","MODULE_DISABLED","CLUB_NOT_GRANTED","SCOPE_DENIED"]},"404":{"description":"El webhook no existe o es de otra integración (no se confirma nada).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["WEBHOOK_NOT_FOUND"]},"409":{"description":"El webhook está DISABLED.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["WEBHOOK_DISABLED"]},"429":{"$ref":"#/components/responses/RateLimitedWrite"},"500":{"$ref":"#/components/responses/InternalError"},"503":{"$ref":"#/components/responses/ServiceUnavailable"}}}}},"components":{"securitySchemes":{"apiKey":{"type":"http","scheme":"bearer","bearerFormat":"sk_live_… / sk_test_…","description":"API key de club emitida desde el panel (se ve una sola vez). `sk_live_` sólo contra producción y `sk_test_` sólo contra el sandbox (si no, 401 API_KEY_WRONG_ENV). Los scopes de la operación van en `x-skalia-scope`."}},"headers":{"ETag":{"description":"ETag fuerte (hash del cuerpo). Mandalo en `If-None-Match` para recibir 304 si nada cambió.","schema":{"type":"string"}},"Idempotent-Replay":{"description":"`true`: el POST devolvió la reserva que ya existía con ese `externalRef` (mismo turno).","schema":{"type":"string","enum":["true"]}},"Location":{"description":"Ruta de la reserva creada (`/api/partner/v1/bookings/<id>`).","schema":{"type":"string"}},"Retry-After":{"description":"Segundos a esperar antes de reintentar (sólo en un 429).","schema":{"type":"integer"}},"X-RateLimit-Limit":{"description":"Cuota por minuto de la integración (o la que frenó, en un 429). Presente desde que la key se resolvió.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests que quedan en la ventana.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Epoch en SEGUNDOS en que la ventana se renueva.","schema":{"type":"integer"}},"X-Request-Id":{"description":"Id de la request (eco del tuyo si lo mandaste con forma sana, o un UUID). Es lo que soporte busca.","schema":{"type":"string"}}},"parameters":{"BookingId":{"name":"id","in":"path","required":true,"description":"Id de la reserva.","schema":{"type":"string","pattern":"^[a-f0-9-]{8,64}$"}},"IdempotencyKey":{"name":"Idempotency-Key","in":"header","required":false,"description":"Alias de `externalRef` en el body (mismas reglas: ≤ 64 chars imprimibles, de un solo uso).","schema":{"type":"string","minLength":1,"maxLength":64}},"IfNoneMatch":{"name":"If-None-Match","in":"header","required":false,"description":"El `ETag` de la respuesta anterior: si nada cambió recibís 304 sin cuerpo.","schema":{"type":"string"}},"RequestId":{"name":"X-Request-Id","in":"header","required":false,"description":"Tu propio id de request: vuelve en el header de la respuesta y en el `requestId` de todo error. Si no tiene esta forma se ignora y se genera un UUID.","schema":{"type":"string","pattern":"^[A-Za-z0-9._-]{8,64}$"},"example":"mi-sistema-0001"}},"schemas":{"AmenitiesPutBody":{"type":"object","properties":{"clubId":{"type":"string","pattern":"^[a-f0-9-]{8,64}$","description":"Club sobre el que opera la request. Opcional: se infiere cuando la integración tiene un solo grant (kind CLUB, v1)."},"ids":{"maxItems":50,"type":"array","items":{"type":"string","minLength":1,"maxLength":64,"pattern":"^[A-Za-z0-9_-]+$"},"description":"Ids del catálogo (`GET /amenities/catalog`). Reemplaza el conjunto completo; `[]` lo vacía."}},"required":["ids"],"example":{"ids":["amen-wifi","amen-estacionamiento"]}},"AmenityCatalogResponse":{"type":"object","properties":{"amenities":{"type":"array","items":{"$ref":"#/components/schemas/AmenityDto"},"description":"Todo el catálogo, orden: nombre."}},"required":["amenities"],"additionalProperties":false,"example":{"amenities":[{"id":"amen-wifi","name":"WiFi","icon":"wifi"},{"id":"amen-estacionamiento","name":"Estacionamiento","icon":"local_parking"}]}},"AmenityDto":{"type":"object","description":"Un espacio o servicio del catálogo de la plataforma. Lista cerrada `AMENITY_DTO_FIELDS`.","properties":{"id":{"type":"string","description":"Id del espacio / servicio en el catálogo (no es UUID).","example":"amen-wifi"},"name":{"type":"string","description":"Nombre (el rótulo del catálogo, en castellano).","example":"WiFi"},"icon":{"oneOf":[{"type":"string","description":"Ícono Material.","example":"wifi"},{"type":"null"}]}},"required":["id","name","icon"],"additionalProperties":false,"example":{"id":"amen-wifi","name":"WiFi","icon":"wifi"}},"AmericanoCancellationReason":{"type":"string","enum":["MANAGER","EXPIRED","CREATOR_DELETED"],"description":"Por qué se canceló (`MANAGER` = el club; `EXPIRED` = quedó OPEN sin rondas 24 h después de `startsAt`)."},"AmericanoDto":{"type":"object","description":"Un americano CLUB_PUBLIC del club. Lista cerrada `AMERICANO_DTO_FIELDS`. Nunca datos de personas: ni quién lo creó ni quiénes juegan (`players` no existe en esta API); tampoco rondas, mesas ni tabla.","properties":{"id":{"type":"string","format":"uuid","description":"Id del americano."},"clubId":{"type":"string","format":"uuid","description":"Club organizador."},"name":{"type":"string","description":"Nombre.","example":"Americano de los viernes"},"status":{"$ref":"#/components/schemas/AmericanoStatus"},"startsAt":{"type":"string","description":"Comienzo `YYYY-MM-DDTHH:MM:SS`, naive en la zona del club.","pattern":"^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}(:\\d{2})?$","example":"2026-09-25T20:00:00"},"maxPlayers":{"type":"integer","minimum":0,"description":"Cupo de jugadores."},"confirmedCount":{"type":"integer","minimum":0,"description":"Inscriptos confirmados (los que ocupan cupo)."},"isFull":{"type":"boolean","description":"`confirmedCount >= maxPlayers`."},"registrationOpen":{"type":"boolean","description":"Si el club sigue aceptando inscripciones."},"roundsPlanned":{"type":"integer","minimum":0,"description":"Rondas previstas."},"currentRound":{"type":"integer","minimum":0,"description":"Ronda en curso (`0` = todavía no arrancó)."},"gamesPerMatch":{"type":"integer","minimum":0,"description":"Games por partido."},"matchMinutes":{"oneOf":[{"type":"integer","minimum":0,"description":"Minutos por partido, si el club los fijó."},{"type":"null"}]},"courtIds":{"type":"array","items":{"type":"string","format":"uuid","description":"Cancha."},"description":"Canchas que el club destinó al americano (`GET /courts`)."},"price":{"type":"number","minimum":0,"description":"Precio de la inscripción (moneda del club)."},"description":{"oneOf":[{"type":"string","description":"Texto del club."},{"type":"null"}]},"location":{"oneOf":[{"type":"string","description":"Lugar, si el club lo escribió a mano."},{"type":"null"}]},"finishedAt":{"oneOf":[{"type":"string","format":"date-time","description":"Cuándo terminó, ISO UTC.","example":"2026-09-14T12:00:00.000Z"},{"type":"null"}]},"cancelledAt":{"oneOf":[{"type":"string","format":"date-time","description":"Cuándo se canceló, ISO UTC.","example":"2026-09-14T12:00:00.000Z"},{"type":"null"}]},"cancellationReason":{"oneOf":[{"$ref":"#/components/schemas/AmericanoCancellationReason"},{"type":"null"}]},"createdAt":{"oneOf":[{"type":"string","format":"date-time","description":"Alta, ISO UTC.","example":"2026-09-14T12:00:00.000Z"},{"type":"null"}]},"updatedAt":{"oneOf":[{"type":"string","format":"date-time","description":"Última escritura, ISO UTC.","example":"2026-09-14T12:00:00.000Z"},{"type":"null"}]}},"required":["id","clubId","name","status","startsAt","maxPlayers","confirmedCount","isFull","registrationOpen","roundsPlanned","currentRound","gamesPerMatch","matchMinutes","courtIds","price","description","location","finishedAt","cancelledAt","cancellationReason","createdAt","updatedAt"],"additionalProperties":false,"example":{"id":"6d5e4f3a-2b1c-4d0e-9f8a-7b6c5d4e3f2a","clubId":"0d6a1f3e-2b4c-4d5e-9f7a-8b9c0d1e2f3a","name":"Americano de los viernes","status":"OPEN","startsAt":"2026-09-25T20:00:00","maxPlayers":8,"confirmedCount":6,"isFull":false,"registrationOpen":true,"roundsPlanned":3,"currentRound":0,"gamesPerMatch":8,"matchMinutes":20,"courtIds":["5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9"],"price":500,"description":"Trae paleta. Se juega con lluvia.","location":null,"finishedAt":null,"cancelledAt":null,"cancellationReason":null,"createdAt":"2026-09-14T12:00:00Z","updatedAt":"2026-09-14T12:00:00Z"}},"AmericanoResponse":{"type":"object","properties":{"americano":{"$ref":"#/components/schemas/AmericanoDto"}},"required":["americano"],"additionalProperties":false,"example":{"americano":{"id":"6d5e4f3a-2b1c-4d0e-9f8a-7b6c5d4e3f2a","clubId":"0d6a1f3e-2b4c-4d5e-9f7a-8b9c0d1e2f3a","name":"Americano de los viernes","status":"OPEN","startsAt":"2026-09-25T20:00:00","maxPlayers":8,"confirmedCount":6,"isFull":false,"registrationOpen":true,"roundsPlanned":3,"currentRound":0,"gamesPerMatch":8,"matchMinutes":20,"courtIds":["5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9"],"price":500,"description":"Trae paleta. Se juega con lluvia.","location":null,"finishedAt":null,"cancelledAt":null,"cancellationReason":null,"createdAt":"2026-09-14T12:00:00Z","updatedAt":"2026-09-14T12:00:00Z"}}},"AmericanoStatus":{"type":"string","enum":["OPEN","IN_PROGRESS","FINISHED","CANCELLED"],"description":"Estado del americano."},"AmericanosListResponse":{"type":"object","properties":{"americanos":{"type":"array","items":{"$ref":"#/components/schemas/AmericanoDto"},"description":"Orden: `startsAt` asc, alta asc. `[]` con el flag de americanos apagado."},"total":{"type":"integer","minimum":0},"limit":{"type":"integer","minimum":1,"maximum":200},"offset":{"type":"integer","minimum":0}},"required":["americanos","total","limit","offset"],"additionalProperties":false,"example":{"americanos":[{"id":"6d5e4f3a-2b1c-4d0e-9f8a-7b6c5d4e3f2a","clubId":"0d6a1f3e-2b4c-4d5e-9f7a-8b9c0d1e2f3a","name":"Americano de los viernes","status":"OPEN","startsAt":"2026-09-25T20:00:00","maxPlayers":8,"confirmedCount":6,"isFull":false,"registrationOpen":true,"roundsPlanned":3,"currentRound":0,"gamesPerMatch":8,"matchMinutes":20,"courtIds":["5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9"],"price":500,"description":"Trae paleta. Se juega con lluvia.","location":null,"finishedAt":null,"cancelledAt":null,"cancellationReason":null,"createdAt":"2026-09-14T12:00:00Z","updatedAt":"2026-09-14T12:00:00Z"}],"total":1,"limit":100,"offset":0}},"ApiScope":{"type":"string","enum":["courts:read","availability:read","bookings:read","bookings:write","events:read","recordings:read","webhooks:manage","courts:write","pricing:read","pricing:write","hours:read","hours:write","blocks:read","blocks:write","recurring:read","recurring:write","attendance:write","tournaments:read","tournaments:write","club:read","club:write","amenities:read","amenities:write","matches:read","matches:write","reports:read","americanos:read"],"description":"Scope de una key / grant."},"AttendanceBody":{"type":"object","properties":{"status":{"type":"string","enum":["ATTENDED","NO_SHOW"],"description":"`ATTENDED` (vinieron) o `NO_SHOW` (no vinieron)."}},"required":["status"],"example":{"status":"ATTENDED"}},"AttendanceResponse":{"type":"object","properties":{"booking":{"oneOf":[{"$ref":"#/components/schemas/BookingDto"},{"$ref":"#/components/schemas/SlotDto"}],"description":"`BookingDto` si es propia, `SlotDto` anónimo si no."},"previousStatus":{"$ref":"#/components/schemas/BookingStatus"},"changed":{"type":"boolean","description":"`false` si ya tenía ese estado (idempotente: no se vuelve a tocar el score)."},"scored":{"type":"boolean","description":"`true` si tocó `attendance_score` / puntos del titular (nunca para la cuenta de servicio ni el vinculado sin consentimiento)."}},"required":["booking","previousStatus","changed","scored"],"additionalProperties":false,"example":{"booking":{"id":"1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d","courtId":"5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9","kind":"COURT","categoryId":null,"resourceNoun":"COURT","date":"2026-09-14","startTime":"08:00","endTime":"09:00","status":"ATTENDED","origin":"APP","owned":false,"matchCount":0,"recurringId":null},"previousStatus":"CONFIRMED","changed":true,"scored":true}},"AttendanceStatus":{"type":"string","enum":["ATTENDED","NO_SHOW"],"description":"Resultado de pasar lista."},"AvailabilityCourt":{"type":"object","properties":{"courtId":{"type":"string","format":"uuid","description":"Cancha."},"name":{"type":"string","description":"Nombre.","example":"Cancha 1"},"kind":{"$ref":"#/components/schemas/ResourceKind"},"categoryId":{"oneOf":[{"type":"string","format":"uuid","description":"Categoría de recurso (F6). `null` para las canchas implícitas."},{"type":"null"}]},"resourceNoun":{"$ref":"#/components/schemas/ResourceNoun"},"starts":{"type":"array","items":{"$ref":"#/components/schemas/AvailabilityStart"},"description":"`[]` si la cancha no tiene franjas ese día."}},"required":["courtId","name","kind","categoryId","resourceNoun","starts"],"additionalProperties":false},"AvailabilityResponse":{"type":"object","description":"Comienzos vendibles por cancha, con duraciones y precio. No es autoritativo: el POST revalida.","properties":{"clubId":{"type":"string","format":"uuid","description":"Club."},"date":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"El día pedido.","example":"2026-09-15"},"timezone":{"type":"string","description":"Zona IANA del club.","example":"America/Montevideo"},"durations":{"type":"array","items":{"$ref":"#/components/schemas/DurationMinutes"},"description":"Duraciones consideradas (todas, o sólo `durationMinutes` si lo mandaste).","example":[30,60,90,120]},"courts":{"type":"array","items":{"$ref":"#/components/schemas/AvailabilityCourt"},"description":"`[]` si el club no abre ese día."}},"required":["clubId","date","timezone","durations","courts"],"additionalProperties":false,"example":{"clubId":"0d6a1f3e-2b4c-4d5e-9f7a-8b9c0d1e2f3a","date":"2026-09-15","timezone":"America/Montevideo","durations":[30,60,90,120],"courts":[{"courtId":"5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9","name":"Cancha 1","kind":"COURT","categoryId":null,"resourceNoun":"COURT","starts":[{"startTime":"10:00","availableDurations":[30,60,90,120],"prices":{"30":500,"60":1000,"90":1400,"120":2000}}]}]}},"AvailabilityStart":{"type":"object","properties":{"startTime":{"type":"string","pattern":"^([01]\\d|2[0-3]):[0-5]\\d$","description":"Comienzo vendible, grilla `:00` / `:30`.","example":"10:00"},"availableDurations":{"type":"array","items":{"$ref":"#/components/schemas/DurationMinutes"},"description":"Duraciones que entran desde ese comienzo.","example":[30,60,90,120]},"prices":{"type":"object","description":"Precio total por duración (clave = minutos como string). `null` si no hay tarifa.","additionalProperties":{"oneOf":[{"type":"number"},{"type":"null"}]},"example":{"30":500,"60":1000,"90":1400,"120":2000}}},"required":["startTime","availableDurations","prices"],"additionalProperties":false},"BlockConflictBooking":{"type":"object","description":"Una reserva viva que el bloqueo pisa, ANÓNIMA (lista cerrada `BLOCK_CONFLICT_BOOKING_FIELDS`). Va en `details.bookings[]` del 409.","properties":{"id":{"type":"string","format":"uuid","description":"Id de la reserva."},"courtId":{"type":"string","format":"uuid","description":"Cancha."},"date":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"Día.","example":"2026-09-15"},"startTime":{"type":"string","pattern":"^([01]\\d|2[0-3]):[0-5]\\d$","description":"Comienzo.","example":"09:00"},"endTime":{"type":"string","pattern":"^(([01]\\d|2[0-3]):[0-5]\\d|24:00)$","description":"Fin.","example":"10:30"},"origin":{"$ref":"#/components/schemas/BookingOrigin"}},"required":["id","courtId","date","startTime","endTime","origin"],"additionalProperties":false,"example":{"id":"1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d","courtId":"5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9","date":"2026-09-21","startTime":"09:00","endTime":"10:30","origin":"APP"}},"BlockCreateBody":{"type":"object","properties":{"clubId":{"type":"string","pattern":"^[a-f0-9-]{8,64}$","description":"Club sobre el que opera la request. Opcional: se infiere cuando la integración tiene un solo grant (kind CLUB, v1)."},"courtId":{"anyOf":[{"type":"string","pattern":"^[a-f0-9-]{8,64}$"},{"type":"null"}],"description":"Cancha a bloquear, o `null` para TODAS las canchas habilitadas del club (una fila por cancha)."},"from":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"Primer día bloqueado, `YYYY-MM-DD` naive del club.","example":"2026-09-20"},"to":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"Último día bloqueado (inclusive); como mucho 366 días.","example":"2026-09-22"},"startTime":{"type":"string","pattern":"^([01]\\d|2[0-3]):[0-5]\\d$","description":"Comienzo de la ventana bloqueada en cada día (`HH:MM`). Sin `startTime` / `endTime`: el día entero.","example":"08:00"},"endTime":{"type":"string","pattern":"^(([01]\\d|2[0-3]):[0-5]\\d|24:00)$","description":"Fin de la ventana bloqueada (`HH:MM`, puede ser `24:00`). Va junto con `startTime`.","example":"12:00"},"reason":{"type":"string","minLength":1,"maxLength":120,"description":"Motivo visible en el panel (≤ 120).","example":"Mantenimiento del césped"}},"required":["courtId","from","to"],"description":"Alta de un bloqueo: rango de días + ventana horaria diaria (como el panel).","example":{"courtId":"5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9","from":"2026-09-20","to":"2026-09-22","startTime":"08:00","endTime":"12:00","reason":"Mantenimiento del césped"}},"BlockDeleteResponse":{"type":"object","properties":{"block":{"$ref":"#/components/schemas/BlockDto"},"deleted":{"type":"boolean","const":true}},"required":["block","deleted"],"additionalProperties":false,"example":{"block":{"id":"3f2a1b0c-9d8e-4f7a-b6c5-d4e3f2a1b0c9","clubId":"0d6a1f3e-2b4c-4d5e-9f7a-8b9c0d1e2f3a","courtId":"5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9","courtName":"Cancha 1","from":"2026-09-20","to":"2026-09-22","startTime":"08:00","endTime":"12:00","reason":"Mantenimiento del césped","holes":[{"id":"7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d","date":"2026-09-21","startTime":"10:00","endTime":"11:00"}],"createdAt":"2026-09-14T12:00:00.000Z"},"deleted":true}},"BlockDto":{"type":"object","description":"Un bloqueo de cancha (mantenimiento, evento, club cerrado). Lista cerrada `BLOCK_DTO_FIELDS`. Nunca quién lo creó.","properties":{"id":{"type":"string","format":"uuid","description":"Id del bloqueo."},"clubId":{"type":"string","format":"uuid","description":"Club de la cancha."},"courtId":{"type":"string","format":"uuid","description":"Cancha bloqueada."},"courtName":{"type":"string","description":"Nombre de la cancha."},"from":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"Primer día bloqueado (inclusive).","example":"2026-09-15"},"to":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"Último día bloqueado (inclusive).","example":"2026-09-15"},"startTime":{"type":"string","pattern":"^([01]\\d|2[0-3]):[0-5]\\d$","description":"Comienzo de la ventana bloqueada en cada día del rango.","example":"08:00"},"endTime":{"type":"string","pattern":"^(([01]\\d|2[0-3]):[0-5]\\d|24:00)$","description":"Fin de la ventana bloqueada (`24:00` = hasta medianoche).","example":"12:00"},"reason":{"oneOf":[{"type":"string","description":"Motivo (texto del club)."},{"type":"null"}]},"holes":{"type":"array","items":{"$ref":"#/components/schemas/BlockHoleDto"},"description":"Huecos que perforan el bloqueo (misma cancha, día dentro del rango, franja solapada)."},"createdAt":{"oneOf":[{"type":"string","format":"date-time","description":"ISO UTC.","example":"2026-09-14T12:00:00.000Z"},{"type":"null"}]}},"required":["id","clubId","courtId","courtName","from","to","startTime","endTime","reason","holes","createdAt"],"additionalProperties":false,"example":{"id":"3f2a1b0c-9d8e-4f7a-b6c5-d4e3f2a1b0c9","clubId":"0d6a1f3e-2b4c-4d5e-9f7a-8b9c0d1e2f3a","courtId":"5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9","courtName":"Cancha 1","from":"2026-09-20","to":"2026-09-22","startTime":"08:00","endTime":"12:00","reason":"Mantenimiento del césped","holes":[{"id":"7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d","date":"2026-09-21","startTime":"10:00","endTime":"11:00"}],"createdAt":"2026-09-14T12:00:00.000Z"}},"BlockHoleCreateBody":{"type":"object","properties":{"date":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"Día del hueco, `YYYY-MM-DD` naive del club: tiene que caer en `[from, to]` del bloqueo.","example":"2026-09-21"},"startTime":{"type":"string","pattern":"^([01]\\d|2[0-3]):[0-5]\\d$","description":"Comienzo de la franja liberada (`HH:MM`), dentro de la ventana del bloqueo.","example":"10:00"},"endTime":{"type":"string","pattern":"^(([01]\\d|2[0-3]):[0-5]\\d|24:00)$","description":"Fin de la franja liberada (`HH:MM`, puede ser `24:00`), posterior a `startTime` y dentro de la ventana del bloqueo.","example":"11:00"}},"required":["date","startTime","endTime"],"additionalProperties":false,"description":"Un hueco (franja liberada) de un día dentro del bloqueo. La cancha sale del bloqueo.","example":{"date":"2026-09-21","startTime":"10:00","endTime":"11:00"}},"BlockHoleCreateResponse":{"type":"object","properties":{"block":{"$ref":"#/components/schemas/BlockDto","description":"El bloqueo releído: `holes[]` ya trae el hueco fusionado con los que hubiera ese día."},"hole":{"$ref":"#/components/schemas/BlockHoleDto","description":"El hueco resultante que contiene la franja pedida (fusionado si tocaba otro)."}},"required":["block","hole"],"additionalProperties":false,"example":{"block":{"id":"3f2a1b0c-9d8e-4f7a-b6c5-d4e3f2a1b0c9","clubId":"0d6a1f3e-2b4c-4d5e-9f7a-8b9c0d1e2f3a","courtId":"5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9","courtName":"Cancha 1","from":"2026-09-20","to":"2026-09-22","startTime":"08:00","endTime":"12:00","reason":"Mantenimiento del césped","holes":[{"id":"7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d","date":"2026-09-21","startTime":"10:00","endTime":"11:00"}],"createdAt":"2026-09-14T12:00:00.000Z"},"hole":{"id":"7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d","date":"2026-09-21","startTime":"10:00","endTime":"11:00"}}},"BlockHoleDeleteResponse":{"type":"object","properties":{"block":{"$ref":"#/components/schemas/BlockDto","description":"El bloqueo releído, ya sin el hueco."},"hole":{"$ref":"#/components/schemas/BlockHoleDto","description":"El hueco que se quitó."},"deleted":{"type":"boolean","const":true}},"required":["block","hole","deleted"],"additionalProperties":false,"example":{"block":{"id":"3f2a1b0c-9d8e-4f7a-b6c5-d4e3f2a1b0c9","clubId":"0d6a1f3e-2b4c-4d5e-9f7a-8b9c0d1e2f3a","courtId":"5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9","courtName":"Cancha 1","from":"2026-09-20","to":"2026-09-22","startTime":"08:00","endTime":"12:00","reason":"Mantenimiento del césped","holes":[],"createdAt":"2026-09-14T12:00:00.000Z"},"hole":{"id":"7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d","date":"2026-09-21","startTime":"10:00","endTime":"11:00"},"deleted":true}},"BlockHoleDto":{"type":"object","description":"Un hueco (franja liberada) dentro del bloqueo. Lista cerrada `BLOCK_HOLE_DTO_FIELDS`.","properties":{"id":{"type":"string","format":"uuid","description":"Id del hueco."},"date":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"Día del hueco.","example":"2026-09-15"},"startTime":{"type":"string","pattern":"^([01]\\d|2[0-3]):[0-5]\\d$","description":"Comienzo `HH:MM` naive del club.","example":"10:00"},"endTime":{"type":"string","pattern":"^(([01]\\d|2[0-3]):[0-5]\\d|24:00)$","description":"Fin `HH:MM` naive del club (`24:00` = medianoche).","example":"11:00"}},"required":["id","date","startTime","endTime"],"additionalProperties":false},"BlocksCreateResponse":{"type":"object","properties":{"blocks":{"type":"array","items":{"$ref":"#/components/schemas/BlockDto"},"description":"Los bloqueos creados (uno por cancha)."},"cancelledBookings":{"type":"integer","minimum":0,"description":"Reservas canceladas por `force=true`."},"detachedMatches":{"type":"integer","minimum":0,"description":"Partidos desligados de esas reservas (nunca cancelados)."}},"required":["blocks","cancelledBookings","detachedMatches"],"additionalProperties":false,"example":{"blocks":[{"id":"3f2a1b0c-9d8e-4f7a-b6c5-d4e3f2a1b0c9","clubId":"0d6a1f3e-2b4c-4d5e-9f7a-8b9c0d1e2f3a","courtId":"5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9","courtName":"Cancha 1","from":"2026-09-20","to":"2026-09-22","startTime":"08:00","endTime":"12:00","reason":"Mantenimiento del césped","holes":[{"id":"7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d","date":"2026-09-21","startTime":"10:00","endTime":"11:00"}],"createdAt":"2026-09-14T12:00:00.000Z"}],"cancelledBookings":0,"detachedMatches":0}},"BlocksListResponse":{"type":"object","properties":{"blocks":{"type":"array","items":{"$ref":"#/components/schemas/BlockDto"},"description":"Orden: primer día, hora, id."}},"required":["blocks"],"additionalProperties":false,"example":{"blocks":[{"id":"3f2a1b0c-9d8e-4f7a-b6c5-d4e3f2a1b0c9","clubId":"0d6a1f3e-2b4c-4d5e-9f7a-8b9c0d1e2f3a","courtId":"5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9","courtName":"Cancha 1","from":"2026-09-20","to":"2026-09-22","startTime":"08:00","endTime":"12:00","reason":"Mantenimiento del césped","holes":[{"id":"7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d","date":"2026-09-21","startTime":"10:00","endTime":"11:00"}],"createdAt":"2026-09-14T12:00:00.000Z"}]}},"BookingCreateBody":{"type":"object","properties":{"clubId":{"type":"string","pattern":"^[a-f0-9-]{8,64}$","description":"Club sobre el que opera la request. Opcional: se infiere cuando la integración tiene un solo grant (kind CLUB, v1)."},"courtId":{"type":"string","pattern":"^[a-f0-9-]{8,64}$","description":"Id de la cancha (`GET /courts`)."},"date":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"`YYYY-MM-DD`, naive en la zona del club (`clubs[].timezone` de `/me`). Un ISO con offset es VALIDATION.","example":"2026-09-15"},"startTime":{"type":"string","pattern":"^([01]\\d|2[0-3]):(00|30)$","description":"Comienzo en la grilla del producto: `HH:00` o `HH:30`, naive en la zona del club.","example":"10:00"},"durationMinutes":{"type":"integer","enum":[30,60,90,120],"description":"Duración del turno en minutos. Exactamente uno de `durationMinutes` / `endTime`.","example":90},"endTime":{"type":"string","pattern":"^(([01]\\d|2[0-3]):[0-5]\\d|24:00)$","description":"Fin del turno `HH:MM` (puede ser `24:00`). Exactamente uno de `durationMinutes` / `endTime`; el resultado tiene que dar 30/60/90/120 (si no, INVALID_DURATION).","example":"11:30"},"externalRef":{"type":"string","minLength":1,"maxLength":64,"pattern":"^[\\x21-\\x7E]+$","description":"Tu id de orden: da idempotencia al POST (mismo ref + mismo turno → 200 replay) y es de UN solo uso (una reserva cancelada bloquea el ref → EXTERNAL_REF_USED). Alias: header `Idempotency-Key`. Sin él no hay idempotencia.","example":"ORD-123"},"customer":{"type":"object","properties":{"name":{"type":"string","minLength":2,"maxLength":80,"description":"Nombre del cliente final.","example":"Juan Pérez"},"phone":{"type":"string","minLength":6,"maxLength":32,"description":"Teléfono del cliente final (se normaliza a E.164). Con `linkPlayersByPhone` puede vincular la reserva a un jugador verificado.","example":"+598 91 234 567"},"email":{"type":"string","maxLength":120,"format":"email","pattern":"^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$","description":"Email del cliente final.","example":"juan@example.com"}},"required":["name"],"description":"Cliente final de la reserva (PII de terceros: sólo lo ve tu integración). El teléfono se guarda normalizado a E.164. En un PATCH reemplaza los tres campos; `null` los borra."},"price":{"type":"number","minimum":0,"description":"Total del turno. Si no viene, el tarifario de la cancha (PRICE_UNAVAILABLE si no hay). En un PATCH pasa a `price_override`.","example":1400},"paymentStatus":{"type":"string","enum":["PAID","PENDING","UNPAID"],"description":"Estado de pago. Default `PENDING` al crear."},"type":{"type":"string","enum":["normal","profe"],"description":"Tipo de reserva (`court_bookings.type`). Default `normal`."}},"required":["courtId","date","startTime"],"description":"Alta de una reserva puntual. Exactamente uno de `durationMinutes` / `endTime`.","example":{"courtId":"5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9","date":"2026-09-15","startTime":"10:00","durationMinutes":90,"externalRef":"ORD-123","customer":{"name":"Juan Pérez","phone":"+598 91 234 567"},"paymentStatus":"PAID"}},"BookingDto":{"type":"object","description":"Una reserva PROPIA (la creó esta integración). Lista cerrada `BOOKING_DTO_FIELDS`. `recording` sólo aparece con scope `recordings:read`.","properties":{"id":{"type":"string","format":"uuid","description":"Id de la reserva."},"clubId":{"type":"string","format":"uuid","description":"Club de la cancha."},"externalRef":{"oneOf":[{"type":"string","description":"El `externalRef` con el que se creó (`null` si no mandaste)."},{"type":"null"}]},"courtId":{"type":"string","format":"uuid","description":"Cancha."},"courtName":{"type":"string","description":"Nombre de la cancha."},"kind":{"$ref":"#/components/schemas/ResourceKind"},"categoryId":{"oneOf":[{"type":"string","format":"uuid","description":"Categoría de recurso (F6). `null` para las canchas implícitas."},{"type":"null"}]},"resourceNoun":{"$ref":"#/components/schemas/ResourceNoun"},"date":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"`YYYY-MM-DD` naive en la zona del club.","example":"2026-09-15"},"startTime":{"type":"string","pattern":"^([01]\\d|2[0-3]):[0-5]\\d$","description":"Comienzo `HH:MM` naive del club.","example":"10:00"},"endTime":{"type":"string","pattern":"^(([01]\\d|2[0-3]):[0-5]\\d|24:00)$","description":"Fin `HH:MM` naive del club (`24:00` = medianoche).","example":"11:30"},"durationMinutes":{"type":"integer","description":"Duración en minutos."},"timezone":{"type":"string","description":"Zona IANA del club (la de `date` / `startTime`).","example":"America/Montevideo"},"status":{"$ref":"#/components/schemas/BookingStatus"},"type":{"type":"string","description":"`court_bookings.type` (`normal` / `profe`).","example":"normal"},"price":{"oneOf":[{"type":"number","description":"Precio efectivo: `price_override` si lo mandaste en un PATCH, si no el congelado al crear."},{"type":"null"}]},"paymentStatus":{"$ref":"#/components/schemas/PaymentStatus"},"source":{"type":"string","description":"`court_bookings.source` crudo (`api` para lo creado por esta API).","example":"api"},"origin":{"$ref":"#/components/schemas/BookingOrigin"},"customer":{"oneOf":[{"$ref":"#/components/schemas/CustomerDto"},{"type":"null"}]},"linked":{"type":"boolean","description":"`true` si la reserva quedó a nombre de un jugador real de Skalia (vinculación por teléfono, opt-in). Nunca quién."},"owned":{"type":"boolean","const":true,"description":"Siempre `true`: la creó esta integración."},"matchCount":{"type":"integer","minimum":0,"description":"Partidos vivos armados en el turno."},"recording":{"oneOf":[{"$ref":"#/components/schemas/RecordingDto"},{"type":"null"}]},"createdAt":{"oneOf":[{"type":"string","format":"date-time","description":"ISO UTC.","example":"2026-09-14T12:00:00.000Z"},{"type":"null"}]},"updatedAt":{"oneOf":[{"type":"string","format":"date-time","description":"ISO UTC. La escribe el emisor único en cualquier cambio de la reserva.","example":"2026-09-14T12:00:00.000Z"},{"type":"null"}]},"recurringId":{"oneOf":[{"type":"string","format":"uuid","description":"Serie de fijas de la que es ocurrencia (`origin: RECURRING`; `GET /recurring-bookings/{id}`), o `null`."},{"type":"null"}]}},"required":["id","clubId","externalRef","courtId","courtName","kind","categoryId","resourceNoun","date","startTime","endTime","durationMinutes","timezone","status","type","price","paymentStatus","source","origin","customer","linked","owned","matchCount","createdAt","updatedAt","recurringId"],"additionalProperties":false,"example":{"id":"9c1b2a3d-4e5f-4a6b-8c7d-0e1f2a3b4c5d","clubId":"0d6a1f3e-2b4c-4d5e-9f7a-8b9c0d1e2f3a","externalRef":"ORD-123","courtId":"5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9","courtName":"Cancha 1","kind":"COURT","categoryId":null,"resourceNoun":"COURT","date":"2026-09-15","startTime":"10:00","endTime":"11:30","durationMinutes":90,"timezone":"America/Montevideo","status":"CONFIRMED","type":"normal","price":1400,"paymentStatus":"PAID","source":"api","origin":"API","customer":{"name":"Juan Pérez","phone":"+59891234567","email":null},"linked":false,"owned":true,"matchCount":0,"recording":null,"createdAt":"2026-09-14T12:00:00.000Z","updatedAt":"2026-09-14T12:00:00.000Z","recurringId":null}},"BookingEvent":{"type":"object","description":"Un evento de la outbox. Trae `booking` (eventos `booking.*`) O `occupancy` (`occupancy.changed`).","properties":{"id":{"type":"integer","minimum":1,"description":"Cursor: monotónico por club. Pasá el último como `since`.","example":1042},"type":{"$ref":"#/components/schemas/BookingEventType"},"createdAt":{"oneOf":[{"type":"string","format":"date-time","description":"ISO UTC.","example":"2026-09-14T12:00:00.000Z"},{"type":"null"}]},"actor":{"$ref":"#/components/schemas/EventActor"},"reason":{"oneOf":[{"type":"string","description":"Vocabulario ABIERTO (`EVENT_REASON`, puede crecer sin aviso): usalo para logs, ramificá por `type`.","example":"club"},{"type":"null"}]},"booking":{"oneOf":[{"$ref":"#/components/schemas/BookingDto"},{"$ref":"#/components/schemas/SlotDto"}],"description":"Para `booking.*`: `BookingDto` hidratado desde la fila actual si es tuya, `SlotDto` anónimo si no (o el guardado con `status: CANCELLED` si la fila ya no existe)."},"occupancy":{"$ref":"#/components/schemas/OccupancyChanged"}},"required":["id","type","createdAt","actor","reason"],"additionalProperties":false,"example":{"id":1042,"type":"booking.cancelled","createdAt":"2026-09-14T15:02:11.000Z","actor":"admin","reason":"club","booking":{"id":"1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d","courtId":"5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9","kind":"COURT","categoryId":null,"resourceNoun":"COURT","date":"2026-09-15","startTime":"16:00","endTime":"17:00","status":"CONFIRMED","origin":"APP","owned":false,"matchCount":1,"recurringId":null}}},"BookingEventType":{"type":"string","enum":["booking.created","booking.confirmed","booking.updated","booking.cancelled","booking.attended","booking.no_show","occupancy.changed","booking.recording_ready"],"description":"`type` de un evento."},"BookingOrigin":{"type":"string","enum":["API","APP","WEB","ADMIN","QJ","RECURRING","TOURNAMENT","MATCH","UNKNOWN"],"description":"De dónde salió la reserva (derivado de `source` y los punteros de la fila)."},"BookingPatchBody":{"type":"object","properties":{"courtId":{"type":"string","pattern":"^[a-f0-9-]{8,64}$","description":"Id de la cancha (`GET /courts`)."},"date":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"`YYYY-MM-DD`, naive en la zona del club (`clubs[].timezone` de `/me`). Un ISO con offset es VALIDATION.","example":"2026-09-15"},"startTime":{"type":"string","pattern":"^([01]\\d|2[0-3]):(00|30)$","description":"Comienzo en la grilla del producto: `HH:00` o `HH:30`, naive en la zona del club.","example":"10:00"},"durationMinutes":{"type":"integer","enum":[30,60,90,120],"description":"Duración del turno en minutos. Exactamente uno de `durationMinutes` / `endTime`.","example":90},"endTime":{"type":"string","pattern":"^(([01]\\d|2[0-3]):[0-5]\\d|24:00)$","description":"Fin del turno `HH:MM` (puede ser `24:00`). Exactamente uno de `durationMinutes` / `endTime`; el resultado tiene que dar 30/60/90/120 (si no, INVALID_DURATION).","example":"11:30"},"customer":{"anyOf":[{"type":"object","properties":{"name":{"type":"string","minLength":2,"maxLength":80,"description":"Nombre del cliente final.","example":"Juan Pérez"},"phone":{"type":"string","minLength":6,"maxLength":32,"description":"Teléfono del cliente final (se normaliza a E.164). Con `linkPlayersByPhone` puede vincular la reserva a un jugador verificado.","example":"+598 91 234 567"},"email":{"type":"string","maxLength":120,"format":"email","pattern":"^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$","description":"Email del cliente final.","example":"juan@example.com"}},"required":["name"]},{"type":"null"}],"description":"Cliente final de la reserva (PII de terceros: sólo lo ve tu integración). El teléfono se guarda normalizado a E.164. En un PATCH reemplaza los tres campos; `null` los borra."},"price":{"type":"number","minimum":0,"description":"Total del turno. Si no viene, el tarifario de la cancha (PRICE_UNAVAILABLE si no hay). En un PATCH pasa a `price_override`.","example":1400},"paymentStatus":{"type":"string","enum":["PAID","PENDING","UNPAID"],"description":"Estado de pago. Default `PENDING` al crear."}},"description":"Parcial: al menos un campo. Mover = cualquiera de `courtId` / `date` / `startTime` / duración. `durationMinutes` y `endTime` no van juntos.","example":{"startTime":"11:00"}},"BookingPolicy":{"type":"string","enum":["AUTO_CONFIRM","MANUAL"],"description":"Política de confirmación."},"BookingResponse":{"type":"object","properties":{"booking":{"oneOf":[{"$ref":"#/components/schemas/BookingDto"},{"$ref":"#/components/schemas/ManagedSlotDto"},{"$ref":"#/components/schemas/SlotDto"}],"description":"`BookingDto` si es propia (`owned: true`); si no, `ManagedSlotDto` con `canManageAllBookings` (`managed: true`) o `SlotDto` anónimo."}},"required":["booking"],"additionalProperties":false,"example":{"booking":{"id":"9c1b2a3d-4e5f-4a6b-8c7d-0e1f2a3b4c5d","clubId":"0d6a1f3e-2b4c-4d5e-9f7a-8b9c0d1e2f3a","externalRef":"ORD-123","courtId":"5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9","courtName":"Cancha 1","kind":"COURT","categoryId":null,"resourceNoun":"COURT","date":"2026-09-15","startTime":"10:00","endTime":"11:30","durationMinutes":90,"timezone":"America/Montevideo","status":"CONFIRMED","type":"normal","price":1400,"paymentStatus":"PAID","source":"api","origin":"API","customer":{"name":"Juan Pérez","phone":"+59891234567","email":null},"linked":false,"owned":true,"matchCount":0,"recording":null,"createdAt":"2026-09-14T12:00:00.000Z","updatedAt":"2026-09-14T12:00:00.000Z","recurringId":null}}},"BookingStatus":{"type":"string","enum":["PENDING","CONFIRMED","CANCELLED","ATTENDED","NO_SHOW"],"description":"`court_bookings.status`."},"BookingsListResponse":{"type":"object","properties":{"bookings":{"type":"array","items":{"oneOf":[{"$ref":"#/components/schemas/BookingDto"},{"$ref":"#/components/schemas/ManagedSlotDto"},{"$ref":"#/components/schemas/SlotDto"}],"description":"`BookingDto` si es propia (`owned: true`); si no, `ManagedSlotDto` con `canManageAllBookings` (`managed: true`) o `SlotDto` anónimo."},"description":"Orden: fecha, hora, id."},"total":{"type":"integer","minimum":0},"limit":{"type":"integer","minimum":1,"maximum":200},"offset":{"type":"integer","minimum":0}},"required":["bookings","total","limit","offset"],"additionalProperties":false,"example":{"bookings":[{"id":"9c1b2a3d-4e5f-4a6b-8c7d-0e1f2a3b4c5d","clubId":"0d6a1f3e-2b4c-4d5e-9f7a-8b9c0d1e2f3a","externalRef":"ORD-123","courtId":"5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9","courtName":"Cancha 1","kind":"COURT","categoryId":null,"resourceNoun":"COURT","date":"2026-09-15","startTime":"10:00","endTime":"11:30","durationMinutes":90,"timezone":"America/Montevideo","status":"CONFIRMED","type":"normal","price":1400,"paymentStatus":"PAID","source":"api","origin":"API","customer":{"name":"Juan Pérez","phone":"+59891234567","email":null},"linked":false,"owned":true,"matchCount":0,"recording":null,"createdAt":"2026-09-14T12:00:00.000Z","updatedAt":"2026-09-14T12:00:00.000Z","recurringId":null}],"total":1,"limit":100,"offset":0}},"BookingsReportDto":{"type":"object","description":"Totales de horas y venta esperada del club. Lista cerrada `BOOKINGS_REPORT_DTO_FIELDS`. Sólo agregados: sin filas, sin personas.","properties":{"clubId":{"type":"string","format":"uuid","description":"Club consultado."},"from":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"Primer día del rango (inclusive).","example":"2026-09-15"},"to":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"Último día del rango (inclusive).","example":"2026-09-15"},"timezone":{"type":"string","description":"Zona IANA del club: `from` / `to` son días naive en esa zona.","example":"America/Montevideo"},"currency":{"oneOf":[{"type":"string","description":"ISO 4217 de los montos (`club_settings.currency`); `null` si el club no la cargó.","example":"UYU"},{"type":"null"}]},"totals":{"$ref":"#/components/schemas/ReportTotals"},"groups":{"oneOf":[{"type":"array","items":{"$ref":"#/components/schemas/ReportGroup"},"description":"Un grupo por valor de `groupBy`; `null` sin `groupBy`."},{"type":"null"}]}},"required":["clubId","from","to","timezone","currency","totals","groups"],"additionalProperties":false,"example":{"clubId":"0d6a1f3e-2b4c-4d5e-9f7a-8b9c0d1e2f3a","from":"2026-09-01","to":"2026-09-30","timezone":"America/Montevideo","currency":"UYU","totals":{"count":42,"durationMinutes":3780,"price":58800,"priceIsExpected":true,"byType":[{"type":"normal","count":38,"durationMinutes":3420,"price":54600},{"type":"profe","count":4,"durationMinutes":360,"price":4200}],"bySource":[{"source":"api","apiClientId":"2b4c6d8e-0f1a-4b2c-9d3e-4f5a6b7c8d9e","count":20,"durationMinutes":1800,"price":28000},{"source":"android","apiClientId":null,"count":12,"durationMinutes":1080,"price":16800},{"source":"admin","apiClientId":null,"count":6,"durationMinutes":540,"price":8400},{"source":"api","apiClientId":null,"count":4,"durationMinutes":360,"price":5600}]},"groups":[{"key":"5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9","label":"Cancha 1","count":24,"durationMinutes":2160,"price":33600},{"key":"7c8d9e0f-1a2b-4c3d-8e4f-5a6b7c8d9e0f","label":"Cancha 2","count":18,"durationMinutes":1620,"price":25200}]}},"BracketResync":{"type":"object","properties":{"regenerated":{"type":"boolean","description":"Se regeneró el cuadro con el resultado corregido."},"blocked":{"type":"boolean","description":"`true` si no se regeneró porque ya hay eliminatorias jugadas."}},"required":["regenerated"],"additionalProperties":false},"ClubAmenitiesResponse":{"type":"object","properties":{"clubId":{"type":"string","format":"uuid","description":"Club."},"amenities":{"type":"array","items":{"$ref":"#/components/schemas/AmenityDto"},"description":"Orden: nombre."}},"required":["clubId","amenities"],"additionalProperties":false,"example":{"clubId":"0d6a1f3e-2b4c-4d5e-9f7a-8b9c0d1e2f3a","amenities":[{"id":"amen-wifi","name":"WiFi","icon":"wifi"}]}},"ClubDto":{"type":"object","description":"La ficha del club. Lista cerrada `CLUB_DTO_FIELDS`: datos públicos del club, nunca credenciales ni datos de personas.","properties":{"id":{"type":"string","format":"uuid","description":"Id del club."},"name":{"type":"string","description":"Nombre.","example":"Club Norte"},"description":{"oneOf":[{"type":"string","description":"Descripción pública."},{"type":"null"}]},"address":{"oneOf":[{"type":"string","description":"Dirección.","example":"Av. Italia 1234"},{"type":"null"}]},"city":{"oneOf":[{"type":"string","description":"Ciudad.","example":"Montevideo"},{"type":"null"}]},"latitude":{"oneOf":[{"type":"number","description":"Latitud.","example":-34.8836},{"type":"null"}]},"longitude":{"oneOf":[{"type":"number","description":"Longitud.","example":-56.1284},{"type":"null"}]},"phone":{"oneOf":[{"type":"string","description":"Teléfono público del club.","example":"+59899000000"},{"type":"null"}]},"website":{"oneOf":[{"type":"string","description":"Sitio web público (`http://` o `https://`).","format":"uri","pattern":"^https?:\\/\\/"},{"type":"null"}]},"email":{"oneOf":[{"type":"string","description":"Email público del club.","format":"email"},{"type":"null"}]},"instagram":{"oneOf":[{"type":"string","description":"Usuario de Instagram."},{"type":"null"}]},"facebook":{"oneOf":[{"type":"string","description":"Página de Facebook."},{"type":"null"}]},"logoUrl":{"oneOf":[{"type":"string","description":"URL pública (`https://`) del logo.","format":"uri","pattern":"^https:\\/\\/"},{"type":"null"}]},"timezone":{"type":"string","description":"Zona IANA (= `settings.timezone`).","example":"America/Montevideo"},"currency":{"type":"string","description":"Moneda (= `settings.currency`).","example":"UYU"},"countryCode":{"oneOf":[{"type":"string","description":"Código telefónico del país.","example":"598"},{"type":"null"}]},"sports":{"type":"array","items":{"type":"string"},"description":"Deportes de las canchas del club (derivado de `GET /courts`).","example":["PADEL"]},"modules":{"$ref":"#/components/schemas/ClubModulesDto"},"settings":{"$ref":"#/components/schemas/ClubSettingsDto"},"followers":{"$ref":"#/components/schemas/ClubFollowersDto"}},"required":["id","name","description","address","city","latitude","longitude","phone","website","email","instagram","facebook","logoUrl","timezone","currency","countryCode","sports","modules","settings","followers"],"additionalProperties":false,"example":{"id":"0d6a1f3e-2b4c-4d5e-9f7a-8b9c0d1e2f3a","name":"Club Norte","description":"Cuatro canchas de pádel techadas.","address":"Av. Italia 1234","city":"Montevideo","latitude":-34.8836,"longitude":-56.1284,"phone":"+59899000000","website":"https://clubnorte.uy","email":"hola@clubnorte.uy","instagram":"clubnorte","facebook":null,"logoUrl":"https://assets.skaliasports.com/clubs/norte.png","timezone":"America/Montevideo","currency":"UYU","countryCode":"598","sports":["PADEL"],"modules":{"bookings":true,"api":true,"streaming":false,"resourceCategories":false},"settings":{"bookingPolicy":"AUTO_CONFIRM","cancellationWindowHours":2,"noShowGraceMinutes":30,"maxBookingsPerUserPerDay":null,"maxAdvanceBookingDays":30,"requirePayment":false,"currency":"UYU","timezone":"America/Montevideo","peakHoursEnabled":true,"peakHoursStart":"18:00","peakHoursEnd":"23:00","peakHoursDays":[1,2,3,4,5],"spontaneousCategories":[],"updatedAt":"2026-09-14T12:00:00.000Z"},"followers":{"total":128,"notifiable":97}}},"ClubFollowersDto":{"type":"object","description":"Seguidores del club: sólo dos enteros. Nunca ids, nombres ni desglose por categoría.","properties":{"total":{"type":"integer","minimum":0,"description":"Jugadores que siguen al club en Skalia (cuentas vivas; los que silenciaron los avisos también cuentan)."},"notifiable":{"type":"integer","minimum":0,"description":"Los que reciben los avisos del club (campanita prendida): la audiencia real de horas pico y partidos espontáneos."}},"required":["total","notifiable"],"additionalProperties":false},"ClubHoursDay":{"type":"object","description":"Un día del horario semanal del club (lista cerrada `CLUB_HOURS_FIELDS`).","properties":{"dayOfWeek":{"type":"integer","minimum":0,"maximum":6,"description":"0 = domingo … 6 = sábado.","example":1},"open":{"type":"string","pattern":"^([01]\\d|2[0-3]):[0-5]\\d$","description":"Apertura `HH:MM`.","example":"08:00"},"close":{"type":"string","pattern":"^(([01]\\d|2[0-3]):[0-5]\\d|24:00)$","description":"Cierre `HH:MM` (`24:00` = medianoche).","example":"24:00"}},"required":["dayOfWeek","open","close"],"additionalProperties":false},"ClubHoursPutBody":{"type":"object","properties":{"clubId":{"type":"string","pattern":"^[a-f0-9-]{8,64}$","description":"Club sobre el que opera la request. Opcional: se infiere cuando la integración tiene un solo grant (kind CLUB, v1)."},"hours":{"maxItems":7,"type":"array","items":{"type":"object","properties":{"dayOfWeek":{"type":"integer","minimum":0,"maximum":6},"open":{"type":"string","pattern":"^([01]\\d|2[0-3]):[0-5]\\d$"},"close":{"type":"string","pattern":"^(([01]\\d|2[0-3]):[0-5]\\d|24:00)$"}},"required":["dayOfWeek","open","close"]},"description":"Un ítem por día abierto (día repetido → 400 VALIDATION). Los días ausentes quedan CERRADOS; `[]` borra el horario (sin techo)."}},"required":["hours"],"description":"Reemplazo completo del horario semanal del club (`club_hours`).","example":{"hours":[{"dayOfWeek":1,"open":"08:00","close":"24:00"},{"dayOfWeek":2,"open":"08:00","close":"24:00"},{"dayOfWeek":3,"open":"08:00","close":"24:00"},{"dayOfWeek":4,"open":"08:00","close":"24:00"},{"dayOfWeek":5,"open":"08:00","close":"24:00"},{"dayOfWeek":6,"open":"09:00","close":"22:00"}]}},"ClubHoursResponse":{"type":"object","properties":{"clubId":{"type":"string","format":"uuid","description":"Club."},"hours":{"type":"array","items":{"$ref":"#/components/schemas/ClubHoursDay"},"description":"Orden por día. Un día ausente = cerrado; `[]` = sin horario cargado (sin techo: mandan las franjas)."}},"required":["clubId","hours"],"additionalProperties":false,"example":{"clubId":"0d6a1f3e-2b4c-4d5e-9f7a-8b9c0d1e2f3a","hours":[{"dayOfWeek":1,"open":"08:00","close":"24:00"},{"dayOfWeek":2,"open":"08:00","close":"24:00"},{"dayOfWeek":3,"open":"08:00","close":"24:00"},{"dayOfWeek":4,"open":"08:00","close":"24:00"},{"dayOfWeek":5,"open":"08:00","close":"24:00"},{"dayOfWeek":6,"open":"09:00","close":"22:00"}]}},"ClubModulesDto":{"type":"object","description":"Banderas de plataforma del club (las prende Skalia): dicen qué rutas tienen sentido.","properties":{"bookings":{"type":"boolean","description":"Módulo de reservas del club (sin él, `/availability` no vende nada)."},"api":{"type":"boolean","description":"Módulo API (siempre `true` para un club con grant usable)."},"streaming":{"type":"boolean","description":"Grabación / streaming de los turnos habilitado (bandera; nunca las credenciales)."},"resourceCategories":{"type":"boolean","description":"Categorías de recurso (F6): sin él, las escrituras de `/resource-categories` responden 403."}},"required":["bookings","api","streaming","resourceCategories"],"additionalProperties":false},"ClubPatchBody":{"type":"object","properties":{"clubId":{"type":"string","pattern":"^[a-f0-9-]{8,64}$","description":"Club sobre el que opera la request. Opcional: se infiere cuando la integración tiene un solo grant (kind CLUB, v1)."},"name":{"type":"string","minLength":2,"maxLength":80,"description":"Nombre del club.","example":"Club Norte"},"description":{"anyOf":[{"type":"string","minLength":0,"maxLength":2000},{"type":"null"}],"description":"Descripción pública. `null` la borra."},"address":{"anyOf":[{"type":"string","minLength":0,"maxLength":200},{"type":"null"}],"description":"Dirección. `null` la borra.","example":"Av. Italia 1234"},"city":{"anyOf":[{"type":"string","minLength":0,"maxLength":80},{"type":"null"}],"description":"Ciudad. `null` la borra.","example":"Montevideo"},"latitude":{"anyOf":[{"type":"number","minimum":-90,"maximum":90},{"type":"null"}],"description":"Latitud (-90..90). `null` la borra.","example":-34.8836},"longitude":{"anyOf":[{"type":"number","minimum":-180,"maximum":180},{"type":"null"}],"description":"Longitud (-180..180). `null` la borra.","example":-56.1284},"phone":{"anyOf":[{"type":"string","minLength":6,"maxLength":32},{"type":"null"}],"description":"Teléfono público del club (el que ven los jugadores). `null` lo borra.","example":"+59899000000"},"website":{"anyOf":[{"type":"string","maxLength":300,"pattern":"^https?:\\/\\/"},{"type":"null"}],"description":"Sitio web público (`http://` o `https://`). `null` lo borra.","example":"https://clubnorte.uy"},"email":{"anyOf":[{"type":"string","maxLength":120,"format":"email","pattern":"^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"},{"type":"null"}],"description":"Email público del club. `null` lo borra.","example":"hola@clubnorte.uy"},"instagram":{"anyOf":[{"type":"string","minLength":0,"maxLength":120},{"type":"null"}],"description":"Usuario de Instagram (sin @). `null` lo borra.","example":"clubnorte"},"facebook":{"anyOf":[{"type":"string","minLength":0,"maxLength":120},{"type":"null"}],"description":"Página de Facebook. `null` la borra."},"logoUrl":{"anyOf":[{"type":"string","maxLength":500,"pattern":"^https:\\/\\/"},{"type":"null"}],"description":"URL `https://` del logo, alojado FUERA del storage de Skalia (esta API no sube binarios; máx. 500 chars). Una URL de `assets.skaliasports.com` que no sea el logo actual es 400 LOGO_URL_RESERVED. `null` lo quita.","example":"https://cdn.miweb.uy/logo.png"}},"description":"Parcial: al menos un campo. `null` borra el campo (salvo `name`).","example":{"description":"Cuatro canchas de pádel techadas.","phone":"+59899000000","website":"https://clubnorte.uy"}},"ClubSettingsDto":{"type":"object","description":"Configuración de NEGOCIO del club (`club_settings`). Lista cerrada `CLUB_SETTINGS_DTO_FIELDS`: nunca credenciales.","properties":{"bookingPolicy":{"$ref":"#/components/schemas/BookingPolicy"},"cancellationWindowHours":{"type":"integer","minimum":0,"description":"Horas antes del turno hasta las que el jugador puede cancelar."},"noShowGraceMinutes":{"type":"integer","minimum":0,"description":"Minutos de tolerancia antes de marcar ausencia."},"maxBookingsPerUserPerDay":{"oneOf":[{"type":"integer","minimum":1,"description":"Tope de reservas por jugador y día; `null` = sin tope."},{"type":"null"}]},"maxAdvanceBookingDays":{"type":"integer","minimum":1,"description":"Anticipación máxima (días) desde la app."},"requirePayment":{"type":"boolean","description":"Si el club exige pago para confirmar."},"currency":{"type":"string","description":"Moneda de los precios (ISO 4217).","example":"UYU"},"timezone":{"type":"string","description":"Zona IANA del club: la de todas las fechas y horas naive.","example":"America/Montevideo"},"peakHoursEnabled":{"type":"boolean","description":"Si el club promociona sus horas pico libres."},"peakHoursStart":{"type":"string","pattern":"^([01]\\d|2[0-3]):[0-5]\\d$","description":"Inicio de la franja pico.","example":"18:00"},"peakHoursEnd":{"type":"string","pattern":"^([01]\\d|2[0-3]):[0-5]\\d$","description":"Fin de la franja pico (posterior al inicio).","example":"23:00"},"peakHoursDays":{"type":"array","items":{"type":"integer","minimum":0,"maximum":6},"description":"Días de la franja pico (0 = domingo … 6 = sábado), ordenados y sin repetir.","example":[1,2,3,4,5]},"spontaneousCategories":{"type":"array","items":{"type":"string","enum":["profesional","1ra","2da","3ra","4ta","5ta","6ta","7ma","iniciación"]},"description":"Cola de categorías de los partidos espontáneos, en orden. `[]` = orden automático."},"updatedAt":{"oneOf":[{"type":"string","format":"date-time","description":"Última escritura de la fila, ISO UTC.","example":"2026-09-14T12:00:00.000Z"},{"type":"null"}]}},"required":["bookingPolicy","cancellationWindowHours","noShowGraceMinutes","maxBookingsPerUserPerDay","maxAdvanceBookingDays","requirePayment","currency","timezone","peakHoursEnabled","peakHoursStart","peakHoursEnd","peakHoursDays","spontaneousCategories","updatedAt"],"additionalProperties":false,"example":{"bookingPolicy":"AUTO_CONFIRM","cancellationWindowHours":2,"noShowGraceMinutes":30,"maxBookingsPerUserPerDay":null,"maxAdvanceBookingDays":30,"requirePayment":false,"currency":"UYU","timezone":"America/Montevideo","peakHoursEnabled":true,"peakHoursStart":"18:00","peakHoursEnd":"23:00","peakHoursDays":[1,2,3,4,5],"spontaneousCategories":[],"updatedAt":"2026-09-14T12:00:00.000Z"}},"ClubSettingsPatchBody":{"type":"object","properties":{"clubId":{"type":"string","pattern":"^[a-f0-9-]{8,64}$","description":"Club sobre el que opera la request. Opcional: se infiere cuando la integración tiene un solo grant (kind CLUB, v1)."},"bookingPolicy":{"type":"string","enum":["AUTO_CONFIRM","MANUAL"],"description":"Política de confirmación de las reservas del club (informativa para esta API: lo que creás por `/bookings` nace CONFIRMED)."},"cancellationWindowHours":{"type":"integer","minimum":0,"maximum":720,"description":"Horas antes del turno hasta las que el jugador puede cancelar.","example":2},"noShowGraceMinutes":{"type":"integer","minimum":0,"maximum":1440,"description":"Minutos de tolerancia antes de marcar ausencia.","example":30},"maxBookingsPerUserPerDay":{"anyOf":[{"type":"integer","minimum":1,"maximum":100},{"type":"null"}],"description":"Tope de reservas por jugador y día; `null` = sin tope.","example":null},"maxAdvanceBookingDays":{"type":"integer","minimum":1,"maximum":365,"description":"Anticipación máxima (días) con la que un jugador reserva desde la app (la API tiene su propio tope de 180 días).","example":30},"requirePayment":{"type":"boolean","description":"Si el club exige pago para confirmar."},"currency":{"type":"string","pattern":"^[A-Z]{3}$","description":"Moneda de los precios (ISO 4217).","example":"UYU"},"timezone":{"type":"string","minLength":1,"maxLength":64,"description":"Zona IANA del club: cambiarla re-lee TODAS las fechas y horas naive del club (reservas, partidos). Sensible.","example":"America/Montevideo"},"peakHoursEnabled":{"type":"boolean","description":"Si el club promociona sus horas pico libres."},"peakHoursStart":{"type":"string","pattern":"^([01]\\d|2[0-3]):[0-5]\\d$","description":"Inicio de la franja pico `HH:MM`.","example":"18:00"},"peakHoursEnd":{"type":"string","pattern":"^([01]\\d|2[0-3]):[0-5]\\d$","description":"Fin de la franja pico `HH:MM`; tiene que ser posterior al inicio (se valida contra lo guardado si sólo mandás uno).","example":"23:00"},"peakHoursDays":{"minItems":1,"maxItems":7,"type":"array","items":{"type":"integer","minimum":0,"maximum":6},"description":"Días de la franja pico (0 = domingo … 6 = sábado), sin repetir.","example":[1,2,3,4,5]},"spontaneousCategories":{"maxItems":18,"type":"array","items":{"type":"string","minLength":1,"maxLength":16},"description":"Cola de categorías de los partidos espontáneos, EN ORDEN (profesional, 1ra, 2da, 3ra, 4ta, 5ta, 6ta, 7ma, iniciación). `[]` vuelve al orden automático.","example":["4ta","5ta"]}},"description":"Parcial: al menos un campo. Mismos campos y validaciones que el panel.","example":{"cancellationWindowHours":4,"peakHoursEnabled":true,"peakHoursStart":"18:00","peakHoursEnd":"23:00","peakHoursDays":[1,2,3,4,5]}},"ConfirmResponse":{"type":"object","properties":{"booking":{"oneOf":[{"$ref":"#/components/schemas/BookingDto"},{"$ref":"#/components/schemas/SlotDto"}],"description":"La reserva confirmada, ANÓNIMA (`SlotDto`) aunque la integración tenga `canManageAllBookings`; `BookingDto` sólo si fuera propia."},"alreadyConfirmed":{"type":"boolean","description":"`true` si ya estaba `CONFIRMED` (la confirmación es idempotente)."}},"required":["booking","alreadyConfirmed"],"additionalProperties":false,"example":{"booking":{"id":"1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d","courtId":"5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9","kind":"COURT","categoryId":null,"resourceNoun":"COURT","date":"2026-09-15","startTime":"16:00","endTime":"17:00","status":"CONFIRMED","origin":"APP","owned":false,"matchCount":1,"recurringId":null},"alreadyConfirmed":false}},"Court":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Id de la cancha (el `courtId` de las demás rutas)."},"name":{"type":"string","description":"Nombre.","example":"Cancha 1"},"kind":{"$ref":"#/components/schemas/ResourceKind"},"categoryId":{"oneOf":[{"type":"string","format":"uuid","description":"Categoría de recurso (F6). `null` para las canchas implícitas."},{"type":"null"}]},"resourceNoun":{"$ref":"#/components/schemas/ResourceNoun"},"sports":{"type":"array","items":{"type":"string"},"description":"Deportes de la cancha (vacío para un recurso que no es cancha).","example":["PADEL"]},"enabled":{"type":"boolean","description":"`false` = deshabilitada (se lista igual para que mapees ids)."},"isIndoor":{"type":"boolean"},"pricing":{"type":"array","items":{"$ref":"#/components/schemas/PriceTier"}}},"required":["id","name","kind","categoryId","resourceNoun","sports","enabled","isIndoor","pricing"],"additionalProperties":false},"CourtCreateBody":{"type":"object","properties":{"clubId":{"type":"string","pattern":"^[a-f0-9-]{8,64}$","description":"Club sobre el que opera la request. Opcional: se infiere cuando la integración tiene un solo grant (kind CLUB, v1)."},"name":{"type":"string","minLength":1,"maxLength":60,"description":"Nombre visible (1–60).","example":"Cancha 5"},"sports":{"minItems":1,"maxItems":3,"type":"array","items":{"type":"string","enum":["PADEL","TENNIS","PICKLEBALL"]},"description":"Deportes de la cancha (PADEL | TENNIS | PICKLEBALL). Default `PADEL`. Se ignora en un recurso que no es cancha.","example":["PADEL"]},"isIndoor":{"type":"boolean","description":"Techada. También lo derivan las features `feat-cancha-interior` / `feat-cancha-exterior`."},"surface":{"anyOf":[{"type":"string","minLength":1,"maxLength":40},{"type":"null"}],"description":"Superficie libre (`Césped sintético`, `Cemento`…)."},"categoryId":{"type":"string","pattern":"^[a-f0-9-]{8,64}$","description":"Categoría de recurso (F6). Obligatoria si el club tiene categorías (400 CATEGORY_REQUIRED); omitila en un club sin categorías."},"order":{"type":"integer","minimum":1,"maximum":999,"description":"Posición en el orden del panel (1-based). Sin él, al final."},"pricePerHour":{"type":"number","minimum":0,"maximum":1000000,"description":"Precio/hora base. Sin él, el default del club (`GET /pricing/defaults`)."}},"required":["name"],"description":"Alta de una cancha (o de un recurso de otra categoría). Nace habilitada, con las franjas del horario por defecto del club. Nunca cámara ni ingest.","example":{"name":"Cancha 5","sports":["PADEL"],"isIndoor":false,"surface":"Césped sintético"}},"CourtDeleteResponse":{"type":"object","properties":{"courtId":{"type":"string","format":"uuid","description":"La cancha borrada."},"deleted":{"type":"boolean","const":true}},"required":["courtId","deleted"],"additionalProperties":false,"example":{"courtId":"5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9","deleted":true}},"CourtPatchBody":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":60,"description":"Nombre visible (1–60)."},"sports":{"minItems":1,"maxItems":3,"type":"array","items":{"type":"string","enum":["PADEL","TENNIS","PICKLEBALL"]},"description":"Deportes (PADEL | TENNIS | PICKLEBALL); reemplaza la lista."},"isIndoor":{"type":"boolean","description":"Techada."},"surface":{"anyOf":[{"type":"string","minLength":1,"maxLength":40},{"type":"null"}],"description":"Superficie; `null` la borra."},"categoryId":{"anyOf":[{"type":"string","pattern":"^[a-f0-9-]{8,64}$"},{"type":"null"}],"description":"Cambiar de categoría; `null` = sin categoría (cancha implícita), que en un club con categorías es 400 CATEGORY_REQUIRED. A otro kind con turnos vendidos (reservas vivas cuyo turno no terminó) → 409 RESOURCE_KIND_CHANGE_NOT_ALLOWED."},"order":{"type":"integer","minimum":1,"maximum":999,"description":"Posición en el orden del panel (1-based)."},"pricePerHour":{"anyOf":[{"type":"number","minimum":0,"maximum":1000000},{"type":"null"}],"description":"Precio/hora base; `null` lo borra (las franjas sin precio propio dejan de cobrarse: PRICE_UNAVAILABLE al reservar)."},"enabled":{"type":"boolean","description":"`false` = deshabilitar (baja lógica: deja de venderse, se sigue listando). Con reservas vivas (PENDING / CONFIRMED) cuyo turno no terminó → 409 salvo `?force=true`; los turnos ya jugados o marcados no cuentan."},"features":{"maxItems":20,"type":"array","items":{"type":"string","minLength":1,"maxLength":64},"description":"Reemplaza el set completo de features (ids del catálogo, `FeatureDto.id`). Sólo canchas (409 RESOURCE_NOT_COURT en otro kind).","example":["feat-cancha-exterior","feat-paredes-cristal"]}},"description":"Parcial: al menos un campo.","example":{"name":"Cancha 1 (cristal)","features":["feat-cancha-exterior","feat-paredes-cristal"]}},"CourtPatchResponse":{"type":"object","properties":{"court":{"$ref":"#/components/schemas/Court"},"features":{"type":"array","items":{"$ref":"#/components/schemas/FeatureDto"}},"cancelledBookings":{"type":"integer","minimum":0,"description":"Reservas futuras canceladas por `enabled: false` + `force=true` (0 en cualquier otro caso)."}},"required":["court","features","cancelledBookings"],"additionalProperties":false,"example":{"court":{"id":"5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9","name":"Cancha 1","kind":"COURT","categoryId":null,"resourceNoun":"COURT","sports":["PADEL"],"enabled":false,"isIndoor":false,"pricing":[{"pricePerHour":1000,"price60":null,"price90":1400,"price120":null,"ranges":[{"start":"08:00","end":"23:00"}]}]},"features":[{"id":"feat-cancha-exterior","name":"Cancha Exterior"},{"id":"feat-paredes-cristal","name":"Paredes de Cristal"}],"cancelledBookings":2}},"CourtPricingPutBody":{"type":"object","properties":{"slots":{"maxItems":200,"type":"array","items":{"type":"object","properties":{"dayOfWeek":{"type":"integer","minimum":0,"maximum":6},"start":{"type":"string","pattern":"^([01]\\d|2[0-3]):(00|30)$"},"end":{"type":"string","pattern":"^(([01]\\d|2[0-3]):(00|30)|24:00)$"},"pricePerHour":{"anyOf":[{"type":"number","minimum":0,"maximum":1000000},{"type":"null"}]},"price60":{"anyOf":[{"type":"number","minimum":0,"maximum":1000000},{"type":"null"}]},"price90":{"anyOf":[{"type":"number","minimum":0,"maximum":1000000},{"type":"null"}]},"price120":{"anyOf":[{"type":"number","minimum":0,"maximum":1000000},{"type":"null"}]}},"required":["dayOfWeek","start","end"]},"description":"Las franjas vendibles de la semana, sin solapes dentro de un día (400 VALIDATION con el índice en `details.issues[].path`). Reemplazo completo; `[]` = la cancha no vende nada."}},"required":["slots"],"description":"Reemplazo completo del tarifario de la cancha (espejo de `PUT /club-owner/courts/:id/slots`).","example":{"slots":[{"dayOfWeek":1,"start":"08:00","end":"18:00","pricePerHour":1000,"price60":null,"price90":1400,"price120":null},{"dayOfWeek":1,"start":"18:00","end":"24:00","pricePerHour":1300,"price60":null,"price90":1800,"price120":null}]}},"CourtPricingResponse":{"type":"object","properties":{"courtId":{"type":"string","format":"uuid","description":"Cancha."},"pricePerHour":{"oneOf":[{"type":"number","minimum":0,"description":"`courts.pricePerHour`: el fallback de las franjas sin precio propio."},{"type":"null"}]},"slots":{"type":"array","items":{"$ref":"#/components/schemas/PricingSlot"},"description":"Orden: día, comienzo. `[]` = la cancha no vende nada."}},"required":["courtId","pricePerHour","slots"],"additionalProperties":false,"example":{"courtId":"5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9","pricePerHour":1000,"slots":[{"dayOfWeek":1,"start":"08:00","end":"18:00","pricePerHour":1000,"price60":null,"price90":1400,"price120":null},{"dayOfWeek":1,"start":"18:00","end":"24:00","pricePerHour":1300,"price60":null,"price90":1800,"price120":null}]}},"CourtResponse":{"type":"object","description":"La cancha con el MISMO `Court` de `GET /courts` más sus features.","properties":{"court":{"$ref":"#/components/schemas/Court"},"features":{"type":"array","items":{"$ref":"#/components/schemas/FeatureDto"},"description":"Features de la cancha (`[]` para un recurso que no es cancha)."}},"required":["court","features"],"additionalProperties":false,"example":{"court":{"id":"5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9","name":"Cancha 1","kind":"COURT","categoryId":null,"resourceNoun":"COURT","sports":["PADEL"],"enabled":true,"isIndoor":false,"pricing":[{"pricePerHour":1000,"price60":null,"price90":1400,"price120":null,"ranges":[{"start":"08:00","end":"23:00"}]}]},"features":[{"id":"feat-cancha-exterior","name":"Cancha Exterior"},{"id":"feat-paredes-cristal","name":"Paredes de Cristal"}]}},"CourtsResponse":{"type":"object","properties":{"clubId":{"type":"string","format":"uuid","description":"Club."},"courts":{"type":"array","items":{"$ref":"#/components/schemas/Court"}}},"required":["clubId","courts"],"additionalProperties":false,"example":{"clubId":"0d6a1f3e-2b4c-4d5e-9f7a-8b9c0d1e2f3a","courts":[{"id":"5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9","name":"Cancha 1","kind":"COURT","categoryId":null,"resourceNoun":"COURT","sports":["PADEL"],"enabled":true,"isIndoor":false,"pricing":[{"pricePerHour":1000,"price60":null,"price90":1400,"price120":null,"ranges":[{"start":"08:00","end":"23:00"}]}]}]}},"CustomerDto":{"type":"object","description":"Cliente final de una reserva propia (lista cerrada `CUSTOMER_DTO_FIELDS`). Sólo lo ve la integración que la creó.","properties":{"name":{"oneOf":[{"type":"string","description":"Nombre del cliente final.","example":"Juan Pérez"},{"type":"null"}]},"phone":{"oneOf":[{"type":"string","description":"E.164.","example":"+59891234567"},{"type":"null"}]},"email":{"oneOf":[{"type":"string","description":"Email.","format":"email"},{"type":"null"}]}},"required":["name","phone","email"],"additionalProperties":false},"DeleteResponse":{"type":"object","properties":{"booking":{"oneOf":[{"$ref":"#/components/schemas/BookingDto"},{"$ref":"#/components/schemas/ManagedSlotDto"},{"$ref":"#/components/schemas/SlotDto"}],"description":"`BookingDto` si es propia (`owned: true`); si no, `ManagedSlotDto` con `canManageAllBookings` (`managed: true`) o `SlotDto` anónimo."},"detachedMatches":{"type":"integer","minimum":0,"description":"Partidos desligados por `force=true` (nunca cancelados)."},"alreadyCancelled":{"type":"boolean","description":"`true` si ya estaba cancelada (la baja es idempotente)."}},"required":["booking","detachedMatches","alreadyCancelled"],"additionalProperties":false,"example":{"booking":{"id":"9c1b2a3d-4e5f-4a6b-8c7d-0e1f2a3b4c5d","clubId":"0d6a1f3e-2b4c-4d5e-9f7a-8b9c0d1e2f3a","externalRef":"ORD-123","courtId":"5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9","courtName":"Cancha 1","kind":"COURT","categoryId":null,"resourceNoun":"COURT","date":"2026-09-15","startTime":"10:00","endTime":"11:30","durationMinutes":90,"timezone":"America/Montevideo","status":"CANCELLED","type":"normal","price":1400,"paymentStatus":"PAID","source":"api","origin":"API","customer":{"name":"Juan Pérez","phone":"+59891234567","email":null},"linked":false,"owned":true,"matchCount":0,"recording":null,"createdAt":"2026-09-14T12:00:00.000Z","updatedAt":"2026-09-14T12:00:00.000Z","recurringId":null},"detachedMatches":0,"alreadyCancelled":false}},"DurationMinutes":{"type":"integer","enum":[30,60,90,120],"description":"Duración del producto en minutos."},"ErrorEnvelope":{"type":"object","description":"Envelope de TODO error del namespace.","properties":{"error":{"type":"string","description":"Texto humano en castellano. No ramifiques por acá."},"code":{"type":"string","enum":["MISSING_API_KEY","MALFORMED_API_KEY","INVALID_API_KEY","API_KEY_WRONG_ENV","CLIENT_DISABLED","MODULE_DISABLED","PARTNER_API_DISABLED","CLUB_NOT_GRANTED","CLUB_REQUIRED","SCOPE_DENIED","RATE_LIMITED","UNSUPPORTED_VERSION","NOT_FOUND","UNSUPPORTED_MEDIA_TYPE","VALIDATION","DATE_IN_PAST","MAX_ADVANCE_EXCEEDED","COURT_NOT_FOUND","BOOKING_NOT_FOUND","BOOKING_NOT_OWNED_BY_CLIENT","BOOKING_CONFLICT","EXTERNAL_REF_CONFLICT","EXTERNAL_REF_USED","BOOKING_HAS_MATCH_ATTACHED","BOOKING_ALREADY_CANCELLED","BOOKING_NOT_MODIFIABLE","BOOKING_IN_PAST","OUTSIDE_OPENING_HOURS","INVALID_DURATION","PRICE_UNAVAILABLE","RESOURCE_NOT_COURT","BOOKING_NOT_PENDING","EVENTS_CURSOR_EXPIRED","CATEGORY_NOT_FOUND","CATEGORY_REQUIRED","RESOURCE_KIND_CHANGE_NOT_ALLOWED","FEATURE_NOT_FOUND","COURT_HAS_FUTURE_BOOKINGS","COURT_HAS_BOOKINGS","BLOCK_NOT_FOUND","BLOCK_CONFLICTS_WITH_BOOKINGS","RECURRING_NOT_FOUND","RECURRING_NOT_OWNED_BY_CLIENT","RECURRING_CONFLICT","RECURRING_ALREADY_CANCELLED","RECURRING_NOT_MODIFIABLE","ATTENDANCE_OUT_OF_WINDOW","HOLE_NOT_FOUND","TOURNAMENT_NOT_FOUND","TOURNAMENT_MATCH_NOT_FOUND","TOURNAMENT_INVALID_TRANSITION","TOURNAMENT_HAS_REGISTRATIONS","TOURNAMENT_STATUS_INVALID","TOURNAMENT_ALREADY_PLAYED","TOURNAMENT_RULE_VIOLATION","TOURNAMENT_SCHEDULE_CONFLICT","BRACKET_ALREADY_GENERATED","DUPLICATE_PAIRS","INCOMPLETE_PAIR_IN_MATCH","CHILD_MATCH_ALREADY_PLAYED","TOURNAMENT_CATEGORY_EXISTS","TOURNAMENT_TEAM_NOT_FOUND","INVALID_PEAK_RANGE","AMENITY_NOT_FOUND","RESOURCE_CATEGORIES_DISABLED","INVALID_CATEGORY_INPUT","RESOURCE_KIND_UNSUPPORTED","AWARDS_POINTS_NOT_ALLOWED","CATEGORY_HAS_RESOURCES","CATEGORY_IS_DEFAULT","LOGO_URL_RESERVED","WEBHOOK_NOT_FOUND","WEBHOOK_LIMIT","WEBHOOK_URL_INVALID","WEBHOOK_DISABLED","WEBHOOK_DELIVERY_NOT_FOUND","WEBHOOKS_UNAVAILABLE","MATCH_NOT_FOUND","MATCH_ALREADY_PUBLISHED","MATCH_NOT_OWNED_BY_CLIENT","MATCH_NOT_MODIFIABLE","AMERICANO_NOT_FOUND","INTERNAL_ERROR"],"description":"Code estable. Lista cerrada: este mismo enum (guía de errores en la doc pública)."},"details":{"type":"object","additionalProperties":true,"description":"Datos para máquinas, sin PII; sólo cuando hay algo que decir (`quota`, `scope`, `issues`, ids en conflicto, `busy`)."},"requestId":{"type":"string","description":"El `X-Request-Id` de la respuesta: es lo que se reporta a soporte.","example":"mi-sistema-0001"}},"required":["error","code","requestId"],"additionalProperties":false,"example":{"error":"El turno ya está ocupado.","code":"BOOKING_CONFLICT","details":{"busy":[{"startTime":"10:00","endTime":"11:30"}]},"requestId":"mi-sistema-0001"}},"EventActor":{"type":"string","enum":["user","admin","api","cron","qj","tournament","match"],"description":"Quién provocó el cambio."},"EventsResponse":{"type":"object","properties":{"events":{"type":"array","items":{"$ref":"#/components/schemas/BookingEvent"},"description":"Ascendente por `id`, hasta `limit`."},"nextSince":{"type":"integer","minimum":0,"description":"Id del último evento (o tu `since` si no hubo nada). Guardalo DESPUÉS de procesar la página.","example":1043}},"required":["events","nextSince"],"additionalProperties":false,"example":{"events":[{"id":1042,"type":"booking.cancelled","createdAt":"2026-09-14T15:02:11.000Z","actor":"admin","reason":"club","booking":{"id":"1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d","courtId":"5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9","kind":"COURT","categoryId":null,"resourceNoun":"COURT","date":"2026-09-15","startTime":"16:00","endTime":"17:00","status":"CONFIRMED","origin":"APP","owned":false,"matchCount":1,"recurringId":null}},{"id":1043,"type":"occupancy.changed","createdAt":"2026-09-14T15:04:00.000Z","actor":"admin","reason":"court_block","occupancy":{"courtId":"5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9","from":"2026-09-20","to":"2026-09-22"}}],"nextSince":1043}},"FeatureDto":{"type":"object","description":"Una feature de cancha (lista cerrada `FEATURE_DTO_FIELDS`).","properties":{"id":{"type":"string","description":"Id del catálogo (`feat-cancha-interior`, `feat-cancha-exterior`, `feat-paredes-cristal`, `feat-paredes-cemento`, `feat-camaras`, `feat-cesped-artificial`, `feat-iluminacion-led`).","example":"feat-cancha-exterior"},"name":{"type":"string","description":"Nombre humano.","example":"Cancha Exterior"}},"required":["id","name"],"additionalProperties":false},"GenerateBracketBody":{"type":"object","properties":{"qualifiedTeamIds":{"minItems":2,"maxItems":16,"type":"array","items":{"type":"string","pattern":"^[a-f0-9-]{8,64}$"},"description":"Parejas que clasifican, en orden de siembra (`TeamDto.id`). Sin ellas, las 2 mejores de cada grupo."}},"example":{}},"GenerateGroupsBody":{"type":"object","properties":{"autoFill":{"type":"boolean","description":"`false` deja sin grupo a las parejas que no son cabeza de serie (default `true`)."},"force":{"type":"boolean","description":"`true` hace lo que haría el panel al confirmar: borrar partidos jugados al regenerar / retroceder, o cancelar un torneo con inscriptos. Nunca revive un torneo terminal (COMPLETED / CANCELLED)."}},"example":{"autoFill":true}},"GroupStandings":{"type":"object","properties":{"groupId":{"type":"string","format":"uuid","description":"Grupo."},"groupName":{"type":"string","description":"Nombre."},"standings":{"type":"array","items":{"$ref":"#/components/schemas/StandingDto"},"description":"Ordenada: la primera es la líder."}},"required":["groupId","groupName","standings"],"additionalProperties":false},"GroupsResponse":{"type":"object","properties":{"groups":{"type":"array","items":{"$ref":"#/components/schemas/TournamentGroupDto"}},"warnings":{"$ref":"#/components/schemas/GroupsWarnings"}},"required":["groups"],"additionalProperties":false,"example":{"groups":[{"id":"4d3c2b1a-aaaa-4a7b-8c9d-0e1f2a3b4c5d","name":"A","timeSlot":"SATURDAY_MORNING","teamIds":["1a2b3c4d-1111-4a7b-8c9d-0e1f2a3b4c5d","1a2b3c4d-2222-4a7b-8c9d-0e1f2a3b4c5d"]}]}},"GroupsWarnings":{"type":"object","properties":{"seedsTogetherGroups":{"type":"array","items":{"type":"object","properties":{"groupId":{"type":"string","format":"uuid","description":"Grupo."},"groupName":{"type":"string","description":"Nombre."},"teamIds":{"type":"array","items":{"type":"string","format":"uuid","description":"Cabeza de serie."}}},"required":["groupId","groupName","teamIds"],"additionalProperties":false},"description":"Grupos con más de una cabeza de serie."},"overriddenTeams":{"type":"array","items":{"type":"object","properties":{"teamId":{"type":"string","format":"uuid","description":"Pareja."},"groupId":{"type":"string","format":"uuid","description":"Grupo."},"groupName":{"type":"string","description":"Nombre."},"groupTimeSlot":{"type":"string","description":"Franja del grupo."}},"required":["teamId","groupId","groupName","groupTimeSlot"],"additionalProperties":false},"description":"Parejas cuya preferencia horaria no coincide con la franja del grupo."}},"required":[],"additionalProperties":false},"ManagedSlotDto":{"type":"object","description":"Una reserva AJENA vista por una integración con `canManageAllBookings` (un POS que cobra todo el club): `SlotDto` + `price` / `paymentStatus`. Sigue sin `customer`, titular ni `type`. Lista cerrada `MANAGED_SLOT_DTO_FIELDS`. Nunca viaja en `/events` ni en webhooks.","properties":{"id":{"type":"string","format":"uuid","description":"Id de la reserva."},"courtId":{"type":"string","format":"uuid","description":"Cancha."},"kind":{"$ref":"#/components/schemas/ResourceKind"},"categoryId":{"oneOf":[{"type":"string","format":"uuid","description":"Categoría de recurso (F6). `null` para las canchas implícitas."},{"type":"null"}]},"resourceNoun":{"$ref":"#/components/schemas/ResourceNoun"},"date":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"`YYYY-MM-DD` naive en la zona del club.","example":"2026-09-15"},"startTime":{"type":"string","pattern":"^([01]\\d|2[0-3]):[0-5]\\d$","description":"Comienzo `HH:MM` naive del club.","example":"16:00"},"endTime":{"type":"string","pattern":"^(([01]\\d|2[0-3]):[0-5]\\d|24:00)$","description":"Fin `HH:MM` naive del club (`24:00` = medianoche).","example":"17:00"},"status":{"$ref":"#/components/schemas/BookingStatus"},"origin":{"$ref":"#/components/schemas/BookingOrigin"},"owned":{"type":"boolean","const":false,"description":"Siempre `false`: la creó otro origen u otra integración."},"matchCount":{"type":"integer","minimum":0,"description":"Partidos vivos armados en el turno."},"recurringId":{"oneOf":[{"type":"string","format":"uuid","description":"Serie de fijas de la que es ocurrencia (`origin: RECURRING`), o `null`. Un id de agenda del club, no un dato de personas."},{"type":"null"}]},"price":{"oneOf":[{"type":"number","description":"Precio efectivo: `price_override` si alguien lo fijó en un PATCH, si no el congelado al crear."},{"type":"null"}]},"paymentStatus":{"$ref":"#/components/schemas/PaymentStatus"},"managed":{"type":"boolean","const":true,"description":"Siempre `true`: la integración tiene `canManageAllBookings`."}},"required":["id","courtId","kind","categoryId","resourceNoun","date","startTime","endTime","status","origin","owned","matchCount","recurringId","price","paymentStatus","managed"],"additionalProperties":false,"example":{"id":"1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d","courtId":"5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9","kind":"COURT","categoryId":null,"resourceNoun":"COURT","date":"2026-09-15","startTime":"16:00","endTime":"17:00","status":"CONFIRMED","origin":"APP","owned":false,"matchCount":1,"recurringId":null,"price":1000,"paymentStatus":"PENDING","managed":true}},"MatchCategory":{"type":"string","enum":["profesional","1ra","2da","3ra","4ta","5ta","6ta","7ma","iniciación"],"description":"Categoría de juego, vocabulario legacy de la plataforma (`\"4ta\"`), de la más alta a la más baja."},"MatchCreateBody":{"type":"object","properties":{"bookingId":{"type":"string","pattern":"^[a-f0-9-]{8,64}$","description":"La reserva sobre la que se publica el partido (propia, o cualquiera del club con `canManageAllBookings`): `CONFIRMED`, con el turno sin terminar, en una cancha. Fecha, hora, duración y cancha salen de ahí."},"matchType":{"type":"string","enum":["SINGLES","DOUBLES"],"description":"`DOUBLES` (default, cupo 4) o `SINGLES` (cupo 2)."},"genderType":{"type":"string","enum":["MALE","FEMALE","MIXED"],"description":"`MIXED` (default), `MALE` o `FEMALE`: también filtra a quién le llega el push de `notifyCategories`."},"isCompetitive":{"type":"boolean","description":"`true` si mueve el nivel. Default `false`."},"categories":{"maxItems":9,"type":"array","items":{"type":"string","enum":["profesional","1ra","2da","3ra","4ta","5ta","6ta","7ma","iniciación"]},"description":"Quién puede SUMARSE (filtro de acceso, vocabulario legacy `\"4ta\"`). Vacío / ausente = cualquiera."},"notifyCategories":{"maxItems":9,"type":"array","items":{"type":"string","enum":["profesional","1ra","2da","3ra","4ta","5ta","6ta","7ma","iniciación"]},"description":"A quién le llega el push \"{club} abrió un partido para tu categoría\" (en background). Vacío / ausente = nadie."}},"required":["bookingId"],"additionalProperties":false,"description":"Publicar un partido sobre una reserva existente. Sin fecha ni cancha en el body (salen de la reserva); un campo desconocido es `VALIDATION`.","example":{"bookingId":"1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d","matchType":"DOUBLES","genderType":"MIXED","isCompetitive":false,"categories":["4ta","5ta"],"notifyCategories":["4ta","5ta"]}},"MatchDto":{"type":"object","description":"Un partido PÚBLICO del club, ANÓNIMO: contadores, nunca jugadores. Lista cerrada `MATCH_DTO_FIELDS`. Sin `invite_code` ni URL del video.","properties":{"id":{"type":"string","format":"uuid","description":"Id del partido."},"clubId":{"type":"string","format":"uuid","description":"Club del partido (el de la cancha, o el del partido si perdió la cancha)."},"courtId":{"oneOf":[{"type":"string","format":"uuid","description":"Cancha. `null` si el partido quedó sin cancha (la reserva se canceló con `?force=true`)."},{"type":"null"}]},"bookingId":{"oneOf":[{"type":"string","format":"uuid","description":"La reserva del turno, sólo si es propia de esta integración o la integración tiene `canManageAllBookings`; si no, `null`."},{"type":"null"}]},"sport":{"type":"string","description":"Deporte (`PADEL`, `TENNIS`, …), el de la cancha al publicar.","example":"PADEL"},"matchType":{"$ref":"#/components/schemas/MatchType"},"status":{"$ref":"#/components/schemas/MatchStatus"},"isCompetitive":{"type":"boolean","description":"`true` si mueve el nivel de los jugadores."},"genderType":{"$ref":"#/components/schemas/MatchGender"},"categories":{"type":"array","items":{"$ref":"#/components/schemas/MatchCategory"},"description":"Quién puede sumarse (filtro de acceso). Vacío = cualquiera."},"date":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"`YYYY-MM-DD` naive en la zona del club.","example":"2026-09-15"},"startTime":{"type":"string","pattern":"^([01]\\d|2[0-3]):[0-5]\\d$","description":"Comienzo `HH:MM` naive del club.","example":"21:00"},"durationMinutes":{"type":"integer","minimum":1,"description":"Duración en minutos."},"spots":{"$ref":"#/components/schemas/MatchSpotsDto"},"owned":{"type":"boolean","description":"`true` si lo publicó ESTA integración (`POST /matches`)."},"result":{"oneOf":[{"$ref":"#/components/schemas/MatchResultDto"},{"type":"null"}]},"cancellationReason":{"oneOf":[{"type":"string","description":"Motivo de la cancelación (texto que ve el jugador en la app); `null` si no está cancelado."},{"type":"null"}]},"joinUrl":{"type":"string","description":"Deep link del detalle en la web: el \"sumate\" del cartel.","format":"uri","example":"https://www.skaliasports.com/partidos/9c1d2e3f-4a5b-4c6d-8e7f-0a1b2c3d4e5f"},"createdAt":{"oneOf":[{"type":"string","format":"date-time","description":"ISO UTC.","example":"2026-09-14T12:00:00.000Z"},{"type":"null"}]}},"required":["id","clubId","courtId","bookingId","sport","matchType","status","isCompetitive","genderType","categories","date","startTime","durationMinutes","spots","owned","result","cancellationReason","joinUrl","createdAt"],"additionalProperties":false,"example":{"id":"9c1d2e3f-4a5b-4c6d-8e7f-0a1b2c3d4e5f","clubId":"0d6a1f3e-2b4c-4d5e-9f7a-8b9c0d1e2f3a","courtId":"5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9","bookingId":"1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d","sport":"PADEL","matchType":"DOUBLES","status":"OPEN","isCompetitive":false,"genderType":"MIXED","categories":["4ta","5ta"],"date":"2026-09-15","startTime":"21:00","durationMinutes":90,"spots":{"total":4,"taken":2},"owned":true,"result":null,"cancellationReason":null,"joinUrl":"https://www.skaliasports.com/partidos/9c1d2e3f-4a5b-4c6d-8e7f-0a1b2c3d4e5f","createdAt":"2026-09-14T12:00:00.000Z"}},"MatchGender":{"type":"string","enum":["MALE","FEMALE","MIXED"],"description":"Género del partido (`MIXED` = cualquiera)."},"MatchResponse":{"type":"object","properties":{"match":{"$ref":"#/components/schemas/TournamentMatchDto"}},"required":["match"],"additionalProperties":false,"example":{"match":{"id":"6f5e4d3c-bbbb-4a7b-8c9d-0e1f2a3b4c5d","tournamentId":"9b8a7c6d-5e4f-4a3b-2c1d-0e9f8a7b6c5d","phase":"GROUP","round":1,"groupId":"4d3c2b1a-aaaa-4a7b-8c9d-0e1f2a3b4c5d","matchOrder":1,"team1Id":"1a2b3c4d-1111-4a7b-8c9d-0e1f2a3b4c5d","team2Id":"1a2b3c4d-2222-4a7b-8c9d-0e1f2a3b4c5d","team1Label":"Los Halcones","team2Label":"Pareja #2","team1FromMatchId":null,"team1FromOutcome":null,"team2FromMatchId":null,"team2FromOutcome":null,"status":"COMPLETED","winnerTeamId":"1a2b3c4d-1111-4a7b-8c9d-0e1f2a3b4c5d","isWalkover":false,"walkoverTeamId":null,"sets":[{"team1":6,"team2":3},{"team1":7,"team2":5}],"courtId":"5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9","scheduledAt":"2026-10-17T09:00:00"}}},"MatchResultBody":{"type":"object","properties":{"sets":{"minItems":1,"maxItems":5,"type":"array","items":{"type":"object","properties":{"team1":{"type":"integer","minimum":0,"maximum":99},"team2":{"type":"integer","minimum":0,"maximum":99}},"required":["team1","team2"]},"description":"Set a set (`{ team1, team2 }` = games). Obligatorio salvo W.O."},"winnerTeamId":{"type":"string","pattern":"^[a-f0-9-]{8,64}$","description":"Opcional; tiene que coincidir con los sets."},"walkover":{"type":"boolean","description":"`true`: una pareja no se presentó. El partido se cierra sin mover el nivel Skalia ni el historial de nadie."},"absentTeamId":{"type":"string","pattern":"^[a-f0-9-]{8,64}$","description":"La pareja que NO se presentó (obligatorio con `walkover: true`)."}},"description":"Resultado set a set, o W.O. (`walkover: true` + `absentTeamId`; los sets son opcionales y el default es 6-0 6-0 a favor de la pareja presente).","example":{"sets":[{"team1":6,"team2":3},{"team1":7,"team2":5}]}},"MatchResultDto":{"type":"object","description":"Resultado confirmado (sólo en `COMPLETED`). Lista cerrada `MATCH_RESULT_DTO_FIELDS`.","properties":{"team1Sets":{"type":"integer","minimum":0,"description":"Sets del equipo 1."},"team2Sets":{"type":"integer","minimum":0,"description":"Sets del equipo 2."}},"required":["team1Sets","team2Sets"],"additionalProperties":false},"MatchResultResponse":{"type":"object","properties":{"match":{"$ref":"#/components/schemas/TournamentMatchDto"},"bracketResync":{"$ref":"#/components/schemas/BracketResync"}},"required":["match"],"additionalProperties":false,"example":{"match":{"id":"6f5e4d3c-bbbb-4a7b-8c9d-0e1f2a3b4c5d","tournamentId":"9b8a7c6d-5e4f-4a3b-2c1d-0e9f8a7b6c5d","phase":"GROUP","round":1,"groupId":"4d3c2b1a-aaaa-4a7b-8c9d-0e1f2a3b4c5d","matchOrder":1,"team1Id":"1a2b3c4d-1111-4a7b-8c9d-0e1f2a3b4c5d","team2Id":"1a2b3c4d-2222-4a7b-8c9d-0e1f2a3b4c5d","team1Label":"Los Halcones","team2Label":"Pareja #2","team1FromMatchId":null,"team1FromOutcome":null,"team2FromMatchId":null,"team2FromOutcome":null,"status":"COMPLETED","winnerTeamId":"1a2b3c4d-1111-4a7b-8c9d-0e1f2a3b4c5d","isWalkover":false,"walkoverTeamId":null,"sets":[{"team1":6,"team2":3},{"team1":7,"team2":5}],"courtId":"5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9","scheduledAt":"2026-10-17T09:00:00"}}},"MatchScheduleBody":{"type":"object","properties":{"courtId":{"anyOf":[{"type":"string","pattern":"^[a-f0-9-]{8,64}$"},{"type":"null"}],"description":"Cancha del club del torneo (`GET /courts`), o `null` para desprogramar."},"scheduledAt":{"anyOf":[{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}T([01]\\d|2[0-3]):[0-5]\\d(:[0-5]\\d)?$"},{"type":"null"}],"description":"`YYYY-MM-DDTHH:MM` naive en la zona del club, o `null` para desprogramar.","example":"2026-10-17T09:00"}},"required":["courtId","scheduledAt"],"description":"`courtId` + `scheduledAt` programan; los dos en `null` desprograman (se libera la reserva del partido).","example":{"courtId":"5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9","scheduledAt":"2026-10-17T09:00"}},"MatchSpotsDto":{"type":"object","description":"Cupo del partido, sólo contadores. Lista cerrada `MATCH_SPOTS_DTO_FIELDS`.","properties":{"total":{"type":"integer","minimum":2,"maximum":4,"description":"Cupo del partido: 2 (singles) o 4 (dobles)."},"taken":{"type":"integer","minimum":0,"description":"Jugadores que ya ocupan lugar (confirmados). Nunca quiénes."}},"required":["total","taken"],"additionalProperties":false},"MatchStatus":{"type":"string","enum":["OPEN","FULL","IN_PROGRESS","COMPLETED","CANCELLED"],"description":"Estado público del partido: `COMPLETED` sólo con resultado confirmado; `IN_PROGRESS` = se juega o falta confirmar el resultado. Nunca `PENDING_APPROVAL`."},"MatchType":{"type":"string","enum":["SINGLES","DOUBLES"],"description":"`matches.match_type`."},"MatchesResponse":{"type":"object","properties":{"matches":{"type":"array","items":{"$ref":"#/components/schemas/TournamentMatchDto"}}},"required":["matches"],"additionalProperties":false,"example":{"matches":[{"id":"6f5e4d3c-bbbb-4a7b-8c9d-0e1f2a3b4c5d","tournamentId":"9b8a7c6d-5e4f-4a3b-2c1d-0e9f8a7b6c5d","phase":"GROUP","round":1,"groupId":"4d3c2b1a-aaaa-4a7b-8c9d-0e1f2a3b4c5d","matchOrder":1,"team1Id":"1a2b3c4d-1111-4a7b-8c9d-0e1f2a3b4c5d","team2Id":"1a2b3c4d-2222-4a7b-8c9d-0e1f2a3b4c5d","team1Label":"Los Halcones","team2Label":"Pareja #2","team1FromMatchId":null,"team1FromOutcome":null,"team2FromMatchId":null,"team2FromOutcome":null,"status":"COMPLETED","winnerTeamId":"1a2b3c4d-1111-4a7b-8c9d-0e1f2a3b4c5d","isWalkover":false,"walkoverTeamId":null,"sets":[{"team1":6,"team2":3},{"team1":7,"team2":5}],"courtId":"5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9","scheduledAt":"2026-10-17T09:00:00"}]}},"MeResponse":{"type":"object","description":"Quién soy, en qué clubes, con qué permisos y cuotas. Nunca ids de usuarios ni datos de personas.","properties":{"integration":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Id de la integración."},"kind":{"type":"string","enum":["CLUB","PARTNER"],"description":"`CLUB` en v1 (una integración del propio club)."},"name":{"type":"string","description":"Nombre que le puso el club en el panel.","example":"Mi web"},"linkPlayersByPhone":{"type":"boolean","description":"Vincular la reserva al jugador verificado cuyo teléfono matchea (opt-in)."},"canManageAllBookings":{"type":"boolean","description":"Puede leer / tocar reservas ajenas del club (siempre como `SlotDto`)."},"notifyStaff":{"type":"boolean","description":"Avisar al staff (push + campana) por cada alta."},"mirrorToQj":{"type":"boolean","description":"Espejar las altas a QuieroJugar."}},"required":["id","kind","name","linkPlayersByPhone","canManageAllBookings","notifyStaff","mirrorToQj"],"additionalProperties":false},"key":{"type":"object","properties":{"prefix":{"type":"string","description":"Prefijo visible de la key.","example":"sk_live_a1b2"},"env":{"type":"string","enum":["live","test"]},"scopes":{"type":"array","items":{"$ref":"#/components/schemas/ApiScope"},"description":"Scopes de la key (el efectivo por club es `clubs[].scopes`)."},"expiresAt":{"oneOf":[{"type":"string","format":"date-time","description":"≠ `null` = la key está en gracia de rotación: cambiá a la nueva antes de esa fecha.","example":"2026-09-14T12:00:00.000Z"},{"type":"null"}]}},"required":["prefix","env","scopes","expiresAt"],"additionalProperties":false},"clubs":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Club."},"name":{"type":"string","description":"Nombre del club.","example":"Club Norte"},"timezone":{"type":"string","description":"Zona IANA: la de todas las fechas y horas naive.","example":"America/Montevideo"},"countryCode":{"oneOf":[{"type":"string","description":"Código telefónico del país.","example":"598"},{"type":"null"}]},"currency":{"oneOf":[{"type":"string","description":"Moneda de los precios.","example":"UYU"},{"type":"null"}]},"hours":{"type":"array","description":"Horario del club, INFORMATIVO (`club_hours`): lo vendible sale de `/availability`.","items":{"type":"object","properties":{"dayOfWeek":{"type":"integer","minimum":0,"maximum":6,"description":"0 = domingo … 6 = sábado."},"open":{"type":"string","pattern":"^([01]\\d|2[0-3]):[0-5]\\d$","description":"Apertura `HH:MM`.","example":"08:00"},"close":{"type":"string","pattern":"^(([01]\\d|2[0-3]):[0-5]\\d|24:00)$","description":"Cierre `HH:MM` (`24:00` = medianoche).","example":"23:00"}},"required":["dayOfWeek","open","close"],"additionalProperties":false}},"scopes":{"type":"array","items":{"$ref":"#/components/schemas/ApiScope"},"description":"Scope EFECTIVO en ese club (key ∩ grant): el que gobierna cada ruta."},"autoConfirm":{"type":"boolean","description":"Política de confirmación del club (informativa: la API siempre crea `CONFIRMED`)."}},"required":["id","name","timezone","countryCode","currency","hours","scopes","autoConfirm"],"additionalProperties":false}},"rateLimit":{"type":"object","description":"Cuotas vigentes de la integración (`details.quota` del 429 dice cuál frenó).","properties":{"perMinute":{"type":"integer","example":120},"perDay":{"type":"integer","example":10000},"writesPerMinute":{"type":"integer","example":20},"writesPerDay":{"type":"integer","example":500},"maxFutureBookings":{"type":"integer","example":2000}},"required":["perMinute","perDay","writesPerMinute","writesPerDay","maxFutureBookings"],"additionalProperties":false},"server":{"type":"object","properties":{"now":{"type":"string","format":"date-time","description":"Hora del server, ISO UTC.","example":"2026-09-14T12:00:00.000Z"}},"required":["now"],"additionalProperties":false}},"required":["integration","key","clubs","rateLimit","server"],"additionalProperties":false,"example":{"integration":{"id":"7f8e9d0c-1b2a-4c3d-9e8f-7a6b5c4d3e2f","kind":"CLUB","name":"Mi web","linkPlayersByPhone":false,"canManageAllBookings":false,"notifyStaff":true,"mirrorToQj":false},"key":{"prefix":"sk_live_a1b2","env":"live","scopes":["courts:read","bookings:read"],"expiresAt":null},"clubs":[{"id":"0d6a1f3e-2b4c-4d5e-9f7a-8b9c0d1e2f3a","name":"Club Norte","timezone":"America/Montevideo","countryCode":"598","currency":"UYU","hours":[{"dayOfWeek":1,"open":"08:00","close":"23:00"}],"scopes":["courts:read","bookings:read"],"autoConfirm":true}],"rateLimit":{"perMinute":120,"perDay":10000,"writesPerMinute":20,"writesPerDay":500,"maxFutureBookings":2000},"server":{"now":"2026-09-14T12:00:00.000Z"}}},"NaiveDateTime":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}T([01]\\d|2[0-3]):[0-5]\\d(:[0-5]\\d)?$","description":"`YYYY-MM-DDTHH:MM[:SS]` naive en la zona del club."},"OccupancyBusy":{"type":"object","properties":{"startTime":{"type":"string","pattern":"^([01]\\d|2[0-3]):[0-5]\\d$","description":"Inicio del rango ocupado.","example":"10:00"},"endTime":{"type":"string","pattern":"^(([01]\\d|2[0-3]):[0-5]\\d|24:00)$","description":"Fin del rango ocupado (`24:00` = medianoche).","example":"11:30"},"bookingId":{"type":"string","format":"uuid","description":"Sólo cuando el rango es una reserva de ESTA integración."}},"required":["startTime","endTime"],"additionalProperties":false},"OccupancyChanged":{"type":"object","description":"Rango a re-leer en `/occupancy` (evento `occupancy.changed`).","properties":{"courtId":{"oneOf":[{"type":"string","format":"uuid","description":"Cancha afectada; `null` = todas las del club."},{"type":"null"}]},"from":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"Primer día a re-leer en `/occupancy`.","example":"2026-09-15"},"to":{"oneOf":[{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"Último día, o `null` si el cambio es abierto hacia adelante (una serie de fijas).","example":"2026-09-15"},{"type":"null"}]}},"required":["courtId","from","to"],"additionalProperties":false},"OccupancyDay":{"type":"object","properties":{"date":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"Día.","example":"2026-09-15"},"courts":{"type":"array","items":{"type":"object","properties":{"courtId":{"type":"string","format":"uuid","description":"Cancha."},"kind":{"$ref":"#/components/schemas/ResourceKind"},"categoryId":{"oneOf":[{"type":"string","format":"uuid","description":"Categoría de recurso (F6). `null` para las canchas implícitas."},{"type":"null"}]},"resourceNoun":{"$ref":"#/components/schemas/ResourceNoun"},"busy":{"type":"array","items":{"$ref":"#/components/schemas/OccupancyBusy"},"description":"Rangos ocupados por cualquier fuente (reservas, fijas, bloqueos, partidos con cancha), sin PII."}},"required":["courtId","kind","categoryId","resourceNoun","busy"],"additionalProperties":false}}},"required":["date","courts"],"additionalProperties":false},"OccupancyResponse":{"type":"object","description":"Fuente de verdad de \"ocupado\", sin PII.","properties":{"clubId":{"type":"string","format":"uuid","description":"Club."},"from":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"Primer día.","example":"2026-09-15"},"to":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"Último día.","example":"2026-09-15"},"timezone":{"type":"string","description":"Zona IANA del club.","example":"America/Montevideo"},"days":{"type":"array","items":{"$ref":"#/components/schemas/OccupancyDay"}}},"required":["clubId","from","to","timezone","days"],"additionalProperties":false,"example":{"clubId":"0d6a1f3e-2b4c-4d5e-9f7a-8b9c0d1e2f3a","from":"2026-09-15","to":"2026-09-16","timezone":"America/Montevideo","days":[{"date":"2026-09-15","courts":[{"courtId":"5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9","kind":"COURT","categoryId":null,"resourceNoun":"COURT","busy":[{"startTime":"10:00","endTime":"11:30"},{"startTime":"12:00","endTime":"13:00","bookingId":"9c1b2a3d-4e5f-4a6b-8c7d-0e1f2a3b4c5d"}]}]}]}},"OccurrenceStatus":{"type":"string","enum":["PROJECTED","PENDING","CONFIRMED","CANCELLED","ATTENDED","NO_SHOW","SKIPPED"],"description":"`status` de una ocurrencia: el de `court_bookings` si está materializada; `PROJECTED` (fecha de la cadencia sin fila, normalmente más allá del horizonte del cron) o `SKIPPED` (fecha salteada sin fila) si no."},"PartnerQuota":{"type":"string","enum":["reads_per_minute","reads_per_day","writes_per_minute","writes_per_day","max_future_bookings","key_prefix","invalid_keys_per_ip"],"description":"`details.quota` de un 429: qué cuota frenó."},"PaymentStatus":{"type":"string","enum":["PAID","PENDING","UNPAID"],"description":"Estado de pago de la reserva."},"PodiumEntryDto":{"type":"object","description":"Una fila del podio que escribe `finalize`. Lista cerrada `PODIUM_ENTRY_DTO_FIELDS`. Nunca quiénes la integran.","properties":{"position":{"type":"integer","minimum":1,"maximum":3,"description":"1 = campeón, 2 = subcampeón, 3 = semifinalistas (una fila por cada uno)."},"teamId":{"type":"string","format":"uuid","description":"Pareja (`TeamDto.id`)."},"label":{"type":"string","description":"Etiqueta anónima de la pareja."}},"required":["position","teamId","label"],"additionalProperties":false,"example":{"position":1,"teamId":"1a2b3c4d-1111-4a7b-8c9d-0e1f2a3b4c5d","label":"Los Halcones"}},"PriceTier":{"type":"object","description":"Tier de precio de una cancha.","properties":{"pricePerHour":{"oneOf":[{"type":"number","description":"Precio/hora base de la franja (pro-rata)."},{"type":"null"}]},"price60":{"oneOf":[{"type":"number","description":"Total por 60 minutos, si la franja lo fija."},{"type":"null"}]},"price90":{"oneOf":[{"type":"number","description":"Total por 90 minutos, si la franja lo fija."},{"type":"null"}]},"price120":{"oneOf":[{"type":"number","description":"Total por 120 minutos, si la franja lo fija."},{"type":"null"}]},"ranges":{"type":"array","description":"Rangos horarios que cubre el tier (mergeados). `[]` = tarifa única de la cancha, todo el día.","items":{"type":"object","properties":{"start":{"type":"string","pattern":"^([01]\\d|2[0-3]):[0-5]\\d$","description":"Inicio `HH:MM`.","example":"08:00"},"end":{"type":"string","pattern":"^(([01]\\d|2[0-3]):[0-5]\\d|24:00)$","description":"Fin `HH:MM` (`24:00` = medianoche).","example":"23:00"}},"required":["start","end"],"additionalProperties":false}}},"required":["pricePerHour","price60","price90","price120","ranges"],"additionalProperties":false},"PriceUnit":{"type":"string","enum":["HOUR","TURN"],"description":"Unidad del precio (derivada de `pricingMode`)."},"PricingDefaults":{"type":"object","description":"Defaults del club que materializan las franjas de cada cancha (lista cerrada `PRICING_DEFAULTS_FIELDS`).","properties":{"clubId":{"type":"string","format":"uuid","description":"Club."},"defaultPricePerHour":{"oneOf":[{"type":"number","minimum":0,"description":"Precio/hora que heredan las canchas nuevas."},{"type":"null"}]},"defaultOpen":{"oneOf":[{"type":"string","pattern":"^([01]\\d|2[0-3]):[0-5]\\d$","description":"Apertura por defecto de las franjas de una cancha nueva.","example":"08:00"},{"type":"null"}]},"defaultClose":{"oneOf":[{"type":"string","pattern":"^(([01]\\d|2[0-3]):[0-5]\\d|24:00)$","description":"Cierre por defecto (`24:00` = medianoche).","example":"24:00"},{"type":"null"}]},"slotIntervalMinutes":{"type":"integer","minimum":30,"description":"Granularidad de las franjas materializadas (múltiplo de 30).","example":30}},"required":["clubId","defaultPricePerHour","defaultOpen","defaultClose","slotIntervalMinutes"],"additionalProperties":false,"example":{"clubId":"0d6a1f3e-2b4c-4d5e-9f7a-8b9c0d1e2f3a","defaultPricePerHour":1000,"defaultOpen":"08:00","defaultClose":"24:00","slotIntervalMinutes":30}},"PricingDefaultsPutBody":{"type":"object","properties":{"clubId":{"type":"string","pattern":"^[a-f0-9-]{8,64}$","description":"Club sobre el que opera la request. Opcional: se infiere cuando la integración tiene un solo grant (kind CLUB, v1)."},"defaultPricePerHour":{"anyOf":[{"type":"number","minimum":0,"maximum":1000000},{"type":"null"}],"description":"Precio/hora por defecto; `null` lo borra. Si cambia, pisa `pricePerHour` de todas las canchas y el base de sus franjas (no los totales por duración)."},"defaultOpen":{"anyOf":[{"type":"string","pattern":"^([01]\\d|2[0-3]):(00|30)$"},{"type":"null"}],"description":"Apertura por defecto. Va junto con `defaultClose` (los dos, o los dos `null`). Si cambia, regenera las franjas de TODAS las canchas conservando la tarifa de cada hora."},"defaultClose":{"anyOf":[{"type":"string","pattern":"^(([01]\\d|2[0-3]):(00|30)|24:00)$"},{"type":"null"}],"description":"Cierre por defecto (`24:00` = medianoche)."},"slotIntervalMinutes":{"type":"integer","minimum":30,"maximum":240,"description":"Granularidad de las franjas (múltiplo de 30). Si cambia, regenera como el horario."}},"description":"Parcial: al menos un default.","example":{"defaultPricePerHour":1100,"defaultOpen":"08:00","defaultClose":"24:00","slotIntervalMinutes":30}},"PricingDefaultsPutResponse":{"type":"object","properties":{"clubId":{"type":"string","format":"uuid","description":"Club."},"defaultPricePerHour":{"oneOf":[{"type":"number","minimum":0,"description":"Precio/hora por defecto."},{"type":"null"}]},"defaultOpen":{"oneOf":[{"type":"string","pattern":"^([01]\\d|2[0-3]):[0-5]\\d$","description":"Apertura por defecto.","example":"08:00"},{"type":"null"}]},"defaultClose":{"oneOf":[{"type":"string","pattern":"^(([01]\\d|2[0-3]):[0-5]\\d|24:00)$","description":"Cierre por defecto.","example":"24:00"},{"type":"null"}]},"slotIntervalMinutes":{"type":"integer","minimum":30,"example":30},"regeneratedCourts":{"type":"integer","minimum":0,"description":"Canchas cuyas franjas se regeneraron (cambió horario / intervalo)."},"durationPriceOverrides":{"type":"integer","minimum":0,"description":"Franjas del club con total por duración cargado: siguen cobrando ese total, no el precio por defecto nuevo."}},"required":["clubId","defaultPricePerHour","defaultOpen","defaultClose","slotIntervalMinutes","regeneratedCourts","durationPriceOverrides"],"additionalProperties":false,"example":{"clubId":"0d6a1f3e-2b4c-4d5e-9f7a-8b9c0d1e2f3a","defaultPricePerHour":1000,"defaultOpen":"08:00","defaultClose":"24:00","slotIntervalMinutes":30,"regeneratedCourts":4,"durationPriceOverrides":12}},"PricingMode":{"type":"string","enum":["PER_SLOT","FLAT","NONE"],"description":"Cómo se cobra el turno de una categoría."},"PricingSlot":{"type":"object","description":"Una franja vendible con su tarifa (lista cerrada `PRICING_SLOT_FIELDS`). Las franjas contiguas del mismo día con la misma tarifa salen fundidas.","properties":{"dayOfWeek":{"type":"integer","minimum":0,"maximum":6,"description":"0 = domingo … 6 = sábado.","example":1},"start":{"type":"string","pattern":"^([01]\\d|2[0-3]):[0-5]\\d$","description":"Comienzo de la franja `HH:MM`, en la grilla de 30'.","example":"08:00"},"end":{"type":"string","pattern":"^(([01]\\d|2[0-3]):[0-5]\\d|24:00)$","description":"Fin de la franja `HH:MM` (`24:00` = medianoche), en la grilla de 30'.","example":"18:00"},"pricePerHour":{"oneOf":[{"type":"number","minimum":0,"description":"Precio/hora base de la franja (pro-rata). `null` = sin precio propio: se cobra `pricePerHour` de la cancha."},{"type":"null"}]},"price60":{"oneOf":[{"type":"number","minimum":0,"description":"Total por 60 minutos; manda sobre el pro-rata."},{"type":"null"}]},"price90":{"oneOf":[{"type":"number","minimum":0,"description":"Total por 90 minutos; manda sobre el pro-rata."},{"type":"null"}]},"price120":{"oneOf":[{"type":"number","minimum":0,"description":"Total por 120 minutos; manda sobre el pro-rata."},{"type":"null"}]}},"required":["dayOfWeek","start","end","pricePerHour","price60","price90","price120"],"additionalProperties":false},"PublicMatchDeleteResponse":{"type":"object","properties":{"match":{"$ref":"#/components/schemas/MatchDto"},"changed":{"type":"boolean","description":"`false` si ya estaba cancelado (la baja es idempotente)."}},"required":["match","changed"],"additionalProperties":false,"example":{"match":{"id":"9c1d2e3f-4a5b-4c6d-8e7f-0a1b2c3d4e5f","clubId":"0d6a1f3e-2b4c-4d5e-9f7a-8b9c0d1e2f3a","courtId":"5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9","bookingId":"1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d","sport":"PADEL","matchType":"DOUBLES","status":"CANCELLED","isCompetitive":false,"genderType":"MIXED","categories":["4ta","5ta"],"date":"2026-09-15","startTime":"21:00","durationMinutes":90,"spots":{"total":4,"taken":2},"owned":true,"result":null,"cancellationReason":"Cancelado por el club","joinUrl":"https://www.skaliasports.com/partidos/9c1d2e3f-4a5b-4c6d-8e7f-0a1b2c3d4e5f","createdAt":"2026-09-14T12:00:00.000Z"},"changed":true}},"PublicMatchResponse":{"type":"object","properties":{"match":{"$ref":"#/components/schemas/MatchDto"}},"required":["match"],"additionalProperties":false,"example":{"match":{"id":"9c1d2e3f-4a5b-4c6d-8e7f-0a1b2c3d4e5f","clubId":"0d6a1f3e-2b4c-4d5e-9f7a-8b9c0d1e2f3a","courtId":"5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9","bookingId":"1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d","sport":"PADEL","matchType":"DOUBLES","status":"OPEN","isCompetitive":false,"genderType":"MIXED","categories":["4ta","5ta"],"date":"2026-09-15","startTime":"21:00","durationMinutes":90,"spots":{"total":4,"taken":2},"owned":true,"result":null,"cancellationReason":null,"joinUrl":"https://www.skaliasports.com/partidos/9c1d2e3f-4a5b-4c6d-8e7f-0a1b2c3d4e5f","createdAt":"2026-09-14T12:00:00.000Z"}}},"PublicMatchesListResponse":{"type":"object","properties":{"matches":{"type":"array","items":{"$ref":"#/components/schemas/MatchDto"},"description":"Orden: fecha y hora, id."},"total":{"type":"integer","minimum":0},"limit":{"type":"integer","minimum":1,"maximum":200},"offset":{"type":"integer","minimum":0}},"required":["matches","total","limit","offset"],"additionalProperties":false,"example":{"matches":[{"id":"9c1d2e3f-4a5b-4c6d-8e7f-0a1b2c3d4e5f","clubId":"0d6a1f3e-2b4c-4d5e-9f7a-8b9c0d1e2f3a","courtId":"5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9","bookingId":"1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d","sport":"PADEL","matchType":"DOUBLES","status":"OPEN","isCompetitive":false,"genderType":"MIXED","categories":["4ta","5ta"],"date":"2026-09-15","startTime":"21:00","durationMinutes":90,"spots":{"total":4,"taken":2},"owned":true,"result":null,"cancellationReason":null,"joinUrl":"https://www.skaliasports.com/partidos/9c1d2e3f-4a5b-4c6d-8e7f-0a1b2c3d4e5f","createdAt":"2026-09-14T12:00:00.000Z"}],"total":1,"limit":100,"offset":0}},"RecordingDto":{"type":"object","description":"Grabación del turno (lista cerrada `RECORDING_DTO_FIELDS`). Sólo con scope `recordings:read`.","properties":{"url":{"type":"string","description":"URL del live / VOD.","format":"uri","example":"https://www.youtube.com/watch?v=abc123"},"status":{"$ref":"#/components/schemas/RecordingStatus"}},"required":["url","status"],"additionalProperties":false},"RecordingStatus":{"type":"string","enum":["SCHEDULED","LIVE","READY"],"description":"Estado de la grabación del turno."},"RecurringBookingDto":{"type":"object","description":"Una serie de reservas fijas del club. Lista cerrada `RECURRING_DTO_FIELDS`. Nunca el titular.","properties":{"id":{"type":"string","format":"uuid","description":"Id de la serie."},"clubId":{"type":"string","format":"uuid","description":"Club de la cancha."},"courtId":{"type":"string","format":"uuid","description":"Cancha."},"courtName":{"type":"string","description":"Nombre de la cancha."},"kind":{"$ref":"#/components/schemas/ResourceKind"},"categoryId":{"oneOf":[{"type":"string","format":"uuid","description":"Categoría de recurso (F6). `null` para las canchas implícitas."},{"type":"null"}]},"resourceNoun":{"$ref":"#/components/schemas/ResourceNoun"},"startDate":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"Primer día de la serie.","example":"2026-09-15"},"endDate":{"oneOf":[{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"Último día (inclusive); `null` = sin fin.","example":"2026-09-15"},{"type":"null"}]},"startTime":{"type":"string","pattern":"^([01]\\d|2[0-3]):[0-5]\\d$","description":"Comienzo `HH:MM` naive del club.","example":"20:00"},"endTime":{"type":"string","pattern":"^(([01]\\d|2[0-3]):[0-5]\\d|24:00)$","description":"Fin `HH:MM` naive del club (`24:00` = medianoche).","example":"21:30"},"durationMinutes":{"type":"integer","description":"Duración en minutos."},"frequency":{"$ref":"#/components/schemas/RecurringFrequency"},"status":{"$ref":"#/components/schemas/RecurringStatus"},"type":{"type":"string","description":"`booking_type` de la serie (`normal` / `profe`).","example":"normal"},"price":{"oneOf":[{"type":"number","description":"`price_override` de la serie; `null` = tarifario de cada ocurrencia."},{"type":"null"}]},"timezone":{"type":"string","description":"Zona IANA del club.","example":"America/Montevideo"},"externalRef":{"oneOf":[{"type":"string","description":"Tu `externalRef` (sólo en series propias)."},{"type":"null"}]},"customer":{"oneOf":[{"$ref":"#/components/schemas/CustomerDto"},{"type":"null"}]},"owned":{"type":"boolean","description":"`true` si la creó esta integración (sólo entonces viajan `customer` y `externalRef`)."},"nextOccurrence":{"oneOf":[{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"Próxima fecha de la cadencia desde hoy, sin las salteadas.","example":"2026-09-15"},{"type":"null"}]},"skippedDates":{"type":"array","items":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"Fecha salteada.","example":"2026-09-15"},"description":"Excepciones de hoy en adelante."},"createdAt":{"oneOf":[{"type":"string","format":"date-time","description":"ISO UTC.","example":"2026-09-14T12:00:00.000Z"},{"type":"null"}]}},"required":["id","clubId","courtId","courtName","kind","categoryId","resourceNoun","startDate","endDate","startTime","endTime","durationMinutes","frequency","status","type","price","timezone","externalRef","customer","owned","nextOccurrence","skippedDates","createdAt"],"additionalProperties":false,"example":{"id":"8b7c6d5e-4f3a-4b2c-9d1e-0f9a8b7c6d5e","clubId":"0d6a1f3e-2b4c-4d5e-9f7a-8b9c0d1e2f3a","courtId":"5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9","courtName":"Cancha 1","kind":"COURT","categoryId":null,"resourceNoun":"COURT","startDate":"2026-09-15","endDate":null,"startTime":"20:00","endTime":"21:30","durationMinutes":90,"frequency":"weekly","status":"ACTIVE","type":"normal","price":1400,"timezone":"America/Montevideo","externalRef":"FIJA-42","customer":{"name":"Juan Pérez","phone":"+59891234567","email":null},"owned":true,"nextOccurrence":"2026-09-15","skippedDates":[],"createdAt":"2026-09-14T12:00:00.000Z"}},"RecurringCreateBody":{"type":"object","properties":{"clubId":{"type":"string","pattern":"^[a-f0-9-]{8,64}$","description":"Club sobre el que opera la request. Opcional: se infiere cuando la integración tiene un solo grant (kind CLUB, v1)."},"courtId":{"type":"string","pattern":"^[a-f0-9-]{8,64}$","description":"Id de la cancha (`GET /courts`)."},"startDate":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"Primer día de la serie, `YYYY-MM-DD` naive del club (no puede ser pasado ni a más de 180 días).","example":"2026-09-15"},"endDate":{"anyOf":[{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},{"type":"null"}],"description":"Último día (inclusive) o `null` / ausente = sin fin.","example":null},"startTime":{"type":"string","pattern":"^([01]\\d|2[0-3]):(00|30)$","description":"Comienzo en la grilla del producto: `HH:00` o `HH:30`, naive en la zona del club.","example":"10:00"},"durationMinutes":{"type":"integer","enum":[30,60,90,120],"description":"Duración del turno en minutos. Exactamente uno de `durationMinutes` / `endTime`.","example":90},"endTime":{"type":"string","pattern":"^(([01]\\d|2[0-3]):[0-5]\\d|24:00)$","description":"Fin del turno `HH:MM` (puede ser `24:00`). Exactamente uno de `durationMinutes` / `endTime`; el resultado tiene que dar 30/60/90/120 (si no, INVALID_DURATION).","example":"11:30"},"frequency":{"type":"string","enum":["daily","weekly","monthly"],"description":"Cadencia: `daily`, `weekly` (mismo día de la semana) o `monthly` (mismo día del mes, anclado al original)."},"externalRef":{"type":"string","minLength":1,"maxLength":64,"pattern":"^[\\x21-\\x7E]+$","description":"Tu id de la fija: da idempotencia al POST (mismo ref + misma serie → 200 replay) y es de un solo uso. Alias: header `Idempotency-Key`.","example":"FIJA-42"},"customer":{"type":"object","properties":{"name":{"type":"string","minLength":2,"maxLength":80,"description":"Nombre del cliente final.","example":"Juan Pérez"},"phone":{"type":"string","minLength":6,"maxLength":32,"description":"Teléfono del cliente final (se normaliza a E.164). Con `linkPlayersByPhone` puede vincular la reserva a un jugador verificado.","example":"+598 91 234 567"},"email":{"type":"string","maxLength":120,"format":"email","pattern":"^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$","description":"Email del cliente final.","example":"juan@example.com"}},"required":["name"],"description":"Cliente final de la reserva (PII de terceros: sólo lo ve tu integración). El teléfono se guarda normalizado a E.164. En un PATCH reemplaza los tres campos; `null` los borra."},"price":{"type":"number","minimum":0,"description":"Total por ocurrencia (`price_override` de la serie). Si no viene, cada ocurrencia toma el tarifario de su día.","example":1400},"type":{"type":"string","enum":["normal","profe"],"description":"Tipo de la serie (`normal` / `profe`). Default `normal`."}},"required":["courtId","startDate","startTime","frequency"],"description":"Alta de una serie de fijas. Exactamente uno de `durationMinutes` / `endTime`.","example":{"courtId":"5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9","startDate":"2026-09-15","endDate":null,"startTime":"20:00","durationMinutes":90,"frequency":"weekly","externalRef":"FIJA-42","customer":{"name":"Juan Pérez","phone":"+598 91 234 567"},"price":1400}},"RecurringCreateResponse":{"type":"object","properties":{"recurringBooking":{"$ref":"#/components/schemas/RecurringBookingDto"},"materializedOccurrences":{"type":"integer","minimum":0,"description":"Ocurrencias creadas ya como reservas (hoy → +30 días); el resto las crea el cron a medida que entran en el horizonte."}},"required":["recurringBooking","materializedOccurrences"],"additionalProperties":false,"example":{"recurringBooking":{"id":"8b7c6d5e-4f3a-4b2c-9d1e-0f9a8b7c6d5e","clubId":"0d6a1f3e-2b4c-4d5e-9f7a-8b9c0d1e2f3a","courtId":"5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9","courtName":"Cancha 1","kind":"COURT","categoryId":null,"resourceNoun":"COURT","startDate":"2026-09-15","endDate":null,"startTime":"20:00","endTime":"21:30","durationMinutes":90,"frequency":"weekly","status":"ACTIVE","type":"normal","price":1400,"timezone":"America/Montevideo","externalRef":"FIJA-42","customer":{"name":"Juan Pérez","phone":"+59891234567","email":null},"owned":true,"nextOccurrence":"2026-09-15","skippedDates":[],"createdAt":"2026-09-14T12:00:00.000Z"},"materializedOccurrences":5}},"RecurringDeleteResponse":{"type":"object","properties":{"recurringBooking":{"$ref":"#/components/schemas/RecurringBookingDto"},"cancelledOccurrences":{"type":"integer","minimum":0,"description":"Ocurrencias futuras canceladas (0 con `keepFuture=true`)."},"detachedMatches":{"type":"integer","minimum":0},"alreadyCancelled":{"type":"boolean","description":"`true` si ya estaba cancelada (la baja es idempotente)."}},"required":["recurringBooking","cancelledOccurrences","detachedMatches","alreadyCancelled"],"additionalProperties":false,"example":{"recurringBooking":{"id":"8b7c6d5e-4f3a-4b2c-9d1e-0f9a8b7c6d5e","clubId":"0d6a1f3e-2b4c-4d5e-9f7a-8b9c0d1e2f3a","courtId":"5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9","courtName":"Cancha 1","kind":"COURT","categoryId":null,"resourceNoun":"COURT","startDate":"2026-09-15","endDate":null,"startTime":"20:00","endTime":"21:30","durationMinutes":90,"frequency":"weekly","status":"CANCELLED","type":"normal","price":1400,"timezone":"America/Montevideo","externalRef":"FIJA-42","customer":{"name":"Juan Pérez","phone":"+59891234567","email":null},"owned":true,"nextOccurrence":null,"skippedDates":[],"createdAt":"2026-09-14T12:00:00.000Z"},"cancelledOccurrences":4,"detachedMatches":0,"alreadyCancelled":false}},"RecurringExceptionBody":{"type":"object","properties":{"date":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"Fecha a saltar: tiene que ser una ocurrencia futura de la serie.","example":"2026-09-22"}},"required":["date"],"example":{"date":"2026-09-22"}},"RecurringExceptionResponse":{"type":"object","properties":{"recurringBooking":{"$ref":"#/components/schemas/RecurringBookingDto"},"skippedDate":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"La fecha salteada.","example":"2026-09-15"},"cancelledBookingId":{"oneOf":[{"type":"string","format":"uuid","description":"La ocurrencia materializada que se canceló, si había."},{"type":"null"}]},"alreadySkipped":{"type":"boolean","description":"`true` si la fecha ya estaba salteada (200 en vez de 201)."}},"required":["recurringBooking","skippedDate","cancelledBookingId","alreadySkipped"],"additionalProperties":false,"example":{"recurringBooking":{"id":"8b7c6d5e-4f3a-4b2c-9d1e-0f9a8b7c6d5e","clubId":"0d6a1f3e-2b4c-4d5e-9f7a-8b9c0d1e2f3a","courtId":"5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9","courtName":"Cancha 1","kind":"COURT","categoryId":null,"resourceNoun":"COURT","startDate":"2026-09-15","endDate":null,"startTime":"20:00","endTime":"21:30","durationMinutes":90,"frequency":"weekly","status":"ACTIVE","type":"normal","price":1400,"timezone":"America/Montevideo","externalRef":"FIJA-42","customer":{"name":"Juan Pérez","phone":"+59891234567","email":null},"owned":true,"nextOccurrence":"2026-09-15","skippedDates":["2026-09-22"],"createdAt":"2026-09-14T12:00:00.000Z"},"skippedDate":"2026-09-22","cancelledBookingId":null,"alreadySkipped":false}},"RecurringFrequency":{"type":"string","enum":["daily","weekly","monthly"],"description":"Cadencia de una serie."},"RecurringGetResponse":{"type":"object","properties":{"recurringBooking":{"$ref":"#/components/schemas/RecurringBookingDto"}},"required":["recurringBooking"],"additionalProperties":false,"example":{"recurringBooking":{"id":"8b7c6d5e-4f3a-4b2c-9d1e-0f9a8b7c6d5e","clubId":"0d6a1f3e-2b4c-4d5e-9f7a-8b9c0d1e2f3a","courtId":"5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9","courtName":"Cancha 1","kind":"COURT","categoryId":null,"resourceNoun":"COURT","startDate":"2026-09-15","endDate":null,"startTime":"20:00","endTime":"21:30","durationMinutes":90,"frequency":"weekly","status":"ACTIVE","type":"normal","price":1400,"timezone":"America/Montevideo","externalRef":"FIJA-42","customer":{"name":"Juan Pérez","phone":"+59891234567","email":null},"owned":true,"nextOccurrence":"2026-09-15","skippedDates":[],"createdAt":"2026-09-14T12:00:00.000Z"}}},"RecurringListResponse":{"type":"object","properties":{"recurringBookings":{"type":"array","items":{"$ref":"#/components/schemas/RecurringBookingDto"},"description":"Orden: primer día, hora, id."},"total":{"type":"integer","minimum":0},"limit":{"type":"integer","minimum":1,"maximum":200},"offset":{"type":"integer","minimum":0}},"required":["recurringBookings","total","limit","offset"],"additionalProperties":false,"example":{"recurringBookings":[{"id":"8b7c6d5e-4f3a-4b2c-9d1e-0f9a8b7c6d5e","clubId":"0d6a1f3e-2b4c-4d5e-9f7a-8b9c0d1e2f3a","courtId":"5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9","courtName":"Cancha 1","kind":"COURT","categoryId":null,"resourceNoun":"COURT","startDate":"2026-09-15","endDate":null,"startTime":"20:00","endTime":"21:30","durationMinutes":90,"frequency":"weekly","status":"ACTIVE","type":"normal","price":1400,"timezone":"America/Montevideo","externalRef":"FIJA-42","customer":{"name":"Juan Pérez","phone":"+59891234567","email":null},"owned":true,"nextOccurrence":"2026-09-15","skippedDates":[],"createdAt":"2026-09-14T12:00:00.000Z"}],"total":1,"limit":100,"offset":0}},"RecurringOccurrenceDto":{"type":"object","description":"Una fecha de la serie en el rango. Lista cerrada `OCCURRENCE_DTO_FIELDS`. Nunca el titular.","properties":{"date":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"Día de la ocurrencia.","example":"2026-09-15"},"startTime":{"type":"string","pattern":"^([01]\\d|2[0-3]):[0-5]\\d$","description":"Comienzo `HH:MM` naive del club (el de la fila materializada, o el de la serie).","example":"20:00"},"endTime":{"type":"string","pattern":"^(([01]\\d|2[0-3]):[0-5]\\d|24:00)$","description":"Fin `HH:MM` naive del club (`24:00` = medianoche).","example":"21:30"},"status":{"$ref":"#/components/schemas/OccurrenceStatus"},"bookingId":{"oneOf":[{"type":"string","format":"uuid","description":"La reserva materializada (`GET /bookings/{id}`). Sólo si la serie es propia o la integración tiene `canManageAllBookings`; si no, `null`."},{"type":"null"}]}},"required":["date","startTime","endTime","status","bookingId"],"additionalProperties":false,"example":{"date":"2026-10-06","startTime":"20:00","endTime":"21:30","status":"ATTENDED","bookingId":"9c1b2a3d-4e5f-4a6b-8c7d-0e1f2a3b4c5d"}},"RecurringOccurrencesResponse":{"type":"object","description":"Lista cerrada `RECURRING_OCCURRENCES_RESPONSE_FIELDS`.","properties":{"recurringId":{"type":"string","format":"uuid","description":"La serie."},"courtId":{"type":"string","format":"uuid","description":"Cancha de la serie."},"from":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"Primer día del rango consultado (el pedido, o hoy del club).","example":"2026-09-15"},"to":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"Último día del rango (el pedido, o `min(endDate, from + 90 días)`).","example":"2026-09-15"},"timezone":{"type":"string","description":"Zona IANA del club.","example":"America/Montevideo"},"occurrences":{"type":"array","items":{"$ref":"#/components/schemas/RecurringOccurrenceDto"},"description":"Orden: fecha. Una por fecha de la cadencia en el rango, más las materializadas fuera de la cadencia si las hubiera."}},"required":["recurringId","courtId","from","to","timezone","occurrences"],"additionalProperties":false,"example":{"recurringId":"8b7c6d5e-4f3a-4b2c-9d1e-0f9a8b7c6d5e","courtId":"5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9","from":"2026-10-01","to":"2026-10-31","timezone":"America/Montevideo","occurrences":[{"date":"2026-10-06","startTime":"20:00","endTime":"21:30","status":"ATTENDED","bookingId":"9c1b2a3d-4e5f-4a6b-8c7d-0e1f2a3b4c5d"},{"date":"2026-10-13","startTime":"20:00","endTime":"21:30","status":"CONFIRMED","bookingId":"2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e"},{"date":"2026-10-20","startTime":"20:00","endTime":"21:30","status":"SKIPPED","bookingId":null},{"date":"2026-10-27","startTime":"20:00","endTime":"21:30","status":"PROJECTED","bookingId":null}]}},"RecurringPatchBody":{"type":"object","properties":{"endDate":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"Nuevo último día (inclusive). Sólo se puede ACORTAR (extender = 409 RECURRING_NOT_MODIFIABLE); las ocurrencias materializadas que quedan afuera se cancelan.","example":"2026-12-31"},"customer":{"anyOf":[{"type":"object","properties":{"name":{"type":"string","minLength":2,"maxLength":80,"description":"Nombre del cliente final.","example":"Juan Pérez"},"phone":{"type":"string","minLength":6,"maxLength":32,"description":"Teléfono del cliente final (se normaliza a E.164). Con `linkPlayersByPhone` puede vincular la reserva a un jugador verificado.","example":"+598 91 234 567"},"email":{"type":"string","maxLength":120,"format":"email","pattern":"^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$","description":"Email del cliente final.","example":"juan@example.com"}},"required":["name"]},{"type":"null"}],"description":"Cliente final de la reserva (PII de terceros: sólo lo ve tu integración). El teléfono se guarda normalizado a E.164. En un PATCH reemplaza los tres campos; `null` los borra."},"price":{"anyOf":[{"type":"number","minimum":0},{"type":"null"}],"description":"Nuevo `price_override` de la serie (`null` lo borra); se propaga a las ocurrencias futuras.","example":1500}},"description":"Parcial: al menos un campo. Mover cancha / día / horario = DELETE + POST.","example":{"endDate":"2026-12-31"}},"RecurringPatchResponse":{"type":"object","properties":{"recurringBooking":{"$ref":"#/components/schemas/RecurringBookingDto"},"cancelledOccurrences":{"type":"integer","minimum":0,"description":"Ocurrencias materializadas que quedaron fuera del `endDate` nuevo."},"detachedMatches":{"type":"integer","minimum":0}},"required":["recurringBooking","cancelledOccurrences","detachedMatches"],"additionalProperties":false,"example":{"recurringBooking":{"id":"8b7c6d5e-4f3a-4b2c-9d1e-0f9a8b7c6d5e","clubId":"0d6a1f3e-2b4c-4d5e-9f7a-8b9c0d1e2f3a","courtId":"5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9","courtName":"Cancha 1","kind":"COURT","categoryId":null,"resourceNoun":"COURT","startDate":"2026-09-15","endDate":"2026-12-31","startTime":"20:00","endTime":"21:30","durationMinutes":90,"frequency":"weekly","status":"ACTIVE","type":"normal","price":1400,"timezone":"America/Montevideo","externalRef":"FIJA-42","customer":{"name":"Juan Pérez","phone":"+59891234567","email":null},"owned":true,"nextOccurrence":"2026-09-15","skippedDates":[],"createdAt":"2026-09-14T12:00:00.000Z"},"cancelledOccurrences":0,"detachedMatches":0}},"RecurringStatus":{"type":"string","enum":["ACTIVE","CANCELLED"],"description":"`recurring_bookings.status`."},"ReportBySource":{"type":"object","description":"Subtotal por origen de la reserva. Lista cerrada `REPORT_BY_SOURCE_DTO_FIELDS`. Orden: mayor venta primero.","properties":{"source":{"$ref":"#/components/schemas/ReportSource"},"apiClientId":{"oneOf":[{"type":"string","format":"uuid","description":"Sólo en la fila `api` de TU integración; `null` en el resto. Las demás integraciones del club van sumadas en una única fila `api` con `apiClientId: null`."},{"type":"null"}]},"count":{"type":"integer","minimum":0,"description":"Reservas que entran (puntuales + ocurrencias de fijas, `status != CANCELLED`)."},"durationMinutes":{"type":"integer","minimum":0,"description":"Minutos de cancha sumados."},"price":{"type":"number","description":"Venta ESPERADA (tarifa de cada reserva, precedencia `price_override` total → `price` materializado → pro-rata del precio/hora), 2 decimales. Nunca lo cobrado."}},"required":["source","apiClientId","count","durationMinutes","price"],"additionalProperties":false},"ReportByType":{"type":"object","description":"Subtotal por tipo de reserva. Lista cerrada `REPORT_BY_TYPE_DTO_FIELDS`. Orden: mayor venta primero.","properties":{"type":{"type":"string","description":"`court_bookings.type` (`normal`, `profe`, …).","example":"normal"},"count":{"type":"integer","minimum":0,"description":"Reservas que entran (puntuales + ocurrencias de fijas, `status != CANCELLED`)."},"durationMinutes":{"type":"integer","minimum":0,"description":"Minutos de cancha sumados."},"price":{"type":"number","description":"Venta ESPERADA (tarifa de cada reserva, precedencia `price_override` total → `price` materializado → pro-rata del precio/hora), 2 decimales. Nunca lo cobrado."}},"required":["type","count","durationMinutes","price"],"additionalProperties":false},"ReportGroup":{"type":"object","description":"Un grupo de `groupBy`. Lista cerrada `REPORT_GROUP_DTO_FIELDS`. Orden: etiqueta (es), clave.","properties":{"key":{"type":"string","description":"Clave estable del grupo: `courtId` (`court`), id de categoría o `court-implicit` (`category`), el tipo (`type`) o el origen (`source`).","example":"5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9"},"label":{"type":"string","description":"Etiqueta humana: nombre de la cancha, label de la categoría (`Canchas` para las implícitas), el tipo o el origen.","example":"Cancha 1"},"count":{"type":"integer","minimum":0,"description":"Reservas que entran (puntuales + ocurrencias de fijas, `status != CANCELLED`)."},"durationMinutes":{"type":"integer","minimum":0,"description":"Minutos de cancha sumados."},"price":{"type":"number","description":"Venta ESPERADA (tarifa de cada reserva, precedencia `price_override` total → `price` materializado → pro-rata del precio/hora), 2 decimales. Nunca lo cobrado."}},"required":["key","label","count","durationMinutes","price"],"additionalProperties":false},"ReportGroupBy":{"type":"string","enum":["court","category","type","source"],"description":"Dimensión de `groups`. `user` no existe: el reporte nunca lista personas."},"ReportSource":{"type":"string","enum":["android","ios","web","admin","qj","recurring","api","unknown"],"description":"`court_bookings.source` de la reserva (`unknown` = anterior a la columna)."},"ReportTotals":{"type":"object","description":"Totales del rango. Lista cerrada `REPORT_TOTALS_DTO_FIELDS`.","properties":{"count":{"type":"integer","minimum":0,"description":"Reservas que entran (puntuales + ocurrencias de fijas, `status != CANCELLED`)."},"durationMinutes":{"type":"integer","minimum":0,"description":"Minutos de cancha sumados."},"price":{"type":"number","description":"Venta ESPERADA (tarifa de cada reserva, precedencia `price_override` total → `price` materializado → pro-rata del precio/hora), 2 decimales. Nunca lo cobrado."},"priceIsExpected":{"type":"boolean","const":true,"description":"Siempre `true`: `price` es tarifa esperada, no cobrado (Skalia no sincroniza pagos)."},"byType":{"type":"array","items":{"$ref":"#/components/schemas/ReportByType"}},"bySource":{"type":"array","items":{"$ref":"#/components/schemas/ReportBySource"}}},"required":["count","durationMinutes","price","priceIsExpected","byType","bySource"],"additionalProperties":false},"ResourceCategoriesResponse":{"type":"object","properties":{"clubId":{"type":"string","format":"uuid","description":"Club."},"moduleEnabled":{"type":"boolean","description":"Si el club tiene el módulo de categorías prendido (gate de las escrituras)."},"categories":{"type":"array","items":{"$ref":"#/components/schemas/ResourceCategoryDto"},"description":"Todas (deshabilitadas y default incluidas), orden `sortOrder, label`."}},"required":["clubId","moduleEnabled","categories"],"additionalProperties":false,"example":{"clubId":"0d6a1f3e-2b4c-4d5e-9f7a-8b9c0d1e2f3a","moduleEnabled":true,"categories":[{"id":"3c9d2f10-7a4b-4e5c-9d6e-1f2a3b4c5d6e","clubId":"0d6a1f3e-2b4c-4d5e-9f7a-8b9c0d1e2f3a","kind":"CLINIC","resourceNoun":"PROFESSIONAL","label":"Clínica","icon":"medical_services","sortOrder":2,"enabled":true,"isDefault":false,"bookingPolicy":"MANUAL","pricingMode":"FLAT","priceUnit":"TURN","allowedDurations":[30,60],"defaultDurationMinutes":30,"approvalTimeoutHours":2,"approvalGraceMinutes":60,"cancellationWindowHours":24,"awardsPoints":false,"resourceCount":1}]}},"ResourceCategoryCreateBody":{"type":"object","properties":{"clubId":{"type":"string","pattern":"^[a-f0-9-]{8,64}$","description":"Club sobre el que opera la request. Opcional: se infiere cuando la integración tiene un solo grant (kind CLUB, v1)."},"kind":{"type":"string","enum":["COURT","CLINIC","RESTAURANT","OTHER"],"description":"Familia del recurso (inmutable después de crear)."},"label":{"type":"string","minLength":2,"maxLength":40,"description":"Nombre visible (2..40).","example":"Clínica"},"resourceNoun":{"type":"string","enum":["COURT","ROOM","PROFESSIONAL","TABLE","GENERIC"],"description":"Sustantivo del recurso (wording). Default por kind. Sólo cambia mientras la categoría no tiene recursos."},"icon":{"anyOf":[{"type":"string","minLength":0,"maxLength":60},{"type":"null"}],"description":"Ícono Material opcional. `null` lo quita.","example":"medical_services"},"bookingPolicy":{"type":"string","enum":["AUTO_CONFIRM","MANUAL"],"description":"`AUTO_CONFIRM` o `MANUAL` (MANUAL no existe para kind COURT: 400 RESOURCE_KIND_UNSUPPORTED)."},"pricingMode":{"type":"string","enum":["PER_SLOT","FLAT","NONE"],"description":"Cómo se cobra el turno: por franja (`PER_SLOT`), fijo (`FLAT`) o sin cargo (`NONE`). Default por kind."},"allowedDurations":{"minItems":1,"maxItems":4,"type":"array","items":{"type":"integer"},"description":"Duraciones ofrecidas, ⊂ [30, 60, 90, 120], no vacío.","example":[30,60]},"defaultDurationMinutes":{"type":"integer","description":"Duración por defecto; tiene que estar en `allowedDurations` (si cambiás las duraciones sin mandarla, se recalcula).","example":30},"approvalTimeoutHours":{"type":"integer","minimum":1,"maximum":72,"description":"MANUAL: margen antes del turno para que el club confirme (1..72).","example":2},"approvalGraceMinutes":{"type":"integer","minimum":0,"maximum":720,"description":"MANUAL: piso desde el pedido (0..720).","example":60},"cancellationWindowHours":{"anyOf":[{"type":"integer","minimum":0,"maximum":168},{"type":"null"}],"description":"Ventana de cancelación de la categoría (0..168); `null` hereda la del club.","example":24},"awardsPoints":{"type":"boolean","description":"Reservado: lo define Skalia. Mandarlo (con cualquier valor) es 400 AWARDS_POINTS_NOT_ALLOWED."}},"required":["kind","label"],"description":"Alta de una categoría. Lo que no viene toma el default del kind (ver la doc).","example":{"kind":"CLINIC","label":"Clínica","bookingPolicy":"MANUAL","pricingMode":"FLAT","allowedDurations":[30,60],"cancellationWindowHours":24}},"ResourceCategoryCreatedResponse":{"type":"object","properties":{"category":{"$ref":"#/components/schemas/ResourceCategoryDto"},"defaultCourtCategoryId":{"oneOf":[{"type":"string","format":"uuid","description":"Id de la \"Canchas\" materializada si esta alta la disparó (primera categoría de un club con canchas sin categoría)."},{"type":"null"}]}},"required":["category","defaultCourtCategoryId"],"additionalProperties":false,"example":{"category":{"id":"3c9d2f10-7a4b-4e5c-9d6e-1f2a3b4c5d6e","clubId":"0d6a1f3e-2b4c-4d5e-9f7a-8b9c0d1e2f3a","kind":"CLINIC","resourceNoun":"PROFESSIONAL","label":"Clínica","icon":"medical_services","sortOrder":2,"enabled":true,"isDefault":false,"bookingPolicy":"MANUAL","pricingMode":"FLAT","priceUnit":"TURN","allowedDurations":[30,60],"defaultDurationMinutes":30,"approvalTimeoutHours":2,"approvalGraceMinutes":60,"cancellationWindowHours":24,"awardsPoints":false,"resourceCount":1},"defaultCourtCategoryId":null}},"ResourceCategoryDeletedResponse":{"type":"object","properties":{"deleted":{"type":"boolean","const":true},"categoryId":{"type":"string","format":"uuid","description":"La categoría borrada."}},"required":["deleted","categoryId"],"additionalProperties":false,"example":{"deleted":true,"categoryId":"3c9d2f10-7a4b-4e5c-9d6e-1f2a3b4c5d6e"}},"ResourceCategoryDto":{"type":"object","description":"Una categoría de recurso del club (F6), tal como la ve el panel.","properties":{"id":{"type":"string","format":"uuid","description":"Id de la categoría."},"clubId":{"type":"string","format":"uuid","description":"Club."},"kind":{"$ref":"#/components/schemas/ResourceKind"},"resourceNoun":{"$ref":"#/components/schemas/ResourceNoun"},"label":{"type":"string","description":"Nombre visible.","example":"Clínica"},"icon":{"oneOf":[{"type":"string","description":"Ícono Material."},{"type":"null"}]},"sortOrder":{"type":"integer","minimum":0},"enabled":{"type":"boolean","description":"`false` = fuera del wizard; reservas existentes intactas."},"isDefault":{"type":"boolean","description":"La \"Canchas\" materializada: no se borra ni cambia de kind."},"bookingPolicy":{"$ref":"#/components/schemas/BookingPolicy"},"pricingMode":{"$ref":"#/components/schemas/PricingMode"},"priceUnit":{"$ref":"#/components/schemas/PriceUnit"},"allowedDurations":{"type":"array","items":{"$ref":"#/components/schemas/DurationMinutes"},"description":"Duraciones ofrecidas."},"defaultDurationMinutes":{"$ref":"#/components/schemas/DurationMinutes"},"approvalTimeoutHours":{"type":"integer","minimum":1,"maximum":72},"approvalGraceMinutes":{"type":"integer","minimum":0,"maximum":720},"cancellationWindowHours":{"oneOf":[{"type":"integer","minimum":0,"description":"`null` hereda `settings.cancellationWindowHours`."},{"type":"null"}]},"awardsPoints":{"type":"boolean","description":"Si las reservas de la categoría suman puntos Skalia (lo define Skalia)."},"resourceCount":{"type":"integer","minimum":0,"description":"Recursos colgados (habilitados o no)."}},"required":["id","clubId","kind","resourceNoun","label","icon","sortOrder","enabled","isDefault","bookingPolicy","pricingMode","priceUnit","allowedDurations","defaultDurationMinutes","approvalTimeoutHours","approvalGraceMinutes","cancellationWindowHours","awardsPoints","resourceCount"],"additionalProperties":false,"example":{"id":"3c9d2f10-7a4b-4e5c-9d6e-1f2a3b4c5d6e","clubId":"0d6a1f3e-2b4c-4d5e-9f7a-8b9c0d1e2f3a","kind":"CLINIC","resourceNoun":"PROFESSIONAL","label":"Clínica","icon":"medical_services","sortOrder":2,"enabled":true,"isDefault":false,"bookingPolicy":"MANUAL","pricingMode":"FLAT","priceUnit":"TURN","allowedDurations":[30,60],"defaultDurationMinutes":30,"approvalTimeoutHours":2,"approvalGraceMinutes":60,"cancellationWindowHours":24,"awardsPoints":false,"resourceCount":1}},"ResourceCategoryPatchBody":{"type":"object","properties":{"kind":{"type":"string","enum":["COURT","CLINIC","RESTAURANT","OTHER"],"description":"Familia del recurso (inmutable después de crear)."},"label":{"type":"string","minLength":2,"maxLength":40,"description":"Nombre visible (2..40).","example":"Clínica"},"enabled":{"type":"boolean","description":"`false` la saca del wizard; las reservas existentes quedan intactas."},"sortOrder":{"type":"integer","minimum":0,"description":"Posición en el wizard (entero ≥ 0).","example":2},"resourceNoun":{"type":"string","enum":["COURT","ROOM","PROFESSIONAL","TABLE","GENERIC"],"description":"Sustantivo del recurso (wording). Default por kind. Sólo cambia mientras la categoría no tiene recursos."},"icon":{"anyOf":[{"type":"string","minLength":0,"maxLength":60},{"type":"null"}],"description":"Ícono Material opcional. `null` lo quita.","example":"medical_services"},"bookingPolicy":{"type":"string","enum":["AUTO_CONFIRM","MANUAL"],"description":"`AUTO_CONFIRM` o `MANUAL` (MANUAL no existe para kind COURT: 400 RESOURCE_KIND_UNSUPPORTED)."},"pricingMode":{"type":"string","enum":["PER_SLOT","FLAT","NONE"],"description":"Cómo se cobra el turno: por franja (`PER_SLOT`), fijo (`FLAT`) o sin cargo (`NONE`). Default por kind."},"allowedDurations":{"minItems":1,"maxItems":4,"type":"array","items":{"type":"integer"},"description":"Duraciones ofrecidas, ⊂ [30, 60, 90, 120], no vacío.","example":[30,60]},"defaultDurationMinutes":{"type":"integer","description":"Duración por defecto; tiene que estar en `allowedDurations` (si cambiás las duraciones sin mandarla, se recalcula).","example":30},"approvalTimeoutHours":{"type":"integer","minimum":1,"maximum":72,"description":"MANUAL: margen antes del turno para que el club confirme (1..72).","example":2},"approvalGraceMinutes":{"type":"integer","minimum":0,"maximum":720,"description":"MANUAL: piso desde el pedido (0..720).","example":60},"cancellationWindowHours":{"anyOf":[{"type":"integer","minimum":0,"maximum":168},{"type":"null"}],"description":"Ventana de cancelación de la categoría (0..168); `null` hereda la del club.","example":24},"awardsPoints":{"type":"boolean","description":"Reservado: lo define Skalia. Mandarlo (con cualquier valor) es 400 AWARDS_POINTS_NOT_ALLOWED."}},"description":"Parcial: al menos un campo. `kind` sólo se acepta igual al actual.","example":{"label":"Consultorios","enabled":true}},"ResourceCategoryResponse":{"type":"object","properties":{"category":{"$ref":"#/components/schemas/ResourceCategoryDto"}},"required":["category"],"additionalProperties":false,"example":{"category":{"id":"3c9d2f10-7a4b-4e5c-9d6e-1f2a3b4c5d6e","clubId":"0d6a1f3e-2b4c-4d5e-9f7a-8b9c0d1e2f3a","kind":"CLINIC","resourceNoun":"PROFESSIONAL","label":"Clínica","icon":"medical_services","sortOrder":2,"enabled":true,"isDefault":false,"bookingPolicy":"MANUAL","pricingMode":"FLAT","priceUnit":"TURN","allowedDurations":[30,60],"defaultDurationMinutes":30,"approvalTimeoutHours":2,"approvalGraceMinutes":60,"cancellationWindowHours":24,"awardsPoints":false,"resourceCount":1}}},"ResourceKind":{"type":"string","enum":["COURT","CLINIC","RESTAURANT","OTHER"],"description":"Familia del recurso (F6). Hoy toda cancha es `COURT`; la v1 sólo vende canchas."},"ResourceNoun":{"type":"string","enum":["COURT","ROOM","PROFESSIONAL","TABLE","GENERIC"],"description":"Sustantivo del recurso (wording)."},"ScheduleSuggestion":{"type":"object","properties":{"groupId":{"type":"string","format":"uuid","description":"Grupo."},"groupName":{"type":"string","description":"Nombre."},"suggestedStartTime":{"type":"string","format":"date-time","description":"Comienzo sugerido del primer partido del grupo.","example":"2026-09-14T12:00:00.000Z"},"timeSlot":{"type":"string","description":"Franja del grupo."}},"required":["groupId","groupName","suggestedStartTime","timeSlot"],"additionalProperties":false},"ScheduleSuggestionResponse":{"type":"object","properties":{"schedule":{"type":"array","items":{"$ref":"#/components/schemas/ScheduleSuggestion"}}},"required":["schedule"],"additionalProperties":false,"example":{"schedule":[{"groupId":"4d3c2b1a-aaaa-4a7b-8c9d-0e1f2a3b4c5d","groupName":"A","suggestedStartTime":"2026-10-17T12:00:00.000Z","timeSlot":"SATURDAY_MORNING"}]}},"SetScoreDto":{"type":"object","description":"Un set.","properties":{"team1":{"type":"integer","minimum":0,"description":"Games de la pareja 1."},"team2":{"type":"integer","minimum":0,"description":"Games de la pareja 2."}},"required":["team1","team2"],"additionalProperties":false,"example":{"team1":6,"team2":3}},"SlotDto":{"type":"object","description":"Una reserva AJENA (otro origen u otra integración del club), anónima: lo mismo que dice `/occupancy`. Lista cerrada `SLOT_DTO_FIELDS`.","properties":{"id":{"type":"string","format":"uuid","description":"Id de la reserva."},"courtId":{"type":"string","format":"uuid","description":"Cancha."},"kind":{"$ref":"#/components/schemas/ResourceKind"},"categoryId":{"oneOf":[{"type":"string","format":"uuid","description":"Categoría de recurso (F6). `null` para las canchas implícitas."},{"type":"null"}]},"resourceNoun":{"$ref":"#/components/schemas/ResourceNoun"},"date":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"`YYYY-MM-DD` naive en la zona del club.","example":"2026-09-15"},"startTime":{"type":"string","pattern":"^([01]\\d|2[0-3]):[0-5]\\d$","description":"Comienzo `HH:MM` naive del club.","example":"16:00"},"endTime":{"type":"string","pattern":"^(([01]\\d|2[0-3]):[0-5]\\d|24:00)$","description":"Fin `HH:MM` naive del club (`24:00` = medianoche).","example":"17:00"},"status":{"$ref":"#/components/schemas/BookingStatus"},"origin":{"$ref":"#/components/schemas/BookingOrigin"},"owned":{"type":"boolean","const":false,"description":"Siempre `false`: la creó otro origen u otra integración."},"matchCount":{"type":"integer","minimum":0,"description":"Partidos vivos armados en el turno."},"recurringId":{"oneOf":[{"type":"string","format":"uuid","description":"Serie de fijas de la que es ocurrencia (`origin: RECURRING`), o `null`. Un id de agenda del club, no un dato de personas."},{"type":"null"}]}},"required":["id","courtId","kind","categoryId","resourceNoun","date","startTime","endTime","status","origin","owned","matchCount","recurringId"],"additionalProperties":false,"example":{"id":"1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d","courtId":"5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9","kind":"COURT","categoryId":null,"resourceNoun":"COURT","date":"2026-09-15","startTime":"16:00","endTime":"17:00","status":"CONFIRMED","origin":"APP","owned":false,"matchCount":1,"recurringId":null}},"StandingDto":{"type":"object","description":"Fila de la tabla de un grupo (ganados → diferencia de sets → diferencia de games). Lista cerrada `STANDING_DTO_FIELDS`.","properties":{"teamId":{"type":"string","format":"uuid","description":"Pareja."},"label":{"type":"string","description":"Etiqueta anónima."},"played":{"type":"integer","minimum":0},"won":{"type":"integer","minimum":0},"lost":{"type":"integer","minimum":0},"setsWon":{"type":"integer","minimum":0},"setsLost":{"type":"integer","minimum":0},"gamesWon":{"type":"integer","minimum":0},"gamesLost":{"type":"integer","minimum":0},"qualified":{"type":"boolean","description":"`true` para las dos primeras del grupo."}},"required":["teamId","label","played","won","lost","setsWon","setsLost","gamesWon","gamesLost","qualified"],"additionalProperties":false,"example":{"teamId":"1a2b3c4d-1111-4a7b-8c9d-0e1f2a3b4c5d","label":"Los Halcones","played":2,"won":2,"lost":0,"setsWon":4,"setsLost":1,"gamesWon":25,"gamesLost":14,"qualified":true}},"StandingsResponse":{"type":"object","properties":{"groups":{"type":"array","items":{"$ref":"#/components/schemas/GroupStandings"}}},"required":["groups"],"additionalProperties":false,"example":{"groups":[{"groupId":"4d3c2b1a-aaaa-4a7b-8c9d-0e1f2a3b4c5d","groupName":"A","standings":[{"teamId":"1a2b3c4d-1111-4a7b-8c9d-0e1f2a3b4c5d","label":"Los Halcones","played":2,"won":2,"lost":0,"setsWon":4,"setsLost":1,"gamesWon":25,"gamesLost":14,"qualified":true}]}]}},"StreamDto":{"type":"object","description":"Un vivo por cancha. Lista cerrada `STREAM_DTO_FIELDS`: nunca ids crudos de YouTube, claves de ingest ni errores del broadcast.","properties":{"courtId":{"type":"string","format":"uuid","description":"Cancha."},"courtName":{"type":"string","description":"Nombre de la cancha.","example":"Cancha 1"},"kind":{"$ref":"#/components/schemas/StreamKind"},"status":{"$ref":"#/components/schemas/StreamStatus"},"url":{"oneOf":[{"type":"string","description":"Link de YouTube. `null` cuando el vivo es el turno de una reserva AJENA (el título del video lleva el nombre del titular): sólo se abre lo `STANDALONE`, lo `TOURNAMENT` y lo `BOOKING` de una reserva creada por TU integración.","example":"https://www.youtube.com/watch?v=dQw4w9WgXcQ"},{"type":"null"}]},"embedUrl":{"oneOf":[{"type":"string","description":"`https://www.youtube.com/embed/<id>` para un iframe. `null` cuando `url` es `null`.","example":"https://www.youtube.com/embed/dQw4w9WgXcQ"},{"type":"null"}]},"bookingId":{"oneOf":[{"type":"string","format":"uuid","description":"Reserva del turno: sólo si es tuya (o de torneo); `null` si es ajena o el vivo no cuelga de una reserva."},{"type":"null"}]},"tournamentId":{"oneOf":[{"type":"string","format":"uuid","description":"Sólo en `kind: TOURNAMENT`."},{"type":"null"}]},"startedAt":{"oneOf":[{"type":"string","format":"date-time","description":"Cuándo salió al aire (ISO UTC); `null` mientras está `SCHEDULED`.","example":"2026-09-14T12:00:00.000Z"},{"type":"null"}]}},"required":["courtId","courtName","kind","status","url","embedUrl","bookingId","tournamentId","startedAt"],"additionalProperties":false,"example":{"courtId":"5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9","courtName":"Cancha 1","kind":"STANDALONE","status":"LIVE","url":"https://www.youtube.com/watch?v=dQw4w9WgXcQ","embedUrl":"https://www.youtube.com/embed/dQw4w9WgXcQ","bookingId":null,"tournamentId":null,"startedAt":"2026-09-15T13:02:11.000Z"}},"StreamKind":{"type":"string","enum":["STANDALONE","BOOKING","TOURNAMENT"],"description":"Qué está transmitiendo la cancha: `STANDALONE` (el club lanzó el vivo a mano), `BOOKING` (el turno de una reserva o de un partido con cancha) o `TOURNAMENT` (slot o partido de torneo, público por diseño)."},"StreamStatus":{"type":"string","enum":["SCHEDULED","LIVE"],"description":"`SCHEDULED` = el broadcast está armado y todavía no salió al aire; `LIVE` = transmitiendo. Un vivo que terminó no aparece (su VOD es `recording` de la reserva)."},"StreamsListResponse":{"type":"object","description":"Qué está transmitiendo cada cancha del club, sin PII.","properties":{"clubId":{"type":"string","format":"uuid","description":"Club."},"streams":{"type":"array","items":{"$ref":"#/components/schemas/StreamDto"},"description":"Una fila por cancha con vivo (LIVE gana sobre SCHEDULED; a igual estado, el más reciente), ordenadas por nombre de cancha. `[]` si el club no tiene el streaming prendido o nada está al aire."}},"required":["clubId","streams"],"additionalProperties":false,"example":{"clubId":"0d6a1f3e-2b4c-4d5e-9f7a-8b9c0d1e2f3a","streams":[{"courtId":"5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9","courtName":"Cancha 1","kind":"STANDALONE","status":"LIVE","url":"https://www.youtube.com/watch?v=dQw4w9WgXcQ","embedUrl":"https://www.youtube.com/embed/dQw4w9WgXcQ","bookingId":null,"tournamentId":null,"startedAt":"2026-09-15T13:02:11.000Z"},{"courtId":"6f7a8b9c-0d1e-4f2a-b3c4-d5e6f7a8b9c0","courtName":"Cancha 2","kind":"BOOKING","status":"LIVE","url":null,"embedUrl":null,"bookingId":null,"tournamentId":null,"startedAt":"2026-09-15T13:00:40.000Z"},{"courtId":"7a8b9c0d-1e2f-4a3b-c4d5-e6f7a8b9c0d1","courtName":"Cancha 3","kind":"TOURNAMENT","status":"SCHEDULED","url":"https://www.youtube.com/watch?v=Xy12AbCdEfG","embedUrl":"https://www.youtube.com/embed/Xy12AbCdEfG","bookingId":"2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e","tournamentId":"8c9d0e1f-2a3b-4c4d-9e5f-6a7b8c9d0e1f","startedAt":null}]}},"SuggestScheduleBody":{"type":"object","properties":{"fridayStart":{"type":"string","pattern":"^([01]\\d|2[0-3]):[0-5]\\d$","description":"Hora del primer turno del viernes (`HH:MM`).","example":"18:00"},"saturdayMorningStart":{"type":"string","pattern":"^([01]\\d|2[0-3]):[0-5]\\d$","description":"Hora del primer turno del sábado a la mañana.","example":"09:00"},"saturdayAfternoonStart":{"type":"string","pattern":"^([01]\\d|2[0-3]):[0-5]\\d$","description":"Hora del primer turno del sábado a la tarde.","example":"14:00"},"matchDuration":{"type":"integer","minimum":30,"maximum":240,"description":"Duración de cada partido en minutos (default 90).","example":90}},"example":{"saturdayMorningStart":"09:00","matchDuration":90}},"TeamDto":{"type":"object","description":"Una pareja inscripta, ANÓNIMA: nunca nombre, email, teléfono, id ni nivel de quienes la integran. Lista cerrada `TEAM_DTO_FIELDS`.","properties":{"id":{"type":"string","format":"uuid","description":"Id de la pareja (sirve para `absentTeamId`, `qualifiedTeamIds`, standings y partidos)."},"label":{"type":"string","description":"Etiqueta anónima: el nombre de equipo, o `Pareja #n` (n = orden de inscripción).","example":"Los Halcones"},"teamName":{"oneOf":[{"type":"string","description":"Nombre de equipo que eligió la pareja, o `null`."},{"type":"null"}]},"seed":{"oneOf":[{"type":"integer","minimum":1,"description":"Número de cabeza de serie, o `null`."},{"type":"null"}]},"groupId":{"oneOf":[{"type":"string","format":"uuid","description":"Grupo asignado, o `null`."},{"type":"null"}]},"status":{"$ref":"#/components/schemas/TeamStatus"},"paid":{"type":"boolean","description":"La inscripción está cobrada (lo marca el panel o `PATCH …/teams/{teamId}`). Plata del club, no un dato de las personas."},"registeredAt":{"oneOf":[{"type":"string","description":"Fecha de inscripción (`datetime` de la DB)."},{"type":"null"}]}},"required":["id","label","teamName","seed","groupId","status","paid","registeredAt"],"additionalProperties":false,"example":{"id":"1a2b3c4d-1111-4a7b-8c9d-0e1f2a3b4c5d","label":"Los Halcones","teamName":"Los Halcones","seed":1,"groupId":"4d3c2b1a-aaaa-4a7b-8c9d-0e1f2a3b4c5d","status":"REGISTERED","paid":true,"registeredAt":"2026-09-18 12:05:00"}},"TeamPatchBody":{"type":"object","properties":{"paid":{"type":"boolean","description":"`true`: la inscripción está cobrada; `false`: pendiente de cobro."},"teamName":{"anyOf":[{"type":"string","minLength":1,"maxLength":40},{"type":"null"}],"description":"Nombre de equipo (1..40), o `null` para volver a la etiqueta anónima `Pareja #n`.","example":"Los Halcones"},"status":{"type":"string","enum":["REGISTERED","WITHDRAWN"],"description":"`WITHDRAWN` da de baja a la pareja (avisa a sus integrantes); `REGISTERED` la reingresa si hay cupo."}},"additionalProperties":false,"description":"Parcial, al menos un campo. Sólo cobro, nombre de equipo y baja / reingreso: identidad (`player1Id` / `player2Id`), consentimiento (`player2Confirmed`), preferencias horarias, `seed` y `groupId` no se tocan por API (400 VALIDATION).","example":{"paid":true}},"TeamPatchStatus":{"type":"string","enum":["REGISTERED","WITHDRAWN"],"description":"Estados que admite `PATCH …/teams/{teamId}`."},"TeamResponse":{"type":"object","properties":{"team":{"$ref":"#/components/schemas/TeamDto"}},"required":["team"],"additionalProperties":false,"example":{"team":{"id":"1a2b3c4d-1111-4a7b-8c9d-0e1f2a3b4c5d","label":"Los Halcones","teamName":"Los Halcones","seed":1,"groupId":"4d3c2b1a-aaaa-4a7b-8c9d-0e1f2a3b4c5d","status":"REGISTERED","paid":true,"registeredAt":"2026-09-18 12:05:00"}}},"TeamStatus":{"type":"string","enum":["REGISTERED","WITHDRAWN","ELIMINATED"],"description":"`tournament_pairs.status`. Por API sólo se pide REGISTERED / WITHDRAWN (`TeamPatchStatus`); ELIMINATED lo decide el cuadro."},"TournamentCategoriesBody":{"type":"object","properties":{"variants":{"minItems":1,"maxItems":12,"type":"array","items":{"type":"object","properties":{"gender":{"type":"string","enum":["MALE","FEMALE","MIXED","OPEN"],"description":"Variante: `MALE` / `FEMALE` / `MIXED` / `OPEN`."},"categories":{"minItems":1,"maxItems":12,"type":"array","items":{"type":"string","minLength":1,"maxLength":20},"description":"Categorías de la variante; se crea una fila por cada una.","example":["4ta","5ta"]},"startDate":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"Primer día, `YYYY-MM-DD` (fecha del club).","example":"2026-10-16"},"endDate":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"Último día, `YYYY-MM-DD`.","example":"2026-10-18"},"maxTeams":{"type":"integer","minimum":2,"maximum":64,"description":"Cupo de parejas de la categoría.","example":12},"price":{"type":"number","minimum":0,"description":"Precio de la inscripción por pareja (moneda del club).","example":1200}},"required":["gender","categories","maxTeams"]},"description":"Una por género; cada una con sus categorías, fechas, cupo y precio."}},"required":["variants"],"additionalProperties":false,"description":"Categorías nuevas sobre el evento de la categoría de la URL: mismo `variants[]` del alta. Nombre, deporte, descripción y lugar se heredan (mandarlos es 400 VALIDATION); fechas y precio también, cuando la variante no los trae.","example":{"variants":[{"gender":"MALE","categories":["6ta"],"maxTeams":12,"price":1200}]}},"TournamentCategoriesResponse":{"type":"object","properties":{"eventId":{"type":"string","format":"uuid","description":"Id del evento al que se sumaron las categorías (el de la categoría de la URL; a un torneo legacy sin evento se le asigna uno)."},"tournaments":{"type":"array","items":{"$ref":"#/components/schemas/TournamentDto"},"description":"SÓLO las categorías nuevas, en el orden del body."}},"required":["eventId","tournaments"],"additionalProperties":false,"example":{"eventId":"7a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d","tournaments":[{"id":"1a2b3c4d-2222-4a7b-8c9d-0e1f2a3b4c5d","eventId":"7a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d","clubId":"3f1c9a52-7d0e-4b6a-9c11-2a4f8e6d5b70","name":"Torneo Primavera — 6ta","sport":"PADEL","format":"CLASICO","status":"OPEN","category":"6ta","gender":"MALE","description":null,"location":null,"price":1200,"maxTeams":12,"registeredTeams":0,"startDate":"2026-10-16","endDate":"2026-10-18","createdAt":"2026-09-18 12:00:00"}]}},"TournamentCreateBody":{"type":"object","properties":{"clubId":{"type":"string","pattern":"^[a-f0-9-]{8,64}$","description":"Club sobre el que opera la request. Opcional: se infiere cuando la integración tiene un solo grant (kind CLUB, v1)."},"name":{"type":"string","minLength":3,"maxLength":80,"description":"Nombre del evento (a cada categoría se le agrega ` — <categoría>`).","example":"Torneo Primavera"},"sport":{"type":"string","minLength":2,"maxLength":20,"description":"Deporte (default `PADEL`).","example":"PADEL"},"description":{"type":"string","maxLength":1000,"description":"Descripción del evento."},"location":{"type":"string","maxLength":160,"description":"Lugar del evento."},"variants":{"minItems":1,"maxItems":12,"type":"array","items":{"type":"object","properties":{"gender":{"type":"string","enum":["MALE","FEMALE","MIXED","OPEN"],"description":"Variante: `MALE` / `FEMALE` / `MIXED` / `OPEN`."},"categories":{"minItems":1,"maxItems":12,"type":"array","items":{"type":"string","minLength":1,"maxLength":20},"description":"Categorías de la variante; se crea una fila por cada una.","example":["4ta","5ta"]},"startDate":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"Primer día, `YYYY-MM-DD` (fecha del club).","example":"2026-10-16"},"endDate":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"Último día, `YYYY-MM-DD`.","example":"2026-10-18"},"maxTeams":{"type":"integer","minimum":2,"maximum":64,"description":"Cupo de parejas de la categoría.","example":12},"price":{"type":"number","minimum":0,"description":"Precio de la inscripción por pareja (moneda del club).","example":1200}},"required":["gender","categories","maxTeams"]},"description":"Una por género; cada una con sus categorías, fechas, cupo y precio."}},"required":["name","variants"],"description":"Alta de un evento con sus categorías: una fila por (género, categoría), todas con el mismo `eventId`.","example":{"name":"Torneo Primavera","variants":[{"gender":"MALE","categories":["4ta","5ta"],"startDate":"2026-10-16","endDate":"2026-10-18","maxTeams":12,"price":1200}]}},"TournamentCreateResponse":{"type":"object","properties":{"eventId":{"type":"string","format":"uuid","description":"Id del evento creado (`tournament_group_id`)."},"tournaments":{"type":"array","items":{"$ref":"#/components/schemas/TournamentDto"},"description":"Una por (género, categoría), en el orden del body."}},"required":["eventId","tournaments"],"additionalProperties":false,"example":{"eventId":"7a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d","tournaments":[{"id":"9b8a7c6d-5e4f-4a3b-2c1d-0e9f8a7b6c5d","eventId":"7a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d","clubId":"3f1c9a52-7d0e-4b6a-9c11-2a4f8e6d5b70","name":"Torneo Primavera — 4ta","sport":"PADEL","format":"CLASICO","status":"OPEN","category":"4ta","gender":"MALE","description":null,"location":null,"price":1200,"maxTeams":12,"registeredTeams":7,"startDate":"2026-10-16","endDate":"2026-10-18","createdAt":"2026-09-18 12:00:00"}]}},"TournamentDeleteResponse":{"type":"object","properties":{"tournament":{"oneOf":[{"$ref":"#/components/schemas/TournamentDto"},{"type":"null"}]},"deleted":{"type":"boolean","description":"`true`: se borró (no tenía inscriptos)."},"cancelled":{"type":"boolean","description":"`true`: tenía inscriptos y con `force=true` quedó CANCELLED (los jugadores reciben el aviso del panel)."}},"required":["tournament","deleted","cancelled"],"additionalProperties":false,"example":{"tournament":null,"deleted":true,"cancelled":false}},"TournamentDetailResponse":{"type":"object","properties":{"tournament":{"$ref":"#/components/schemas/TournamentDto"},"categories":{"type":"array","items":{"$ref":"#/components/schemas/TournamentDto"},"description":"Todas las categorías del mismo evento (esta incluida)."},"teams":{"type":"array","items":{"$ref":"#/components/schemas/TeamDto"}},"groups":{"type":"array","items":{"$ref":"#/components/schemas/TournamentGroupDto"}},"matches":{"type":"array","items":{"$ref":"#/components/schemas/TournamentMatchDto"},"description":"Todos los partidos (grupos + cuadro)."},"podium":{"type":"array","items":{"$ref":"#/components/schemas/PodiumEntryDto"},"description":"Podio (1/2/3) si la categoría se finalizó; `[]` si no (o si se cerró antes de que el cierre otorgara podio)."}},"required":["tournament","categories","teams","groups","matches","podium"],"additionalProperties":false,"example":{"tournament":{"id":"9b8a7c6d-5e4f-4a3b-2c1d-0e9f8a7b6c5d","eventId":"7a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d","clubId":"3f1c9a52-7d0e-4b6a-9c11-2a4f8e6d5b70","name":"Torneo Primavera — 4ta","sport":"PADEL","format":"CLASICO","status":"OPEN","category":"4ta","gender":"MALE","description":null,"location":null,"price":1200,"maxTeams":12,"registeredTeams":7,"startDate":"2026-10-16","endDate":"2026-10-18","createdAt":"2026-09-18 12:00:00"},"categories":[{"id":"9b8a7c6d-5e4f-4a3b-2c1d-0e9f8a7b6c5d","eventId":"7a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d","clubId":"3f1c9a52-7d0e-4b6a-9c11-2a4f8e6d5b70","name":"Torneo Primavera — 4ta","sport":"PADEL","format":"CLASICO","status":"OPEN","category":"4ta","gender":"MALE","description":null,"location":null,"price":1200,"maxTeams":12,"registeredTeams":7,"startDate":"2026-10-16","endDate":"2026-10-18","createdAt":"2026-09-18 12:00:00"}],"teams":[{"id":"1a2b3c4d-1111-4a7b-8c9d-0e1f2a3b4c5d","label":"Los Halcones","teamName":"Los Halcones","seed":1,"groupId":"4d3c2b1a-aaaa-4a7b-8c9d-0e1f2a3b4c5d","status":"REGISTERED","paid":true,"registeredAt":"2026-09-18 12:05:00"},{"id":"1a2b3c4d-2222-4a7b-8c9d-0e1f2a3b4c5d","label":"Pareja #2","teamName":null,"seed":null,"groupId":"4d3c2b1a-aaaa-4a7b-8c9d-0e1f2a3b4c5d","status":"REGISTERED","paid":false,"registeredAt":"2026-09-18 12:07:00"}],"groups":[{"id":"4d3c2b1a-aaaa-4a7b-8c9d-0e1f2a3b4c5d","name":"A","timeSlot":"SATURDAY_MORNING","teamIds":["1a2b3c4d-1111-4a7b-8c9d-0e1f2a3b4c5d","1a2b3c4d-2222-4a7b-8c9d-0e1f2a3b4c5d"]}],"matches":[{"id":"6f5e4d3c-bbbb-4a7b-8c9d-0e1f2a3b4c5d","tournamentId":"9b8a7c6d-5e4f-4a3b-2c1d-0e9f8a7b6c5d","phase":"GROUP","round":1,"groupId":"4d3c2b1a-aaaa-4a7b-8c9d-0e1f2a3b4c5d","matchOrder":1,"team1Id":"1a2b3c4d-1111-4a7b-8c9d-0e1f2a3b4c5d","team2Id":"1a2b3c4d-2222-4a7b-8c9d-0e1f2a3b4c5d","team1Label":"Los Halcones","team2Label":"Pareja #2","team1FromMatchId":null,"team1FromOutcome":null,"team2FromMatchId":null,"team2FromOutcome":null,"status":"COMPLETED","winnerTeamId":"1a2b3c4d-1111-4a7b-8c9d-0e1f2a3b4c5d","isWalkover":false,"walkoverTeamId":null,"sets":[{"team1":6,"team2":3},{"team1":7,"team2":5}],"courtId":"5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9","scheduledAt":"2026-10-17T09:00:00"}],"podium":[]}},"TournamentDto":{"type":"object","description":"Una categoría de un evento. Lista cerrada `TOURNAMENT_DTO_FIELDS`. Nunca datos de personas.","properties":{"id":{"type":"string","format":"uuid","description":"Id de la categoría (una fila de `tournaments`)."},"eventId":{"oneOf":[{"type":"string","format":"uuid","description":"Id del EVENTO que agrupa las categorías (`tournament_group_id`)."},{"type":"null"}]},"clubId":{"type":"string","format":"uuid","description":"Club organizador."},"name":{"type":"string","description":"Nombre completo (con el sufijo de la categoría).","example":"Torneo Primavera — 4ta"},"sport":{"type":"string","description":"Deporte.","example":"PADEL"},"format":{"type":"string","description":"Formato. Hoy siempre `CLASICO` (grupos + eliminatorias).","example":"CLASICO"},"status":{"$ref":"#/components/schemas/TournamentStatus"},"category":{"oneOf":[{"type":"string","description":"Categoría (`6ta` … `1ra`).","example":"4ta"},{"type":"null"}]},"gender":{"$ref":"#/components/schemas/TournamentGender"},"description":{"oneOf":[{"type":"string","description":"Descripción (compartida por el evento)."},{"type":"null"}]},"location":{"oneOf":[{"type":"string","description":"Lugar (compartido por el evento)."},{"type":"null"}]},"price":{"type":"number","minimum":0,"description":"Precio de la inscripción por pareja (moneda del club)."},"maxTeams":{"oneOf":[{"type":"integer","minimum":1,"description":"Cupo de parejas."},{"type":"null"}]},"registeredTeams":{"type":"integer","minimum":0,"description":"Parejas inscriptas (REGISTERED + ELIMINATED)."},"startDate":{"oneOf":[{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"Primer día, `YYYY-MM-DD`.","example":"2026-09-15"},{"type":"null"}]},"endDate":{"oneOf":[{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"Último día, `YYYY-MM-DD`.","example":"2026-09-15"},{"type":"null"}]},"createdAt":{"oneOf":[{"type":"string","description":"Fecha de alta (`datetime` de la DB)."},{"type":"null"}]}},"required":["id","eventId","clubId","name","sport","format","status","category","gender","description","location","price","maxTeams","registeredTeams","startDate","endDate","createdAt"],"additionalProperties":false,"example":{"id":"9b8a7c6d-5e4f-4a3b-2c1d-0e9f8a7b6c5d","eventId":"7a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d","clubId":"3f1c9a52-7d0e-4b6a-9c11-2a4f8e6d5b70","name":"Torneo Primavera — 4ta","sport":"PADEL","format":"CLASICO","status":"OPEN","category":"4ta","gender":"MALE","description":null,"location":null,"price":1200,"maxTeams":12,"registeredTeams":7,"startDate":"2026-10-16","endDate":"2026-10-18","createdAt":"2026-09-18 12:00:00"}},"TournamentFinalizeResponse":{"type":"object","properties":{"tournament":{"$ref":"#/components/schemas/TournamentDto"},"podium":{"type":"array","items":{"$ref":"#/components/schemas/PodiumEntryDto"},"description":"Podio 1/2/3 recién escrito (o el que ya tenía, si la categoría ya estaba COMPLETED)."}},"required":["tournament","podium"],"additionalProperties":false,"example":{"tournament":{"id":"9b8a7c6d-5e4f-4a3b-2c1d-0e9f8a7b6c5d","eventId":"7a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d","clubId":"3f1c9a52-7d0e-4b6a-9c11-2a4f8e6d5b70","name":"Torneo Primavera — 4ta","sport":"PADEL","format":"CLASICO","status":"COMPLETED","category":"4ta","gender":"MALE","description":null,"location":null,"price":1200,"maxTeams":12,"registeredTeams":7,"startDate":"2026-10-16","endDate":"2026-10-18","createdAt":"2026-09-18 12:00:00"},"podium":[{"position":1,"teamId":"1a2b3c4d-1111-4a7b-8c9d-0e1f2a3b4c5d","label":"Los Halcones"},{"position":2,"teamId":"1a2b3c4d-2222-4a7b-8c9d-0e1f2a3b4c5d","label":"Pareja #2"}]}},"TournamentGender":{"type":"string","enum":["MALE","FEMALE","MIXED","OPEN"],"description":"Variante de la categoría."},"TournamentGroupDto":{"type":"object","description":"Grupo de la fase de grupos. Lista cerrada `TOURNAMENT_GROUP_DTO_FIELDS`.","properties":{"id":{"type":"string","format":"uuid","description":"Id del grupo."},"name":{"type":"string","description":"Nombre (`A`, `B`, …).","example":"A"},"timeSlot":{"oneOf":[{"type":"string","description":"Franja mayoritaria de preferencia (`FRIDAY_NIGHT`, `SATURDAY_MORNING`, `SATURDAY_AFTERNOON`, `SATURDAY_NIGHT`)."},{"type":"null"}]},"teamIds":{"type":"array","items":{"type":"string","format":"uuid","description":"Pareja del grupo."},"description":"Parejas del grupo, en orden de siembra."}},"required":["id","name","timeSlot","teamIds"],"additionalProperties":false,"example":{"id":"4d3c2b1a-aaaa-4a7b-8c9d-0e1f2a3b4c5d","name":"A","timeSlot":"SATURDAY_MORNING","teamIds":["1a2b3c4d-1111-4a7b-8c9d-0e1f2a3b4c5d","1a2b3c4d-2222-4a7b-8c9d-0e1f2a3b4c5d"]}},"TournamentMatchDto":{"type":"object","description":"Partido de torneo. Lista cerrada `TOURNAMENT_MATCH_DTO_FIELDS`. El cuadro son los partidos con `phase != GROUP`.","properties":{"id":{"type":"string","format":"uuid","description":"Id del partido."},"tournamentId":{"type":"string","format":"uuid","description":"Categoría."},"phase":{"$ref":"#/components/schemas/TournamentPhase"},"round":{"type":"integer","minimum":1,"description":"Ronda dentro de la fase."},"groupId":{"oneOf":[{"type":"string","format":"uuid","description":"Grupo (sólo en fase GROUP)."},{"type":"null"}]},"matchOrder":{"type":"integer","minimum":0,"description":"Orden dentro del grupo / posición en el cuadro."},"team1Id":{"oneOf":[{"type":"string","format":"uuid","description":"Pareja 1 (`null` hasta que se defina)."},{"type":"null"}]},"team2Id":{"oneOf":[{"type":"string","format":"uuid","description":"Pareja 2 (`null` hasta que se defina)."},{"type":"null"}]},"team1Label":{"oneOf":[{"type":"string","description":"Etiqueta anónima de la pareja 1."},{"type":"null"}]},"team2Label":{"oneOf":[{"type":"string","description":"Etiqueta anónima de la pareja 2."},{"type":"null"}]},"team1FromMatchId":{"oneOf":[{"type":"string","format":"uuid","description":"Cadena \"ganador queda en cancha\": la pareja 1 sale de este partido."},{"type":"null"}]},"team1FromOutcome":{"oneOf":[{"type":"string","description":"`WINNER` / `LOSER` del partido de `team1FromMatchId`."},{"type":"null"}]},"team2FromMatchId":{"oneOf":[{"type":"string","format":"uuid","description":"Ídem para la pareja 2."},{"type":"null"}]},"team2FromOutcome":{"oneOf":[{"type":"string","description":"`WINNER` / `LOSER`."},{"type":"null"}]},"status":{"$ref":"#/components/schemas/TournamentMatchStatus"},"winnerTeamId":{"oneOf":[{"type":"string","format":"uuid","description":"Ganadora, o `null` hasta cargar el resultado."},{"type":"null"}]},"isWalkover":{"type":"boolean","description":"`true` si se cerró por W.O. (no mueve nivel ni historial de nadie)."},"walkoverTeamId":{"oneOf":[{"type":"string","format":"uuid","description":"La pareja que NO se presentó, si fue W.O."},{"type":"null"}]},"sets":{"type":"array","items":{"$ref":"#/components/schemas/SetScoreDto"},"description":"Set a set, en orden."},"courtId":{"oneOf":[{"type":"string","format":"uuid","description":"Cancha asignada."},{"type":"null"}]},"scheduledAt":{"oneOf":[{"type":"string","description":"`YYYY-MM-DDTHH:MM:SS` naive en la zona del club, o `null`.","example":"2026-10-17T09:00:00"},{"type":"null"}]}},"required":["id","tournamentId","phase","round","groupId","matchOrder","team1Id","team2Id","team1Label","team2Label","team1FromMatchId","team1FromOutcome","team2FromMatchId","team2FromOutcome","status","winnerTeamId","isWalkover","walkoverTeamId","sets","courtId","scheduledAt"],"additionalProperties":false,"example":{"id":"6f5e4d3c-bbbb-4a7b-8c9d-0e1f2a3b4c5d","tournamentId":"9b8a7c6d-5e4f-4a3b-2c1d-0e9f8a7b6c5d","phase":"GROUP","round":1,"groupId":"4d3c2b1a-aaaa-4a7b-8c9d-0e1f2a3b4c5d","matchOrder":1,"team1Id":"1a2b3c4d-1111-4a7b-8c9d-0e1f2a3b4c5d","team2Id":"1a2b3c4d-2222-4a7b-8c9d-0e1f2a3b4c5d","team1Label":"Los Halcones","team2Label":"Pareja #2","team1FromMatchId":null,"team1FromOutcome":null,"team2FromMatchId":null,"team2FromOutcome":null,"status":"COMPLETED","winnerTeamId":"1a2b3c4d-1111-4a7b-8c9d-0e1f2a3b4c5d","isWalkover":false,"walkoverTeamId":null,"sets":[{"team1":6,"team2":3},{"team1":7,"team2":5}],"courtId":"5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9","scheduledAt":"2026-10-17T09:00:00"}},"TournamentMatchStatus":{"type":"string","enum":["PENDING","COMPLETED"],"description":"`tournament_matches.status`."},"TournamentPatchBody":{"type":"object","properties":{"name":{"type":"string","minLength":3,"maxLength":80,"description":"Nombre del evento (a cada categoría se le agrega ` — <categoría>`).","example":"Torneo Primavera"},"description":{"anyOf":[{"type":"string","maxLength":1000},{"type":"null"}],"description":"Descripción del evento."},"location":{"anyOf":[{"type":"string","maxLength":160},{"type":"null"}],"description":"Lugar del evento."},"startDate":{"anyOf":[{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},{"type":"null"}],"description":"Primer día, `YYYY-MM-DD` (fecha del club).","example":"2026-10-16"},"endDate":{"anyOf":[{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},{"type":"null"}],"description":"Último día, `YYYY-MM-DD`.","example":"2026-10-18"},"maxTeams":{"type":"integer","minimum":2,"maximum":64,"description":"Cupo de parejas de la categoría.","example":12},"price":{"type":"number","minimum":0,"description":"Precio de la inscripción por pareja (moneda del club).","example":1200},"status":{"type":"string","enum":["OPEN","CLOSED","SCHEDULE_BUILDING","GROUP_PHASE","ELIMINATION_PHASE","COMPLETED","CANCELLED"],"description":"Transición de estado (máquina de estados de docs/enums.md § Tournament Status)."},"force":{"type":"boolean","description":"`true` hace lo que haría el panel al confirmar: borrar partidos jugados al regenerar / retroceder, o cancelar un torneo con inscriptos. Nunca revive un torneo terminal (COMPLETED / CANCELLED)."}},"description":"Parcial: al menos un campo además de `force`. `status` sólo admite las transiciones de la máquina de estados (la única hacia atrás es ELIMINATION_PHASE → GROUP_PHASE, para regenerar el cuadro; con eliminatorias jugadas exige `force: true`).","example":{"status":"CLOSED"}},"TournamentPhase":{"type":"string","enum":["GROUP","R16","QF","SF","FINAL"],"description":"`tournament_matches.phase`."},"TournamentResponse":{"type":"object","properties":{"tournament":{"$ref":"#/components/schemas/TournamentDto"},"warnings":{"type":"array","items":{"$ref":"#/components/schemas/TournamentWarning"},"description":"Avisos no bloqueantes (sin nombres: sólo ids)."},"podium":{"type":"array","items":{"$ref":"#/components/schemas/PodiumEntryDto"},"description":"Sólo cuando el body trae `status: COMPLETED`: el podio que otorgó el cierre (mismo op que `POST …/finalize`)."}},"required":["tournament","warnings"],"additionalProperties":false,"example":{"tournament":{"id":"9b8a7c6d-5e4f-4a3b-2c1d-0e9f8a7b6c5d","eventId":"7a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d","clubId":"3f1c9a52-7d0e-4b6a-9c11-2a4f8e6d5b70","name":"Torneo Primavera — 4ta","sport":"PADEL","format":"CLASICO","status":"OPEN","category":"4ta","gender":"MALE","description":null,"location":null,"price":1200,"maxTeams":12,"registeredTeams":7,"startDate":"2026-10-16","endDate":"2026-10-18","createdAt":"2026-09-18 12:00:00"},"warnings":[]}},"TournamentStatus":{"type":"string","enum":["OPEN","CLOSED","SCHEDULE_BUILDING","GROUP_PHASE","ELIMINATION_PHASE","COMPLETED","CANCELLED"],"description":"`tournaments.status`. OPEN → CLOSED → (SCHEDULE_BUILDING →) GROUP_PHASE → ELIMINATION_PHASE → COMPLETED; CANCELLED desde cualquier no terminal; ELIMINATION_PHASE → GROUP_PHASE es la única flecha hacia atrás (regenerar el cuadro). El legacy `FINISHED` que el panel todavía escribe se publica como COMPLETED."},"TournamentWarning":{"type":"object","properties":{"code":{"type":"string","enum":["INCOMPLETE_TEAMS"],"description":"`INCOMPLETE_TEAMS`: arrancó la fase de grupos con parejas sin compañero."},"teamIds":{"type":"array","items":{"type":"string","format":"uuid","description":"Pareja incompleta."}}},"required":["code","teamIds"],"additionalProperties":false},"TournamentsListResponse":{"type":"object","properties":{"tournaments":{"type":"array","items":{"$ref":"#/components/schemas/TournamentDto"},"description":"Orden: fecha de inicio desc, alta desc."},"total":{"type":"integer","minimum":0},"limit":{"type":"integer","minimum":1,"maximum":200},"offset":{"type":"integer","minimum":0}},"required":["tournaments","total","limit","offset"],"additionalProperties":false,"example":{"tournaments":[{"id":"9b8a7c6d-5e4f-4a3b-2c1d-0e9f8a7b6c5d","eventId":"7a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d","clubId":"3f1c9a52-7d0e-4b6a-9c11-2a4f8e6d5b70","name":"Torneo Primavera — 4ta","sport":"PADEL","format":"CLASICO","status":"OPEN","category":"4ta","gender":"MALE","description":null,"location":null,"price":1200,"maxTeams":12,"registeredTeams":7,"startDate":"2026-10-16","endDate":"2026-10-18","createdAt":"2026-09-18 12:00:00"}],"total":1,"limit":100,"offset":0}},"ValidationIssue":{"type":"object","description":"Un problema de validación (`details.issues[]` del 400 VALIDATION). Nunca trae el valor recibido.","properties":{"path":{"type":"string","description":"Campo (`a.b[0]`); vacío para un error de la raíz.","example":"durationMinutes"},"message":{"type":"string","description":"Motivo.","example":"mandá durationMinutes O endTime (uno de los dos)"},"code":{"type":"string","description":"Code de Zod.","example":"custom"}},"required":["path","message","code"],"additionalProperties":false},"WaitlistDemandItem":{"type":"object","description":"Una franja con gente esperando que se libere. Sin nada de la persona ni de qué la ocupaba.","properties":{"date":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"Día pedido.","example":"2026-09-15"},"startTime":{"type":"string","pattern":"^([01]\\d|2[0-3]):[0-5]\\d$","description":"Comienzo pedido, naive del club.","example":"20:00"},"durationMinutes":{"$ref":"#/components/schemas/DurationMinutes"},"sport":{"oneOf":[{"type":"string","description":"Deporte de la suscripción (`null` = recurso sin deporte).","example":"PADEL"},{"type":"null"}]},"categoryId":{"oneOf":[{"type":"string","format":"uuid","description":"Categoría de recurso (F6); `null` = canchas."},{"type":"null"}]},"waiting":{"type":"integer","minimum":1,"description":"Usuarios DISTINTOS esperando esa franja. Nunca quiénes."}},"required":["date","startTime","durationMinutes","sport","categoryId","waiting"],"additionalProperties":false},"WaitlistDemandResponse":{"type":"object","description":"Demanda insatisfecha por franja (lista de espera de turnos). Reporte, no operativo: sin ETag ni eventos.","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/WaitlistDemandItem"},"description":"Orden: día, comienzo, duración. `[]` sin demanda o con la lista de espera apagada en la plataforma."},"from":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"Primer día del rango.","example":"2026-09-15"},"to":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"Último día del rango.","example":"2026-09-15"}},"required":["items","from","to"],"additionalProperties":false,"example":{"items":[{"date":"2026-09-15","startTime":"20:00","durationMinutes":90,"sport":"PADEL","categoryId":null,"waiting":3}],"from":"2026-09-14","to":"2026-09-21"}},"WebhookCreateBody":{"type":"object","properties":{"clubId":{"type":"string","pattern":"^[a-f0-9-]{8,64}$","description":"Club sobre el que opera la request. Opcional: se infiere cuando la integración tiene un solo grant (kind CLUB, v1)."},"url":{"type":"string","minLength":12,"maxLength":512,"pattern":"^https:\\/\\/","description":"URL https absoluta y pública del receptor. Se rechaza si resuelve a loopback / red privada / link-local / multicast (también en cada entrega)."},"events":{"minItems":1,"maxItems":9,"type":"array","items":{"type":"string","enum":["*","booking.created","booking.confirmed","booking.updated","booking.cancelled","booking.attended","booking.no_show","occupancy.changed","booking.recording_ready"]},"description":"Eventos a recibir: `[\"*\"]` o una lista de `booking.*` / `occupancy.changed`."}},"required":["url","events"],"additionalProperties":false,"example":{"url":"https://pos.miclub.uy/skalia/webhook","events":["booking.created","booking.cancelled"]}},"WebhookCreatedResponse":{"type":"object","properties":{"webhook":{"$ref":"#/components/schemas/WebhookDto"},"secret":{"type":"string","description":"Secreto de firma (`whsec_…`). Se muestra SÓLO en esta respuesta: guardalo. Con él verificás `X-Skalia-Signature`.","example":"whsec_Q2xhdmUgZGUgZWplbXBsbywgbm8gbGEgdXNlcw"}},"required":["webhook","secret"],"additionalProperties":false,"example":{"webhook":{"id":"7c1d2e3f-4a5b-4c6d-8e9f-0a1b2c3d4e5f","url":"https://pos.miclub.uy/skalia/webhook","events":["booking.created","booking.cancelled"],"status":"ACTIVE","failuresInARow":0,"lastSuccessAt":"2026-09-18T12:00:00.000Z","lastFailureAt":null,"disabledAt":null,"disabledReason":null,"createdAt":"2026-09-18T11:00:00.000Z","updatedAt":"2026-09-18T12:00:00.000Z"},"secret":"whsec_Q2xhdmUgZGUgZWplbXBsbywgbm8gbGEgdXNlcw"}},"WebhookDeleteResponse":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"El webhook borrado."},"deleted":{"type":"boolean","const":true}},"required":["id","deleted"],"additionalProperties":false},"WebhookDeliveriesResponse":{"type":"object","properties":{"deliveries":{"type":"array","items":{"$ref":"#/components/schemas/WebhookDeliveryDto"},"description":"Más recientes primero."},"total":{"type":"integer","minimum":0},"limit":{"type":"integer","minimum":1},"offset":{"type":"integer","minimum":0}},"required":["deliveries","total","limit","offset"],"additionalProperties":false},"WebhookDeliveryDto":{"type":"object","description":"Una entrega. Lista cerrada `WEBHOOK_DELIVERY_DTO_FIELDS` (11 campos). Nunca trae el cuerpo enviado.","properties":{"id":{"type":"string","format":"uuid","description":"Id de la entrega (`X-Skalia-Delivery-Id`; el mismo en cada reintento y en un redeliver)."},"eventId":{"type":"integer","minimum":0,"description":"`booking_events.id` (`X-Skalia-Event-Id`, la clave de dedupe). `0` en una prueba."},"event":{"type":"string","description":"`type` del evento (`booking.*`, `occupancy.changed` o `webhook.test`).","example":"booking.created"},"status":{"$ref":"#/components/schemas/WebhookDeliveryStatus"},"attempts":{"type":"integer","minimum":0,"description":"Intentos hechos, automáticos y manuales. El ciclo automático para en 6 (DEAD); un redeliver suma uno más, así que puede superar ese número."},"nextAttemptAt":{"oneOf":[{"type":"string","format":"date-time","description":"Próximo intento (si está PENDING / FAILED).","example":"2026-09-14T12:00:00.000Z"},{"type":"null"}]},"lastStatus":{"oneOf":[{"type":"integer","description":"HTTP status del último intento; `null` si no hubo respuesta (timeout, red, SSRF)."},{"type":"null"}]},"lastError":{"oneOf":[{"type":"string","description":"Motivo del último fallo (`timeout`, `http_status: …`, `ssrf_rejected: …`)."},{"type":"null"}]},"lastAttemptAt":{"oneOf":[{"type":"string","format":"date-time","description":"Último intento.","example":"2026-09-14T12:00:00.000Z"},{"type":"null"}]},"deliveredAt":{"oneOf":[{"type":"string","format":"date-time","description":"Cuándo respondió 2xx.","example":"2026-09-14T12:00:00.000Z"},{"type":"null"}]},"createdAt":{"oneOf":[{"type":"string","format":"date-time","description":"ISO UTC.","example":"2026-09-14T12:00:00.000Z"},{"type":"null"}]}},"required":["id","eventId","event","status","attempts","nextAttemptAt","lastStatus","lastError","lastAttemptAt","deliveredAt","createdAt"],"additionalProperties":false,"example":{"id":"9f8e7d6c-5b4a-4c3d-9e2f-1a0b9c8d7e6f","eventId":1042,"event":"booking.created","status":"DELIVERED","attempts":1,"nextAttemptAt":"2026-09-18T12:00:00.000Z","lastStatus":200,"lastError":null,"lastAttemptAt":"2026-09-18T12:00:01.000Z","deliveredAt":"2026-09-18T12:00:01.000Z","createdAt":"2026-09-18T12:00:00.000Z"}},"WebhookDeliveryResponse":{"type":"object","properties":{"delivery":{"$ref":"#/components/schemas/WebhookDeliveryDto"}},"required":["delivery"],"additionalProperties":false},"WebhookDeliveryStatus":{"type":"string","enum":["PENDING","DELIVERED","FAILED","DEAD"],"description":"`PENDING` (esperando), `DELIVERED` (2xx), `FAILED` (falló, reintenta en `nextAttemptAt`), `DEAD` (agotó los intentos; sólo `redeliver`)."},"WebhookDto":{"type":"object","description":"Un webhook de la integración. Lista cerrada `WEBHOOK_ENDPOINT_DTO_FIELDS` (11 campos). Nunca trae el secreto.","properties":{"id":{"type":"string","format":"uuid","description":"Id del webhook."},"url":{"type":"string","description":"URL https del receptor (validada contra redes privadas al registrar y en cada entrega).","example":"https://pos.miclub.uy/skalia/webhook"},"events":{"type":"array","items":{"$ref":"#/components/schemas/WebhookEvent"},"description":"Eventos suscriptos, canonizados (`[\"*\"]` si incluye el comodín)."},"status":{"$ref":"#/components/schemas/WebhookStatus"},"failuresInARow":{"type":"integer","minimum":0,"description":"Fallos seguidos del receptor (cualquier entrega). A los 25 el webhook pasa a DISABLED solo."},"lastSuccessAt":{"oneOf":[{"type":"string","format":"date-time","description":"Última entrega con 2xx.","example":"2026-09-14T12:00:00.000Z"},{"type":"null"}]},"lastFailureAt":{"oneOf":[{"type":"string","format":"date-time","description":"Último intento fallido.","example":"2026-09-14T12:00:00.000Z"},{"type":"null"}]},"disabledAt":{"oneOf":[{"type":"string","format":"date-time","description":"Cuándo se deshabilitó.","example":"2026-09-14T12:00:00.000Z"},{"type":"null"}]},"disabledReason":{"oneOf":[{"type":"string","enum":["auto_failures","manual"],"description":"`auto_failures` (lo apagó el server) o `manual`."},{"type":"null"}]},"createdAt":{"oneOf":[{"type":"string","format":"date-time","description":"ISO UTC.","example":"2026-09-14T12:00:00.000Z"},{"type":"null"}]},"updatedAt":{"oneOf":[{"type":"string","format":"date-time","description":"ISO UTC.","example":"2026-09-14T12:00:00.000Z"},{"type":"null"}]}},"required":["id","url","events","status","failuresInARow","lastSuccessAt","lastFailureAt","disabledAt","disabledReason","createdAt","updatedAt"],"additionalProperties":false,"example":{"id":"7c1d2e3f-4a5b-4c6d-8e9f-0a1b2c3d4e5f","url":"https://pos.miclub.uy/skalia/webhook","events":["booking.created","booking.cancelled"],"status":"ACTIVE","failuresInARow":0,"lastSuccessAt":"2026-09-18T12:00:00.000Z","lastFailureAt":null,"disabledAt":null,"disabledReason":null,"createdAt":"2026-09-18T11:00:00.000Z","updatedAt":"2026-09-18T12:00:00.000Z"}},"WebhookEvent":{"type":"string","enum":["*","booking.created","booking.confirmed","booking.updated","booking.cancelled","booking.attended","booking.no_show","occupancy.changed","booking.recording_ready"],"description":"Evento suscribible: uno de la outbox o `*` (todos)."},"WebhookPatchBody":{"type":"object","properties":{"url":{"type":"string","minLength":12,"maxLength":512,"pattern":"^https:\\/\\/","description":"Nueva URL (mismas reglas que el alta)."},"events":{"minItems":1,"maxItems":9,"type":"array","items":{"type":"string","enum":["*","booking.created","booking.confirmed","booking.updated","booking.cancelled","booking.attended","booking.no_show","occupancy.changed","booking.recording_ready"]},"description":"Reemplaza la lista de eventos."},"status":{"type":"string","enum":["ACTIVE","DISABLED"],"description":"`DISABLED` deja de entregar (las pendientes esperan); `ACTIVE` lo reactiva y resetea la racha de fallos."}},"additionalProperties":false,"example":{"events":["booking.created","booking.updated","booking.cancelled"]}},"WebhookPatchResponse":{"type":"object","properties":{"webhook":{"$ref":"#/components/schemas/WebhookDto"},"changed":{"type":"array","items":{"type":"string","enum":["url","events","status"]},"description":"Qué cambió (vacío si el body no cambiaba nada)."}},"required":["webhook","changed"],"additionalProperties":false},"WebhookResponse":{"type":"object","properties":{"webhook":{"$ref":"#/components/schemas/WebhookDto"}},"required":["webhook"],"additionalProperties":false},"WebhookStatus":{"type":"string","enum":["ACTIVE","DISABLED"],"description":"Estado del webhook."},"WebhooksListResponse":{"type":"object","properties":{"webhooks":{"type":"array","items":{"$ref":"#/components/schemas/WebhookDto"}}},"required":["webhooks"],"additionalProperties":false}},"responses":{"InternalError":{"description":"Excepción no manejada: reportá el `requestId`.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["INTERNAL_ERROR"]},"RateLimitedRead":{"description":"Cuota agotada (`details.quota`: `reads_per_minute`, `reads_per_day`, o las de plataforma `key_prefix` / `invalid_keys_per_ip`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"},"Retry-After":{"$ref":"#/components/headers/Retry-After"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["RATE_LIMITED"]},"RateLimitedWrite":{"description":"Cuota agotada (`details.quota`: `writes_per_minute`, `writes_per_day`, `max_future_bookings`, o las de plataforma).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"},"Retry-After":{"$ref":"#/components/headers/Retry-After"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["RATE_LIMITED"]},"ServiceUnavailable":{"description":"Kill switch global de la Partner API apagado (tarda ≤ 1 min en propagarse).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["PARTNER_API_DISABLED"]},"Unauthorized":{"description":"Sin key, key del otro ambiente, o key inexistente / revocada / vencida (`details.reason`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["MISSING_API_KEY","API_KEY_WRONG_ENV","INVALID_API_KEY"]},"UnsupportedMediaType":{"description":"Mutación sin `Content-Type: application/json`.","headers":{"X-Request-Id":{"$ref":"#/components/headers/X-Request-Id"},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}},"x-skalia-codes":["UNSUPPORTED_MEDIA_TYPE"]}}}}