Webhooks
Today Canverly delivers one event by webhook: lead.created, fired on
every accepted submission of a form. Webhooks are configured
in the site’s admin, in the Forms app area.
Setting up
Section titled “Setting up”- Enter the destination URL. It must be
https, without a user and password in the URL and without a service port. The URL is validated on save and again on every delivery. - Choose the scope. One specific form, or every form of the site.
- Store the secret. The signing secret is generated by the platform and shown once, at creation. To change it, delete the webhook and create a new one.
If your endpoint requires its own authentication, you can register one
header (name and value). The value is stored encrypted and is never returned
afterwards. Names with the x-canverly- prefix are refused, because that
prefix carries the signature.
What arrives
Section titled “What arrives”A POST with a JSON body and these headers:
| Header | Content |
|---|---|
x-canverly-signature |
t=<unix>,v1=<hmac>: the signature (see below). |
x-canverly-timestamp |
The same instant, in clear, for diagnostics only. |
x-canverly-event |
The event, today always lead.created. |
x-canverly-delivery |
Delivery id, for deduplication. |
Without a mapping, the body is the event with the fields event, site_id,
form_id, form_slug, lead_id, created_at (RFC 3339), data (the filled
fields) and meta (source page, UTM and technical data about the submission).
With a mapping, the body has the shape the owner designed in the admin, and the
admin preview shows the exact JSON that will be sent.
Verifying the signature
Section titled “Verifying the signature”The signature is an HMAC-SHA256, hex-encoded (64 characters), computed with
the secret over the text <t>.<body>: the value of t, a dot and the raw
body, byte for byte, as received.
import crypto from 'node:crypto';
function signatureIsValid(header, rawBody, secret, toleranceSec = 300) { const parts = Object.fromEntries(header.split(',').map((p) => p.split('='))); const t = Number(parts.t); if (!Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false; const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex'); const received = String(parts.v1 ?? ''); return received.length === expected.length && crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));}The v1= prefix exists for a future algorithm change: during the transition,
receivers will be able to accept both versions.
Delivery and retries
Section titled “Delivery and retries”- Delivery is at least once: a lost response makes the same lead arrive
again. Use
x-canverly-deliveryto drop repeats. - If the endpoint fails, the platform retries with growing waits (30 s, 1 min, 2 min, 4 min…, capped at 6 hours between attempts), up to 10 attempts, about 8.5 hours in total.
- After the last one, the delivery is marked as failed and stays visible in the admin.