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

# Webhooks Enterprise

> Webhook da conta: configure uma vez e receba os eventos de teste e cobrança de todas as suas instâncias

O **webhook da conta** é a forma recomendada de receber os eventos Enterprise. Você o configura **uma única vez na sua conta** e ele passa a receber os eventos de **todas** as suas instâncias, sem precisar configurar instância por instância.

O webhook por instância continua funcionando (compatível com o que já existe), mas para os eventos de teste e cobrança o webhook da conta é o preferido: um só endpoint, um só segredo de assinatura, e ele acompanha as instâncias que você cria e deleta sem trabalho extra.

## Configurar o webhook da conta

Há duas formas de configurar.

### Pelo painel

No painel, em **`/settings`**, você define a URL que recebe os eventos, escolhe quais eventos quer receber, vê e rotaciona o segredo de assinatura, e dispara um evento de teste para validar que o seu endpoint está recebendo e conferindo a assinatura.

### Pela API

Todas as rotas aceitam o **TokenAccount** da conta, ou o token global passando `?account=<nome-da-conta>`.

**`GET /api/account/webhook`**

Devolve a configuração atual. O segredo nunca volta em texto puro na leitura, apenas mascarado.

```json 200 OK theme={null}
{
  "url": "https://seu-servidor.com/webhooks/ryze",
  "events": [
    "enterprise.trial.started",
    "enterprise.trial.ending",
    "enterprise.instance.billable",
    "enterprise.cycle.renewed"
  ],
  "enabled": true,
  "secretMasked": "whsec_••••a1b2"
}
```

**`PATCH /api/account/webhook`**

Grava a URL, a lista de eventos e o liga ou desliga. Na **primeira** configuração, quando a conta ainda não tem segredo, a resposta traz o `secret` em texto puro **uma única vez**. Guarde esse valor, ele não é mostrado de novo. Para obter um novo, use a rotação de segredo.

<ParamField body="url" type="string">
  Endpoint que recebe as entregas. Use HTTPS. Uma URL insegura ou não permitida é recusada com `400`.
</ParamField>

<ParamField body="events" type="array">
  Os nomes dos eventos que você quer receber. Se você não enviar o campo, a conta usa os quatro eventos por padrão. Uma lista vazia (`[]`) significa "não receber nenhum".
</ParamField>

<ParamField body="enabled" type="boolean">
  Liga ou desliga a entrega. Com `false`, a configuração é preservada mas nada é entregue.
</ParamField>

<ParamField query="account" type="string">
  Nome da conta. Obrigatório apenas quando você usa o token global. Com o TokenAccount, a conta já vem do próprio token.
</ParamField>

<CodeGroup>
  ```bash cURL theme={null}
  curl -X PATCH "https://ryzeapi.cloud/api/account/webhook" \
    -H "token: $Token_Account" \
    -H "Content-Type: application/json" \
    -d '{
      "url": "https://seu-servidor.com/webhooks/ryze",
      "events": [
        "enterprise.trial.started",
        "enterprise.trial.ending",
        "enterprise.instance.billable",
        "enterprise.cycle.renewed"
      ],
      "enabled": true
    }'
  ```

  ```javascript JavaScript theme={null}
  await fetch("https://ryzeapi.cloud/api/account/webhook", {
    method: "PATCH",
    headers: {
      "token": process.env.Token_Account,
      "Content-Type": "application/json"
    },
    body: JSON.stringify({
      url: "https://seu-servidor.com/webhooks/ryze",
      events: [
        "enterprise.trial.started",
        "enterprise.trial.ending",
        "enterprise.instance.billable",
        "enterprise.cycle.renewed"
      ],
      enabled: true
    })
  });
  ```

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

  requests.patch(
      "https://ryzeapi.cloud/api/account/webhook",
      headers={
          "token": os.environ["Token_Account"],
          "Content-Type": "application/json",
      },
      json={
          "url": "https://seu-servidor.com/webhooks/ryze",
          "events": [
              "enterprise.trial.started",
              "enterprise.trial.ending",
              "enterprise.instance.billable",
              "enterprise.cycle.renewed",
          ],
          "enabled": True,
      },
  )
  ```
</CodeGroup>

```json 200 OK (primeira configuração) theme={null}
{
  "url": "https://seu-servidor.com/webhooks/ryze",
  "events": [
    "enterprise.trial.started",
    "enterprise.trial.ending",
    "enterprise.instance.billable",
    "enterprise.cycle.renewed"
  ],
  "enabled": true,
  "secret": "whsec_0a1b2c3d4e5f60718293a4b5c6d7e8f9",
  "secretMasked": "whsec_••••e8f9"
}
```

<Warning>
  O `secret` só aparece nesta primeira resposta (e ao rotacionar). Depois disso, a leitura devolve apenas o `secretMasked`. Se você perder o valor, rotacione para gerar um novo.
</Warning>

**`POST /api/account/webhook/rotate-secret`**

Gera um segredo novo e o devolve **uma vez**. O segredo anterior deixa de ser usado nas próximas entregas.

```json 200 OK theme={null}
{
  "secret": "whsec_112233445566778899aabbccddeeff00",
  "secretMasked": "whsec_••••ff00"
}
```

**`POST /api/account/webhook/test`**

Envia um evento `enterprise.webhook.test` para a URL configurada, para você validar a recepção e a assinatura. Responde `400` se não houver URL configurada. O evento de teste é entregue mesmo com a configuração desligada.

```json evento de teste entregue theme={null}
{
  "event": "enterprise.webhook.test",
  "account": "minha-conta",
  "description": "This is a test event from RyzeAPI. Your account webhook is configured correctly.",
  "data": {},
  "timestamp": "2026-08-19T12:00:00Z"
}
```

## Os quatro eventos

| Evento                         | Quando dispara                                                                      |
| ------------------------------ | ----------------------------------------------------------------------------------- |
| `enterprise.trial.started`     | O período de teste de uma instância começou.                                        |
| `enterprise.trial.ending`      | O teste de uma instância termina em aproximadamente 24 horas.                       |
| `enterprise.instance.billable` | A instância passou a ser cobrada.                                                   |
| `enterprise.cycle.renewed`     | A renovação mensal da conta foi cobrada (valor cobrado, base, excedente e período). |

## Envelope

Toda entrega tem o mesmo corpo JSON:

<ResponseField name="event" type="string">
  Nome do evento no formato `enterprise.<tipo>`, um dos quatro acima (ou `enterprise.webhook.test`).
</ResponseField>

<ResponseField name="account" type="string">
  Nome da sua conta.
</ResponseField>

<ResponseField name="description" type="string">
  Um resumo legível, em inglês, do que aconteceu. Bom para logs e para exibir ao seu time.
</ResponseField>

<ResponseField name="data" type="object">
  Os campos do evento. O conteúdo varia por tipo de evento, veja abaixo.
</ResponseField>

<ResponseField name="timestamp" type="string">
  Momento da entrega, em RFC3339 (UTC).
</ResponseField>

### `enterprise.instance.billable`

```json theme={null}
{
  "event": "enterprise.instance.billable",
  "account": "minha-conta",
  "description": "Instance 'cliente-acme' is now billable. It will be charged from Aug 25, 2026 until the end of the current cycle.",
  "data": {
    "instance": "cliente-acme",
    "numberJid": "5511999998888@s.whatsapp.net",
    "billableSince": "2026-08-25T12:00:00Z"
  },
  "timestamp": "2026-08-25T12:00:05Z"
}
```

### `enterprise.cycle.renewed`

```json theme={null}
{
  "event": "enterprise.cycle.renewed",
  "account": "minha-conta",
  "description": "Your monthly cycle renewed. You were charged R$ 500.00 (base R$ 500.00 + usage R$ 0.00) for the period Aug 1, 2026 to Sep 1, 2026.",
  "data": {
    "amountChargedCents": 50000,
    "baseCents": 50000,
    "overageCents": 0,
    "periodStart": "2026-08-01T00:00:00Z",
    "periodEnd": "2026-09-01T00:00:00Z"
  },
  "timestamp": "2026-09-01T00:00:03Z"
}
```

## Campos do `data`

O objeto `data` traz só os campos relevantes de cada evento.

### Eventos de teste (`trial.started`, `trial.ending`)

<ResponseField name="instance" type="string">
  Nome da instância.
</ResponseField>

<ResponseField name="numberJid" type="string">
  Número de WhatsApp conectado à instância. Vem preenchido quando já há um número associado.
</ResponseField>

<ResponseField name="trialEndsAt" type="string">
  Data e hora em que o teste termina, em RFC3339.
</ResponseField>

### Virada para cobrança (`instance.billable`)

<ResponseField name="instance" type="string">
  Nome da instância.
</ResponseField>

<ResponseField name="numberJid" type="string">
  Número de WhatsApp conectado à instância. Vem preenchido quando já há um número associado.
</ResponseField>

<ResponseField name="billableSince" type="string">
  Data e hora a partir da qual a instância é cobrada, em RFC3339.
</ResponseField>

### Renovação mensal (`cycle.renewed`)

<ResponseField name="amountChargedCents" type="integer">
  Valor efetivamente cobrado na renovação, em centavos.
</ResponseField>

<ResponseField name="baseCents" type="integer">
  A base do período (o piso), em centavos.
</ResponseField>

<ResponseField name="overageCents" type="integer">
  Quanto o uso passou da base, em centavos. Fica `0` quando o uso cabe na base.
</ResponseField>

<ResponseField name="periodStart" type="string">
  Início do período cobrado, em RFC3339.
</ResponseField>

<ResponseField name="periodEnd" type="string">
  Fim do período cobrado, em RFC3339.
</ResponseField>

## Validar a assinatura

Toda entrega carrega o cabeçalho **`X-Ryze-Signature: sha256=<hex>`**, um HMAC-SHA256 do corpo **cru** da requisição usando o segredo de assinatura da sua conta. Calcule o mesmo HMAC no seu servidor e compare, para ter certeza de que a entrega veio da RyzeAPI e não foi alterada.

<Warning>
  Compare em **tempo constante** (`timingSafeEqual` / `compare_digest`), nunca com `==` de string. E use o corpo exatamente como recebido (os bytes crus), não o JSON reserializado, senão a assinatura não bate.
</Warning>

<CodeGroup>
  ```javascript JavaScript theme={null}
  import crypto from "crypto";
  import express from "express";

  const app = express();

  function verify(rawBody, header, secret) {
    const expected = crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
    const received = (header || "").replace(/^sha256=/, "");
    const a = Buffer.from(expected, "hex");
    const b = Buffer.from(received, "hex");
    return a.length === b.length && crypto.timingSafeEqual(a, b);
  }

  // express.raw preserva os bytes crus, necessários para o HMAC bater.
  app.post("/webhooks/ryze", express.raw({ type: "application/json" }), (req, res) => {
    const ok = verify(req.body, req.get("X-Ryze-Signature"), process.env.RYZE_WEBHOOK_SECRET);
    if (!ok) return res.status(401).send("bad signature");

    const event = JSON.parse(req.body.toString("utf8"));
    // ... trate o evento (event.event, event.data) ...
    res.sendStatus(200);
  });
  ```

  ```python Python theme={null}
  import hmac, hashlib, os
  from flask import Flask, request, abort

  app = Flask(__name__)

  def verify(raw_body: bytes, header: str, secret: str) -> bool:
      expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
      received = (header or "").removeprefix("sha256=")
      return hmac.compare_digest(expected, received)

  @app.post("/webhooks/ryze")
  def ryze_webhook():
      raw = request.get_data()  # bytes crus, necessários para o HMAC bater
      if not verify(raw, request.headers.get("X-Ryze-Signature", ""), os.environ["RYZE_WEBHOOK_SECRET"]):
          abort(401)

      event = request.get_json()
      # ... trate o evento (event["event"], event["data"]) ...
      return "", 200
  ```
</CodeGroup>

<Note>
  Ao **rotacionar** o segredo, aceite por um curto período tanto o segredo novo quanto o anterior. Assim, entregas que já estavam a caminho durante a troca continuam validando.
</Note>

## Webhook por instância (ainda suportado)

Antes do webhook da conta, os eventos Enterprise eram entregues no webhook da própria instância. Isso continua valendo: se o `events` do webhook (ou do WebSocket) de uma instância inclui os nomes `enterprise.*`, os eventos daquela instância também chegam ali, no mesmo envelope dos demais eventos da API. Veja [Configurar webhook](/pt/api/events/webhook-configure).

Para cobrança e ciclo de vida, prefira o webhook da conta: um só endpoint recebe tudo, sem configurar instância por instância, e as entregas são assinadas.

## Próximo

<CardGroup cols={2}>
  <Card title="Controlar a cobrança" icon="sliders" href="/pt/enterprise/billing-control">
    Force a cobrança de uma instância.
  </Card>

  <Card title="Perguntas frequentes" icon="circle-question" href="/pt/enterprise/faq">
    Dúvidas comuns sobre teste, cobrança e eventos.
  </Card>
</CardGroup>
