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

# Set Webhook

> Creates or updates a webhook (up to 3 enabled per instance) with event filtering, base64 media and its own Authorization header

**Auth:** `TokenAccount` or `TokenInstance` • **Rate limit:** `Global` (100/min) • **Idempotent:** yes (upsert by `label`)

## Description

Creates or updates the `(instance, label)` row in `webhook_configs`. Each instance accepts up to **3 simultaneously enabled webhooks**, identified by a free-form `label`. Webhooks with `enabled=false` are kept in the database but do not count toward the limit and do not receive deliveries.

Use cases:

* **Create a new webhook**: send a `label` not used yet.
* **Update existing**: send the same `label`, all new fields overwrite the old ones.
* **Soft-disable**: send the same `label` with `{"enabled": false}` (clears `url`, `authorization`, `events`, `mediaBase64`, but preserves the row).
* **Parallel migration**: run the old webhook while validating the new one under a different `label`; once confirmed, disable the old one.

## Examples

### Minimum

Enables the `default` webhook pointing to `https://meuapp.com/webhook`. Without `events`, it receives the 6 types; without `authorization`, it sends no auth header.

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

### With label, filter and Authorization

Creates a webhook named `analytics-pipeline` that only receives `message.exchange` and `message.status`, sends the header `Authorization: Bearer svc-token-xyz` on each delivery, and turns off the base64 backup.

<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 (URL-based routing)

Turns on `byEvents: true` so each delivery is sent with the event name as a URL suffix (e.g., `https://meuapp.com/wh/message.exchange`), allowing server-side routing per endpoint without inspecting the 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 preserving the label

Disables the `analytics-pipeline` webhook by sending only `enabled: false`. The row stays in the database but `url`, `authorization`, `events` and `mediaBase64` are cleared, and the entry no longer counts toward the limit of 3 active webhooks.

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

### Base64 media (raw backup)

Configures a webhook dedicated to backup that only receives `message.exchange` with `mediaBase64: true`, so each message with media includes the binary content base64-encoded inside the 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>

## Success response

The response returns the `webhook` object with the configuration actually persisted (`label`, `enabled`, `url`, `authorization`, `byEvents`, `events`, `mediaBase64`), mirrors the request body after the upsert. When `enabled=false`, the `url`, `authorization`, `events` and `mediaBase64` fields come back cleared. The dispatcher starts using the new config immediately (the internal 30s cache is invalidated on 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
  }
}
```

## Path parameters

<ParamField path="instance" type="string" required>
  Instance name (e.g., `$Instance_Name`).
</ParamField>

## Headers

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

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

## Request body

<ParamField body="label" type="string" default="default">
  Local identifier. Max 50 chars; accepts `[a-zA-Z0-9_-]`. Empty or omitted becomes `"default"`. Allows multiple webhooks per instance.
</ParamField>

<ParamField body="enabled" type="boolean" required>
  Turns the webhook on/off. When `false`, the `url`, `authorization`, `byEvents`, `events` and `mediaBase64` fields are **cleared before saving**.
</ParamField>

<ParamField body="url" type="string">
  Destination URL. **Required when `enabled=true`**. Goes through the SSRF guard (see below), blocks `localhost`, private IPs, link-local, multicast.
</ParamField>

<ParamField body="authorization" type="string | null" default="null">
  Literal content of the `Authorization` header sent on each delivery (e.g., `Bearer secret-key-123`). **Encrypted at rest** with AES-256-GCM when `ENCRYPTION_KEY` is configured.
</ParamField>

<ParamField body="byEvents" type="boolean" default="false">
  If `true`, the URL receives the `/<event-name>` suffix on each delivery, useful for endpoint-based routing without inspecting the payload (`https://app/wh/message.exchange`).
</ParamField>

<ParamField body="events" type="string[]" default="[]">
  Filter. Empty array = receive all 6 types. Each entry must be in `{message.exchange, message.status, call.update, group.flow, instance.state, label.update}`.
</ParamField>

<ParamField body="mediaBase64" type="boolean" default="false">
  When `true`, `message.exchange` events with media include `media.base64` (increases payload, may exceed 100KB).
</ParamField>

## SSRF guard

The `url` is validated **at configuration time and before each delivery**. It blocks destinations that point to internal infrastructure:

| Range                 | Example                                         |
| --------------------- | ----------------------------------------------- |
| Loopback              | `localhost`, `127.0.0.1`, `::1`                 |
| Private IPv4          | `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>
  If you need to test against a local server in development, expose it through a public tunnel (ngrok, cloudflared, localhost.run), the SSRF guard is active in every environment.
</Warning>

## Notes

<Note>
  * **Empty `authorization` vs. `null`**: send `null` or omit it to skip the header. An empty string `""` would result in an empty `Authorization:` header, which some proxies reject.
  * **`enabled=false` clears the fields**: re-enabling the same `label` later requires re-sending the `url` (and any other fields you want to preserve).
  * **There is no `DELETE`**: to "remove" a webhook, `POST` with `{"enabled": false}` keeping the `label`. Operators can inspect/clean the row directly in the database when needed.
  * **Optional encryption**: if `ENCRYPTION_KEY` is not configured, `authorization` is stored in plain text. In production, always configure the key.
  * **Cache invalidated**: the new config becomes visible to the dispatcher immediately (the internal 30s TTL is invalidated on save).
</Note>

## Errors

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

Envelope:

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

<Note>
  The 3-webhook limit check is only enforced when **creating** a new row with `enabled=true`. Editing an existing row (same `label`) never triggers the limit.
</Note>

## Next

<CardGroup cols={2}>
  <Card title="List webhooks" icon="list" href="/en/api/events/webhook-list">
    `GET /api/events/getWebhook/:instance`, all or by `?label=`.
  </Card>

  <Card title="Event catalog" icon="book" href="/en/api/events/catalog">
    What each `event` carries in `data`.
  </Card>
</CardGroup>
