Skip to content

Webhooks

CloakAPI delivers real-time event notifications to your server via signed HTTPS POST. This page documents the wire format, signature verification, retry behaviour, idempotency, and event catalog.

Quick start

  1. Create an endpoint at /portal/webhooks or via the API:
    Terminal window
    curl -X POST https://api.cloakapi.io/api/v1/webhooks/endpoints \
    -H "Authorization: Bearer $CLOAKAPI_TOKEN" \
    -H "Content-Type: application/json" \
    -d '{"url":"https://yourserver.example/hooks","events":["receipt.signed","chain.broken"]}'
  2. Save the returned secret. You will not see it again — rotate via POST /endpoints/{id}/rotate.
  3. Verify the signature on every incoming POST (see below).
  4. Return HTTP 2xx within 8 seconds to acknowledge. Any 5xx / timeout triggers retry.

Signature scheme

Every outbound POST carries two signature headers. New consumers SHOULD use the v1 scheme.

X-CloakAPI-Signature: t=1716508800,v1=8d3a4f...d2b6

The signed string is "{t}.{raw_body}" (literal dot). Compute on the receiver:

# Python receiver — constant-time + 5-minute replay window
import hmac, hashlib, time
def verify(headers, raw_body: bytes, secret: str) -> bool:
sig = headers["X-CloakAPI-Signature"] # "t=…,v1=…"
parts = dict(p.split("=", 1) for p in sig.split(","))
t, v1 = int(parts["t"]), parts["v1"]
# Replay window: reject anything older than 5 minutes
if abs(time.time() - t) > 300:
return False
signed = f"{t}.".encode() + raw_body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, v1) # constant-time
// Node.js receiver
import { createHmac, timingSafeEqual } from "node:crypto";
export function verify(headers, rawBody, secret) {
const sig = headers["x-cloakapi-signature"]; // "t=…,v1=…"
const parts = Object.fromEntries(sig.split(",").map(p => p.split("=")));
const t = parseInt(parts.t, 10);
if (Math.abs(Date.now() / 1000 - t) > 300) return false; // 5-min window
const expected = createHmac("sha256", secret)
.update(`${t}.`).update(rawBody).digest("hex");
const a = Buffer.from(expected, "hex");
const b = Buffer.from(parts.v1, "hex");
return a.length === b.length && timingSafeEqual(a, b); // constant-time
}

v0 (legacy)

X-CloakAPI-Webhook-Signature: sha256=<hex>
X-CloakAPI-Webhook-Signature-Alg: HMAC-SHA256

The signed string is the canonical JSON payload itself (which already contains a timestamp field). Equivalent security to v1 but requires re-serializing the body — prefer v1 for new code.

Idempotency

Every delivery carries an Idempotency-Key header equal to the unique event_id:

Idempotency-Key: 4f8b9e2c-1a2b-4c3d-9e8f-7a6b5c4d3e2f

Receivers SHOULD store delivered event ids for at least 24 hours and treat a duplicate Idempotency-Key as a no-op (return 2xx). A delivery that retries will arrive with the same key, so deduping prevents double-processing.

Retry schedule

CloakAPI attempts each delivery up to 7 times with exponential backoff:

AttemptDelay after previousCumulative
10 (immediate)0
215 s15 s
31 m1 m 15 s
45 m6 m 15 s
515 m21 m 15 s
61 h≈ 1 h 21 m
76 h≈ 7 h 21 m
(final)1 d≈ 1 d 7 h

Terminal outcomes:

  • 2xxdelivered (terminal success).
  • 410 Gonedead and the endpoint is automatically paused (Stripe convention — return 410 from a decommissioned endpoint).
  • 4xx (non-410)dead immediately. We treat client errors as consumer rejection and do not retry.
  • 5xx / timeout / network → retry per the schedule above. After the 7th attempt, the delivery is marked dead and surfaces in the delivery log with attempt_count = 7.

Delivery log + replay

Every attempt is recorded in webhook_deliveries and visible at /portal/webhooks under “Recent deliveries”. Each row exposes its event, status, last HTTP code, attempt count, and timestamps. Use Replay to re-fire any past delivery (audit row preserved; a new row is created with event_id prefixed replay-).

API surface (org-scoped, requires webhooks.manage for writes):

GET /api/v1/webhooks/deliveries list recent across all endpoints
GET /api/v1/webhooks/deliveries/{id} full row incl. payload + last error
POST /api/v1/webhooks/deliveries/{id}/replay fire a replay (new row)

Test event

Use the “Send test” button on any endpoint or:

POST /api/v1/webhooks/endpoints/{id}/test

This dispatches a synthetic webhook.test event whose body is:

{
"event": "webhook.test",
"event_id": "test-…",
"timestamp": 1716508800,
"data": { "endpoint_id": 42, "sent_at": "2026-05-23T14:00:00+00:00" }
}

Event catalog

Subscribe to events on endpoint create/update via the events array. Use * to subscribe to all events; use prefix.* (e.g. receipt.*) for namespace globs.

EventFired when
receipt.signedA new privacy receipt is sealed into the chain.
chain.brokenContinuous verifier detects a gap or bad signature.
chain.repairedA broken chain segment is reconstructed + re-attested.
user.createdNew user joins an organisation (via SSO or self-serve invite).
user.deletedUser account erased (DSAR / right-to-be-forgotten).
org.member_addedExisting user added to a new org (multi-org membership).
key.createdNew API key minted.
key.revokedKey revoked (admin action or compromise response).
key.rotatedKey auto-rotated by scheduled rotation policy.
billing.low_balancePrepaid balance crossed warning threshold.
billing.credit_addedTop-up succeeded.
billing.invoice_paidMonthly invoice settled (Stripe webhook → mirror).
subscriber.bouncedOutbound email bounced (hard / soft per Mailwizz classifier).
subscriber.unsubscribedRecipient opted out of marketing email.
webhook.testManual smoke-test from the portal / API.

This list is the canonical source — additions appear in this table before they are fired in production.

Headers reference

HeaderDescription
Idempotency-KeyEqual to event_id. Dedupe on this for ≥24 h.
X-CloakAPI-Signaturev1 scheme: t=<unix>,v1=<hmac-hex>. Prefer this.
X-CloakAPI-Webhook-Signaturev0 scheme: sha256=<hmac-hex> of canonical payload.
X-CloakAPI-Webhook-Signature-AlgAlways HMAC-SHA256.
X-CloakAPI-Webhook-TimestampUnix seconds (also embedded inside payload.timestamp).
X-CloakAPI-Webhook-EventEvent type (e.g. receipt.signed).
X-CloakAPI-Webhook-Event-IdUUID; matches Idempotency-Key.
X-CloakAPI-Webhook-Delivery-IdNumeric delivery row id (changes per attempt).
X-CloakAPI-Webhook-AttemptAttempt counter (1, 2, …, 7).
User-AgentCloakAPI-Webhook/1.0.

Security notes

  • Constant-time compare: always use hmac.compare_digest / crypto.timingSafeEqual — string equality leaks bytes through timing.
  • 5-minute replay window: reject any delivery whose t is more than 300 s from your wall clock. Combined with Idempotency-Key deduping, this defeats replay even if an attacker captures the body in flight.
  • Rotate on suspicion: POST /endpoints/{id}/rotate invalidates the old secret immediately.
  • SSRF guard: we refuse to POST to private/reserved/metadata IPs; redirects are not followed. DNS is re-resolved just before each attempt to defeat rebinding.

Last updated: