> ## Documentation Index
> Fetch the complete documentation index at: https://commet.co/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Introducción

> Recibe notificaciones en tiempo real cuando ocurren eventos en tu cuenta de Commet.

Los webhooks permiten que tu aplicación reciba notificaciones HTTP en tiempo real cuando ocurren eventos en Commet — como una suscripción que se activa, un pago que falla, o un recibo que se crea.

## Cómo funciona

1. Registra un endpoint URL en el dashboard de Commet
2. Selecciona los eventos que quieres recibir
3. Cuando ocurre un evento, Commet envía un request `POST` a tu URL con la data del evento

## Desarrollo local

Usa el CLI de Commet para reenviar eventos de webhook a tu servidor local en tiempo real — sin necesidad de herramientas de tunneling:

```bash theme={null}
commet listen localhost:3000/api/webhooks/commet
```

El CLI se conecta al stream de eventos de Commet y reenvía cada webhook directamente a tu URL local. Vas a ver cada evento, su status de respuesta y el tiempo en la terminal:

```
  ✓ Authenticated (org: Acme Inc)
  ✓ Connected to Commet webhook stream
  ⟶ Forwarding to http://localhost:3000/api/webhooks/commet/
  ⟶ Signing secret: whsec_abc123...

  Ready! Listening for webhook events...

  14:32:01  subscription.activated        →  200 OK    (12ms)
  14:32:05  invoice.created               →  200 OK    (8ms)
```

Puedes apuntar a cualquier URL local — un hostname custom, otro puerto, o un path específico:

```bash theme={null}
commet listen localhost:3000/api/webhooks/commet
commet listen local.myapp.dev:3010/webhooks
commet listen 4000
```

Filtra eventos específicos con `--events`:

```bash theme={null}
commet listen localhost:3000/api/webhooks/commet --events subscription.activated,payment.failed
```

El signing secret se muestra cuando la sesión inicia. Configúralo como `COMMET_WEBHOOK_SECRET` en tu `.env` para verificar firmas localmente con el mismo código que vas a usar en producción.

## Estructura del payload

Cada webhook entrega un payload JSON con este envelope:

```json theme={null}
{
  "id": "whev_8AzNvGSAZJw0YBOUMTn9vM1V",
  "event": "subscription.activated",
  "timestamp": "2026-03-25T14:30:00.000Z",
  "organizationId": "8f14e45f-ceea-4e7a-9c3d-1c2b3a4d5e6f",
  "mode": "live",
  "apiVersion": "2026-05-25",
  "data": {
    // Campos específicos del evento
  }
}
```

| Campo | Tipo | Descripción |
| - | - | - |
| `id` | string | ID único del evento (ej. `whev_…`). Estable entre reintentos de entrega — deduplicá por este valor. También se envía en el header `X-Commet-Event-Id` |
| `event` | string | El tipo de evento (ej. `subscription.activated`) |
| `timestamp` | string | Fecha y hora ISO 8601 cuando se emitió el evento |
| `organizationId` | string | El ID de tu organización |
| `mode` | string | `"live"` o `"sandbox"` — el entorno que disparó el evento |
| `apiVersion` | string | La versión de la API usada para armar este payload (ej. `2026-05-25`). Ver [Versionado de API](/docs/es/api-versioning) |
| `data` | object | Payload específico del evento — ver páginas individuales abajo |

El mismo evento se reintenta con el **mismo `id`** cuando tu endpoint no responde `2xx`, así que guardá los ids procesados y descartá duplicados.

## Manejo de webhooks

Recibe eventos exponiendo un endpoint HTTP. El SDK de Node.js incluye un handler dedicado para Next.js que verifica firmas y rutea eventos automáticamente; en otros lenguajes, verifica el payload y despacha según `event`.

<Tabs>
  <Tab title="Node.js">
    ```typescript title="app/api/webhooks/commet/route.ts" theme={null}
    import { Webhooks } from "@commet/next"

    export const POST = Webhooks({
      webhookSecret: process.env.COMMET_WEBHOOK_SECRET!,

      onSubscriptionActivated: async (payload) => {
        await db.update(users)
          .set({ isPaid: true })
          .where(eq(users.id, payload.data.customerId))
      },

      onSubscriptionCanceled: async (payload) => {
        await db.update(users)
          .set({ isPaid: false })
          .where(eq(users.id, payload.data.customerId))
      },

      onPayload: async (payload) => {
        console.log(`Received: ${payload.event}`)
      },
    })
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    import os
    from flask import Flask, request, Response
    from commet import Commet

    app = Flask(__name__)
    commet = Commet(api_key=os.environ['COMMET_API_KEY'])

    @app.post('/api/webhooks/commet')
    def handle_webhook():
        payload = commet.webhooks.verify_and_parse(
            raw_body=request.get_data(as_text=True),
            signature=request.headers.get('x-commet-signature'),
            secret=os.environ['COMMET_WEBHOOK_SECRET'],
        )

        if payload is None:
            return Response('Invalid signature', status=401)

        if payload['event'] == 'subscription.activated':
            # Otorgar acceso
            pass
        elif payload['event'] == 'subscription.canceled':
            # Revocar acceso
            pass

        return Response('OK', status=200)
    ```
  </Tab>

  <Tab title="Go">
    ```go theme={null}
    import (
        "io"
        "net/http"
        "os"

        "github.com/commet-labs/commet-go/v9"
    )

    client, _ := commet.New(os.Getenv("COMMET_API_KEY"))

    http.HandleFunc("/api/webhooks/commet", func(w http.ResponseWriter, r *http.Request) {
        body, _ := io.ReadAll(r.Body)

        payload, err := client.Webhooks.VerifyAndParse(
            string(body),
            r.Header.Get("X-Commet-Signature"),
            os.Getenv("COMMET_WEBHOOK_SECRET"),
        )
        if err != nil {
            http.Error(w, "Invalid signature", http.StatusUnauthorized)
            return
        }

        switch payload["event"] {
        case "subscription.activated":
            // Otorgar acceso
        case "subscription.canceled":
            // Revocar acceso
        }

        w.WriteHeader(http.StatusOK)
    })
    ```
  </Tab>

  <Tab title="Java">
    ```java theme={null}
    import co.commet.Commet;
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.*;
    import java.util.Map;

    @RestController
    public class CommetWebhookController {

        private final Commet commet = Commet.builder()
            .apiKey(System.getenv("COMMET_API_KEY"))
            .build();

        @PostMapping("/api/webhooks/commet")
        public ResponseEntity<String> handle(
            @RequestBody String rawBody,
            @RequestHeader("X-Commet-Signature") String signature
        ) {
            Map<String, Object> payload = commet.webhooks().verifyAndParse(
                rawBody,
                signature,
                System.getenv("COMMET_WEBHOOK_SECRET")
            );

            if (payload == null) {
                return ResponseEntity.status(401).body("Invalid signature");
            }

            String event = (String) payload.get("event");
            switch (event) {
                case "subscription.activated" -> { /* Otorgar acceso */ }
                case "subscription.canceled" -> { /* Revocar acceso */ }
            }

            return ResponseEntity.ok("OK");
        }
    }
    ```
  </Tab>

  <Tab title="PHP">
    ```php theme={null}
    use Commet\Commet;

    $commet = new Commet(apiKey: getenv('COMMET_API_KEY'));

    $rawBody = file_get_contents('php://input');
    $signature = $_SERVER['HTTP_X_COMMET_SIGNATURE'] ?? null;

    $payload = $commet->webhooks->verifyAndParse(
        $rawBody,
        $signature,
        getenv('COMMET_WEBHOOK_SECRET'),
    );

    if ($payload === null) {
        http_response_code(401);
        exit('Invalid signature');
    }

    match ($payload['event']) {
        'subscription.activated' => null, // Otorgar acceso
        'subscription.canceled' => null,  // Revocar acceso
        default => null,
    };

    http_response_code(200);
    echo 'OK';
    ```
  </Tab>
</Tabs>

## Verificando firmas manualmente

Si no usas `@commet/next`, verifica la firma HMAC-SHA256 con el SDK:

<Tabs>
  <Tab title="Node.js">
    ```typescript theme={null}
    import { Commet } from "@commet/node"

    const commet = new Commet({ apiKey: process.env.COMMET_API_KEY! })

    export async function POST(request: Request) {
      const rawBody = await request.text()
      const signature = request.headers.get("x-commet-signature")

      const payload = commet.webhooks.verifyAndParse({
        rawBody,
        signature,
        secret: process.env.COMMET_WEBHOOK_SECRET!,
      })

      if (!payload) {
        return new Response("Invalid signature", { status: 403 })
      }

      switch (payload.event) {
        case "subscription.activated":
          // Otorgar acceso
          break
        case "subscription.canceled":
          // Revocar acceso
          break
      }

      return new Response("OK", { status: 200 })
    }
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    import os
    from flask import Flask, request, Response
    from commet import Commet

    app = Flask(__name__)
    commet = Commet(api_key=os.environ['COMMET_API_KEY'])

    @app.post('/webhooks/commet')
    def commet_webhook():
        raw_body = request.get_data(as_text=True)
        signature = request.headers.get('x-commet-signature')

        payload = commet.webhooks.verify_and_parse(
            raw_body=raw_body,
            signature=signature,
            secret=os.environ['COMMET_WEBHOOK_SECRET'],
        )

        if payload is None:
            return Response('Invalid signature', status=403)

        if payload['event'] == 'subscription.activated':
            # Otorgar acceso
            pass
        elif payload['event'] == 'subscription.canceled':
            # Revocar acceso
            pass

        return Response('OK', status=200)
    ```
  </Tab>

  <Tab title="Go">
    ```go theme={null}
    import (
        "io"
        "net/http"
        "os"

        "github.com/commet-labs/commet-go/v9"
    )

    client, _ := commet.New(os.Getenv("COMMET_API_KEY"))

    http.HandleFunc("/webhooks/commet", func(w http.ResponseWriter, r *http.Request) {
        body, _ := io.ReadAll(r.Body)
        signature := r.Header.Get("X-Commet-Signature")

        payload, err := client.Webhooks.VerifyAndParse(
            string(body),
            signature,
            os.Getenv("COMMET_WEBHOOK_SECRET"),
        )
        if err != nil {
            http.Error(w, "Invalid signature", http.StatusForbidden)
            return
        }

        switch payload["event"] {
        case "subscription.activated":
            // Otorgar acceso
        case "subscription.canceled":
            // Revocar acceso
        }

        w.WriteHeader(http.StatusOK)
        w.Write([]byte("OK"))
    })
    ```
  </Tab>

  <Tab title="Java">
    ```java theme={null}
    import co.commet.Commet;
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.*;
    import java.util.Map;

    @RestController
    public class WebhookController {

        private final Commet commet = Commet.builder()
            .apiKey(System.getenv("COMMET_API_KEY"))
            .build();

        @PostMapping("/webhooks/commet")
        public ResponseEntity<String> handle(
            @RequestBody String rawBody,
            @RequestHeader("X-Commet-Signature") String signature
        ) {
            Map<String, Object> payload = commet.webhooks().verifyAndParse(
                rawBody,
                signature,
                System.getenv("COMMET_WEBHOOK_SECRET")
            );

            if (payload == null) {
                return ResponseEntity.status(403).body("Invalid signature");
            }

            String event = (String) payload.get("event");
            switch (event) {
                case "subscription.activated" -> { /* Otorgar acceso */ }
                case "subscription.canceled" -> { /* Revocar acceso */ }
            }

            return ResponseEntity.ok("OK");
        }
    }
    ```
  </Tab>

  <Tab title="PHP">
    ```php theme={null}
    use Commet\Commet;

    $commet = new Commet(apiKey: getenv('COMMET_API_KEY'));

    $rawBody = file_get_contents('php://input');
    $signature = $_SERVER['HTTP_X_COMMET_SIGNATURE'] ?? null;

    $payload = $commet->webhooks->verifyAndParse(
        $rawBody,
        $signature,
        getenv('COMMET_WEBHOOK_SECRET'),
    );

    if ($payload === null) {
        http_response_code(403);
        echo 'Invalid signature';
        exit;
    }

    match ($payload['event']) {
        'subscription.activated' => null, // Otorgar acceso
        'subscription.canceled' => null,  // Revocar acceso
        default => null,
    };

    http_response_code(200);
    echo 'OK';
    ```
  </Tab>
</Tabs>

## Headers

Commet envía estos headers con cada request de webhook:

| Header | Descripción |
| - | - |
| `X-Commet-Signature` | Firma HMAC-SHA256 en hex del body crudo |
| `X-Commet-Event` | El tipo de evento (ej. `subscription.activated`) |
| `X-Commet-Timestamp` | Fecha y hora ISO 8601 cuando se emitió el evento |
| `Content-Type` | `application/json` |

## Política de reintentos

Si tu endpoint devuelve un status code distinto de 2xx o hace timeout (10 segundos), Commet reintenta con backoff exponencial. La entrega inicial cuenta como el primer intento, seguida de hasta 7 reintentos — 8 intentos en total en una ventana de aproximadamente 8 horas:

| Reintento | Demora desde el intento anterior |
| - | - |
| 1er reintento | 1 minuto |
| 2do reintento | 5 minutos |
| 3er reintento | 15 minutos |
| 4to reintento | 30 minutos |
| 5to reintento | 1 hora |
| 6to reintento | 2 horas |
| 7mo reintento | 4 horas |

Después de 8 intentos fallidos (la entrega inicial más 7 reintentos), la entrega se marca como fallida y enviamos un email al destinatario de notificaciones de tu organización con la URL del endpoint, el tipo de evento, y la última response que recibimos (status code HTTP o código de error).

## Auto-desactivación de endpoints rotos

Si tres eventos consecutivos fallan al entregarse, Commet desactiva automáticamente el endpoint para que deje de consumir reintentos. Vas a recibir un segundo email confirmando que el endpoint fue desactivado.

Una vez que arregles el problema en tu receptor, reactiva el endpoint desde el dashboard de Commet en **Settings → Webhooks → Endpoints**. Los eventos que llegaron mientras el endpoint estaba desactivado no se reenvían automáticamente — contacta a soporte si necesitas recuperar eventos perdidos.

Puedes monitorear el estado de entrega, inspeccionar payloads y reintentar entregas individuales desde el dashboard en cualquier momento.

## Ciclo de vida del status de suscripción

Cada webhook `subscription.*` incluye un campo `status`. Estos son los valores válidos y cuáles otorgan acceso a tu producto:

| Status | ¿Otorga acceso? | Significado |
| - | - | - |
| `draft` | No | Estado interno de setup antes de que se dispare cualquier evento |
| `pending_payment` | **No** | Suscripción creada, esperando que el primer cobro confirme |
| `trialing` | **Sí** | Trial activo; tarjeta capturada, sin cobro todavía |
| `active` | **Sí** | Paga y al día |
| `past_due` | Sí (período de gracia) | Falló un cobro de renovación; reintentos de dunning en curso — el uso y los asientos siguen funcionando, las compras nuevas se bloquean |
| `canceled` | No | Terminal — la cancelación se ejecutó al fin del período, o el dunning agotó sus reintentos |

Flujo típico:

```
draft → pending_payment → trialing → active ⇄ past_due
                  │                   ↑ │        │
                  └───────────────────┘ ↓        ↓
                                         canceled
```

* Sin trial, `pending_payment` pasa directo a `active` cuando el primer cobro confirma; con trial, pasa a `trialing`.
* `trialing → active` ocurre cuando el trial convierte — y también cuando expira: la expiración del trial activa la suscripción y comienza la facturación regular.
* `active ⇄ past_due`: una renovación fallida inicia el dunning; un pago recuperado devuelve la suscripción a `active`.
* `canceled` es terminal: se llega cuando una cancelación programada se ejecuta al fin del período o cuando el dunning agota sus reintentos.

Regla práctica: condiciona el acceso con `status === "active" || status === "trialing"`. Apóyate en `subscription.activated` para activar acceso y en `subscription.canceled` para revocarlo.


## Related topics

- [Introducción](/docs/es/index.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.