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
- Registra un endpoint URL en el dashboard de Commet
- Selecciona los eventos que quieres recibir
- Cuando ocurre un evento, Commet envía un request
POSTa tu URL con la data del evento
Estructura del payload
Cada webhook entrega un payload JSON con este envelope:
{
"event": "subscription.activated",
"timestamp": "2026-03-25T14:30:00.000Z",
"organizationId": "org_abc123",
"mode": "live",
"apiVersion": "2026-05-01",
"data": {
// Campos específicos del evento
}
}| Campo | Tipo | Descripción |
|---|---|---|
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-01). Ver Versionado de API |
data | object | Payload específico del evento — ver páginas individuales abajo |
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.
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/commet-go"
)
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';Verificando firmas manualmente
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/commet-go"
)
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';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:
| Intento | Demora |
|---|---|
| 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 |
| 8vo reintento | 6 horas |
Después de 8 intentos fallidos, 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) | Pago fallido; reintentos automáticos en curso |
paused | No | Pausado por admin; sin cobros |
canceled | No | Cliente o admin canceló |
expired | No | El trial terminó sin pago, o estado terminal |
Flujo típico:
draft → pending_payment → trialing → active → canceled
↓
paused → past_due → expiredRegla práctica: condiciona el acceso con status === "active" || status === "trialing". Apóyate en subscription.activated para activar acceso y en subscription.canceled para revocarlo.
Relacionado
- Eventos de Webhook — todos los eventos y sus payloads
¿Cómo está esta guía?