Gareth Wilson Gareth Wilson

Guide to Fillout Webhooks: Features and Best Practices

Published


Fillout builds forms, surveys, quizzes and scheduling pages. A Fillout webhook is one of a form's integrations: when someone submits the form, Fillout sends the submission to your URL. It's the simplest way to move answers into your own database or service without going through Zapier or Make.

This guide covers how Fillout webhooks are set up, what the two modes send, how to authenticate them, the trade-offs that come with that, and the best practices for running a Fillout receiver in production.

What are Fillout webhooks?

A Fillout webhook belongs to one form and runs when that form is submitted. It has two modes. Simple, the default, sends the full submission as JSON. Advanced lets you choose the HTTP method and add headers, cookies and URL parameters, but replaces the full submission with a body you map yourself.

Fillout doesn't sign its webhooks. There is no signature header, signing secret or timestamp. The way Fillout documents to protect an endpoint is a custom header holding a secret, such as Authorization: Bearer <secret_token>, and that is only available in Advanced mode.

Fillout webhook features

FeatureDetails
ConfigurationIntegrate > Webhook in the form editor, or POST /v1/api/webhook/create
TriggerForm submission
Event type fieldNone
Simple modeThe full submission as JSON
Advanced modePOST or GET, with a mapped JSON body, URL parameters, headers and cookies
SignatureNone
AuthenticationCustom header in Advanced mode, for example Authorization: Bearer <secret_token>
RetriesManual: Retry integration on the Results page, or resubmit the response
Source IPsNot documented

Common events

Fillout webhooks have one trigger, form submission, and no event type field. The Simple mode body uses the same format as the responses entries from the REST API's submissions endpoint:

{
  "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" }
  ]
}
FieldContents
submissionIdUnique identifier for the submission
submissionTimeWhen the submission was made
lastUpdatedAtWhen the submission was last updated
questionsOne entry per question, with id, name (the question text), type and value
calculationsCalculation results
urlParametersURL parameter values captured with the submission
scheduling, payments, quiz, loginPresent when the form uses Fillout Scheduling, Payments, a quiz or a login page

Answers sit in the questions array, so look them up by id rather than by position. Fillout warns that "new field types are added regularly" and that applications "should discard fields with unknown types". The webhook doesn't include the respondent's browser, operating system or network ID, or the edit link; fetch the submission from the API with includeEditLink if you need the link.

Scheduling forms

A webhook on a scheduling form sends these fields by default: fullName, email, phone (if available), timezone, eventStartTime, eventEndTime, eventId, eventUrl, rescheduleOrCancelUrl, scheduledUserEmail and scheduledUserName. Start and end times are always UTC.

The webhook only runs for the original booking. To hear about a cancellation or reschedule, Fillout says to use a workflow that sends a webhook.

Setting up Fillout webhooks

  1. Open your form and click Integrate, then Webhook.
  2. Enter your webhook URL and click Test.
  3. To authenticate, switch from Simple to Advanced (see below).
  4. Click Finish setup, then publish the form.

In Advanced mode:

  • HTTP method: choose POST or GET. Use POST, because only POST carries a body.
  • Body: each row is a key on the left and a value on the right. A value can be a form field, a URL parameter, a calculation, a date utility or static text.
  • URL parameters: keys and values appended to the URL.
  • Headers: extra request headers, such as Authorization set to Bearer <secret_token>.
  • Cookies: extra cookies.

You can also create webhooks with the REST API. Fillout's API docs say they "receive submissions in the same format as the entries in the responses list". The request takes only formId and url, and webhooks made this way don't appear in the Fillout dashboard, so remove them with the API too.

Securing Fillout webhooks

Generate a long random token (for example with openssl rand -hex 32). In the webhook's Advanced settings, add a header named Authorization with the value Bearer <token>. On your endpoint, compare the whole header value in constant time and reject anything else.

const crypto = require('crypto');
const express = require('express');

function verifyHeader(value, expected) {
  if (typeof value !== 'string' || !expected) return false;
  const received = Buffer.from(value, 'utf8');
  const wanted = Buffer.from(expected, 'utf8');
  return received.length === wanted.length && crypto.timingSafeEqual(received, wanted);
}

const app = express();

app.post('/webhooks/fillout', express.raw({ type: '*/*' }), (req, res) => {
  const token = process.env.FILLOUT_WEBHOOK_TOKEN;
  if (!token) return res.sendStatus(500); // fail closed

  if (!verifyHeader(req.headers['authorization'], `Bearer ${token}`)) {
    return res.sendStatus(401);
  }

  const body = JSON.parse(req.body.toString('utf8'));
  // Deduplicate on the submission ID you mapped, then process asynchronously.
  res.sendStatus(200);
});

The same check in Python:

import hmac
import os


def verify_fillout_header(value) -> bool:
    token = os.environ.get("FILLOUT_WEBHOOK_TOKEN")
    if not token or not value:
        return False
    return hmac.compare_digest(value.encode(), f"Bearer {token}".encode())

A header token proves the caller knows your secret. It doesn't protect the body, since nothing is signed, and anyone who sees one request can replay it. Serve the endpoint over HTTPS only.

Make Fillout webhooks production-ready. Hookdeck Event Gateway checks your header on every Fillout submission, durably queues it, and retries delivery when your service is down, which Fillout only does when someone clicks Retry integration.

Fillout webhook limitations and pain points

Authentication and the full submission don't mix

The Problem: You want the whole submission and an authentication header, and Fillout only lets you have one.

Why It Happens: Headers exist only in Advanced mode. Fillout's docs say that "in Advanced, Fillout no longer sends the full submission. You map the data yourself in the Body section."

Workarounds:

  • Use Advanced mode and map every field you need, plus an identifier you can deduplicate on.
  • Keep Simple mode and protect the endpoint another way, such as a long unguessable URL. That URL becomes the only secret, so treat it like one.
  • Use the mapped body as a notification and fetch the full submission from the REST API.

How Hookdeck Can Help: Hookdeck's Fillout source type checks a header name and value you choose, compared exactly. If you stay in Simple mode with no header, leave verification off if you didn't set a secret, and rely on the Hookdeck source URL instead.

API-created webhooks can't send headers

The Problem: Webhooks you create with the API arrive without your Authorization header and fail verification.

Why It Happens: POST /webhook/create accepts only formId and url. There's no field for headers, and API webhooks don't appear in the dashboard where you could add them.

Workarounds:

  • Create webhooks that need authentication in the form editor.
  • If you must use the API, treat the URL as the secret, or verify by fetching the submission from the API before acting on it.

How Hookdeck Can Help: Give each API-created webhook its own Hookdeck source URL. Hookdeck still records, queues and retries every delivery, and filters can drop bodies that don't look like Fillout submissions.

Failed deliveries need a manual retry

The Problem: Your endpoint was down for an hour and those submissions never arrived.

Why It Happens: Fillout documents no automatic retries. A failed webhook shows an error next to the submission on the form's Results page, and someone has to click Retry integration. If that button isn't there, the alternative is to resubmit the response, which reruns every integration, workflow and notification on the form, not just the webhook.

Workarounds:

  • Check the Results page after any outage and retry failed submissions.
  • Reconcile with the REST API's submissions endpoint, filtering with afterDate, on a schedule.
  • Avoid resubmitting unless every integration on the form is idempotent.

How Hookdeck Can Help: Hookdeck accepts each Fillout delivery as soon as it arrives and stores it, so an outage on your side doesn't turn into a failed webhook in Fillout. Hookdeck can then retry delivery to your service automatically, and you can replay any submission.

Mapped bodies may not include an ID

The Problem: In Advanced mode you have nothing to deduplicate on.

Why It Happens: Advanced mode sends only the keys you map. Fillout's list of mappable values is form fields, URL parameters, calculations, date utilities and static text, which doesn't promise a submission ID.

Workarounds:

  • If the value picker offers a submission ID, map it to a submissionId key.
  • Otherwise pass an ID in through a URL parameter, or deduplicate on a stable answer such as an email address plus a time window.

How Hookdeck Can Help: Hookdeck's deduplication can drop repeated deliveries based on fields in the body, before they reach your service.

Best practices

Compare the whole header

Check the full Authorization value, scheme included, with crypto.timingSafeEqual (after a length check) or hmac.compare_digest. Fail closed when the token isn't configured.

Look answers up by question ID

Find answers in questions by id rather than by position, and skip question types you don't recognise instead of failing.

Deduplicate submissions

Retrying an integration or resubmitting a response sends the submission again. Store submissionId (or the ID you mapped) and skip ones you've already handled. See our guide to webhook idempotency.

Acknowledge first, process after

Store the submission, return 2xx, then do the work. See why to process webhooks asynchronously.

Don't wait for other integrations

Fillout runs every integration at submission time, so a record created by another integration, such as an Airtable row, doesn't exist yet when the webhook is sent. Look it up later; its ID won't be in the payload.

Conclusion

Fillout sends each form submission to your webhook, either as the full submission (Simple mode) or as a body you map yourself (Advanced mode). There's no signature, so authentication means an Advanced-mode header compared in constant time, at the cost of the full submission. Map an identifier, look answers up by question ID, and plan for manual retries, because Fillout doesn't retry on its own.

Hookdeck Event Gateway checks each Fillout delivery, stores it, and retries into your service on your schedule.

Get started with Hookdeck for free and handle Fillout webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.