Agent skill
Fillout Webhooks Skill
Receive and authenticate Fillout form submission webhooks. Use when setting up a Fillout webhook handler, adding the optional Authorization: Bearer <secret_token> header in Fillout's Advanced webhook settings, or parsing the submission payload (submissionId, submissionTime, questions, calculations, urlParameters, scheduling, payments). Fillout does NOT sign webhooks: there is no signature header, no HMAC and no signing secret, so verify by comparing the whole custom header value in constant time. Fillout has no event types; a webhook fires on form submission only.
Install this skill
npx skills add hookdeck/webhook-skills --skill fillout-webhooks
Fillout is a form builder (forms, surveys, quizzes and scheduling). A Fillout webhook is a form integration: when someone submits the form, Fillout sends the submission to your URL as JSON.
When to Use This Skill
- How do I receive Fillout form submissions with a webhook?
- How do I verify a Fillout webhook signature? (You can't. Fillout doesn't sign webhooks. Authenticate with a custom header instead.)
- How do I protect my Fillout webhook endpoint with
Authorization: Bearer <token>? - How do I read answers from the
questionsarray in a Fillout webhook payload? - How do I handle Fillout scheduling form bookings or payments in a webhook?
- Why does my Fillout webhook only send some fields? (Advanced mode replaces the full submission with your own mapped body.)
There Is No Signature
Fillout's webhook docs describe no signature header, no signing secret, no timestamp and no HMAC. Don't invent an X-Fillout-Signature verifier: no real delivery would pass it.
The documented verification mechanism is the optional Headers section of the webhook's Advanced settings. Fillout's docs give this as the typical use:
The most typical use case for headers is if you have some verification required for your endpoint, and you have a special key you want to send, which will look something like:
Authorization —> Bearer <secret_token>
So you:
- Generate a random token yourself (
openssl rand -hex 32). - In Fillout, open Integrate → Webhook → Advanced → Headers and add
Authorization=Bearer <token>. - On your endpoint, compare the whole
Authorizationheader value withBearer <token>in constant time. Return401if it's missing or wrong. - Fail closed: if the token isn't configured on your server, reject the request.
This proves the caller knows the shared secret. It does not protect the body from tampering, since nothing is signed over it. Because nothing is signed over the body, you don't need the raw body either: parsing JSON before checking the header is fine.
Verification (core)
const crypto = require('crypto');
// Fillout sends the header you configured: Authorization: Bearer <token>.
// Compare the WHOLE header value, not just the part after "Bearer ".
function verifyFilloutAuth(authorizationHeader, token) {
if (!token) return false; // fail closed when FILLOUT_WEBHOOK_TOKEN is unset
if (typeof authorizationHeader !== 'string') return false;
// Hash both sides so timingSafeEqual always gets equal-length buffers
const a = crypto.createHash('sha256').update(authorizationHeader).digest();
const b = crypto.createHash('sha256').update(`Bearer ${token}`).digest();
return crypto.timingSafeEqual(a, b);
}
// verifyFilloutAuth(req.headers.authorization, process.env.FILLOUT_WEBHOOK_TOKEN)
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.
Events: Form Submission Only
Fillout webhooks have no event type field. A webhook added under Integrate runs once, when the form is submitted. On a scheduling form that means the original booking; Fillout's docs say to use a workflow for cancellations and reschedules.
In Simple mode (the default), Fillout sends the full submission. The documented submission object (the same shape the REST API returns, and what API-created webhooks send) looks like this:
{
"submissionId": "6bcedd3a-fe26-4bcb-9ce9-d3850e38d42c",
"submissionTime": "2026-10-21T10:15:30.000Z",
"lastUpdatedAt": "2026-10-21T10:15:30.000Z",
"questions": [
{ "id": "abc1", "name": "What's your email?", "type": "EmailInput", "value": "ada@example.com" }
],
"calculations": [],
"urlParameters": [{ "id": "utm_source", "name": "utm_source", "value": "newsletter" }]
}
Optional members: scheduling, payments, quiz and login. The examples also accept the submission wrapped as { "submission": { ... } }.
Headers live in Advanced mode, and Advanced mode changes the body. Fillout's docs: "In Advanced, Fillout no longer sends the full submission. You map the data yourself in the Body section." So a webhook that sends the Authorization header also sends a flat JSON object of keys you chose. Map a submissionId key to Fillout's Submission ID value so you can deduplicate, then map the answers you need:
{ "submissionId": "6bcedd3a-fe26-4bcb-9ce9-d3850e38d42c", "email": "ada@example.com", "name": "Ada Lovelace" }
The examples handle both shapes: a full submission (with a questions array) and a flat mapped body.
See references/overview.md for the payload fields.
Environment Variables
# The token YOU generated. Fillout must send: Authorization: Bearer <this value>
FILLOUT_WEBHOOK_TOKEN=generate_with_openssl_rand_hex_32
Local Development
npx hookdeck-cli listen 3000 fillout --path /webhooks/fillout
No account required. The CLI creates a guest account on first run and gives you a public URL and a web UI for inspecting requests. Use port 8000 for the FastAPI example.
Hookdeck's Fillout source type checks the header for you. Set Header Name to Authorization and Header Value to the full value, including Bearer .
Reference Materials
- references/overview.md - How Fillout webhooks work, payload fields, Simple vs Advanced mode
- references/setup.md - Adding the webhook and the Authorization header in Fillout, the REST API, retries
- references/verification.md - Header authentication per framework, gotchas, debugging