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:

  1. 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.
  2. 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 event field 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 TypeContent-Type you receiveParse as
JSONapplication/jsonJSON
Formapplication/x-www-form-urlencodedURL-encoded form
XML / Raw / Custom Requestwhatever the Zap sendsraw text
  • Keys come from the Data section. parent__child keys 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 an event field 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


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Oct 28, 2026

View on GitHub →