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

# Consultar Webhooks

> Lista todos los webhooks configurados en la instancia o retorna uno solo por label

**Auth:** `TokenAccount` o `TokenInstance` • **Rate limit:** `Global` (100/min) • **Idempotente:** sí

## Descripción

Endpoint de lectura. Sin query, retorna **todos** los webhooks (habilitados y deshabilitados) de la instancia. Con `?label=<name>`, retorna el único webhook con ese label (o `404`).

## Ejemplos

### Listar todos

Sin query string, retorna el array `webhooks[]` con todos los webhooks de la instancia (habilitados y deshabilitados), ordenados alfabéticamente por `label`.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X GET "https://ryzeapi.cloud/api/events/getWebhook/$Instance_Name" \
    -H "token: $Token_Instance"
  ```

  ```javascript JavaScript theme={null}
  await fetch(`https://ryzeapi.cloud/api/events/getWebhook/${process.env.Instance_Name}`, {
    method: "GET",
    headers: {
      "token": process.env.Token_Instance
    }
  });
  ```

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

  requests.get(
      f"https://ryzeapi.cloud/api/events/getWebhook/{os.environ['Instance_Name']}",
      headers={
          "token": os.environ["Token_Instance"]
      }
  )
  ```

  ```go Go theme={null}
  package main

  import (
      "net/http"
      "os"
  )

  func main() {
      req, _ := http.NewRequest("GET", "https://ryzeapi.cloud/api/events/getWebhook/"+os.Getenv("Instance_Name"), nil)
      req.Header.Set("token", os.Getenv("Token_Instance"))
      http.DefaultClient.Do(req)
  }
  ```
</CodeGroup>

### Uno específico

Pasando `?label=analytics-pipeline`, retorna solo el webhook con ese label en el campo `webhook` del envoltorio (o `404` si no existe).

<CodeGroup>
  ```bash cURL theme={null}
  curl -X GET "https://ryzeapi.cloud/api/events/getWebhook/$Instance_Name?label=analytics-pipeline" \
    -H "token: $Token_Instance"
  ```

  ```javascript JavaScript theme={null}
  await fetch(`https://ryzeapi.cloud/api/events/getWebhook/${process.env.Instance_Name}?label=analytics-pipeline`, {
    method: "GET",
    headers: {
      "token": process.env.Token_Instance
    }
  });
  ```

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

  requests.get(
      f"https://ryzeapi.cloud/api/events/getWebhook/{os.environ['Instance_Name']}?label=analytics-pipeline",
      headers={
          "token": os.environ["Token_Instance"]
      }
  )
  ```

  ```go Go theme={null}
  package main

  import (
      "net/http"
      "os"
  )

  func main() {
      req, _ := http.NewRequest("GET", "https://ryzeapi.cloud/api/events/getWebhook/"+os.Getenv("Instance_Name")+"?label=analytics-pipeline", nil)
      req.Header.Set("token", os.Getenv("Token_Instance"))
      http.DefaultClient.Do(req)
  }
  ```
</CodeGroup>

### Default explícito

Obtiene el webhook default pasando `?label=default`. Útil cuando creaste el webhook sin especificar un `label` y quieres leer solo esa entrada en lugar de la lista completa.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X GET "https://ryzeapi.cloud/api/events/getWebhook/$Instance_Name?label=default" \
    -H "token: $Token_Instance"
  ```

  ```javascript JavaScript theme={null}
  await fetch(`https://ryzeapi.cloud/api/events/getWebhook/${process.env.Instance_Name}?label=default`, {
    method: "GET",
    headers: {
      "token": process.env.Token_Instance
    }
  });
  ```

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

  requests.get(
      f"https://ryzeapi.cloud/api/events/getWebhook/{os.environ['Instance_Name']}?label=default",
      headers={
          "token": os.environ["Token_Instance"]
      }
  )
  ```

  ```go Go theme={null}
  package main

  import (
      "net/http"
      "os"
  )

  func main() {
      req, _ := http.NewRequest("GET", "https://ryzeapi.cloud/api/events/getWebhook/"+os.Getenv("Instance_Name")+"?label=default", nil)
      req.Header.Set("token", os.Getenv("Token_Instance"))
      http.DefaultClient.Do(req)
  }
  ```
</CodeGroup>

## Respuesta exitosa

Sin `?label=`, retorna `webhooks[]` (ordenados alfabéticamente por `label`, siempre presente, regresa como `[]` si no existe ningún webhook, y **también incluye los que tienen `enabled=false`** para inspección operacional). Con `?label=<name>`, retorna el objeto único en `webhook` (misma forma que `POST`). El `authorization` se descifra cuando `ENCRYPTION_KEY` está configurado; si la clave fue rotada y algún valor no puede descifrarse, el campo se retorna cifrado en lugar de fallar la solicitud.

```json 200 OK (list, no ?label=) theme={null}
{
  "success": true,
  "message": "Webhook configurations retrieved",
  "webhooks": [
    {
      "label":         "analytics-pipeline",
      "enabled":       true,
      "url":           "https://analytics.meuapp.com/events",
      "authorization": "Bearer svc-token-xyz",
      "byEvents":      false,
      "events":        ["message.exchange", "message.status"],
      "mediaBase64":   false
    },
    {
      "label":       "default",
      "enabled":     true,
      "url":         "https://meuapp.com/webhook",
      "byEvents":    false,
      "events":      [],
      "mediaBase64": false
    },
    {
      "label":       "legacy",
      "enabled":     false,
      "url":         "",
      "byEvents":    false,
      "events":      [],
      "mediaBase64": false
    }
  ]
}
```

```json 200 OK (?label=analytics-pipeline) theme={null}
{
  "success": true,
  "message": "Webhook configuration retrieved",
  "webhook": {
    "label":         "analytics-pipeline",
    "enabled":       true,
    "url":           "https://analytics.meuapp.com/events",
    "authorization": "Bearer svc-token-xyz",
    "byEvents":      false,
    "events":        ["message.exchange", "message.status"],
    "mediaBase64":   false
  }
}
```

<ResponseField name="webhooks" type="array">
  Presente solo cuando **no** hay `?label=`. Ordenado alfabéticamente por `label`. Siempre se retorna incluso cuando no existe ningún webhook (`webhooks: []`).
</ResponseField>

<ResponseField name="webhook" type="object">
  Presente solo cuando se usa `?label=<name>`. Misma forma que `POST`.
</ResponseField>

## Parámetros de ruta

<ParamField path="instance" type="string" required>
  Nombre de la instancia.
</ParamField>

## Cabeceras

<ParamField header="token" type="string" required>
  `TokenAccount` o `TokenInstance`.
</ParamField>

## Parámetros de consulta

<ParamField query="label" type="string">
  Cuando está presente, retorna un único webhook (`webhook` en el envoltorio). Cuando está ausente, retorna la lista (`webhooks[]`).

  Pasar `?label=` (vacío) sigue considerándose "presente" → se convierte en `"default"` y busca la fila con ese label.
</ParamField>

<Note>
  * **El listado incluye `enabled=false`**, los operadores ven el historial completo. Para listar solo los activos, filtra del lado del cliente por `w.enabled === true`.
  * **`authorization` descifrado**: si `ENCRYPTION_KEY` está configurado y el valor está cifrado en reposo, el repositorio lo descifra antes de retornar. Si la clave fue rotada y un valor no puede descifrarse, el campo se retorna cifrado (con una advertencia en el log) en lugar de fallar la solicitud.
</Note>

***

## Entrega: cola, retry, DLQ

La entrega del webhook es **asíncrona** y persistida. Cada evento que coincide con un webhook se encola en `webhook_queue` y es procesado por workers paralelos.

### Flujo

```
EventsService.sendWebhook()
   └─► webhook_queue.Enqueue()        [INSERT row, status=pending]
                                          │
WebhookDispatcher (4 workers + janitor) ──┘
   ├─► ClaimNext()  (FOR UPDATE SKIP LOCKED)
   ├─► HTTP POST + SSRF re-check
   ├─► MarkDelivered (2xx)
   ├─► MarkRetry (4xx except 408/429, redeliver; 408/429/5xx, retry)
   └─► MarkFailed (attempts > max_attempts → DLQ)

Janitor: every 15min, DELETE rows delivered > 24h ago.
```

### Backoff exponencial

| Intento  | Próximo retry |
| -------- | ------------- |
| 1 (fail) | +1s           |
| 2 (fail) | +5s           |
| 3 (fail) | +30s          |
| 4 (fail) | +5min         |
| 5 (fail) | +30min        |
| 6+       | +1h (cap)     |

Después de `max_attempts` (default 5), `status` se vuelve `failed` (DLQ). La fila **no** se elimina automáticamente, los operadores pueden inspeccionar `last_error` y reencolar manualmente (`UPDATE webhook_queue SET status='pending', next_retry_at=now()`).

### Tabla `webhook_queue` (resumen ops)

| Columna                     | Descripción                                               |
| --------------------------- | --------------------------------------------------------- |
| `id`                        | `BIGSERIAL` PK                                            |
| `instance_name`             | Nombre de la instancia                                    |
| `url`                       | Destino final (ya con sufijo `/event-name` si `byEvents`) |
| `payload`                   | `BYTEA`, cuerpo JSON del evento                           |
| `auth_header`               | `Authorization` configurado (cifrado en reposo)           |
| `event_type`                | Nombre del evento (`message.exchange` etc.)               |
| `status`                    | `pending` \| `delivered` \| `failed`                      |
| `attempts`                  | Contador de intentos                                      |
| `max_attempts`              | Default 5                                                 |
| `next_retry_at`             | Próxima ventana de intento                                |
| `last_error`                | Último mensaje de error del consumidor                    |
| `created_at` / `updated_at` | Timestamps                                                |

## Cabeceras entregadas

Cada `POST` a tu webhook llega con:

```http theme={null}
POST <url> HTTP/1.1
Host: <your-host>
Content-Type: application/json
Authorization: <config.authorization>      # if configured
User-Agent: RyzeAPI/<version>

{ "event": "...", "data": { ... }, "instanceData": { ... } }
```

<Warning>
  **No hay HMAC automático.** La validación del origen es responsabilidad del consumidor, configura un `authorization` (Bearer token, API key) y valídalo en tu endpoint.
</Warning>

## Errores

| HTTP | `error.message`                                              | Aplica a  |
| ---- | ------------------------------------------------------------ | --------- |
| 400  | `label too long (max 50 chars)`                              | `?label=` |
| 400  | `label may only contain letters, digits, underscore or dash` | `?label=` |
| 401  | `Invalid token`                                              | ambos     |
| 404  | `Instance not found`                                         | ambos     |
| 404  | `Webhook not configured for this label`                      | `?label=` |
| 429  | `Rate limit exceeded. Try again later.`                      | ambos     |
| 500  | `Failed to get instance`                                     | ambos     |
| 500  | `Failed to list webhook configurations`                      | sin query |

Envoltorio:

```json theme={null}
{
  "success": false,
  "error": { "message": "Webhook not configured for this label" }
}
```

## Siguiente

<CardGroup cols={2}>
  <Card title="Configurar webhook" icon="webhook" href="/es/api/events/webhook-configure">
    `POST /api/events/webhook/:instance`
  </Card>

  <Card title="Catálogo de eventos" icon="book" href="/es/api/events/catalog">
    Esquemas de los 6 tipos de eventos.
  </Card>
</CardGroup>
