Webhooks
Webhooks let Postbox push data to you the moment a submission arrives - instead of you having to check for it. Think of it as a notification system for your code: “Hey, someone just submitted something, here’s the data.”
If you’ve ever used Zapier, Make, or n8n, you already know how this works. Postbox sends an HTTP POST request to a URL you control, with the full submission data as JSON. Your server (or automation tool) receives it and does whatever you want - saves it to a database, sends an email, triggers a workflow, creates a CRM record, posts to a Slack channel, anything.
When to use webhooks
Section titled “When to use webhooks”- You have a backend and want to process submissions in your own system (store in your database, run custom logic, etc.)
- You use automation tools like Make, Zapier, or n8n and want to trigger workflows from submissions
- You need real-time processing - webhooks fire within seconds of a submission being processed
If you just want notifications in Discord or Slack, use the Connected Channels feature on the Webhooks tab instead - no coding needed.
- Go to Forms > [Your Form] > Webhooks
- In the Custom Webhook card, enter the URL where you want submission data sent
- Set a signing secret (explained below) - this is how you verify the data is really from Postbox
- Click Save Webhook
That’s it. Postbox will automatically start sending data to your URL when submissions come in.
Using Make, Zapier, or n8n
Section titled “Using Make, Zapier, or n8n”These tools give you a webhook URL you can paste straight into Postbox:
- Make: Create a scenario, add a “Webhooks” module as the trigger, choose “Custom webhook”, and copy the URL it gives you
- Zapier: Create a Zap, pick “Webhooks by Zapier” as the trigger, choose “Catch Hook”, and copy the URL
- n8n: Add a “Webhook” trigger node and copy its production URL
Paste that URL into the Your server’s URL field and save. Send a test submission to make sure data flows through.
Signing secret
Section titled “Signing secret”The signing secret is a password that Postbox uses to sign every webhook request. Your server uses it to verify the request genuinely came from Postbox - not from someone trying to send fake submissions to your endpoint.
How it works: Postbox takes your secret, combines it with the request data, and creates a cryptographic signature. It sends that signature in a header. Your server does the same calculation - if the signatures match, the request is legitimate.
Do I need one? If you’re sending webhooks to Make, Zapier, or similar tools for non-sensitive data, you can set any value (it’s required, minimum 8 characters). If you’re sending to your own server and the data matters, you should verify signatures - see the verification section below.
Updating it: Leave the field blank when saving to keep your current secret. Enter a new value to rotate it (you’ll need to update your server too).
Payload format
Section titled “Payload format”Every webhook sends a JSON POST request with this structure:
{ "id": "evt_submission_created_abc123", "type": "submission.created", "created_at": "2026-03-10T12:30:00Z", "data": { "form": { "id": "form-uuid", "name": "Contact", "slug": "contact", "visibility": "public", "version": 1 }, "submission": { "id": "submission-uuid", "data": { "name": "Ada Lovelace", "email": "ada@example.com" }, "spam_flag": false, "spam_confidence": null, "spam_reason": null, "processing_status": "completed", "processing_reason": null, "original_language": "en", "translated_data": null, "reply_status": "pending", "reply_content": null, "reply_reason": null, "replied_at": null, "inserted_at": "2026-03-10T12:30:00Z", "updated_at": "2026-03-10T12:30:00Z" } }}The data field in submission contains whatever fields the submitter sent - it matches your form’s schema.
Verifying signatures
Section titled “Verifying signatures”Every webhook request includes three headers:
| Header | What it is |
|---|---|
webhook-id | Unique event identifier |
webhook-timestamp | Unix timestamp (seconds) when the event was sent |
webhook-signature | HMAC-SHA256 signature in the format v1,{base64} |
To verify a request is legitimate:
- Concatenate the
webhook-id,webhook-timestamp, and the raw request body, separated by dots - Compute HMAC-SHA256 using your signing secret
- Compare against the
webhook-signatureheader
Python example
Section titled “Python example”import hmac, hashlib, base64, time
webhook_id = headers["webhook-id"]timestamp = headers["webhook-timestamp"]signature = headers["webhook-signature"]
# Reject old events (replay attack prevention)if abs(time.time() - int(timestamp)) > 300: raise ValueError("Timestamp too old")
# Verify signaturesigned_content = f"{webhook_id}.{timestamp}.{body}"digest = hmac.new( secret.encode(), signed_content.encode(), hashlib.sha256).digest()expected = "v1," + base64.b64encode(digest).decode()
if not hmac.compare_digest(signature, expected): raise ValueError("Invalid signature")Node.js example
Section titled “Node.js example”const crypto = require("crypto");
const webhookId = headers["webhook-id"];const timestamp = headers["webhook-timestamp"];const signature = headers["webhook-signature"];
// Reject old events (replay attack prevention)if (Math.abs(Date.now() / 1000 - parseInt(timestamp)) > 300) { throw new Error("Timestamp too old");}
// Verify signatureconst signedContent = `${webhookId}.${timestamp}.${body}`;const digest = crypto .createHmac("sha256", secret) .update(signedContent) .digest("base64");const expected = `v1,${digest}`;
if (signature !== expected) { throw new Error("Invalid signature");}Retry behavior
Section titled “Retry behavior”If your endpoint doesn’t respond with an HTTP 2xx status within 5 seconds, Postbox retries up to 3 times with exponential backoff.
After all retries fail, the webhook is automatically disabled and you’ll get an email alert. The Webhooks tab will show the last error so you can debug.
To resume: fix the issue at your endpoint, then save the webhook again. Postbox will re-enable it automatically.
Events
Section titled “Events”Currently one event type is supported:
| Event | When it fires |
|---|---|
submission.created | A submission finishes processing (spam filtering, translation, etc.) |
Both legitimate and spam-flagged submissions trigger webhooks - check the spam_flag field if you want to filter on your end.