Webhooks
Send every new response to your own server as a signed JSON request, manage endpoints in the app or through the API, and understand retries and the delivery log.
What a webhook is
A webhook sends each new response to a URL you control, as soon as it is submitted. It suits pushing answers into a CRM, a chat channel or a database without polling the API.
Set one up in the app
On the form's Settings tab, the Integrations card has Manage webhooks. There:
- Under Add an endpoint, enter the address, for example
https://example.com/hooks/formwork, and choose Add endpoint. - Copy the signing secret that appears. It is shown once. It starts with
whsec_. - Use Send test event to check that your server answers.
Each endpoint can be paused and resumed, its address changed, its secret replaced with New secret (the old one stops working at once), or removed together with its log.
Rules for the address:
- It must be
https://in production. - Hosts that resolve to private, loopback or link-local addresses are refused, and so are ports other than 80, 443, 8080 and 8443, and addresses with a username or password.
- Up to 500 characters.
A form can have up to 10 endpoints. Your plan also sets how many endpoints the whole workspace can have; see Default plan limits.
Manage them through the API
GET /forms/{id}/webhooks lists endpoints. The secret is never returned again.
curl -s https://formwork.movortech.com/api/v1/forms/hng8zzkl1lpd/webhooks -H "Authorization: Bearer $FORMWORK_KEY"
POST /forms/{id}/webhooks (write) with {"url":"https://example.com/hook"} creates one. The 201 response contains the signing secret once:
curl -s -X POST https://formwork.movortech.com/api/v1/forms/hng8zzkl1lpd/webhooks \
-H "Authorization: Bearer $FORMWORK_KEY" -H "Content-Type: application/json" \
-d '{"url":"https://example.com/hooks/formwork"}'
{ "data": { "id": 7, "url": "https://example.com/hooks/formwork", "active": true, "created_at": "2026-09-30T04:00:00+00:00", "secret": "whsec_4ad0521445f8b9b9eb48eb9652aa8e887dfe598af87cabcb" } }
DELETE /forms/{id}/webhooks/{webhook_id} (write) removes one. Bad addresses are refused with 422. See API keys, requests and errors for the error format.
The delivery
Each new response is sent to every active endpoint as a POST with a JSON body:
{
"event": "response.submitted",
"form": { "id": "hng8zzkl1lpd", "title": "Customer feedback" },
"response": {
"id": 941,
"submitted_at": "2026-09-30T03:36:40+00:00",
"answers": [
{ "question_id": "q_4u31tx3z", "title": "What should we improve?", "type": "long_text", "value": "Dark mode, please.", "text": "Dark mode, please." }
],
"score": null,
"max_score": null,
"meta": { "device": "desktop" }
}
}
The answers use the same shape as the REST API; see Forms and responses endpoints.
The Send test event button sends "event": "test.ping" with one example answer and "id": 0.
Headers:
| Header | Value |
|---|---|
X-Formwork-Event | response.submitted or test.ping |
X-Formwork-Delivery | Delivery id. The same response replayed later gets a new id |
X-Formwork-Timestamp | Unix seconds when this attempt was signed |
X-Formwork-Signature | sha256= and the hex HMAC-SHA256 of timestamp + "." + raw body, keyed with the endpoint secret |
Verify the signature before you trust a request; the next article shows how. See Verifying webhook signatures.
Retries and failure
Answer with any 2xx within 10 seconds. Formwork does not follow redirects, and a 3xx counts as a failure.
Failures are retried after 1 minute, 5 minutes, 30 minutes, 2 hours and 12 hours, which is six attempts in all. After the last one the delivery is marked failed.
Deliveries can arrive more than once and out of order, so use X-Formwork-Delivery or response.id to deduplicate.
The delivery log
The Delivery log on the webhook page shows the latest 50 deliveries with their status. Open one to see the payload that was sent, the HTTP status your server returned and the first 300 characters of its answer. Replay queues a copy of any delivery, and it appears in the log as a new one. Entries older than 30 days that are no longer pending are deleted.
A paused endpoint receives nothing new. Deliveries that were already waiting when you paused are marked failed with the note "Endpoint is paused. Resume it and replay this delivery."
Tips
- Answer quickly and do the real work afterwards. Store the payload, return
200, then process it. - Keep the secret in an environment variable or secrets manager, not in source control.
- Use Send test event after any change to your endpoint.
- Only responses submitted after the endpoint was added are sent. To backfill older ones, use the API's
sinceparameter.
Updated Sep 30, 2026