{
  "openapi": "3.1.0",
  "info": {
    "title": "Grupify Public API",
    "version": "1.0.0",
    "summary": "API pública de Grupify para integradores: eventos, inscritos, estadísticas y carga de resultados por dorsal.",
    "description": "API REST sobre los eventos de tu organización en Grupify. Todo es de lectura salvo la carga de resultados.\n\n**Autenticación.** Cada petición lleva una API key de la organización, como `Authorization: Bearer cok_live_…` o `X-API-Key: cok_live_…`. La clave se crea desde el panel de la organización, con los scopes que necesite cada integración.\n\n**Scopes.** `events:read`, `participants:read`, `stats:read` y `results:write`. Con clave válida pero sin el scope: `403 INSUFFICIENT_SCOPE`.\n\n**Límites.** 120 peticiones por minuto por clave (no por IP); por encima, `429 RATE_LIMITED`.\n\n**Envelope.** Las listas devuelven `{ data, meta }` y se paginan con `page` (>= 1) y `page_size` (1..100). Los detalles devuelven el objeto directo. Los errores son `{ detail, code }`: usa `code`, que es estable.\n\nGuía completa con ejemplos: https://www.grupify.com/docs/api. Portal de desarrolladores: https://www.grupify.com/developers.",
    "contact": {
      "name": "Grupify",
      "url": "https://www.grupify.com/contacto",
      "email": "legal@grupify.com"
    },
    "termsOfService": "https://www.grupify.com/cocora/terminos"
  },
  "externalDocs": {
    "description": "Documentación de la API pública",
    "url": "https://www.grupify.com/docs/api"
  },
  "servers": [
    {
      "url": "https://api.grupify.app",
      "description": "Producción"
    }
  ],
  "tags": [
    {
      "name": "public-api",
      "description": "API pública con API key."
    }
  ],
  "security": [
    {
      "ApiKeyHeader": []
    },
    {
      "ApiKeyBearer": []
    }
  ],
  "paths": {
    "/public/v1/events": {
      "get": {
        "operationId": "listEvents",
        "summary": "Listar eventos de la organización",
        "description": "Lista paginada de los eventos de la organización dueña de la API key. `status` es derivado: `open`, `scheduled` (inscripciones aún no abren), `closed` (cerradas), `past` (ya pasó) o `inactive`.\n\nRequiere el scope `events:read`. Guía con ejemplos: https://www.grupify.com/docs/api.",
        "tags": [
          "public-api"
        ],
        "security": [
          {
            "ApiKeyHeader": []
          },
          {
            "ApiKeyBearer": []
          }
        ],
        "x-scope": "events:read",
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "Número de página (empieza en 1).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "page_size",
            "in": "query",
            "required": false,
            "description": "Elementos por página (1..100).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Eventos, dentro del envelope `{ data, meta }`.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Event"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Falta la API key o es inválida, revocada o expirada (`UNAUTHORIZED`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "La clave no tiene el scope `events:read` (`INSUFFICIENT_SCOPE`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "Más de 120 peticiones por minuto con la misma clave (`RATE_LIMITED`). Reintenta tras `Retry-After`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/public/v1/events/{event_id}": {
      "get": {
        "operationId": "getEvent",
        "summary": "Obtener un evento",
        "description": "Un evento de la organización, como objeto directo.\n\nRequiere el scope `events:read`. Guía con ejemplos: https://www.grupify.com/docs/api.",
        "tags": [
          "public-api"
        ],
        "security": [
          {
            "ApiKeyHeader": []
          },
          {
            "ApiKeyBearer": []
          }
        ],
        "x-scope": "events:read",
        "parameters": [
          {
            "name": "event_id",
            "in": "path",
            "required": true,
            "description": "Id del evento (UUID). Tiene que ser de tu organización.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "El evento.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Event"
                }
              }
            }
          },
          "401": {
            "description": "Falta la API key o es inválida, revocada o expirada (`UNAUTHORIZED`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "La clave no tiene el scope `events:read` (`INSUFFICIENT_SCOPE`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "El evento no existe o no pertenece a tu organización (`NOT_FOUND`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "Más de 120 peticiones por minuto con la misma clave (`RATE_LIMITED`). Reintenta tras `Retry-After`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/public/v1/events/{event_id}/participants": {
      "get": {
        "operationId": "listEventParticipants",
        "summary": "Listar participantes pagados de un evento",
        "description": "Solo inscripciones pagadas y no canceladas. Devuelve datos personales completos (documento, correo y celular) y el dorsal cuando ya está asignado: trátalos como PII y limita el scope a quien lo necesite. Los campos de persona son los que se diligenciaron al inscribirse; los que no se diligenciaron van `null`.\n\nRequiere el scope `participants:read`. Guía con ejemplos: https://www.grupify.com/docs/api.",
        "tags": [
          "public-api"
        ],
        "security": [
          {
            "ApiKeyHeader": []
          },
          {
            "ApiKeyBearer": []
          }
        ],
        "x-scope": "participants:read",
        "parameters": [
          {
            "name": "event_id",
            "in": "path",
            "required": true,
            "description": "Id del evento (UUID). Tiene que ser de tu organización.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "Número de página (empieza en 1).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "page_size",
            "in": "query",
            "required": false,
            "description": "Elementos por página (1..100).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Inscritos, dentro del envelope `{ data, meta }`.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Participant"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Falta la API key o es inválida, revocada o expirada (`UNAUTHORIZED`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "La clave no tiene el scope `participants:read` (`INSUFFICIENT_SCOPE`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "El evento no existe o no pertenece a tu organización (`NOT_FOUND`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "Más de 120 peticiones por minuto con la misma clave (`RATE_LIMITED`). Reintenta tras `Retry-After`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/public/v1/events/{event_id}/participants/lookup": {
      "get": {
        "operationId": "lookupEventParticipants",
        "summary": "Buscar inscripciones por dorsal, chip o documento",
        "description": "Búsqueda exacta dentro de un evento. Al menos uno de `bib`, `chip` o `document` es obligatorio; si envías varios se combinan con AND. Excluye inscripciones canceladas e incluye cualquier estado de pago. El documento vuelve enmascarado; dorsal y chip, completos.\n\nRequiere el scope `participants:read`. Guía con ejemplos: https://www.grupify.com/docs/api.",
        "tags": [
          "public-api"
        ],
        "security": [
          {
            "ApiKeyHeader": []
          },
          {
            "ApiKeyBearer": []
          }
        ],
        "x-scope": "participants:read",
        "parameters": [
          {
            "name": "event_id",
            "in": "path",
            "required": true,
            "description": "Id del evento (UUID). Tiene que ser de tu organización.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "bib",
            "in": "query",
            "required": false,
            "description": "Dorsal exacto.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "chip",
            "in": "query",
            "required": false,
            "description": "Chip exacto.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "document",
            "in": "query",
            "required": false,
            "description": "Número de documento completo, exacto.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "Número de página (empieza en 1).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "page_size",
            "in": "query",
            "required": false,
            "description": "Elementos por página (1..100).",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Inscripciones que coinciden, dentro del envelope `{ data, meta }`.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "meta"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ParticipantLookup"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Sin ninguno de `bib`, `chip` o `document` (`VALIDATION_ERROR`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "Falta la API key o es inválida, revocada o expirada (`UNAUTHORIZED`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "La clave no tiene el scope `participants:read` (`INSUFFICIENT_SCOPE`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "El evento no existe o no pertenece a tu organización (`NOT_FOUND`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "Más de 120 peticiones por minuto con la misma clave (`RATE_LIMITED`). Reintenta tras `Retry-After`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/public/v1/events/{event_id}/stats": {
      "get": {
        "operationId": "getEventStats",
        "summary": "Estadísticas agregadas de un evento",
        "description": "Objeto directo, sin envelope. Los desgloses `by_*` cuentan todas las inscripciones (cualquier estado); `paid_inscriptions` es el número de participantes reales (pagados y no cancelados) y `total_participants` el de personas distintas.\n\nRequiere el scope `stats:read`. Guía con ejemplos: https://www.grupify.com/docs/api.",
        "tags": [
          "public-api"
        ],
        "security": [
          {
            "ApiKeyHeader": []
          },
          {
            "ApiKeyBearer": []
          }
        ],
        "x-scope": "stats:read",
        "parameters": [
          {
            "name": "event_id",
            "in": "path",
            "required": true,
            "description": "Id del evento (UUID). Tiene que ser de tu organización.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Las cifras del evento.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EventStats"
                }
              }
            }
          },
          "401": {
            "description": "Falta la API key o es inválida, revocada o expirada (`UNAUTHORIZED`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "La clave no tiene el scope `stats:read` (`INSUFFICIENT_SCOPE`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "El evento no existe o no pertenece a tu organización (`NOT_FOUND`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "Más de 120 peticiones por minuto con la misma clave (`RATE_LIMITED`). Reintenta tras `Retry-After`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/public/v1/events/{event_id}/results": {
      "post": {
        "operationId": "uploadEventResults",
        "summary": "Cargar resultados por dorsal (JSON)",
        "description": "Carga tiempos por dorsal. Es un upsert: reenviar un dorsal ya cargado actualiza su tiempo, su estado y sus parciales, así que reintentar es seguro. Máximo 5000 filas por petición. Sirve para el archivo final o para un feed en vivo (agrupa las llegadas de cada 2-5 segundos en una petición). Al terminar se recalculan las posiciones general, por género y por categoría.\n\nRequiere el scope `results:write`. Guía con ejemplos: https://www.grupify.com/docs/api.",
        "tags": [
          "public-api"
        ],
        "security": [
          {
            "ApiKeyHeader": []
          },
          {
            "ApiKeyBearer": []
          }
        ],
        "x-scope": "results:write",
        "parameters": [
          {
            "name": "event_id",
            "in": "path",
            "required": true,
            "description": "Id del evento (UUID). Tiene que ser de tu organización.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ResultsUploadIn"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Resumen de la carga. `failed` trae todas las filas que no quedaron cargadas, con su motivo: corrígelas y reenvía solo esas.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ResultsUploadOut"
                }
              }
            }
          },
          "401": {
            "description": "Falta la API key o es inválida, revocada o expirada (`UNAUTHORIZED`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "La clave no tiene el scope `results:write` (`INSUFFICIENT_SCOPE`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "El evento no existe o no pertenece a tu organización (`NOT_FOUND`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "Más de 120 peticiones por minuto con la misma clave (`RATE_LIMITED`). Reintenta tras `Retry-After`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/public/v1/events/{event_id}/results/file": {
      "post": {
        "operationId": "uploadEventResultsFile",
        "summary": "Cargar resultados por dorsal (archivo CSV)",
        "description": "Igual que la carga JSON, con un CSV en `multipart/form-data` (campo `file`), UTF-8 o latin-1. Columnas: dorsal (`dorsal`, `bib`, `bib_number`, `numero`), tiempo de chip (`chip_time`, `tiempo_chip`, `chip`, `tiempo`, `time`, `finish_time`, `resultado`), tiempo de pistola (`gun_time`, `tiempo_pistola`, `pistola`, `gun`), estado opcional (`estado`, `status`); cualquier otra columna se toma como parcial con el nombre de la columna.\n\nRequiere el scope `results:write`. Guía con ejemplos: https://www.grupify.com/docs/api.",
        "tags": [
          "public-api"
        ],
        "security": [
          {
            "ApiKeyHeader": []
          },
          {
            "ApiKeyBearer": []
          }
        ],
        "x-scope": "results:write",
        "parameters": [
          {
            "name": "event_id",
            "in": "path",
            "required": true,
            "description": "Id del evento (UUID). Tiene que ser de tu organización.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "file"
                ],
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "CSV con una fila por dorsal."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "El mismo resumen que la carga JSON; en `failed`, `row` es el número de línea del archivo e `item` la fila tal como se leyó.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ResultsUploadOut"
                }
              }
            }
          },
          "401": {
            "description": "Falta la API key o es inválida, revocada o expirada (`UNAUTHORIZED`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "La clave no tiene el scope `results:write` (`INSUFFICIENT_SCOPE`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "El evento no existe o no pertenece a tu organización (`NOT_FOUND`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "Más de 120 peticiones por minuto con la misma clave (`RATE_LIMITED`). Reintenta tras `Retry-After`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiKeyHeader": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key",
        "description": "API key de la organización (`cok_live_…`)."
      },
      "ApiKeyBearer": {
        "type": "http",
        "scheme": "bearer",
        "description": "La misma API key, como Bearer."
      }
    },
    "schemas": {
      "ErrorResponse": {
        "type": "object",
        "required": [
          "detail",
          "code"
        ],
        "properties": {
          "detail": {
            "type": "string",
            "description": "Texto para humanos. No lo uses en lógica de cliente."
          },
          "code": {
            "type": "string",
            "description": "Código estable: `UNAUTHORIZED`, `INSUFFICIENT_SCOPE`, `NOT_FOUND`, `VALIDATION_ERROR`, `RATE_LIMITED`.",
            "enum": [
              "UNAUTHORIZED",
              "INSUFFICIENT_SCOPE",
              "NOT_FOUND",
              "VALIDATION_ERROR",
              "RATE_LIMITED"
            ]
          }
        }
      },
      "Meta": {
        "type": "object",
        "required": [
          "page",
          "page_size",
          "total",
          "total_pages"
        ],
        "description": "Paginación del envelope `{ data, meta }`.",
        "properties": {
          "page": {
            "type": "integer",
            "description": "Página actual."
          },
          "page_size": {
            "type": "integer",
            "description": "Elementos por página."
          },
          "total": {
            "type": "integer",
            "description": "Total de elementos."
          },
          "total_pages": {
            "type": "integer",
            "description": "Total de páginas."
          }
        }
      },
      "Event": {
        "type": "object",
        "required": [
          "id",
          "name",
          "event_date",
          "status",
          "city"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Id del evento.",
            "format": "uuid"
          },
          "name": {
            "type": "string",
            "description": "Nombre del evento."
          },
          "event_date": {
            "type": "string",
            "description": "Fecha y hora del evento.",
            "format": "date-time"
          },
          "status": {
            "type": "string",
            "description": "Estado derivado.",
            "enum": [
              "open",
              "scheduled",
              "closed",
              "past",
              "inactive"
            ]
          },
          "city": {
            "type": "string",
            "description": "Ciudad."
          }
        }
      },
      "Participant": {
        "type": "object",
        "required": [
          "inscription_id"
        ],
        "description": "Inscripción pagada y no cancelada, con los datos que la persona diligenció al inscribirse.",
        "properties": {
          "inscription_id": {
            "type": "string",
            "description": "Id de la inscripción.",
            "format": "uuid"
          },
          "given_names": {
            "type": [
              "string",
              "null"
            ],
            "description": "Nombres."
          },
          "surnames": {
            "type": [
              "string",
              "null"
            ],
            "description": "Apellidos."
          },
          "document": {
            "type": [
              "string",
              "null"
            ],
            "description": "Número de documento, sin enmascarar."
          },
          "email": {
            "type": [
              "string",
              "null"
            ],
            "description": "Correo."
          },
          "mobile_phone": {
            "type": [
              "string",
              "null"
            ],
            "description": "Celular."
          },
          "bib_number": {
            "type": [
              "string",
              "null"
            ],
            "description": "Dorsal, cuando ya está asignado."
          },
          "distance": {
            "type": [
              "string",
              "null"
            ],
            "description": "Distancia."
          },
          "category": {
            "type": [
              "string",
              "null"
            ],
            "description": "Categoría."
          },
          "kit_status": {
            "type": [
              "string",
              "null"
            ],
            "description": "Estado del kit, por ejemplo `PENDING`, `READY`, `DELIVERED`."
          }
        }
      },
      "ParticipantLookup": {
        "type": "object",
        "required": [
          "inscription_id"
        ],
        "properties": {
          "inscription_id": {
            "type": "string",
            "description": "Id de la inscripción.",
            "format": "uuid"
          },
          "bib_number": {
            "type": [
              "string",
              "null"
            ],
            "description": "Dorsal completo."
          },
          "chip": {
            "type": [
              "string",
              "null"
            ],
            "description": "Chip completo."
          },
          "full_name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Nombre completo."
          },
          "gender": {
            "type": [
              "string",
              "null"
            ],
            "description": "Género."
          },
          "category": {
            "type": [
              "string",
              "null"
            ],
            "description": "Categoría."
          },
          "distance": {
            "type": [
              "string",
              "null"
            ],
            "description": "Distancia."
          },
          "kit_status": {
            "type": [
              "string",
              "null"
            ],
            "description": "Estado del kit."
          },
          "document": {
            "type": [
              "string",
              "null"
            ],
            "description": "Documento enmascarado, por ejemplo `****2366`."
          }
        }
      },
      "EventStats": {
        "type": "object",
        "required": [
          "event_id",
          "total_inscriptions",
          "paid_inscriptions",
          "total_participants"
        ],
        "properties": {
          "event_id": {
            "type": "string",
            "description": "Id del evento.",
            "format": "uuid"
          },
          "total_inscriptions": {
            "type": "integer",
            "description": "Inscripciones en cualquier estado."
          },
          "paid_inscriptions": {
            "type": "integer",
            "description": "Participantes reales: pagados y no cancelados."
          },
          "total_participants": {
            "type": "integer",
            "description": "Personas distintas."
          },
          "by_status": {
            "type": "object",
            "description": "Conteo por estado de la inscripción, por ejemplo `PAID`, `PENDING`, `CANCELLED`.",
            "additionalProperties": {
              "type": "integer"
            }
          },
          "by_distance": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "distance",
                "count"
              ],
              "properties": {
                "distance": {
                  "type": "string",
                  "description": "Distancia."
                },
                "count": {
                  "type": "integer",
                  "description": "Conteo."
                }
              }
            }
          },
          "by_category": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "category",
                "count"
              ],
              "properties": {
                "category": {
                  "type": "string",
                  "description": "Categoría."
                },
                "count": {
                  "type": "integer",
                  "description": "Conteo."
                }
              }
            }
          },
          "by_kit_status": {
            "type": "object",
            "description": "Conteo por estado del kit.",
            "additionalProperties": {
              "type": "integer"
            }
          },
          "by_gender": {
            "type": "object",
            "description": "Conteo por género.",
            "additionalProperties": {
              "type": "integer"
            }
          }
        }
      },
      "ResultIn": {
        "type": "object",
        "required": [
          "bib_number"
        ],
        "description": "Un resultado identificado por el dorsal. Un `FINISHED` necesita `chip_time` o `gun_time`; un `DNS`/`DNF`/`DSQ` se guarda sin tiempos.",
        "properties": {
          "bib_number": {
            "type": "string",
            "description": "Dorsal."
          },
          "chip_time": {
            "type": "string",
            "description": "Tiempo neto, de la línea de salida a meta. `HH:MM:SS`, `HH:MM:SS.mmm` o `MM:SS`."
          },
          "gun_time": {
            "type": "string",
            "description": "Tiempo bruto, del disparo a meta. Mismo formato."
          },
          "time": {
            "type": "string",
            "description": "Alias de `chip_time`."
          },
          "status": {
            "type": "string",
            "description": "Estado; por defecto `FINISHED`.",
            "enum": [
              "FINISHED",
              "DNS",
              "DNF",
              "DSQ"
            ],
            "default": "FINISHED"
          },
          "splits": {
            "type": "object",
            "description": "Parciales como `{ \"nombre\": \"tiempo\" }`. Un parcial con formato inválido se ignora.",
            "additionalProperties": {
              "type": "string"
            }
          }
        }
      },
      "ResultsUploadIn": {
        "type": "object",
        "required": [
          "results"
        ],
        "properties": {
          "results": {
            "type": "array",
            "maxItems": 5000,
            "items": {
              "$ref": "#/components/schemas/ResultIn"
            }
          }
        }
      },
      "ResultFailure": {
        "type": "object",
        "required": [
          "row",
          "reason"
        ],
        "properties": {
          "row": {
            "type": "integer",
            "description": "Fila que falló (en CSV, la línea del archivo; la 2 es la primera de datos)."
          },
          "bib_number": {
            "type": [
              "string",
              "null"
            ],
            "description": "Dorsal de la fila."
          },
          "reason": {
            "type": "string",
            "description": "Por qué no quedó cargada."
          },
          "item": {
            "type": "object",
            "description": "Eco de lo que enviaste en esa fila.",
            "additionalProperties": true
          }
        }
      },
      "ResultsUploadOut": {
        "type": "object",
        "required": [
          "batch_id",
          "total_processed",
          "uploaded",
          "updated",
          "not_found",
          "errors",
          "duplicates",
          "failed"
        ],
        "properties": {
          "batch_id": {
            "type": "string",
            "description": "Id de la carga.",
            "format": "uuid"
          },
          "total_processed": {
            "type": "integer",
            "description": "Filas procesadas."
          },
          "uploaded": {
            "type": "integer",
            "description": "Filas nuevas."
          },
          "updated": {
            "type": "integer",
            "description": "Filas que ya existían y se actualizaron."
          },
          "not_found": {
            "type": "integer",
            "description": "Dorsales sin inscripción."
          },
          "errors": {
            "type": "integer",
            "description": "Filas con error."
          },
          "duplicates": {
            "type": "integer",
            "description": "Dorsales repetidos en la misma carga."
          },
          "splits_detected": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Nombres de los parciales encontrados."
          },
          "failed": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ResultFailure"
            },
            "description": "Todas las filas que no quedaron cargadas. Vacío si todo entró."
          }
        }
      }
    }
  }
}
