Webhooks and deploy hooks
Signed outbound webhooks with retries and replay, and inbound hook URLs that trigger a deploy.
"Webhooks" covers two different things here, and they face opposite directions:
- Project webhooks — Camplax calls your HTTPS endpoint when something happens in the project.
- Deploy hooks — you call a secret Camplax URL to trigger a deploy.
Project webhooks (outbound)
Create an endpoint in the console, or with POST /v1/projects/<slug>/webhooks:
{ "url": "https://your-app.example.com/hooks/camplax", "events": ["order.paid", "deploy.*"], "maxAttempts": 6 }
The URL must be https, and private or loopback hosts are refused. The response includes the endpoint's secret — whsec_…, shown once. Store it; you need it to verify deliveries. An empty events list means every event; a trailing * subscribes to a family.
The events a project can emit:
| Event | Fires when |
|---|---|
user.created, user.deleted | An account is added or removed |
order.created, order.paid, order.refunded | A payment moves — see Payments |
db.row.created, db.row.updated, db.row.deleted | A watched table changes |
deploy.succeeded, deploy.failed | A deploy finishes either way |
email.received | Mail lands in one of the project's mailbox inboxes |
webhook.test | The test button or POST /test |
email.received is opt-in only: catch-all endpoints (events: []) never receive it — a project endpoint must name email.received or email.* explicitly. Its payload is { inbox, thread, message } — the message carries from/fromName, to, subject, capped text/html (plus a truncated flag), attachments metadata, senderAuthenticated (aligned-DKIM verdict), rfcMessageId, and rawKey pointing at the stored .eml. An inbox's own webhookUrl rides this same pipeline and signing scheme, signed with the inbox's whsec_… secret — but that endpoint is managed through the mailbox routes only, and never appears in your project webhook list.
Verifying a delivery
Deliveries are POSTs with a JSON body { "event", "data", "sentAt" } and a signature header:
Camplax-Signature: t=1736…,v1=9c86…
User-Agent: Camplax-Webhooks/1.0
The scheme is Stripe's: v1 is the hex HMAC-SHA256 of <t>.<raw body> keyed with your endpoint secret. The timestamp is inside the signed string, so a captured delivery cannot be replayed with a fresh t — anything more than five minutes old fails anyway. While a secret is being rotated the header can carry several v1 digests; accepting any one that verifies keeps you up through the overlap.
const [t, v1] = header.split(",").map((p) => p.split("=")[1]);
const expected = hmacSha256Hex(secret, `${t}.${rawBody}`);
// compare expected to each v1, then check t is within 5 minutes
Retries, dead-letters, replay
Your endpoint gets 10 seconds per attempt; only the status code matters — the response body is never read. Failures back off roughly 10s, 40s, 3m, 11m, 43m with jitter (up to maxAttempts, 1–10, default 6), and a minute-by-minute sweep picks up whatever fell due. After the last attempt the delivery is dead-lettered, not lost — it sits in the delivery log with its error.
GET /v1/projects/<slug>/webhooks/:id/deliveries shows the last 100 attempts per endpoint. POST /deliveries/:id/replay resets a delivery to pending with a fresh attempt budget. POST /test fires a webhook.test event — the one endpoint call a webhooks:send API key may make; everything else here is console-session only. POST /:id/rotate-secret mints a new whsec_…, shown once.
Deploy hooks (inbound)
A deploy hook is a secret URL that queues a deploy — for CI systems, other hosts' build pipelines, or a button anywhere:
POST https://camplax.dev/v1/hooks/cxhook_…
Create one in the console or with POST /v1/projects/<slug>/deploys/hooks (GET lists, DELETE removes). The cxhook_… token is shown once and only its hash is stored. Each hook is bound to a branch: matching the project's default branch queues a production deploy, any other branch a preview. The answer is 202 with the deployId, or 409 while a production deploy is already running — the URL is the secret, so anyone holding it can deploy.
If the caller can sign, send X-Hub-Signature-256: sha256=<hex> — an HMAC-SHA256 of the raw body keyed with the hook token itself (X-Camplax-Signature works too). With no header the token alone is the check.
The platform's own inbound hooks
For completeness — these exist so external providers can reach Camplax, and are not yours to call: POST /v1/webhooks/github (GitHub events, signature-checked), POST /v1/webhooks/stripe and /stripe/connect (billing and connected-account events), and POST /v1/webhooks/resend (email delivery events — see Email). Each verifies its provider's signature; the signature is the auth.