Webhooks
HMAC-SHA256 signatures, delivery headers, and retry behavior
Register HTTPS endpoints with POST /webhooks (webhooks:write). Choose the events you want and store the signing secret securely — it is shown only at create or rotate.
Event types
| Event | When |
|---|---|
order.completed | Order finished successfully |
order.cancelled | Order cancelled |
ticket.issued | Ticket issued |
ticket.checked_in | Ticket checked in |
Delivery headers
Each delivery is a POST with Content-Type: application/json and:
| Header | Purpose |
|---|---|
X-Cowtic-Signature | Hex HMAC-SHA256 of {timestamp}.{rawBody} |
X-Cowtic-Timestamp | Unix timestamp (seconds) used in the signature |
X-Cowtic-Event-Id | Stable event id (idempotency for your receiver) |
X-Cowtic-Event-Type | Public event type (for example order.completed) |
User-Agent | Cowtic-Webhooks/1.0 |
Payload envelope
{
"id": "evt_…",
"type": "order.completed",
"createdAt": "2026-07-20T12:00:00.000Z",
"data": {
"orderId": "…"
}
}Verify signatures
import { createHmac, timingSafeEqual } from "node:crypto";
function verifyCowticSignature(params: {
secret: string;
timestamp: string;
rawBody: string;
signature: string;
}): boolean {
const expected = createHmac("sha256", params.secret)
.update(`${params.timestamp}.${params.rawBody}`)
.digest("hex");
const a = Buffer.from(expected, "utf8");
const b = Buffer.from(params.signature, "utf8");
if (a.length !== b.length) return false;
return timingSafeEqual(a, b);
}Use the raw body
Compute the HMAC over the exact request body bytes (string) you received — do not re-serialize JSON before verifying.
Reject deliveries with timestamps that are too old (for example older than five minutes) to reduce replay risk. Deduplicate with X-Cowtic-Event-Id.
Retries
Failed deliveries (non-2xx or network errors) are retried with exponential backoff:
| Attempt | Delay before next try |
|---|---|
| 1 | 1 minute |
| 2 | 2 minutes |
| 3 | 5 minutes |
| 4 | 10 minutes |
| 5 | 20 minutes |
| 6 | 40 minutes |
| 7 | 80 minutes |
| 8 | 160 minutes |
After 8 failed attempts the outbox item is marked dead. Inspect history with GET /webhooks/{webhookId}/deliveries.
Secrets
- Rotate with
POST /webhooks/{webhookId}/rotate-secret. - Update your receiver before or immediately after rotation.
- Respond quickly with
2xxonce the event is accepted (process asynchronously if needed).