Skip to main content

Webhooks API

Overview

The Webhooks API lets you register an outbound HTTP subscription that Stack9 calls whenever a specific event happens, so an external system (a CRM, a lead router, a mailbox handler) is notified in real time instead of having to poll the Stack9 API.

Shipped scope

Today there is exactly one webhook event: form.submission, bound to a single form via form_code. A webhook cannot yet be applied to "all forms," and the delivered payload does not yet carry a business_unit field. See Roadmap below.

Each webhook has a name, a target URL, the event it listens for, and optional custom HTTP headers (useful for authenticating your endpoint, e.g. an API key or bearer token). When a website visitor submits the form the webhook is registered against, Stack9 POSTs a JSON payload containing the submission to the webhook's target URL. Every webhook registered for that form is called — a form with multiple webhooks fans out to multiple deliveries.

Screenshot of the webhook registration screen in the Stack9 back office, showing the Create Webhook drawer with Name, Event (form.submission + form picker), Target URL, custom Headers list with an is_secret toggle, and Active switch

Authentication

All endpoints require API key authentication:

X-API-Key: your-api-key-here

The webhook object

FieldTypeNotes
idstringServer-generated.
namestring (1–200 chars)Required.
eventobject { event_type, form_code }event_type is currently always "form.submission". Immutable after creation — update requests cannot change it.
target.urlstring (URI, http/https)Required. Destination for deliveries.
target.headers[]array of { name, value, is_secret }Custom headers sent with every delivery. value max 500 chars. Defaults to [].
activebooleanDefault true. Set false to stop deliveries without deleting the webhook.
test_dataobjectFree-form JSON body used by the test endpoint.
last_attempt_atdate-timeRead-only. Timestamp of the most recent delivery attempt (any recipient).
last_attempt_status"success" | "failed"Read-only. Outcome of the most recent attempt only — this is a per-webhook status, not a per-submission history.

Secret headers

Set is_secret: true on a header to have its value encrypted at rest. Responses from GET/LIST never return the value of a secret header — only name and is_secret come back. To rotate a secret header on update, send a new value; omit value to keep the existing secret unchanged.

No signature scheme

There is no HMAC / payload-signature mechanism for these webhooks. The only way a receiver can authenticate a delivery is a secret custom header you configure yourself (e.g. Authorization: Bearer … or X-Api-Key: …). Require HTTPS and treat the header value as the sole credential — there is nothing else to verify the call came from Stack9. See Security considerations.

Create a webhook

POST /api/webhooks

Request body

{
"event": { "event_type": "form.submission", "form_code": "form_Xk9mNp2Q" },
"name": "CRM notification",
"target": {
"url": "https://example.com/hooks/stack9",
"headers": [
{ "name": "X-Api-Key", "value": "secret-123", "is_secret": true }
]
},
"active": true,
"test_data": { "email_address": "example@april9.com.au", "question1": ["Item 1"] }
}

Example response

{ "id": "wh_9kL3mP5n" }

List webhooks

GET /api/webhooks

Optional query parameter form_code filters to webhooks registered for one form.

Example response

{
"results": [
{
"version": 1,
"created_at": "2026-01-01T00:00:00.123Z",
"id": "wh_9kL3mP5n",
"name": "CRM notification",
"event": { "event_type": "form.submission", "form_code": "form_Xk9mNp2Q" },
"active": true,
"last_attempt_at": "2026-02-14T09:12:00.000Z",
"last_attempt_status": "success",
"target": {
"url": "https://example.com/hooks/stack9",
"headers": [
{ "name": "X-Api-Key", "is_secret": true }
]
}
}
]
}

Get a webhook by ID

GET /api/webhooks/{id}

Returns a single webhook using the same shape as the list items above.

Update a webhook

PUT /api/webhooks/{id}

event cannot be included — the event type and form_code are fixed at creation.

Request body

{
"name": "CRM notification (prod)",
"active": true,
"target": {
"url": "https://example.com/hooks/stack9",
"headers": [
{ "name": "X-Api-Key", "is_secret": true }
]
}
}

Omit a secret header's value to keep the current secret; include a new value to rotate it.

Example response

{ "id": "wh_9kL3mP5n" }

Delete a webhook

DELETE /api/webhooks/{id}

Example response

{ "id": "wh_9kL3mP5n" }

Send a test payload

POST /api/webhooks/{id}/test

Sends the webhook's stored test_data to its configured target.url (with its configured headers) and reports what happened — useful for validating a receiver before it goes live, without waiting for a real form submission.

Example response

{
"success": true,
"status_code": 200,
"response_body": "OK",
"duration_ms": 184
}

If the request could not complete, success is false and an error string is returned instead of status_code/response_body.

Delivery payload

When a visitor submits the form a webhook is registered against, Stack9 POSTs this envelope to target.url:

{
"data": {
"version": 1,
"created_at": "2026-01-01T00:00:00.123Z",
"updated_at": null,
"id": "sub_9kL3mP5n",
"form_code": "form_Xk9mNp2Q",
"email_address": "visitor@example.com",
"submission_date": "2026-01-01T00:00:00.000Z",
"form_data": { "firstName": "John", "interests": "Product Updates" },
"events": [
{ "type": "string", "message": "string", "isError": false, "timestamp": "2026-01-01T00:00:00.100Z" }
]
}
}

data is the same object returned by GET /api/form_submissions/{id} (see Forms API). The delivered content type is JSON. Custom headers configured on target.headers[] are sent with the request.

Not in the payload today

business_unit is not present on the delivered submission. If you need to route deliveries by Business Unit, register a separate webhook per form for now.

Delivery semantics

  • Fan-out: a submission is delivered once per webhook registered for that form. A form with three webhooks produces three separate delivery attempts.
  • Asynchronous, queue-based: delivery happens after the submission is accepted, not inline with the visitor's request.
  • Retries: failed deliveries are retried, up to a ceiling of 5 attempts. The retry backoff schedule and which failures are considered retryable are not part of the public contract — treat retries as best-effort.
  • Duplicate deliveries are possible by design (e.g. two webhooks pointed at the same URL, or a retry after an ambiguous failure). Your receiver should be idempotent on data.id.
  • Status visibility: the webhook's last_attempt_at / last_attempt_status reflect only the most recent delivery attempt across all submissions for that webhook — there is no per-submission delivery history in the API today.

Security considerations

  • Authenticate deliveries with a secret custom header (is_secret: true) — there is no signature to verify instead.
  • Always use HTTPS target URLs in production.
  • De-duplicate on data.id — retries and multi-webhook fan-out can both cause the same submission to arrive more than once.
  • Secret header values are never returned by the API after creation; rotate by sending a new value on update.

Roadmap

Roadmap — not yet shipped

The following are described in draft internal specs but are not available today. Do not build against them until they ship:

  • Apply a webhook to all forms (including forms created later), instead of a single form_code.
  • business_unit in the delivered payload, so one shared endpoint can route submissions from many forms/sites.
  • Per-submission delivery status (Delivered / Failed / Processing) and a webhook delivery history with captured response bodies, viewable per form submission.
  • Forms API — create the form a webhook listens to, and inspect submissions directly via /form_submissions.
  • Email Settings API — a separate, SparkPost-backed email-event "webhooks" concept for delivery/bounce/open/click notifications; unrelated to the form.submission webhooks on this page.
  • Entity Versioning & Audit APIwebhook is a supported entity type, so you can retrieve version history or recover a deleted webhook registration.