Skip to content

Form Webhooks

When a form is configured with a webhook URL (Form Settings → Webhook), every successful submission is delivered to that URL as a JSON POST from RocketLead’s backend. Use webhooks to land submissions in your CRM, kick off workflows, or trigger conversion tracking server-side.

A webhook delivery is queued in the same atomic transaction as the submission. After the transaction commits, a worker picks it up and delivers. If queue dispatch fails, a sweeper retries — the delivery is durable.

The event is always form.submission for now. Future event types are reserved.

  • HTTP POST to the configured webhookUrl.
  • Content-Type: application/json.
  • 10-second receiver timeout (AbortController).
{
"event": "form.submission",
"formId": "<uuid>",
"formName": "Probetraining Berlin",
"submittedAt": "2026-04-22T12:00:00Z",
"data": {
"field-loc-abc": "3a1b2c4d-5e6f-4a8b-9c0d-1e2f3a4b5c6d",
"field-apt-xyz": "7d9e8f2c-4b1a-4c3d-8e5f-6a7b8c9d0e1f",
"field-email": "user@example.com"
},
"tableEntryId": "entry_...",
"calendarBookingId": "booking_...",
"fieldContext": {
"field-loc-abc": {
"label": "Standort",
"type": "location",
"name": "standort",
"resolved": { "label": "Berlin Mitte" }
},
"field-apt-xyz": {
"label": "Kursinteresse",
"type": "appointment-type",
"name": "kursinteresse",
"resolved": { "label": "Anfängerkurs" }
},
"field-email": {
"label": "E-Mail",
"type": "email",
"name": "email",
"specialType": "email"
}
},
"resolvedData": {
"standort": "Berlin Mitte",
"kursinteresse": "Anfängerkurs",
"email": "user@example.com"
}
}
FieldDescription
eventAlways "form.submission".
formIdUUID of the form.
formNameEditor-set form name at delivery time.
submittedAtISO-8601 submission timestamp. Frozen at submission.
dataRaw submitted values, keyed by field UUID. Bit-identical to the persisted submission — UUIDs preserved as-is for GDPR/audit.
tableEntryIdUUID of the created Lead-Pool entry. null for webhook-only forms.
calendarBookingIdUUID of the created booking. null if the form has no booking-calendar field or none was picked.
fieldContextPer-field metadata keyed by field UUID. See Field Context.
resolvedDataConvenience flat projection keyed by stable field name. See Resolved Data.

fieldContext contains an entry for every field in the published config — including showIf-hidden fields and unanswered fields. Object.keys(fieldContext) may therefore be a superset of Object.keys(data). Entries without a submitted value omit the resolved block.

KeyDescription
labelUser-set form label, e.g. “Kursinteresse”.
typeThe field type enum (text, email, tel, select, checkbox, location, appointment-type, booking-calendar, captcha, privacy). See Field Types.
nameStable field name, when set. Optional.
specialTypeDestination-column key (email, firstName, etc.) when the field is mapped to a Lead-Pool column.
resolvedResolved human label for UUID-bearing fields. Omitted for primitives and for UUIDs that couldn’t be resolved (e.g., admin deleted the referenced entity post-submission).

For location and appointment-type fields, resolved is { label: "..." }. For booking-calendar fields, resolved is the structured slot context:

"resolved": {
"appointmentTypeLabel": "Anfängerkurs",
"calendarLabel": "Berlin Kalender",
"resourceLabel": "Trainer A",
"startDateTime": "2026-05-01T10:00:00Z",
"endDateTime": "2026-05-01T10:30:00Z"
}

resolvedData is a flat object keyed by the stable field name (when set) or the field UUID (fallback), with the human-facing value pre-substituted:

  • Primitive fields → the raw submitted value (same as data[fieldId]).
  • location / appointment-type → the resolved label string. Falls back to the raw UUID when resolution failed.
  • booking-calendar → the structured resolved block (same shape as fieldContext[...].resolved).

Fields with no submitted value AND no resolved block are omitted — showIf-hidden siblings drop out naturally.

FieldFrozen at submissionRebuilt every delivery
data
submittedAt
tableEntryId / calendarBookingId
formName
fieldContext (labels, resolved blocks)
resolvedData

Failed deliveries (any non-2xx response or fetch error) are retried with exponential backoff:

AttemptBackoff
1(immediate)
230s
360s
4120s
5240s (capped at 300s)

After 5 attempts the delivery is marked failed and won’t retry without admin intervention from the form’s Deliveries view. Response bodies are captured (truncated to 4096 chars) for debugging.

A signed-webhook upgrade is on the roadmap.

app.post('/webhook', (req, res) => {
const { data, resolvedData, fieldContext, submissionId } = req.body;
// Idempotency — bail if we've already seen this submission
if (await alreadyProcessed(submissionId)) {
return res.status(200).end();
}
// Fast path: read pre-resolved labels keyed by stable name
console.log(`Email: ${resolvedData.email}`);
console.log(`Course interest: ${resolvedData.kursinteresse}`);
// Deep inspection: walk fieldContext + data for every field
for (const [fieldId, value] of Object.entries(data)) {
const ctx = fieldContext[fieldId];
console.log(`[${ctx.label}] (${ctx.type}) = ${value}`);
}
res.status(200).end();
});