# 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:

```javascript
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:

```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/](https://github.com/hookdeck/webhook-skills/tree/main/skills/zapier-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/zapier-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/zapier-webhooks/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__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](/webhooks/skills/svix-webhooks) skill. This skill's examples do not cover them.

## Environment Variables

```bash
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

```bash
# 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](https://github.com/hookdeck/webhook-skills/blob/main/skills/zapier-webhooks/references/overview.md) - Webhooks by Zapier action events, payload types, limits
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/zapier-webhooks/references/setup.md) - Configure the action, headers and Basic Auth in a Zap
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/zapier-webhooks/references/verification.md) - Authentication options and gotchas