Webhooks
Set your endpoint
Section titled “Set your endpoint”One HTTPS URL per workspace. Without events you get all of them. The signing secret is only returned when it is created: keep it.
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"]Events
Section titled “Events”| Event | When | data |
|---|---|---|
post.scheduled |
Scheduled or queued | post |
post.published |
Live on every network | post |
post.partial |
Live on some, failed on others | post |
post.failed |
Live nowhere | post |
post.cancelled |
Cancelled | post |
post.removed |
Taken down from a network | post |
post.rejected |
Rejected in review and back to draft | post |
post.recycled |
A recycled copy was created | post (the copy), recycledFrom |
account.connected |
An account was connected | account |
account.disconnected |
Disconnected or needs reconnecting | account |
message.received (soon) |
Someone sent a direct message | accountId, conversationId, message |
comment.received |
Someone commented on a post | accountId, postId, comment |
review.received (soon) |
A new review arrived (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/…" } ] } }}Verify the signature
Section titled “Verify the signature”Each delivery carries Crier-Event, Crier-Event-Id and Crier-Signature: t=<seconds>,v1=<hex>. v1 is the HMAC-SHA256 of <t>.<body> with your secret. Compare it against the unparsed body and reject signatures older than 5 minutes.
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"])Retries
Section titled “Retries”- Answer with a
2xxwithin 10 seconds. Do the heavy work afterwards. - If you fail, we retry several times with growing waits.
- The same event may arrive more than once: use
Crier-Event-Idto process it only once.
© 2026 Crier by TWOFACES ·usecrier.com ·Dashboard ·soporte@twofacesco.com