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

# Configurar Webhook

> Crea o actualiza un webhook (hasta 3 habilitados por instancia) con filtrado de eventos, media en base64 y su propia cabecera Authorization

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

## Descripción

Crea o actualiza la fila `(instance, label)` en `webhook_configs`. Cada instancia acepta hasta **3 webhooks habilitados simultáneamente**, identificados por un `label` libre. Los webhooks con `enabled=false` se mantienen en la base de datos pero no cuentan para el límite y no reciben entregas.

Casos de uso:

* **Crear un nuevo webhook**: envía un `label` aún no utilizado.
* **Actualizar uno existente**: envía el mismo `label`, todos los nuevos campos sobrescriben los antiguos.
* **Soft-disable**: envía el mismo `label` con `{"enabled": false}` (limpia `url`, `authorization`, `events`, `mediaBase64`, pero preserva la fila).
* **Migración paralela**: ejecuta el webhook viejo mientras validas el nuevo bajo un `label` diferente; una vez confirmado, desactiva el viejo.

## Ejemplos

### Mínimo

Habilita el webhook `default` apuntando a `https://meuapp.com/webhook`. Sin `events`, recibe los 6 tipos; sin `authorization`, no envía cabecera de auth.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST "https://ryzeapi.cloud/api/events/webhook/$Instance_Name" \
    -H "token: $Token_Instance" \
    -H "Content-Type: application/json" \
    -d '{
      "enabled": true,
      "url":     "https://meuapp.com/webhook"
    }'
  ```

  ```javascript JavaScript theme={null}
  await fetch(`https://ryzeapi.cloud/api/events/webhook/${process.env.Instance_Name}`, {
    method: "POST",
    headers: {
      "token":        process.env.Token_Instance,
      "Content-Type": "application/json"
    },
    body: JSON.stringify({
      enabled: true,
      url:     "https://meuapp.com/webhook"
    })
  });
  ```

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

  requests.post(
      f"https://ryzeapi.cloud/api/events/webhook/{os.environ['Instance_Name']}",
      headers={
          "token":        os.environ["Token_Instance"],
          "Content-Type": "application/json"
      },
      json={
          "enabled": True,
          "url":     "https://meuapp.com/webhook"
      }
  )
  ```

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

  import (
      "net/http"
      "os"
      "strings"
  )

  func main() {
      body := strings.NewReader(`{
          "enabled": true,
          "url":     "https://meuapp.com/webhook"
      }`)
      req, _ := http.NewRequest("POST", "https://ryzeapi.cloud/api/events/webhook/"+os.Getenv("Instance_Name"), body)
      req.Header.Set("token", os.Getenv("Token_Instance"))
      req.Header.Set("Content-Type", "application/json")
      http.DefaultClient.Do(req)
  }
  ```
</CodeGroup>

### Con label, filtro y Authorization

Crea un webhook llamado `analytics-pipeline` que solo recibe `message.exchange` y `message.status`, envía la cabecera `Authorization: Bearer svc-token-xyz` en cada entrega y desactiva el respaldo en base64.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST "https://ryzeapi.cloud/api/events/webhook/$Instance_Name" \
    -H "token: $Token_Instance" \
    -H "Content-Type: application/json" \
    -d '{
      "label":         "analytics-pipeline",
      "enabled":       true,
      "url":           "https://analytics.meuapp.com/events",
      "authorization": "Bearer svc-token-xyz",
      "events":        ["message.exchange", "message.status"],
      "mediaBase64":   false
    }'
  ```

  ```javascript JavaScript theme={null}
  await fetch(`https://ryzeapi.cloud/api/events/webhook/${process.env.Instance_Name}`, {
    method: "POST",
    headers: {
      "token":        process.env.Token_Instance,
      "Content-Type": "application/json"
    },
    body: JSON.stringify({
      label:         "analytics-pipeline",
      enabled:       true,
      url:           "https://analytics.meuapp.com/events",
      authorization: "Bearer svc-token-xyz",
      events:        ["message.exchange", "message.status"],
      mediaBase64:   false
    })
  });
  ```

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

  requests.post(
      f"https://ryzeapi.cloud/api/events/webhook/{os.environ['Instance_Name']}",
      headers={
          "token":        os.environ["Token_Instance"],
          "Content-Type": "application/json"
      },
      json={
          "label":         "analytics-pipeline",
          "enabled":       True,
          "url":           "https://analytics.meuapp.com/events",
          "authorization": "Bearer svc-token-xyz",
          "events":        ["message.exchange", "message.status"],
          "mediaBase64":   False
      }
  )
  ```

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

  import (
      "net/http"
      "os"
      "strings"
  )

  func main() {
      body := strings.NewReader(`{
          "label":         "analytics-pipeline",
          "enabled":       true,
          "url":           "https://analytics.meuapp.com/events",
          "authorization": "Bearer svc-token-xyz",
          "events":        ["message.exchange", "message.status"],
          "mediaBase64":   false
      }`)
      req, _ := http.NewRequest("POST", "https://ryzeapi.cloud/api/events/webhook/"+os.Getenv("Instance_Name"), body)
      req.Header.Set("token", os.Getenv("Token_Instance"))
      req.Header.Set("Content-Type", "application/json")
      http.DefaultClient.Do(req)
  }
  ```
</CodeGroup>

### byEvents = true (enrutamiento basado en URL)

Activa `byEvents: true` para que cada entrega se envíe con el nombre del evento como sufijo en la URL (p. ej., `https://meuapp.com/wh/message.exchange`), permitiendo enrutamiento del lado servidor por endpoint sin inspeccionar el payload.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST "https://ryzeapi.cloud/api/events/webhook/$Instance_Name" \
    -H "token: $Token_Instance" \
    -H "Content-Type: application/json" \
    -d '{
      "label":    "router",
      "enabled":  true,
      "url":      "https://meuapp.com/wh",
      "byEvents": true,
      "events":   []
    }'
  # Deliveries go to https://meuapp.com/wh/message.exchange, /group.flow, ...
  ```

  ```javascript JavaScript theme={null}
  await fetch(`https://ryzeapi.cloud/api/events/webhook/${process.env.Instance_Name}`, {
    method: "POST",
    headers: {
      "token":        process.env.Token_Instance,
      "Content-Type": "application/json"
    },
    body: JSON.stringify({
      label:    "router",
      enabled:  true,
      url:      "https://meuapp.com/wh",
      byEvents: true,
      events:   []
    })
  });
  ```

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

  requests.post(
      f"https://ryzeapi.cloud/api/events/webhook/{os.environ['Instance_Name']}",
      headers={
          "token":        os.environ["Token_Instance"],
          "Content-Type": "application/json"
      },
      json={
          "label":    "router",
          "enabled":  True,
          "url":      "https://meuapp.com/wh",
          "byEvents": True,
          "events":   []
      }
  )
  ```

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

  import (
      "net/http"
      "os"
      "strings"
  )

  func main() {
      body := strings.NewReader(`{
          "label":    "router",
          "enabled":  true,
          "url":      "https://meuapp.com/wh",
          "byEvents": true,
          "events":   []
      }`)
      req, _ := http.NewRequest("POST", "https://ryzeapi.cloud/api/events/webhook/"+os.Getenv("Instance_Name"), body)
      req.Header.Set("token", os.Getenv("Token_Instance"))
      req.Header.Set("Content-Type", "application/json")
      http.DefaultClient.Do(req)
  }
  ```
</CodeGroup>

### Soft-disable preservando el label

Desactiva el webhook `analytics-pipeline` enviando solo `enabled: false`. La fila queda en la base de datos pero `url`, `authorization`, `events` y `mediaBase64` se limpian, y la entrada deja de contar para el límite de 3 webhooks activos.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST "https://ryzeapi.cloud/api/events/webhook/$Instance_Name" \
    -H "token: $Token_Instance" \
    -H "Content-Type: application/json" \
    -d '{
      "label":   "analytics-pipeline",
      "enabled": false
    }'
  ```

  ```javascript JavaScript theme={null}
  await fetch(`https://ryzeapi.cloud/api/events/webhook/${process.env.Instance_Name}`, {
    method: "POST",
    headers: {
      "token":        process.env.Token_Instance,
      "Content-Type": "application/json"
    },
    body: JSON.stringify({
      label:   "analytics-pipeline",
      enabled: false
    })
  });
  ```

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

  requests.post(
      f"https://ryzeapi.cloud/api/events/webhook/{os.environ['Instance_Name']}",
      headers={
          "token":        os.environ["Token_Instance"],
          "Content-Type": "application/json"
      },
      json={
          "label":   "analytics-pipeline",
          "enabled": False
      }
  )
  ```

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

  import (
      "net/http"
      "os"
      "strings"
  )

  func main() {
      body := strings.NewReader(`{
          "label":   "analytics-pipeline",
          "enabled": false
      }`)
      req, _ := http.NewRequest("POST", "https://ryzeapi.cloud/api/events/webhook/"+os.Getenv("Instance_Name"), body)
      req.Header.Set("token", os.Getenv("Token_Instance"))
      req.Header.Set("Content-Type", "application/json")
      http.DefaultClient.Do(req)
  }
  ```
</CodeGroup>

### Media en Base64 (respaldo crudo)

Configura un webhook dedicado al respaldo que solo recibe `message.exchange` con `mediaBase64: true`, de modo que cada mensaje con media incluya el contenido binario codificado en base64 dentro del payload.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST "https://ryzeapi.cloud/api/events/webhook/$Instance_Name" \
    -H "token: $Token_Instance" \
    -H "Content-Type: application/json" \
    -d '{
      "label":       "backup-raw",
      "enabled":     true,
      "url":         "https://backup.meuapp.com/raw",
      "events":      ["message.exchange"],
      "mediaBase64": true
    }'
  ```

  ```javascript JavaScript theme={null}
  await fetch(`https://ryzeapi.cloud/api/events/webhook/${process.env.Instance_Name}`, {
    method: "POST",
    headers: {
      "token":        process.env.Token_Instance,
      "Content-Type": "application/json"
    },
    body: JSON.stringify({
      label:       "backup-raw",
      enabled:     true,
      url:         "https://backup.meuapp.com/raw",
      events:      ["message.exchange"],
      mediaBase64: true
    })
  });
  ```

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

  requests.post(
      f"https://ryzeapi.cloud/api/events/webhook/{os.environ['Instance_Name']}",
      headers={
          "token":        os.environ["Token_Instance"],
          "Content-Type": "application/json"
      },
      json={
          "label":       "backup-raw",
          "enabled":     True,
          "url":         "https://backup.meuapp.com/raw",
          "events":      ["message.exchange"],
          "mediaBase64": True
      }
  )
  ```

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

  import (
      "net/http"
      "os"
      "strings"
  )

  func main() {
      body := strings.NewReader(`{
          "label":       "backup-raw",
          "enabled":     true,
          "url":         "https://backup.meuapp.com/raw",
          "events":      ["message.exchange"],
          "mediaBase64": true
      }`)
      req, _ := http.NewRequest("POST", "https://ryzeapi.cloud/api/events/webhook/"+os.Getenv("Instance_Name"), body)
      req.Header.Set("token", os.Getenv("Token_Instance"))
      req.Header.Set("Content-Type", "application/json")
      http.DefaultClient.Do(req)
  }
  ```
</CodeGroup>

## Respuesta exitosa

La respuesta retorna el objeto `webhook` con la configuración efectivamente persistida (`label`, `enabled`, `url`, `authorization`, `byEvents`, `events`, `mediaBase64`), refleja el body de la solicitud después del upsert. Cuando `enabled=false`, los campos `url`, `authorization`, `events` y `mediaBase64` regresan limpios. El dispatcher comienza a usar la nueva configuración inmediatamente (el caché interno de 30s se invalida al guardar).

```json 200 OK theme={null}
{
  "success": true,
  "message": "Webhook configured successfully",
  "webhook": {
    "label":         "default",
    "enabled":       true,
    "url":           "https://meuapp.com/webhook",
    "authorization": "Bearer secret-key-123",
    "byEvents":      false,
    "events":        ["message.exchange", "call.update"],
    "mediaBase64":   true
  }
}
```

## Parámetros de ruta

<ParamField path="instance" type="string" required>
  Nombre de la instancia (p. ej., `$Instance_Name`).
</ParamField>

## Cabeceras

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

<ParamField header="Content-Type" type="string" required>
  `application/json`
</ParamField>

## Cuerpo de la solicitud

<ParamField body="label" type="string" default="default">
  Identificador local. Máx. 50 caracteres; acepta `[a-zA-Z0-9_-]`. Vacío u omitido se convierte en `"default"`. Permite múltiples webhooks por instancia.
</ParamField>

<ParamField body="enabled" type="boolean" required>
  Activa/desactiva el webhook. Cuando es `false`, los campos `url`, `authorization`, `byEvents`, `events` y `mediaBase64` se **limpian antes de guardar**.
</ParamField>

<ParamField body="url" type="string">
  URL de destino. **Requerida cuando `enabled=true`**. Pasa por el guard SSRF (ver abajo), bloquea `localhost`, IPs privadas, link-local, multicast.
</ParamField>

<ParamField body="authorization" type="string | null" default="null">
  Contenido literal de la cabecera `Authorization` enviada en cada entrega (p. ej., `Bearer secret-key-123`). **Cifrado en reposo** con AES-256-GCM cuando `ENCRYPTION_KEY` está configurado.
</ParamField>

<ParamField body="byEvents" type="boolean" default="false">
  Si es `true`, la URL recibe el sufijo `/<event-name>` en cada entrega, útil para enrutamiento basado en endpoints sin inspeccionar el payload (`https://app/wh/message.exchange`).
</ParamField>

<ParamField body="events" type="string[]" default="[]">
  Filtro. Array vacío = recibir los 6 tipos. Cada entrada debe estar en `{message.exchange, message.status, call.update, group.flow, instance.state, label.update}`.
</ParamField>

<ParamField body="mediaBase64" type="boolean" default="false">
  Cuando es `true`, los eventos `message.exchange` con media incluyen `media.base64` (aumenta el payload, puede exceder 100KB).
</ParamField>

## Guard SSRF

La `url` se valida **al momento de la configuración y antes de cada entrega**. Bloquea destinos que apunten a infraestructura interna:

| Rango                 | Ejemplo                                         |
| --------------------- | ----------------------------------------------- |
| Loopback              | `localhost`, `127.0.0.1`, `::1`                 |
| IPv4 privada          | `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16` |
| Link-local IPv4       | `169.254.0.0/16`                                |
| Link-local IPv6       | `fe80::/10`                                     |
| Multicast / broadcast | `224.0.0.0/4`, `255.255.255.255`                |

<Warning>
  Si necesitas probar contra un servidor local en desarrollo, expónlo a través de un túnel público (ngrok, cloudflared, localhost.run), el guard SSRF está activo en todos los entornos.
</Warning>

## Notas

<Note>
  * **`authorization` vacío vs. `null`**: envía `null` u omítelo para saltar la cabecera. Una cadena vacía `""` resultaría en una cabecera `Authorization:` vacía, que algunos proxies rechazan.
  * **`enabled=false` limpia los campos**: re-habilitar el mismo `label` después requiere reenviar la `url` (y cualquier otro campo que quieras preservar).
  * **No hay `DELETE`**: para "remover" un webhook, haz `POST` con `{"enabled": false}` manteniendo el `label`. Los operadores pueden inspeccionar/limpiar la fila directamente en la base de datos cuando sea necesario.
  * **Cifrado opcional**: si `ENCRYPTION_KEY` no está configurado, `authorization` se almacena en texto plano. En producción, siempre configura la clave.
  * **Caché invalidado**: la nueva configuración se vuelve visible para el dispatcher inmediatamente (el TTL interno de 30s se invalida al guardar).
</Note>

## Errores

| HTTP | `error.message`                                              |
| ---- | ------------------------------------------------------------ |
| 400  | `Invalid request body`                                       |
| 400  | `URL is required when enabled is true`                       |
| 400  | `URL must not target localhost or private network`           |
| 400  | `invalid event type: <value>`                                |
| 400  | `label too long (max 50 chars)`                              |
| 400  | `label may only contain letters, digits, underscore or dash` |
| 401  | `Invalid token`                                              |
| 404  | `Instance not found`                                         |
| 409  | `webhook limit reached (max 3 enabled per instance)`         |
| 429  | `Rate limit exceeded. Try again later.`                      |
| 500  | `Failed to get instance`                                     |

Envoltorio:

```json theme={null}
{
  "success": false,
  "error": { "message": "URL must not target localhost or private network" }
}
```

<Note>
  La verificación del límite de 3 webhooks solo se aplica al **crear** una nueva fila con `enabled=true`. Editar una fila existente (mismo `label`) nunca dispara el límite.
</Note>

## Siguiente

<CardGroup cols={2}>
  <Card title="Listar webhooks" icon="list" href="/es/api/events/webhook-list">
    `GET /api/events/getWebhook/:instance`, todos o por `?label=`.
  </Card>

  <Card title="Catálogo de eventos" icon="book" href="/es/api/events/catalog">
    Qué transporta cada `event` en `data`.
  </Card>
</CardGroup>
