Webhooks
Webhooks let an external system be notified the moment something happens in Stack9 Experience, instead of polling an API for changes. You register a webhook in the back office — a name, a target URL, the event to listen for, and any custom HTTP headers — and the platform POSTs a JSON payload to that URL when the event fires.
The only webhook event available today is form.submission, registered against one form at a time. Everything in the Roadmap section is specified but not yet shipped. Build against what is described in the sections above it.
What webhooks are for
The canonical case is lead capture. A visitor submits an enquiry form on a website; a CRM, a lead-routing service, or a dealership mailbox handler needs to know immediately. Without webhooks, that integration polls the submissions API on a timer and inherits every problem polling brings — latency, wasted calls, and duplicate-detection logic.
With a webhook:
- ✅ Push, not poll — the receiver is called as the submission lands.
- ✅ Per-target authentication — custom headers carry the receiver's own API key or bearer token.
- ✅ Asynchronous and retried — delivery runs through a queue, so a slow or briefly unavailable receiver does not affect the visitor submitting the form.
- ✅ Testable before go-live — a built-in test call exercises the real URL and headers and reports exactly what came back.
Not to be confused with
Three different mechanisms in the platform are called "webhooks". They are unrelated, and mixing them up is the most common integration mistake.
| Mechanism | Direction | What it is |
|---|---|---|
| DXP webhooks (this page) | Outbound | Registered against a platform event — today, a form submission — and delivered to your URL. |
| Email event webhooks | Outbound | Configured under Email Settings and backed by the email delivery provider. Reports delivery, bounce, open, and click events with a different schema and a different management surface. See Email & Communications. |
| Stack9 Core automation webhook trigger | Inbound | An HTTP endpoint the platform exposes so an external system can trigger an automation. The opposite direction, documented in the Stack9 Core guides. |
subscriber.added and email.received are not DXP webhook eventsYou may see these named in older product summaries. They do not exist in the DXP webhook event list. Subscriber and email-delivery events are served by the email event webhooks under Email Settings, which is a separate mechanism. Do not build a /webhooks registration expecting them.
Supported events
| Event | Fires when | Extra configuration |
|---|---|---|
form.submission | A visitor submits the form identified by form_code. | form_code — required, and immutable after creation. |
The event key is a namespaced string, and the field is designed to accept further values over time. Today the list has exactly one entry.
The registration model
A webhook registration carries:
| Field | Notes |
|---|---|
name | Human label, for example "CRM notification". Required. |
event | The event type plus its form_code. Immutable after creation — to point a webhook at a different form, create a new one. |
target.url | Destination URL. Must be http or https; use https. Required. |
target.headers | Custom HTTP headers sent on every delivery. Each header has a name, a value, and an is_secret flag. |
active | Defaults to true. An inactive webhook is retained but does not deliver. |
test_data | A sample payload used by the test call. The interface seeds a sensible default for form submissions. |
last_attempt_at | Read-only. Timestamp of the most recent delivery attempt. |
last_attempt_status | Read-only. success or failed for the most recent attempt — per webhook, not per submission. |
Secret headers. Marking a header as secret means its value is encrypted at rest and omitted from API responses; the response returns the header's name and its secret flag, but never the value. To rotate a secret, submit a new value on update; to keep the current one, omit the value entirely.
Fan-out is allowed. Several webhooks may target the same form, and several may point at the same URL. Each is delivered independently.
Delivery semantics
Delivery is asynchronous and queue-based. A submission is recorded first and delivery is handed to a queue, so a slow receiver never slows down the visitor.
- Fan-out — one submission produces one delivery per applicable webhook, each with its own attempt record.
- Retries — a delivery carries a retry counter bounded at five, so a failing delivery is retried up to five times.
- Observability today — the webhook record's
last_attempt_atandlast_attempt_statusreflect the most recent attempt only. There is no per-attempt history and no stored response body for live deliveries yet.
The retry ceiling of five is confirmed. The backoff schedule, which HTTP statuses and timeouts are treated as retryable, and the delivery timeout value are not specified in the public contract. Design your receiver to be tolerant: respond quickly, acknowledge with a 2xx before doing slow work, and do not rely on a specific retry cadence.
Payload envelope
The delivered body wraps the submission in a data envelope. The submission object is identical to what the submissions API returns for the same record.
{
"data": {
"version": 1,
"created_at": "2026-01-01T00:00:00.123Z",
"updated_at": null,
"id": "01HZX9K7QK8M4V2S6T3N5Y7B1C",
"form_code": "abc12",
"email_address": "visitor@example.com",
"submission_date": "2026-01-01T00:00:00.000Z",
"form_data": {
"first_name": "Alex",
"enquiry_type": "Test drive",
"message": "Interested in the hybrid model."
},
"events": [
{
"type": "created",
"message": "Submission received",
"isError": false,
"timestamp": "2026-01-01T00:00:00.200Z"
}
]
}
}
Notes for integrators:
form_codeidentifies which form produced the submission, so one receiver can route by form even though each webhook is bound to a single form today.form_datais free-form: its keys are whatever the form's schema defines. Treat it as an open map and tolerate added fields.eventsis an optional lifecycle log on the submission.- The delivery body is JSON, plus whatever custom headers the registration defines.
business_unit in the payload yetThe originating Business Unit is not present in the delivered payload today. Carrying it is part of the roadmap below. If you need to route by brand or dealership right now, map from form_code on your side.
Testing a webhook
A test call sends the webhook's stored test_data to its configured URL, with its real custom headers, without waiting for a live submission. The response tells you precisely what happened:
| Field | Meaning |
|---|---|
success | Whether the call completed successfully. |
status_code | The HTTP status your endpoint returned. |
response_body | The body your endpoint returned. |
duration_ms | Round-trip time. |
error | Present when the call could not be completed. |
This is the fastest way to diagnose an integration: a wrong path, a missing API key, or a TLS problem shows up as a concrete status code and body rather than a silent non-delivery.
Security
The webhook security model is deliberately simple today, and its limits should be understood before you go live.
What you get:
- Custom header authentication. You configure the header your receiver requires — typically
Authorization: Bearer …orX-Api-Key: …— and mark it secret so it is encrypted at rest and never returned by the API. - Secret rotation. Update with a new value to rotate; omit the value to keep the existing secret.
- HTTPS transport. Target URLs must be
httporhttps; receivers should requirehttps.
The platform does not sign webhook payloads. There is no signature header, no shared signing secret, and no timestamp or nonce header, so a receiver cannot cryptographically verify that a payload came from Stack9.
Compensate with all of the following:
- Require a secret header on every request and reject anything without it.
- Restrict the receiving endpoint to Stack9 source addresses where your infrastructure allows it.
- Serve the receiver over HTTPS only, and treat the URL itself as sensitive.
- Be idempotent. Deduplicate on
data.id. Duplicate deliveries are expected: retries can re-deliver, and two webhooks pointing at the same URL both fire. There is no replay protection and no delivery-id header to key on.
API surface
| Method | Path | Purpose |
|---|---|---|
POST | /webhooks | Register a webhook. |
GET | /webhooks | List webhooks, optionally filtered by form_code. |
GET | /webhooks/{id} | Retrieve one webhook. Secret header values are omitted. |
PUT | /webhooks/{id} | Update name, active state, target, or test data. The event cannot be changed. |
DELETE | /webhooks/{id} | Delete a webhook. |
POST | /webhooks/{id}/test | Send the stored test payload to the target URL and report the result. |
API Reference: full request and response schemas for these endpoints are documented in the Webhooks API.
API Reference: submissions delivered by webhooks are the same records served by /form_submissions, /form_submissions/{id}, /form_submissions/count, and /form_submissions/list — useful as a reconciliation path if a receiver was down.
Roadmap
The following are drafted specifications, not current behaviour. They are listed so you can design integrations that will not need rewriting — not so you can build against them today. Confirm release status with your Stack9 delivery team before depending on any of it.
All-forms binding. A webhook will be able to apply to all forms in the tenant, including forms created after the webhook was registered, rather than being bound to one form_code. Today, a new form needs a new webhook registration.
Business Unit in the payload. Every delivered submission will carry its originating Business Unit, so one shared endpoint can route submissions from many sites, brands, and dealerships. See Business Units.
Per-submission delivery status and history. Each submission will carry a delivery status with exactly three values — Delivered when every hand-off succeeded, Failed when at least one attempted hand-off failed, and Processing when nothing has failed and at least one hand-off is still outstanding — with precedence Failed over Processing over Delivered when a submission mixes outcomes. Alongside it, a webhook history per submission will record one line per hand-off with the destination, the attempt time, and the response body the receiver returned. Capturing response bodies on live deliveries is new work; only the test call retains a response body today. Submissions on forms with no webhook will appear with an empty history and a Processing status, never Failed, and submissions predating the feature will not be back-filled.
Best practices
- Acknowledge fast, process later. Return 2xx as soon as you have durably accepted the payload, then do downstream work asynchronously. Slow receivers accumulate retries.
- Deduplicate on
data.id. Treat every delivery as at-least-once. - Tolerate schema growth.
form_datakeys change whenever the form changes; parse defensively and never fail on an unexpected field. - Use the test call in every environment before pointing production traffic at a receiver.
- One receiver, many forms. Route on
form_codeinside your handler rather than building a bespoke endpoint per form — that also positions you for all-forms binding. - Keep a reconciliation path. Poll
/form_submissionson a slow schedule, or after an incident, to catch anything a prolonged receiver outage lost. - Register a secret header on every webhook. It is the only authentication the receiver can rely on today.
Real-world example
A dealer group wires enquiry forms into its CRM:
- One receiver per environment, authenticated with a secret
X-Api-Keyheader, validated with the test call before launch. - A webhook per enquiry form, each active and independently monitored via its last-attempt status.
- Routing on
form_codeinside the receiver, mapping each form to a dealership queue. - Idempotent ingest keyed on the submission
id, so retries and double registrations cannot create duplicate leads. - A nightly reconciliation job against the submissions API, comparing counts to catch silent gaps.
Next steps
- Content Management — the form builder that produces the submissions webhooks deliver
- Customer Data Platform — how submissions become subscribers
- Email & Communications — email event webhooks, a separate mechanism
- Business Units — the scoping model referenced by the webhook roadmap
Webhooks turn form capture into a push integration: registered per form, delivered through a queue with retries, authenticated with your own headers, and verifiable with a built-in test call.