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.
POST a tu URL con la data del eventoUsa el CLI de Commet para reenviar eventos de webhook a tu servidor local en tiempo real — sin necesidad de herramientas de tunneling:
commet listen localhost:3000/api/webhooks/commetEl 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:
commet listen localhost:3000/api/webhooks/commet
commet listen local.myapp.dev:3010/webhooks
commet listen 4000Filtra eventos específicos con --events:
commet listen localhost:3000/api/webhooks/commet --events subscription.activated,payment.failedEl 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.
Cada webhook entrega un payload JSON con este envelope:
{
"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 |
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.
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.
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}`)
},
})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)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)
})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");
}
}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';Si no usas @commet/next, verifica la firma HMAC-SHA256 con el SDK:
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 })
}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)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"))
})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");
}
}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';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 |
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).
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.
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
│ ↑ │ │
└───────────────────┘ ↓ ↓
canceledpending_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.
¿Cómo está esta guía?