Start a Zap with a Catch Hook
In Zapier, create a new Zap and choose Webhooks by Zapier as the trigger, with the Catch Hook event. Zapier hands you a custom URL — that's the letterbox your Zap listens at. Copy it.
Give the hook to Postbag
Create a webhook Destination pointing at the Catch Hook URL. Being plain about what this is: Postbag posts JSON, Zapier catches it — a webhook wearing a nice hat, not a special integration. Which is exactly why it will keep working next year.
form_id="fm_YOUR_FORM_ID" # Copy yours from: npx postbag forms list
destination_json="$(curl --fail --silent --show-error -X POST https://postbag.dev/v1/destinations \
-H "Authorization: Bearer pb_live_…" \
-H "content-type: application/json" \
-d '{
"type": "webhook",
"name": "Zapier",
"config": { "url": "https://hooks.zapier.com/hooks/catch/…" }
}')"
destination_id="$(printf '%s\n' "$destination_json" | jq -er '.id')"
printf 'Destination ID: %s\n' "$destination_id" form_id="fm_YOUR_FORM_ID" # Copy yours from: npx postbag forms list
destination_json="$(curl --fail --silent --show-error -X POST https://postbag.dev/v1/destinations \
-H "Authorization: Bearer pb_live_…" \
-H "content-type: application/json" \
-d '{
"type": "webhook",
"name": "Zapier",
"config": { "url": "https://hooks.zapier.com/hooks/catch/…" }
}')"
destination_id="$(printf '%s\n' "$destination_json" | jq -er '.id')"
printf 'Destination ID: %s\n' "$destination_id" Teach the Zap what a message looks like
Send a test through the Destination, then click Test trigger in Zapier — it will show the fields it caught. Your visitor's answers ride inside data; the rest is the envelope: which Form, which delivery, when.
curl --fail --silent --show-error -X POST https://postbag.dev/v1/destinations/"$destination_id"/test \
-H "Authorization: Bearer pb_live_…" curl --fail --silent --show-error -X POST https://postbag.dev/v1/destinations/"$destination_id"/test \ -H "Authorization: Bearer pb_live_…"
{
"id": "dl_a91x02",
"type": "submission.received",
"schema_version": 1,
"form": { "id": "fm_8f3kq2", "name": "Contact" },
"data": { "email": "[email protected]", "message": "hello" },
"extras": {},
"meta": { "received_at": "2026-09-09T12:00:00Z" }
} {
"id": "dl_a91x02",
"type": "submission.received",
"schema_version": 1,
"form": { "id": "fm_8f3kq2", "name": "Contact" },
"data": { "email": "[email protected]", "message": "hello" },
"extras": {},
"meta": { "received_at": "2026-09-09T12:00:00Z" }
} Route your Form to it
Tie the Form to the Destination and finish building the Zap — a spreadsheet row, a CRM contact, a Slack message, whatever the day calls for. Each new submission is stored first, then posted; if Zapier hiccups, Postbag retries up to ten times and then leaves the delivery clearly marked for you.
route_body="$(jq -n --arg form_id "$form_id" --arg destination_id "$destination_id" \
'{ form_id: $form_id, destination_id: $destination_id }')"
curl --fail --silent --show-error -X POST https://postbag.dev/v1/routes \
-H "Authorization: Bearer pb_live_…" \
-H "content-type: application/json" \
-d "$route_body" route_body="$(jq -n --arg form_id "$form_id" --arg destination_id "$destination_id" \
'{ form_id: $form_id, destination_id: $destination_id }')"
curl --fail --silent --show-error -X POST https://postbag.dev/v1/routes \
-H "Authorization: Bearer pb_live_…" \
-H "content-type: application/json" \
-d "$route_body"