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 questions array 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:

  1. Generate a random token yourself (openssl rand -hex 32).
  2. In Fillout, open Integrate → Webhook → Advanced → Headers and add Authorization = Bearer <token>.
  3. On your endpoint, compare the whole Authorization header value with Bearer <token> in constant time. Return 401 if it's missing or wrong.
  4. 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


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Oct 28, 2026

View on GitHub →