What Zapier Webhooks Actually Do When Nobody Is Watching
1. Webhooks fire once unless they secretly fire twice
The first time I had a Zap connected to a webhook step, it sent two Slack messages instead of one—like a ghost echo. I thought maybe I double-clicked something or refreshed weirdly, but nope. It turned out my test submission had hit the webhook twice thanks to an old reCAPTCHA setting on the form.
A Zapier webhook trigger looks simple: Zapier gives you a URL, you send JSON to it, Zapier starts the Zap. But Zapier doesn’t always filter duplicate payloads unless you tell it to. And there’s no dedupe flag like you get with Airtable’s automations or some API endpoints.
The workaround is buried in the Zap editor: you have to add a deduplication step using a Filter or custom code. I now throw a “Has this ID already been seen?” check into every webhook. If the payload has a unique ID field (like a transaction ID or form entry ID), feed that to a Storage step with the key equals the ID. If it already exists, bail out early.
What’s strange is Zapier’s webhook test tool doesn’t simulate this. If you click “Test Trigger” in the webhook step and re-send from Postman, it happily takes repeated payloads. But in a live Zap, users (or bots) pushing the same data twice can cause double usage. No error. Just twice the cost.
2. The webhook URL does not persist across Zap drafts
There’s a really subtle landmine when you’re cloning or versioning Zaps: Zapier assigns a new webhook URL for each new webhook step—even if it looks the same.
I found this out migrating a set of staging to production Zaps. I copied a test Zap and didn’t think much about the webhook step because the label stayed “Incoming Lead Payload” across both. But the new Zap had a fresh URL, which meant that the frontend app was still hitting the old endpoint. Took me a good 45 minutes and a Slack thread with 6 “wait it worked yesterday” messages to figure out.
Zapier webhook URLs are unique per Zap and per step—even across identical-looking copies.
If you need webhook URLs to stay stable across environments, either reserve a single Zap for handling inputs and delegate the downstream logic elsewhere (e.g. via paths or sub-Zaps), or use a proxy layer like Cloudflare Workers to absorb requests and forward to your Zap URL. It’s annoying, but it works.
3. Payload size gets chopped without visible error messages
Zapier accepts up to “a little over 1MB” in webhook request size, but if you go over it, the platform behaves inconsistently. Sometimes it fails entirely (500 error), but other times it accepts the request and just truncates the payload. No notification. No warning in task history. Just missing fields later downstream.
For most people, this won’t matter. But if you’re dealing with files encoded as base64, or long arrays (like multiple-choice questions or chat transcripts), you’ll hit the ceiling fast. I had a Typeform → Zapier → OpenAI pipeline that broke when someone wrote an essay in one of the form inputs. The webhook triggered, but the field cut off midway through a sentence. It looked like:
{
"field_7_long_text": "The reason I believe AI is useful in enda"
}
No error thrown. Just a missing chunk of your data. Fun!
If you can’t trim payloads at source, send only minimal data to Zapier and use an external fetch mechanism—e.g. include a Row ID or record pointer, then retrieve the full contents in a later step. Especially critical when dealing with Airtable attachments, ChatGPT conversations, or large file metadata.
4. Webhook tests do not match real-world usage conditions
This one burns everyone eventually. You set up a webhook trigger step in your Zap. You send a test from Postman, it works. You click “Continue” in Zapier, and it shows the test data. All good. You publish it.
Then someone sends real data from a Node server or a Webflow form or a Python script—and it breaks. Sometimes due to headers, sometimes encoding, sometimes body structure. Zapier doesn’t validate Content-Type aggressively during tests. A form that sends as application/x-www-form-urlencoded often parses differently than a JSON post, even if the values are the same.
What actually matters when testing
- Use the exact headers your live app will send
- Match the Content-Type (
application/jsonis usually safest) - Send deeply nested fields if you expect Zapier to parse them
- Don’t compress or gzip unless your client supports fallback
- Log what hits Zapier using a proxy tool like Pipedream to debug differences
One of my working Zaps broke when a code update switched the frontend to fetch-with-mode: “no-cors” — which stripped out headers and mangled the body. The Zap never even triggered because Zapier rejects opaque-mode requests silently. No logs. No task history.
5. Zapier webhooks support query strings but not consistently
If you send data to a Zapier webhook like https://hooks.zapier.com/hooks/catch/123456/abcde?source=formA, Zapier will parse the query string—but it might end up in either the body or inputData depending on HTTP method and headers.
In GET requests, query parameters go into a special top-level dictionary. But in POST requests with a JSON or form body, Zapier may ignore query string values altogether unless you specify them redundantly inside the body.
There’s no official schema inspection for this. If you’re using query parameters to pass routing or environment metadata (e.g. “env=dev”), it’s safer to pass it in both the body and URL. This becomes a big deal when trying to differentiate inbound webhook sources.
I finally just made a helper step that reassigns everything to known variable names so I don’t mistake “source” in payload vs source in URL. Trigger logic doesn’t let you combine both anyway—“only run this when query.param = X” isn’t a supported filter directly.
6. Don’t map dynamic fields you haven’t tested with real payloads
This is the classic “it works until it doesn’t” trap with webhook-driven Zaps. You create the Zap, use a sample payload with placeholder data, and map fields into actions based on those test inputs. All seems fine. Zap runs, but the actual payload structure is missing some fields—or uses different casing, like userId vs user_id—and your downstream actions quietly fail or insert null values.
I ended up with around 80 blank rows in an Airtable base because the webhook from an in-house app changed submission_id to submissionId during a patch update. The Zap still ran. Value just didn’t appear. No error.
If your source app payload isn’t hardened or versioned, never trust a sample payload. Instead, use the “Code by Zapier” step or a scripting tool to flatten + coerce the payload before using it.
Here’s a quick example I now use in a code step:
let id = inputData.submission_id || inputData.submissionId || "MISSING";
let name = inputData.user?.name || inputData.username || "Unknown";
return { id, name };
This at least gives some fallback if structure drifts downstream.
7. Zapier silently retries webhook triggers with network blips
Zapier doesn’t make a lot of noise about retries on webhook triggers. But if your outbound app—whatever’s POSTing to the Zapier webhook URL—times out or loses network, Zapier may re-accept the same payload again on reconnection. It depends partly on your client logic.
I didn’t notice this until one of my apps got rate-limited upstream, and a stuck cron job hit retry logic. Sent the same payload four times. Zapier accepted each one. I thought the Storage step I added would handle duplicates, but I had forgotten to set expiration on keys—so it failed once the cache filled up.
According to buried release notes, platform-triggered Zaps will attempt retries if they detect a server-side drop after payload handoff—but webhook-triggered Zaps can’t detect client resends unless you implement idempotency yourself.
I now attach a UUID with uuid.v4() in each client request and log it with a Storage key that expires in 1 day. Then Filter → don’t proceed if seen before. Cheap insurance.
8. Webhook paths and structured events do not exist in Zapier
You might come from something like n8n or AWS Lambda where webhooks can be routed like /purchase or /auth with different payloads. Zapier doesn’t support routing by URL path segment—each webhook has exactly one endpoint, full stop.
That means if your app pushes 5 types of events (e.g., signup, purchase, cancel, refund), your Zap either has to:
- Handle all event types in one long logic chain (Paths, Filters, Router steps); or
- Create 5 different Zaps, each with its own webhook URL and handling logic
For small payloads, one Zap works. But if you’re hitting usage limits or don’t want one Zap to cost you five times per batch, split it.
Another trick: set “event_type” or “action” as a field in your JSON, and use a Filter step or first code block to branch early. Just don’t expect Zapier to natively decompose webhooks by route.
