Forms
Webhooks
Receive each submission at your own endpoint, signed and verifiable.
What is a webhook?
A webhook is an automatic message Devlune sends to a URL you choose the moment something happens. Instead of you polling our API to ask “any new submissions?”, we push the data to you in real time. Think of it as a phone call your server receives whenever a form is submitted.
How it works
- You add one or more HTTPS endpoint URLs to a form (Forms → select a form → Webhooks).
- On every new submission, we send an HTTP POST with a JSON body to each active webhook.
- Each request carries an X-Devlune-Signature header so you can prove it really came from us.
- Your server responds 2xx to acknowledge. We don't wait on slow endpoints, so keep handlers fast.
Event types
Each delivery's `type` field tells you what happened. Today there is one event; the envelope (type / form_id / data / ts) is stable, so new event types can ship without breaking your handler. Always switch on `type` and ignore types you don't recognize.
- form.submission — fired once for every new, stored form submission (after spam filtering, CAPTCHA and rate limits pass). `data` carries the submission: id, payload, geo.
Planned events
form.spam_blocked, counter.milestone and analytics.spike are on the roadmap — they'll use the same envelope and signature. Build your handler to ignore unknown types and you won't need changes when they arrive.
The payload
Every webhook receives a JSON body shaped like this:
{
"type": "form.submission",
"form_id": "…",
"data": { "id": "…", "payload": { "email": "a@b.com" }, "geo": { } },
"ts": 1733760000000
}Field reference
- type (string) — event name. Currently always "form.submission"; more event types may be added, so switch on it.
- form_id (string, UUID) — the form that was submitted. Matches the ID in your dashboard embed code.
- data.id (string, UUID) — unique submission ID. Use it for idempotency: deliveries can repeat, dedupe on this.
- data.payload (object) — the visitor's answers, keyed by your field keys (e.g. { name, email, message }). File-upload fields arrive as { __upload, name, size, type } references.
- data.geo (object) — approximate location: city, region, country (ISO-2), country_name, postal, timezone, lat, lon. Fields may be empty strings when unresolved.
- ts (number) — delivery timestamp in Unix milliseconds. Reject anything older than ~5 minutes to harden against replays.
Request headers
- Content-Type: application/json
- X-Devlune-Signature: sha256=<hex> — HMAC-SHA256 of the raw body, keyed with your webhook secret.
Delivery behavior
- Fired once per active webhook per submission, in parallel, fire-and-forget from the edge.
- There is no automatic retry today — if your endpoint is down the delivery is skipped (the submission itself is always stored and visible in the dashboard).
- Paused webhooks are skipped; deleted ones stop immediately.
- Respond 2xx quickly (do heavy work async) — we don't wait on slow endpoints.
What you can build with them
- Push new leads straight into your CRM, Notion, Airtable, or Google Sheets.
- Post an alert into Discord or Telegram when someone fills a form.
- Trigger an automation in Zapier, Make, or n8n via their “Webhook / Catch Hook” trigger.
- Kick off your own workflow — send an SMS, create a ticket, start an onboarding email.
Verifying the signature
Each request includes an X-Devlune-Signature header of the form 'sha256=<hex>'. Compute HMAC-SHA256 over the raw request body using your webhook secret (shown when you create the webhook) and compare. This proves the request is authentic and untampered.
import crypto from "crypto";
function verify(rawBody, signature, secret) {
const expected = "sha256=" + crypto
.createHmac("sha256", secret)
.update(rawBody)
.digest("hex");
// use a constant-time compare in production
return expected === signature;
}Always verify
Treat the webhook secret like a password and always verify the signature before trusting a payload. Webhook URLs must use HTTPS so payloads are encrypted in transit. You can pause or delete a webhook anytime from the form's Webhooks panel.
