Platform
Hosting & deploysPush to a branch, get a live linkDatabaseReal Postgres, one per appSign-inEmail and Google, users includedFile storageUploads, photos and documentsAI gatewayEvery model, with a hard spend cap
Resources
DocsGuides written for people who are newFor AI agentsClaude Code, Cursor, MCP, JSON CLICompareCamplax beside the stack it replacesPricingFrom $25/month plus prepaid creditsSupportA person answers, not a queue botPoliciesTerms, privacy and how we handle data Get started
Open in ChatGPT Open in Claude Last updated 2026-11-03

Webhooks and deploy hooks

Signed outbound webhooks with retries and replay, inbound webhooks from Stripe, GitHub, Clerk and Slack forwarded to your app, and 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.
  • Inbound webhooks — Stripe, GitHub, Clerk or Slack call Camplax, which verifies and forwards to your app.
  • 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": ["deploy.*", "user.deleted"], "maxAttempts": 6 }

The URL must be https, and private or loopback hosts are refused. There is nothing to store: Camplax signs every delivery with its own key, and your receiver checks it against Camplax's public key (below). An empty events list means every event; a trailing * subscribes to a family.

The events a project can emit:

EventFires when
user.deletedSomeone deletes one of your app's users in the console (Users). The data is { user: { id, email }, deletedFrom: "console" }. Sign-ups and deletions inside your app happen in your own code, which Camplax does not see — for those, add a database webhook on the user table
order.paid, order.refundedReserved names. Camplax has no payments feature, so nothing sends them
deploy.succeeded, deploy.failedA deploy finishes either way
email.receivedMail lands in one of the project's mailbox inboxes — registered now, fires when inbound mail goes live (not live yet)
form.submittedA post to one of the project's form endpoints passes its spam rails and is stored. The data is { formId, formName, submissionId, fields, submittedAt }, with no IP, browser or captcha token
webhook.testThe test button or POST /test

email.received and form.submitted are opt-in only: catch-all endpoints (events: []) never receive them — a project endpoint must name the event or its family (email.*, form.*) explicitly, since both carry what a stranger typed. 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 is signed the same way — but that endpoint is managed through the mailbox routes only, and never appears in your project webhook list.

Verifying a delivery

There is no secret on your side. Camplax signs every delivery with its own key — the same one as cron calls and log drains — and your receiver checks it against Camplax's public key, which anyone can fetch. Nothing to create, copy, store or rotate.

Deliveries are POSTs with a JSON body { "id", "event", "data", "sentAt" } and these headers:

HeaderValue
Camplax-TimestampUnix seconds when it was signed
Camplax-KeyWhich public key signed it
Camplax-Signaturev1= + base64url Ed25519 signature
Camplax-Purposewebhook
Camplax-ProjectThe project id it was signed for

In a Plax app, check the raw body before you parse it:

import { readRawBody, sendFail, sendOk, verifyCamplaxWebhook } from "@camplax/plax";

"POST /hooks/camplax": async (req, res) => {
  const raw = await readRawBody(req);
  const verified = await verifyCamplaxWebhook(req, raw, { purpose: "webhook" });
  if (!verified.ok) return sendFail(res, verified); // 401
  const { id, event, data } = JSON.parse(raw.toString());
  // skip it if you already handled this id
  sendOk(res, { ok: true });
},

verifyCamplaxWebhook accepts a delivery only when the signature checks out, Camplax-Timestamp is within 5 minutes of your clock, and it was signed for your app's own project (CAMPLAX_PROJECT_ID, which Camplax injects) — so another project's webhook pointed at your URL fails, and so does a cron call, drain batch or alert replayed at it. Dedupe on id: it is inside the signed body, the same across retries and a replay, so a captured request re-sent inside the 5 minutes is recognised.

Outside Plax, fetch https://camplax.dev/v1/signing/keys (public, no credential), take the key whose kid matches Camplax-Key, and verify the Ed25519 signature over ` webhook.${timestamp}.${projectId}.${rawBody} , where projectId is your project's id. Cache the keys, and refetch when an unknown Camplax-Key` shows up — that is a key rotation.

A receiver outside Camplax that would rather keep a shared secret can ask for an HMAC as well. It is off until someone asks: Add an HMAC secret in the endpoint's ⋯ menu (or POST /:id/rotate-secret) shows one once, and from then on every delivery also carries Camplax-Hmac-Signature: t=<unix>,v1=<hex> — Stripe's scheme: v1 is the hex HMAC-SHA256 of <t>.<raw body>. Creating an endpoint never hands out a secret, and nothing on Camplax needs one.

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. Delivered and dead-lettered deliveries are kept for your plan's history window — 7 days on Indie, 14 on Pro, 30 on Scale — and then removed; a delivery still retrying is never removed. 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 an HMAC secret (whsec_…), shown once — the first call switches the HMAC on. On a later call the old one keeps signing beside the new one for 24 hours — Camplax-Hmac-Signature carries two v1 digests — so a receiver that checks the HMAC can switch without dropping a delivery; previousSecretValidUntil in the answer says when the old one stops. An inbox's webhook works the same way through POST /email/inboxes/:id/rotate-secret.

Inbound webhooks (for your app)

Stripe, GitHub, Clerk, Slack and any Svix-style sender can post to your app through Camplax. Camplax checks the sender's signature, stores the event, answers the sender straight away, and forwards the event to a path on your app — retrying if your app is down or mid-deploy.

  1. On Keys → Inbound, add an endpoint: the sender, a name, and the path to forward to (/webhooks/stripe). You get a URL:

``text https://camplax.dev/v1/webhooks/in/<slug>/ibh_… ``

  1. Paste that URL into the sender (Stripe → Developers → Webhooks, a GitHub repo's Webhooks, Clerk → Webhooks, your Slack app's Event Subscriptions).
  2. Paste the sender's signing secret back into the endpoint. This is the one secret in the whole path, and it comes from them, not from Camplax. It is stored encrypted and is write-only — after saving you only ever see its last four characters. Until it is set, the endpoint refuses every request.
SenderChecksDedupes on
StripeStripe-Signature (HMAC of t.body, 5-minute window)event id
GitHubX-Hub-Signature-256a hash of the signed body (GitHub signs no timestamp or delivery id)
ClerkSvix headers (svix-id / -timestamp / -signature)svix-id
SlackX-Slack-Signature (v0:ts:body, 5-minute window); answers url_verification itselfevent_id, or the signed bytes
Svix / Standard Webhookswebhook-id / -timestamp / -signature (or the svix-* names)webhook-id

A sender's retry of an event already received is acknowledged and not forwarded twice. Bodies over 256 KB get 413; a bad or missing signature gets 401; an unknown, paused or switched-off endpoint gets 404. Each project can take 10,000 deliveries and 64 MB of payloads per rolling 24 hours; past that the receiver answers 429 and the sender retries later.

What your app receives. A POST to your path with Camplax's own signature (Camplax-Purpose: inbound-webhook) — your app holds no secret:

import { readRawBody, sendFail, sendOk, verifyCamplaxWebhook } from "@camplax/plax";

"POST /webhooks/stripe": async (req, res) => {
  const raw = await readRawBody(req);
  const verified = await verifyCamplaxWebhook(req, raw, { purpose: "inbound-webhook" });
  if (!verified.ok) return sendFail(res, verified);
  const delivery = JSON.parse(raw.toString()); // { id, attempt, endpoint, event: { id, type }, headers, body }
  const event = JSON.parse(delivery.body);     // the sender's payload, byte for byte
  sendOk(res, { ok: true });
},

Dedupe on delivery.id (the same across Camplax's retries) or delivery.event.id. A 2xx in 15 seconds is delivered; anything else retries after 30s, 2m, 8m, 32m, then every 2h — eight attempts, about eight hours — and is then kept as dead. Ten dead deliveries in a row pause forwarding and email the owners. The endpoint keeps receiving (a sender will not resend what it was told arrived); everything it takes is held, and Resume forwarding (PATCH … { "enabled": true }) resets the count and sends what was held. Switching an endpoint off yourself is different: it stops receiving, and senders get 404. Replay in the console (POST /v1/projects/<slug>/inbound-webhooks/deliveries/:id/replay) re-sends a delivery as a new one with replayOf set. Delivered events are kept (and replayable) for 3 days, dead and held ones for 14; the record that an event already arrived is kept 30 days, so a late redelivery is still recognised as a duplicate.

The endpoints API (GET/POST /v1/projects/<slug>/inbound-webhooks, PATCH/DELETE /:id, GET /:id/deliveries) takes a console session, a CLI token or a manage key. Adding an endpoint turns on the Webhooks inbox card in Integrations; switching that card off makes every endpoint answer 404.

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/ses (Amazon SES email delivery events, signed by SNS — see Email). Each verifies its provider's signature; the signature is the auth.

Camplax

Hosting, database, sign-in and storage in one project. One login, one bill.

Platform
More
Legal
Company
© 2026 Camplax From $25 / month · credits stop at zero