Form webhooks explained: get every response where you need it

How YeetForm webhooks work, how to verify the signature, and three recipes for Sheets, Zapier and Slack.

Mari Luukkainen··4 min read

What a webhook actually is

A webhook is a form telling another system "something happened" the moment it happens, instead of that system having to ask over and over. No polling, no cron job checking every five minutes for new rows. When someone submits your form, YeetForm sends a JSON payload to a URL you configure, right away.

That's the whole idea. You give YeetForm a URL, you get a POST request every time there's a new response. What you do with that request is up to you: write it to a spreadsheet, post it in Slack, kick off a workflow in your automation tool, or handle it with your own code.

What YeetForm sends

Configure a webhook per form under the form's Integrations tab. YeetForm POSTs on response.created (and webhook.test when you send a test event from the dashboard), with a payload shaped like this:

{
  "event": "response.created",
  "form_id": "<uuid>",
  "form_title": "Customer Feedback",
  "response_id": "<uuid>",
  "response_count": 42,
  "answers": { "fld_x": "..." },
  "fields": [{ "id": "fld_x", "label": "Your name", "type": "text" }],
  "submitted_at": "2026-09-13T12:00:00.000Z"
}

answers is keyed by field id, and fields gives you the labels and types so you can render something readable without a second API call back to YeetForm. response_count tells you where this response sits in the running total for the form, handy if you want to fire something only on the first response of the day, or the hundredth.

Verifying the signature

Every request carries three headers:

  • X-YeetForm-Event: response.created or webhook.test
  • X-YeetForm-Delivery: a unique id for this delivery attempt
  • X-YeetForm-Signature: t=<unix ts>,v1=<hex hmac-sha256 of ${t}.${body} >

That signature is how you know the request actually came from YeetForm and not from someone who found your webhook URL. Verify it before you trust anything in the body. Here's the Node snippet, straight from our own docs:

import { createHmac, timingSafeEqual } from "crypto";

function verify(secret, signatureHeader, rawBody) {
  const [tPart, v1Part] = signatureHeader.split(",");
  const timestamp = tPart.split("=")[1];
  const signature = v1Part.split("=")[1];

  const expected = createHmac("sha256", secret)
    .update(`${timestamp}.${rawBody}`)
    .digest("hex");

  const a = Buffer.from(signature, "hex");
  const b = Buffer.from(expected, "hex");
  if (a.length !== b.length) return false;
  return timingSafeEqual(a, b);
}

Two details worth flagging if you're implementing this yourself. First, you need the raw request body, not a parsed object, because the signature is computed over the exact bytes sent. If your framework parses JSON before you can get at the raw body, you'll need to grab it earlier in the request pipeline. Second, the comparison uses timingSafeEqual rather than ===, so an attacker can't learn anything from how fast your endpoint rejects a guess.

Retries: up to three attempts

If your endpoint doesn't respond with a success status, YeetForm makes up to three delivery attempts: the original, then one after 30 seconds, then one after 5 minutes. That covers the two most common failure modes: your server was mid-deploy for a second, or it was briefly overloaded.

It also means your endpoint needs to handle the same delivery arriving more than once. That's what response_id is for: treat it as an idempotency key. Before you act on a payload, check whether you've already processed that response_id (a quick lookup against whatever you wrote it to) and skip if you have. It's a few extra lines of code and it saves you from double-booked appointments, duplicate rows, or two Slack messages for one submission.

Three recipes

Sheets, without a server. If you want responses landing in a spreadsheet, point the webhook at a short Google Apps Script that appends a row per response. There is no native Sheets sync; the Google Sheets recipe walks through the fifteen lines of script and where to paste the webhook URL.

Zapier, Make or n8n. Point your webhook URL at a "Catch Hook" trigger in any of these tools and you can fan a single response out to email, a CRM, a database, or all three, without hosting anything yourself. The Zapier, Make and n8n guide walks through wiring the trigger and mapping fields from the answers object.

Slack, for a live feed. For a lighter touch than a full webhook receiver, YeetForm can post directly to a Slack channel on every response, which is often all a small team actually needs. See connecting Slack if you want new responses to show up where your team already looks. If you need more than a notification, a webhook into your own endpoint (or through Zapier into Slack) gives you full control over formatting and routing.

When to write your own receiver

The three recipes above cover most teams. Write your own webhook endpoint when you need logic the no-code tools can't express cleanly: routing a response to different systems based on an answer, enriching it with data from elsewhere before storing it, or triggering something with side effects like provisioning an account. The developer docs have the full endpoint list and the OpenAPI spec if you're building beyond just webhooks.

Test before you trust it

Every webhook config in the dashboard has a "send test event" button, which fires a webhook.test payload at your URL with the same headers and signature scheme as a real response. Use it. It's faster than filling in your own form five times to check whether your endpoint is parsing the body correctly, and it exercises the exact signature verification path your real traffic will hit.

Wire one up this afternoon. The test event button on the integrations tab tells you within a second whether your endpoint is listening.

Try it on your next form

Describe it in a sentence. Free plan, no card.

Start free

Keep reading