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

# Definir Webhook

> Cria ou atualiza um webhook (até 3 habilitados por instância) com filtro de eventos, mídia em base64 e header Authorization próprio

**Auth:** `TokenAccount` ou `TokenInstance` • **Rate-limit:** `Global` (100/min) • **Idempotente:** sim (upsert por `label`)

## Descrição

Cria ou atualiza a linha `(instance, label)` em `webhook_configs`. Cada instância aceita até **3 webhooks habilitados simultâneos**, identificados por um `label` livre. Webhooks com `enabled=false` ficam preservados no banco mas não contam para o limite e não recebem entregas.

Casos de uso:

* **Criar novo webhook**: envie um `label` ainda não usado.
* **Atualizar existente**: envie o mesmo `label`, todos os campos novos sobrescrevem os antigos.
* **Soft-disable**: envie o mesmo `label` com `{"enabled": false}` (limpa `url`, `authorization`, `events`, `mediaBase64`, mas preserva a linha).
* **Migração paralela**: rode o webhook antigo enquanto valida o novo num `label` diferente; quando confirmar, desabilita o antigo.

## Exemplos

### Mínimo

Habilita o webhook `default` apontando para `https://meuapp.com/webhook`. Sem `events`, recebe os 6 tipos; sem `authorization`, não envia header de autenticação.

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

### Com label, filtro e Authorization

Cria um webhook nomeado `analytics-pipeline` que recebe apenas `message.exchange` e `message.status`, envia o header `Authorization: Bearer svc-token-xyz` em cada entrega e desliga o backup em 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 (roteamento por URL)

Liga `byEvents: true` para que cada delivery seja enviada com o nome do evento como sufixo da URL (ex.: `https://meuapp.com/wh/message.exchange`), permitindo rotear no servidor por endpoint sem precisar inspecionar o 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":   []
    }'
  # Entregas vão para 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 o label

Desliga o webhook `analytics-pipeline` enviando apenas `enabled: false`. A linha permanece no banco mas `url`, `authorization`, `events` e `mediaBase64` são zerados, e a entrada deixa de contar para o limite de 3 webhooks ativos.

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

### Mídia em base64 (backup raw)

Configura um webhook dedicado a backup que recebe somente `message.exchange` com `mediaBase64: true`, fazendo cada mensagem com mídia incluir o conteúdo binário codificado em base64 dentro do 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>

## Resposta de sucesso

A resposta devolve o objeto `webhook` com a configuração efetivamente persistida (`label`, `enabled`, `url`, `authorization`, `byEvents`, `events`, `mediaBase64`), espelha o body do request após o upsert. Quando `enabled=false`, os campos `url`, `authorization`, `events` e `mediaBase64` voltam zerados. O dispatcher passa a usar a nova config imediatamente (o cache interno de 30s é invalidado no save).

```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 rota

<ParamField path="instance" type="string" required>
  Nome da instância (ex.: `$Instance_Name`).
</ParamField>

## Headers

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

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

## Request body

<ParamField body="label" type="string" default="default">
  Identificador local. Máx. 50 chars; aceita `[a-zA-Z0-9_-]`. Vazio ou omitido vira `"default"`. Permite múltiplos webhooks por instância.
</ParamField>

<ParamField body="enabled" type="boolean" required>
  Liga/desliga o webhook. Quando `false`, os campos `url`, `authorization`, `byEvents`, `events` e `mediaBase64` são **zerados antes de salvar**.
</ParamField>

<ParamField body="url" type="string">
  URL de destino. **Obrigatória quando `enabled=true`**. Passa pelo SSRF guard (ver abaixo), bloqueia `localhost`, IPs privados, link-local, multicast.
</ParamField>

<ParamField body="authorization" type="string | null" default="null">
  Conteúdo literal do header `Authorization` enviado em cada delivery (ex.: `Bearer secret-key-123`). **Encriptado at-rest** com AES-256-GCM quando `ENCRYPTION_KEY` está configurada.
</ParamField>

<ParamField body="byEvents" type="boolean" default="false">
  Se `true`, a URL recebe sufixo `/<event-name>` em cada entrega, útil para roteamento por endpoint sem inspecionar o payload (`https://app/wh/message.exchange`).
</ParamField>

<ParamField body="events" type="string[]" default="[]">
  Filtro. Array vazio = recebe todos os 6 tipos. Cada entrada precisa estar em `{message.exchange, message.status, call.update, group.flow, instance.state, label.update}`.
</ParamField>

<ParamField body="mediaBase64" type="boolean" default="false">
  Quando `true`, eventos `message.exchange` com mídia incluem `media.base64` (aumenta payload, pode passar de 100KB).
</ParamField>

## SSRF guard

A `url` é validada **na configuração e antes de cada delivery**. Bloqueia destinos que apontem para a infraestrutura interna:

| Faixa                 | Exemplo                                         |
| --------------------- | ----------------------------------------------- |
| Loopback              | `localhost`, `127.0.0.1`, `::1`                 |
| IPv4 privado          | `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>
  Se você precisa testar contra um servidor local em desenvolvimento, exponha-o por um túnel público (ngrok, cloudflared, localhost.run), a SSRF guard é ativa em todos os ambientes.
</Warning>

## Notas

<Note>
  * **`authorization` vazio vs. `null`**: envie `null` ou omita para não enviar header. String vazia `""` resultaria em `Authorization:` vazio, que alguns proxies rejeitam.
  * **`enabled=false` zera os campos**: re-habilitar o mesmo `label` depois exige re-enviar a `url` (e os outros campos que você quiser preservar).
  * **Não há `DELETE`**: para "remover" um webhook, faça `POST` com `{"enabled": false}` mantendo o `label`. Operadores podem inspecionar/limpar a linha diretamente no banco quando necessário.
  * **Encriptação opcional**: se `ENCRYPTION_KEY` não está configurada, o `authorization` é armazenado em texto puro. Em produção, sempre configure a chave.
  * **Cache invalidado**: a config nova fica visível para o dispatcher imediatamente (TTL interno de 30s é invalidado no save).
</Note>

## Erros

| 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: <valor>`                                |
| 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`                                     |

Envelope:

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

<Note>
  O check do limite de 3 só é aplicado ao **criar** uma linha nova com `enabled=true`. Editar uma linha já existente (mesmo `label`) nunca aciona o limite.
</Note>

## Próximo

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

  <Card title="Catálogo de eventos" icon="book" href="/pt/api/events/catalog">
    O que cada `event` carrega no `data`.
  </Card>
</CardGroup>
