Integrations
Webhooks
Send every completed response to your own server, Zapier, Make or any other URL as signed JSON, with retries and a delivery log.
A webhook sends each completed response to a URL of your choice the moment it's submitted, so you can add it to your CRM, post it to a chat, or start any other automation. For a spreadsheet, you don't need one: Google Sheets adds each response as a row, with no code.
Adding a webhook
- Open the form's Settings tab and choose Webhooks.
- Choose Add webhook and enter the Endpoint URL. It must start with
https://. - Copy the Secret that's created for it. You'll need it to check deliveries are genuine.
- Choose Send test to send a test delivery, and check it arrived under Recent deliveries.
A form can have up to 5 webhooks, each with its own URL and secret. Turn off pauses one without deleting it. Webhooks apply right away, without publishing.
Only workspace editors can see or change webhooks, since the secret could be used to fake deliveries.
What's sent
Each delivery is a POST with a JSON body:
{
"event": "response.completed",
"form": { "id": "3b1c…", "title": "Customer feedback" },
"response": {
"id": "8f2a…",
"completedAt": "2026-09-24T10:15:02.000Z",
"language": "en",
"hiddenFields": { "source": "newsletter" },
"answers": { "q_name": "Sam", "q_rating": 4, "q_topics": ["c_price", "c_support"] },
"fields": [
{
"id": "q_name",
"type": "short_text",
"title": "What's your name?",
"value": "Sam",
"text": "Sam"
},
{
"id": "q_topics",
"type": "multiple_choice",
"title": "What should we improve?",
"value": ["c_price", "c_support"],
"text": "Price, Support",
"followUps": [{ "prompt": "What about support?", "answer": "Replies took two days." }]
}
]
}
}
answersholds the raw answers, keyed by question id: choice ids rather than labels, numbers as numbers.fieldsrepeats them in form order with each question'stitle, and a readabletext: choice labels, "Yes"/"No", file names and so on. Most integrations only needfields.languageis the code of the language the response was answered in, likeenores. Titles andtextare always in the form's own language.followUpsis only present when follow-up questions were asked about that answer.
The raw value for each question type is described on its page under Question types.
A test delivery has "event": "webhook.test", the form, and a sentAt time, and no response.
Headers
| Header | Value |
|---|---|
X-Hootform-Event |
response.completed or webhook.test |
X-Hootform-Timestamp |
When it was sent, in seconds since 1970 |
X-Hootform-Signature |
sha256= and an HMAC of the timestamp and body |
User-Agent |
hootform-webhooks/1 |
Checking a delivery is genuine
Anyone could send a request to your URL, so check the signature before trusting one. Compute an HMAC-SHA256 of {timestamp}.{body} with your webhook's secret, using the raw request body exactly as received, and compare it with the header. Reject deliveries whose timestamp is more than a few minutes old, so an old delivery can't be replayed.
import { createHmac, timingSafeEqual } from "node:crypto";
function isGenuine(rawBody, headers, secret) {
const timestamp = headers["x-hootform-timestamp"];
const signature = headers["x-hootform-signature"] ?? "";
const expected =
"sha256=" + createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(timestamp)) < 300;
return (
fresh &&
signature.length === expected.length &&
timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
);
}
Deliveries, failures and retries
Your endpoint should answer with a 2xx status within 10 seconds. Anything else counts as a failure: another status, a timeout, or a redirect (redirects aren't followed).
A failed delivery is retried twice: after a minute, and again after ten minutes. Retries are stored, so they happen even if hootform restarts in between, and they're skipped if you turn the webhook off in the meantime. Because a delivery can be retried, use the response id to ignore one you've already processed.
Recent deliveries shows the latest 50 attempts for each webhook: whether each was Delivered or Failed, the status code or error, and how long it took. Respondents never wait for a webhook: the form finishes straight away, and deliveries happen in the background.
Encrypted forms
Deliveries always contain the decrypted answers, since your endpoint needs to read them. Make sure it's somewhere you trust with that data. See Encryption.