Agent skill
Zapier Webhooks Skill
Receive and authenticate webhooks sent by the Webhooks by Zapier action (POST, PUT, Custom Request). Use when building an endpoint that a Zap calls, when asking how to verify a Zapier webhook signature (there is none: add an X-Webhook-Secret header or Basic Auth in the Zap and compare constant-time), when parsing the user-defined Form, JSON, XML or Raw payload, or when a Zap's webhook step shows as errored. Also points to Standard Webhooks verification for Zapier white-label connection webhooks, which are a separate product.
Install this skill
npx skills add hookdeck/webhook-skills --skill zapier-webhooks
Webhooks by Zapier is the built-in Zapier app whose action step sends an HTTP request from a Zap to your URL. You choose the event (POST, PUT, GET or Custom Request), the Payload Type (Form, JSON, XML or Raw), the Data fields, the Headers, and an optional Basic Auth credential.
Two things make Zapier webhooks unlike most providers in this repo:
- There is no signature. Zapier does not sign Webhooks by Zapier requests: no HMAC, no signing secret, no signature header. You authenticate them with a secret you put into the Zap, either as a custom header (
X-Webhook-Secret) or with the action's Basic Auth field (username|password), and compare it in constant time. - There are no event types. Each request is one run of a Zap action, and the body is whatever you mapped in the Data section. If one endpoint serves several Zaps, add your own
eventfield in Data and dispatch on it.
When to Use This Skill
- How do I receive webhooks from a Zap (Webhooks by Zapier POST / PUT / Custom Request)?
- How do I verify a Zapier webhook signature? (You can't: there isn't one. Check a shared-secret header or Basic Auth instead.)
- How do I secure a webhook endpoint that Zapier calls?
- How do I parse a Zapier webhook sent as Form vs JSON vs Raw?
- Why does my Zap's webhook step show an error? (Your endpoint returned non-2xx.)
- How do I verify Zapier white-label connection webhooks? (Different product, Standard Webhooks; see below.)
Verification (core): compare a shared secret, constant-time
Node:
const crypto = require('crypto');
// Hash both sides so timingSafeEqual gets equal-length buffers and never throws.
function safeEqual(a, b) {
if (typeof a !== 'string' || typeof b !== 'string') return false;
const h = (s) => crypto.createHash('sha256').update(s).digest();
return crypto.timingSafeEqual(h(a), h(b));
}
const secret = process.env.ZAPIER_WEBHOOK_SECRET;
const user = process.env.ZAPIER_BASIC_AUTH_USERNAME;
const pass = process.env.ZAPIER_BASIC_AUTH_PASSWORD;
// Fail closed: never let an empty/unset credential match an empty header
if (!secret && !(user && pass)) return res.status(500).send('Auth not configured');
// Option A: header added in the Zap's Headers section
const okHeader = Boolean(secret) && safeEqual(req.headers['x-webhook-secret'], secret);
// Option B: Basic Auth field "user|password" -> Authorization: Basic base64(user:pass)
const m = (req.headers.authorization || '').match(/^Basic\s+(.+)$/i);
const okBasic = Boolean(user && pass) && !!m &&
safeEqual(Buffer.from(m[1], 'base64').toString('utf8'), `${user}:${pass}`);
if (!okHeader && !okBasic) return res.status(401).send('Unauthorized');
Python:
import hashlib, hmac, os
secret = os.environ.get("ZAPIER_WEBHOOK_SECRET", "")
if not secret: # fail closed (check Basic Auth vars too if you use them)
raise HTTPException(500, "Webhook authentication not configured")
h = lambda s: hashlib.sha256(s.encode()).digest()
if not hmac.compare_digest(h(request.headers.get("x-webhook-secret") or ""), h(secret)):
raise HTTPException(401, "Invalid or missing credentials")
Fail closed: if no credential is configured, return 500 rather than accepting everything, and never let an empty secret match an empty header. Return 401 on a missing or wrong credential. No raw-body handling is needed because nothing is signed.
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.
Payload and "events"
| Payload Type | Content-Type you receive | Parse as |
|---|---|---|
| JSON | application/json | JSON |
| Form | application/x-www-form-urlencoded | URL-encoded form |
| XML / Raw / Custom Request | whatever the Zap sends | raw text |
- Keys come from the Data section.
parent__childkeys become nested objects. If Data is left blank, all fields from the previous step are sent. - The examples log every request as
zap action, or dispatch on aneventfield when you add one in Data (e.g.event | lead.created). That value is yours, not Zapier's. - Max payload is 5 MB. Custom Request cannot send files and sends the Data field exactly as typed.
Zapier white-label connection webhooks (separate product)
Zapier's white-label / embedded connection webhooks are not Webhooks by Zapier. They are signed with Standard Webhooks: webhook-id, webhook-timestamp and webhook-signature headers, HMAC-SHA256 over {id}.{timestamp}.{body} with a whsec_ secret. Verify them with the standardwebhooks library and the raw body, as in the svix-webhooks skill. This skill's examples do not cover them.
Environment Variables
ZAPIER_WEBHOOK_SECRET=random_shared_secret # value of X-Webhook-Secret set in the Zap
ZAPIER_BASIC_AUTH_USERNAME= # optional: Basic Auth field "username|password"
ZAPIER_BASIC_AUTH_PASSWORD=
Local Development
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 zapier --path /webhooks/zapier
Paste the URL into the action's URL field and click Test step in the Zap editor.
Reference Materials
- references/overview.md - Webhooks by Zapier action events, payload types, limits
- references/setup.md - Configure the action, headers and Basic Auth in a Zap
- references/verification.md - Authentication options and gotchas