> ## 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 de la cuenta: configúrelo una vez y reciba los eventos de prueba y cobro de todas sus instancias

El **webhook de la cuenta** es la forma recomendada de recibir los eventos Enterprise. Lo configura **una sola vez en su cuenta** y pasa a recibir los eventos de **todas** sus instancias, sin necesidad de configurar instancia por instancia.

El webhook por instancia sigue funcionando (compatible con lo que ya existe), pero para los eventos de prueba y cobro el webhook de la cuenta es el preferido: un solo endpoint, un solo secreto de firma, y sigue a las instancias que crea y elimina sin trabajo extra.

## Configurar el webhook de la cuenta

Hay dos formas de configurarlo.

### Desde el panel

En el panel, en **`/settings`**, define la URL que recibe los eventos, elige qué eventos quiere recibir, ve y rota el secreto de firma, y dispara un evento de prueba para confirmar que su endpoint está recibiendo y verificando la firma.

### Vía la API

Todas las rutas aceptan el **TokenAccount** de la cuenta, o el token global pasando `?account=<nombre-de-la-cuenta>`.

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

Devuelve la configuración actual. El secreto nunca vuelve en texto plano en la lectura, solo enmascarado.

```json 200 OK theme={null}
{
  "url": "https://su-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`**

Guarda la URL, la lista de eventos y lo activa o desactiva. En la **primera** configuración, cuando la cuenta aún no tiene secreto, la respuesta trae el `secret` en texto plano **una sola vez**. Guarde ese valor, no se muestra de nuevo. Para obtener uno nuevo, rote el secreto.

<ParamField body="url" type="string">
  Endpoint que recibe las entregas. Use HTTPS. Una URL insegura o no permitida se rechaza con `400`.
</ParamField>

<ParamField body="events" type="array">
  Los nombres de los eventos que quiere recibir. Si no envía el campo, la cuenta usa los cuatro eventos por defecto. Una lista vacía (`[]`) significa "no recibir ninguno".
</ParamField>

<ParamField body="enabled" type="boolean">
  Activa o desactiva la entrega. Con `false`, la configuración se conserva pero no se entrega nada.
</ParamField>

<ParamField query="account" type="string">
  Nombre de la cuenta. Obligatorio solo cuando usa el token global. Con el TokenAccount, la cuenta ya viene del propio 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://su-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://su-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://su-servidor.com/webhooks/ryze",
          "events": [
              "enterprise.trial.started",
              "enterprise.trial.ending",
              "enterprise.instance.billable",
              "enterprise.cycle.renewed",
          ],
          "enabled": True,
      },
  )
  ```
</CodeGroup>

```json 200 OK (primera configuración) theme={null}
{
  "url": "https://su-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>
  El `secret` solo aparece en esta primera respuesta (y al rotarlo). Después de eso, la lectura devuelve solo el `secretMasked`. Si pierde el valor, rote para generar uno nuevo.
</Warning>

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

Genera un secreto nuevo y lo devuelve **una vez**. El secreto anterior deja de usarse en las siguientes entregas.

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

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

Envía un evento `enterprise.webhook.test` a la URL configurada, para que valide la recepción y la firma. Responde `400` si no hay URL configurada. El evento de prueba se entrega incluso con la configuración desactivada.

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

## Los cuatro eventos

| Evento                         | Cuándo se dispara                                                                       |
| ------------------------------ | --------------------------------------------------------------------------------------- |
| `enterprise.trial.started`     | El período de prueba de una instancia comenzó.                                          |
| `enterprise.trial.ending`      | La prueba de una instancia termina en aproximadamente 24 horas.                         |
| `enterprise.instance.billable` | La instancia pasó a cobrarse.                                                           |
| `enterprise.cycle.renewed`     | La renovación mensual de la cuenta se cobró (monto cobrado, base, excedente y período). |

## Envelope

Cada entrega tiene el mismo cuerpo JSON:

<ResponseField name="event" type="string">
  Nombre del evento en el formato `enterprise.<tipo>`, uno de los cuatro de arriba (o `enterprise.webhook.test`).
</ResponseField>

<ResponseField name="account" type="string">
  Nombre de su cuenta.
</ResponseField>

<ResponseField name="description" type="string">
  Un resumen legible, en inglés, de lo que ocurrió. Útil para logs y para mostrar a su equipo.
</ResponseField>

<ResponseField name="data" type="object">
  Los campos del evento. El contenido varía según el tipo de evento, vea abajo.
</ResponseField>

<ResponseField name="timestamp" type="string">
  Momento de la entrega, en RFC3339 (UTC).
</ResponseField>

### `enterprise.instance.billable`

```json theme={null}
{
  "event": "enterprise.instance.billable",
  "account": "mi-cuenta",
  "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": "mi-cuenta",
  "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 de `data`

El objeto `data` trae solo los campos relevantes de cada evento.

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

<ResponseField name="instance" type="string">
  Nombre de la instancia.
</ResponseField>

<ResponseField name="numberJid" type="string">
  Número de WhatsApp conectado a la instancia. Viene poblado cuando ya hay un número asociado.
</ResponseField>

<ResponseField name="trialEndsAt" type="string">
  Fecha y hora en que termina la prueba, en RFC3339.
</ResponseField>

### Paso a cobro (`instance.billable`)

<ResponseField name="instance" type="string">
  Nombre de la instancia.
</ResponseField>

<ResponseField name="numberJid" type="string">
  Número de WhatsApp conectado a la instancia. Viene poblado cuando ya hay un número asociado.
</ResponseField>

<ResponseField name="billableSince" type="string">
  Fecha y hora a partir de la cual se cobra la instancia, en RFC3339.
</ResponseField>

### Renovación mensual (`cycle.renewed`)

<ResponseField name="amountChargedCents" type="integer">
  Monto efectivamente cobrado en la renovación, en centavos.
</ResponseField>

<ResponseField name="baseCents" type="integer">
  La base del período (el piso), en centavos.
</ResponseField>

<ResponseField name="overageCents" type="integer">
  Cuánto pasó el uso de la base, en centavos. Es `0` cuando el uso cabe dentro de la base.
</ResponseField>

<ResponseField name="periodStart" type="string">
  Inicio del período cobrado, en RFC3339.
</ResponseField>

<ResponseField name="periodEnd" type="string">
  Fin del período cobrado, en RFC3339.
</ResponseField>

## Validar la firma

Cada entrega lleva el encabezado **`X-Ryze-Signature: sha256=<hex>`**, un HMAC-SHA256 del cuerpo **crudo** de la solicitud usando el secreto de firma de su cuenta. Calcule el mismo HMAC en su servidor y compare, para asegurarse de que la entrega vino de RyzeAPI y no fue alterada.

<Warning>
  Compare en **tiempo constante** (`timingSafeEqual` / `compare_digest`), nunca con un `==` de cadena. Y use el cuerpo exactamente como se recibió (los bytes crudos), no el JSON reserializado, de lo contrario la firma no coincidirá.
</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 conserva los bytes crudos, necesarios para que el HMAC coincida.
  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 el 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 crudos, necesarios para que el HMAC coincida
      if not verify(raw, request.headers.get("X-Ryze-Signature", ""), os.environ["RYZE_WEBHOOK_SECRET"]):
          abort(401)

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

<Note>
  Al **rotar** el secreto, acepte durante un breve período tanto el secreto nuevo como el anterior. Así, las entregas que ya estaban en camino durante el cambio siguen validando.
</Note>

## Webhook por instancia (aún soportado)

Antes del webhook de la cuenta, los eventos Enterprise se entregaban en el webhook de la propia instancia. Eso sigue vigente: si la lista `events` del webhook (o del WebSocket) de una instancia incluye los nombres `enterprise.*`, los eventos de esa instancia también llegan ahí, en el mismo envelope que los demás eventos de la API. Vea [Configurar webhook](/es/api/events/webhook-configure).

Para cobro y ciclo de vida, prefiera el webhook de la cuenta: un solo endpoint recibe todo, sin configurar instancia por instancia, y las entregas van firmadas.

## Siguiente

<CardGroup cols={2}>
  <Card title="Controlar el cobro" icon="sliders" href="/es/enterprise/billing-control">
    Fuerce el cobro de una instancia.
  </Card>

  <Card title="Preguntas frecuentes" icon="circle-question" href="/es/enterprise/faq">
    Dudas comunes sobre prueba, cobro y eventos.
  </Card>
</CardGroup>
