Webhooks
Configura tu endpoint
Una URL HTTPS por workspace. Sin events recibes todos. El secreto de firma solo se devuelve al crearlo: guárdalo.
curl -X PUT "https://api.usecrier.com/v1/webhook-endpoint" \
-H "Authorization: Bearer $CRIER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://app.acme.com/webhooks/crier",
"events": [
"post.published",
"post.failed",
"account.disconnected"
]
}'const res = await fetch('https://api.usecrier.com/v1/webhook-endpoint', {
method: 'PUT',
headers: {
Authorization: `Bearer ${process.env.CRIER_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"url": "https://app.acme.com/webhooks/crier",
"events": [
"post.published",
"post.failed",
"account.disconnected"
]
}),
});
const { data } = await res.json();import os, requests
res = requests.put(
"https://api.usecrier.com/v1/webhook-endpoint",
headers={"Authorization": f"Bearer {os.environ['CRIER_API_KEY']}"},
json={
"url": "https://app.acme.com/webhooks/crier",
"events": [
"post.published",
"post.failed",
"account.disconnected"
]
},
)
data = res.json()["data"]Eventos
| Evento | Cuándo | data |
|---|---|---|
post.scheduled | Se programó o entró en la cola | post |
post.published | Salió en todas las redes | post |
post.partial | Salió en algunas y falló en otras | post |
post.failed | No salió en ninguna | post |
post.cancelled | Se canceló | post |
post.removed | Se retiró de una red | post |
post.rejected | Se rechazó en la revisión y volvió a borrador | post |
post.recycled | Se creó una copia reciclada | post (la copia), recycledFrom |
account.connected | Se conectó una cuenta | account |
account.disconnected | Se desconectó o hay que reconectarla | account |
message.received | Alguien escribió un mensaje directo | accountId, conversationId, message |
comment.received | Alguien comentó una publicación | accountId, postId, comment |
review.received | Llegó una reseña nueva (Google Business) | accountId, review |
{
"id": "evt_9a8s7d",
"type": "post.published",
"createdAt": "2026-10-23T10:00:05.000Z",
"data": {
"post": {
"id": "pst_7q1w0e",
"status": "published",
"targets": [
{ "accountId": "acc_3k9x2m", "platform": "instagram", "status": "published", "url": "https://www.instagram.com/p/…" }
]
}
}
}Verifica la firma
Cada envío lleva Crier-Event, Crier-Event-Id y Crier-Signature: t=<segundos>,v1=<hex>. v1 es el HMAC-SHA256 de <t>.<cuerpo> con tu secreto. Compáralo con el cuerpo sin parsear y rechaza firmas de hace más de 5 minutos.
import { createHmac, timingSafeEqual } from 'node:crypto';
export const verifyCrierSignature = (rawBody, header, secret) => {
const { t, v1 } = Object.fromEntries(header.split(',').map((part) => part.split('=')));
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
return timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
};import hmac, hashlib, time
def verify_crier_signature(raw_body: bytes, header: str, secret: str) -> bool:
parts = dict(part.split("=", 1) for part in header.split(","))
if abs(time.time() - int(parts["t"])) > 300:
return False
expected = hmac.new(secret.encode(), f"{parts['t']}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts["v1"])Reintentos
- Responde con un
2xxen menos de 10 segundos. Haz el trabajo pesado después. - Si fallas, reintentamos varias veces con espera creciente.
- Un mismo evento puede llegar más de una vez: usa
Crier-Event-Idpara no procesarlo dos veces.