> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ventry.es/llms.txt
> Use this file to discover all available pages before exploring further.

# Errores

> Forma de los errores, catálogo de códigos y qué hacer con cada uno.

Todos los errores comparten la misma forma:

```json theme={null}
{
  "error": {
    "type": "invalid_request_error",
    "code": "ticket_type_not_allowed",
    "message": "Esta API key no tiene acceso al tipo de entrada indicado.",
    "param": "ticket_type",
    "request_id": "req_9f2c1a4e7b304d51"
  }
}
```

| Campo        | Uso                                                                |
| ------------ | ------------------------------------------------------------------ |
| `type`       | Categoría general. Sirve para decidir si tiene sentido reintentar. |
| `code`       | **Estable**. Es sobre esto sobre lo que debes ramificar.           |
| `message`    | Texto para humanos. Puede cambiar de redacción sin previo aviso.   |
| `param`      | Campo de la petición que provocó el error, cuando aplica.          |
| `request_id` | Identificador de la petición. También viaja en `X-Request-Id`.     |

<Warning>
  Nunca ramifiques sobre `message`. Cambiar una redacción no se considera un cambio
  incompatible; cambiar un `code`, sí.
</Warning>

## Tipos y códigos HTTP

| `type`                  | HTTP      | ¿Reintentar?              |
| ----------------------- | --------- | ------------------------- |
| `authentication_error`  | 401       | No. Arregla la clave.     |
| `permission_error`      | 403       | No. Faltan permisos.      |
| `invalid_request_error` | 400 / 422 | No. Corrige la petición.  |
| `not_found_error`       | 404       | No.                       |
| `conflict_error`        | 409       | Depende. Lee el `code`.   |
| `rate_limit_error`      | 429       | Sí, tras `retry-after`.   |
| `api_error`             | 500       | Sí, con espera creciente. |

## Catálogo de códigos

### Autenticación y permisos

| Código                   | HTTP | Significado                                          |
| ------------------------ | ---- | ---------------------------------------------------- |
| `missing_api_key`        | 401  | Falta la cabecera `Authorization` o está mal formada |
| `invalid_api_key`        | 401  | La clave no existe                                   |
| `revoked_api_key`        | 401  | La clave fue revocada                                |
| `expired_api_key`        | 401  | La clave ha caducado                                 |
| `key_pending_activation` | 403  | Pendiente de activación manual                       |
| `ip_not_allowed`         | 403  | Origen no autorizado                                 |
| `insufficient_scope`     | 403  | Falta el permiso que exige el endpoint               |

### Petición

| Código             | HTTP      | Significado                                      |
| ------------------ | --------- | ------------------------------------------------ |
| `validation_error` | 400       | El cuerpo o los parámetros no cumplen el esquema |
| `invalid_limit`    | 400       | `limit` fuera de rango (1–200)                   |
| `invalid_cursor`   | 400       | `starting_after` no es un cursor válido          |
| `invalid_date`     | 400       | Una fecha no es ISO 8601                         |
| `invalid_id`       | 400 / 422 | Un identificador no tiene el formato esperado    |
| `unknown_endpoint` | 404       | La ruta no existe                                |
| `empty_batch`      | 400       | Se envió un lote sin ninguna entrada             |

### Ámbito de la clave

| Código                    | HTTP | Significado                                          |
| ------------------------- | ---- | ---------------------------------------------------- |
| `ticket_type_not_allowed` | 422  | El tipo de entrada está fuera del ámbito de tu clave |
| `day_not_allowed`         | 422  | El día está fuera del ámbito de tu clave             |
| `day_required`            | 422  | Tu clave está limitada a días concretos: indica cuál |

### Recursos

| Código                  | HTTP | Significado                                         |
| ----------------------- | ---- | --------------------------------------------------- |
| `ticket_not_found`      | 404  | No existe la entrada, **o está fuera de tu ámbito** |
| `wristband_not_found`   | 404  | No existe la pulsera, o está fuera de tu ámbito     |
| `ticket_type_not_found` | 422  | El tipo de entrada no existe                        |
| `day_not_found`         | 422  | El día no existe                                    |
| `zone_not_found`        | 422  | La zona no existe                                   |

<Note>
  Un recurso fuera del ámbito de tu clave responde `404`, no `403`. Es deliberado:
  distinguirlos confirmaría que ese código de entrada existe, que es justo lo que
  buscaría alguien enumerando códigos.
</Note>

### Conflictos

| Código                    | HTTP | Significado                                                   |
| ------------------------- | ---- | ------------------------------------------------------------- |
| `resource_already_exists` | 409  | Un código de entrada del lote ya existe                       |
| `ticket_already_claimed`  | 409  | La entrada ya se acreditó                                     |
| `wristband_in_use`        | 409  | Esa pulsera ya está asignada a otro asistente                 |
| `idempotency_in_flight`   | 409  | La misma `Idempotency-Key` se está procesando ahora mismo     |
| `api_key_device_missing`  | 409  | La clave no tiene dispositivo asociado: pide que la reactiven |

### Escrituras

| Código                    | HTTP | Significado                                              |
| ------------------------- | ---- | -------------------------------------------------------- |
| `missing_idempotency_key` | 400  | Falta la cabecera `Idempotency-Key`                      |
| `invalid_idempotency_key` | 400  | La clave de idempotencia no tiene una longitud razonable |
| `idempotency_key_reused`  | 422  | Esa clave ya se usó con un cuerpo distinto               |
| `checkin_failed`          | 422  | La acreditación no se pudo completar                     |
| `topup_failed`            | 422  | La pulsera no admite recargas (bloqueada, en revisión…)  |

### Otros

| Código                | HTTP | Significado                                                                    |
| --------------------- | ---- | ------------------------------------------------------------------------------ |
| `rate_limit_exceeded` | 429  | Cupo agotado. Espera lo que diga `retry-after`.                                |
| `internal_error`      | 500  | Fallo del servidor. Reintenta y, si persiste, escribe citando el `request_id`. |

## Reintentos recomendados

<CodeGroup>
  ```js Node.js theme={null}
  async function callVentry(path, options = {}, attempt = 0) {
    const response = await fetch(`https://api.ventry.es/v1${path}`, {
      ...options,
      headers: { Authorization: `Bearer ${process.env.VENTRY_KEY}`, ...options.headers },
    });

    if (response.ok) return response.json();

    const { error } = await response.json();

    // 429: el servidor dice exactamente cuánto esperar.
    if (response.status === 429 && attempt < 5) {
      const wait = Number(response.headers.get("retry-after") ?? 1) * 1000;
      await new Promise((r) => setTimeout(r, wait));
      return callVentry(path, options, attempt + 1);
    }

    // 5xx: espera creciente. Con Idempotency-Key, reintentar es seguro.
    if (response.status >= 500 && attempt < 3) {
      await new Promise((r) => setTimeout(r, 2 ** attempt * 1000));
      return callVentry(path, options, attempt + 1);
    }

    // El resto son errores tuyos: reintentar da exactamente el mismo resultado.
    throw new Error(`${error.code}: ${error.message} (${error.request_id})`);
  }
  ```

  ```python Python theme={null}
  import os, time, requests

  BASE = "https://api.ventry.es/v1"

  def call_ventry(method, path, attempt=0, **kwargs):
      headers = {
          "Authorization": f"Bearer {os.environ['VENTRY_KEY']}",
          **kwargs.pop("headers", {}),
      }
      response = requests.request(method, f"{BASE}{path}", headers=headers, timeout=30, **kwargs)

      if response.ok:
          return response.json()

      error = response.json()["error"]

      # 429: el servidor dice exactamente cuánto esperar.
      if response.status_code == 429 and attempt < 5:
          time.sleep(int(response.headers.get("retry-after", 1)))
          return call_ventry(method, path, attempt + 1, headers=headers, **kwargs)

      # 5xx: espera creciente. Con Idempotency-Key, reintentar es seguro.
      if response.status_code >= 500 and attempt < 3:
          time.sleep(2 ** attempt)
          return call_ventry(method, path, attempt + 1, headers=headers, **kwargs)

      # El resto son errores tuyos: reintentar da exactamente el mismo resultado.
      raise RuntimeError(f"{error['code']}: {error['message']} ({error['request_id']})")
  ```

  ```php PHP theme={null}
  <?php
  function callVentry(string $method, string $path, array $options = [], int $attempt = 0): array
  {
      $ch = curl_init("https://api.ventry.es/v1$path");
      curl_setopt_array($ch, [
          CURLOPT_RETURNTRANSFER => true,
          CURLOPT_HEADER => true,
          CURLOPT_CUSTOMREQUEST => $method,
          CURLOPT_HTTPHEADER => array_merge(
              ["Authorization: Bearer " . getenv("VENTRY_KEY")],
              $options["headers"] ?? []
          ),
      ] + (isset($options["body"]) ? [CURLOPT_POSTFIELDS => $options["body"]] : []));

      $raw = curl_exec($ch);
      $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
      $headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
      $rawHeaders = substr($raw, 0, $headerSize);
      $body = json_decode(substr($raw, $headerSize), true);
      curl_close($ch);

      if ($status < 400) {
          return $body;
      }

      // 429: el servidor dice exactamente cuánto esperar.
      if ($status === 429 && $attempt < 5) {
          preg_match('/retry-after:\s*(\d+)/i', $rawHeaders, $m);
          sleep((int) ($m[1] ?? 1));
          return callVentry($method, $path, $options, $attempt + 1);
      }

      // 5xx: espera creciente. Con Idempotency-Key, reintentar es seguro.
      if ($status >= 500 && $attempt < 3) {
          sleep(2 ** $attempt);
          return callVentry($method, $path, $options, $attempt + 1);
      }

      // El resto son errores tuyos: reintentar da exactamente el mismo resultado.
      throw new RuntimeException(sprintf(
          "%s: %s (%s)",
          $body["error"]["code"],
          $body["error"]["message"],
          $body["error"]["request_id"]
      ));
  }
  ```
</CodeGroup>
