Webhooks
Send every response to your own endpoint as JSON.
A webhook is the most direct way to get responses out of Questionful: you give a form a URL, and every submission is POSTed to it as JSON.
This is the same mechanism the Zapier and Make integrations use. Those are webhooks with a URL you copy from their side.
Setting one up
There is no card labelled “Webhook”. The Zapier and Make cards are generic JSON POSTs to whatever URL you give them, so either one is how you point a form at your own endpoint:
- Open the form and go to its integrations.
- Choose Zapier or Make. It makes no difference which; the payload is the same.
- Paste the URL of your endpoint.
- Save. The next response is delivered to it.
The URL has to be a publicly reachable http(s) address. Addresses inside a private network (localhost, 10.x, 192.168.x, link-local) are refused, because our server is the one making the call.
What arrives
A JSON body shaped like this:
{
"event": "form.response.created",
"form": { "uuid": "8f14…", "title": "Demo Request" },
"response": { "uuid": "b21c…", "submitted_at": "2026-08-18T09:24:11+00:00" },
"answers": [
{ "question": "Work email", "name": "q1a2…", "value": "alex@example.com" },
{ "question": "Monthly budget", "name": "q7f3…", "value": "10k – 50k" }
],
"data": { "q1a2…": "alex@example.com", "q7f3…": "10k – 50k" }
}
answers is the readable form: each entry carries the question title as shown to the respondent, the internal field name, and the value. data is the same answers as a flat name-to-value map, which is easier to address from code that already knows the field names. Both are always present, so you can use whichever suits the receiver.
The request also carries X-Questionful-Event: form.response.created, so you can route on the header without parsing the body.
Where a response includes uploaded files, the payload carries signed download links rather than the file contents.
Verifying the delivery
The integration card has an optional signing secret. Set one and every delivery carries:
X-Questionful-Signature: sha256=<hex HMAC-SHA256 of the raw request body>
Compute the same HMAC over the exact bytes you received, using your secret, and compare it in constant time. A body that does not match did not come from us. Without a secret configured, no signature header is sent.
Redirects are not followed, and the destination host is re-resolved and re-checked at the moment the connection is opened, so a URL cannot be rebound to an internal address after you save it.
Delivery behaviour
The call is made while the response is being submitted, not from a queue. It times out after 15 seconds and is attempted twice; after that the failure is recorded and the response is saved anyway. There is no later retry, so a receiver that was down when the response arrived has missed it. The delivery is visible under Recent Activity on the form’s Integrations tab, which keeps the 20 most recent attempts.
Building the receiving end
- Respond quickly with a 2xx. Do the real work asynchronously.
- Expect the same response to arrive more than once and make your handler idempotent.
- Treat the body as untrusted input. It contains whatever your respondents typed.
- Set a signing secret and verify the signature. Without one, the URL is the only thing standing between your endpoint and anyone who finds it.
- Do not put the webhook URL somewhere public.
Testing
Point the form at a request-inspection service (RequestBin, webhook.site, or an ngrok tunnel to your machine) and submit the form yourself. You will see the exact payload before you write any code against it.