INTEGRATION · OUTBOUND WEBHOOKS
Send PostKnock events anywhere
Direct mail usually disappears the moment it leaves the building. Webhooks close that loop: the moment a postcard is delivered, a QR code is scanned, or a follow-up call is logged, PostKnock POSTs a signed JSON payload to a URL you choose. Wire it into your CRM, your on-call channel, or an automation platform, and the rest of your stack finds out at the same time you do.
Set up a webhook
About five minutes, and you never leave the Admin screen.
-
1
Open Admin › Webhooks in PostKnock
Sign in, open Admin, then the Webhooks tab. Select New webhook.
-
2
Paste the URL that should receive events
Any HTTPS endpoint you control. It has to be reachable from the public internet — PostKnock refuses private, loopback and internal addresses, and re-checks that every time it delivers.
-
3
Choose the events you care about
Tick individual events, or subscribe to everything. Most teams start with
postcard.deliveredandcontact.responded— the two that say a mailer is working. -
4
Copy the signing secret
On save, PostKnock shows a signing secret once. Store it somewhere safe: it is how your endpoint proves a request really came from us, and it is never displayed again. You can rotate it later if it leaks.
-
5
Verify the signature on your side
Every request carries
X-PostKnock-Signature: sha256=<hmac>, computed over the raw request body with your secret. Compare it before trusting a payload — sample code is below. -
6
Send a test and watch it land
Use Test on the webhook to fire a sample event immediately, then open Deliveries to see the status code and response your endpoint returned.
What lands on your endpoint
One POST per event, Content-Type: application/json, sent as PostKnock-Webhook/1.0. Every event uses the same envelope; only data changes shape.
{
"id": "6f1c9b2e-6c4a-4a1e-9a2f-6c0f4b1d8e77",
"type": "postcard.delivered",
"tenant_id": 42,
"occurred_at": "2026-08-26T14:03:11Z",
"data": {
"campaign_id": 7,
"contact_id": 1183,
"wave_number": 1,
"lob_id": "psc_9f2c...",
"delivered_date": "2026-08-26"
}
}
Return any 2xx to acknowledge. Anything else — or a timeout — is treated as a failure and retried.
Every event you can subscribe to
Pick the ones you want per webhook, or subscribe to all of them with *. Each event carries the fields listed here inside data.
| Event | Fires when | data fields |
|---|---|---|
| postcard.sent | A postcard was handed to the print partner and mailed. | campaign_id, contact_id, wave_number, lob_id |
| postcard.in_transit | The postal service scanned the piece in transit. | campaign_id, contact_id, wave_number, lob_id |
| postcard.delivered | Delivery confirmed. Never presumed — only a real carrier confirmation fires this. | campaign_id, contact_id, wave_number, lob_id, delivered_date |
| postcard.failed | The piece failed or came back as return-to-sender. | campaign_id, contact_id, wave_number, lob_id, reason |
| call.scheduled | A follow-up call was queued for a contact. | campaign_id, contact_id, wave_number, scheduled_for |
| call.completed | A call was completed and its outcome recorded. | campaign_id, contact_id, wave_number, call_result, contact_type, duration_seconds |
| call.no_answer | A call rang out or the line was unavailable. | campaign_id, contact_id, wave_number |
| contact.responded | A contact scanned the QR code on their postcard. | contact_id, campaign_id, method, tracked_url_id |
| contact.unsubscribed | A contact was marked do-not-contact. | contact_id, reason |
| campaign.activated | A campaign started sending. | campaign_id, name, total_contacts |
| campaign.completed | Every wave finished, or someone marked the campaign complete. | campaign_id, name |
| campaign.paused | A campaign paused — by request, or automatically on a low wallet balance. | campaign_id, name, reason |
| touchpoint.failed | An individual postcard or email touchpoint failed. | campaign_id, contact_id, touchpoint_type, reason |
IDs in data are the same per-account IDs you see in the PostKnock URL bar, so campaign_id: 7 is the campaign at /campaigns/7.
Verifying the signature
Compute an HMAC-SHA256 of the raw request body using your signing secret and compare it to the header. Parse the JSON only after that check passes — and compare in constant time, so a mismatch cannot be probed a byte at a time.
Node.js (Express)
const crypto = require('crypto');
// Give this route the RAW body — JSON.parse first and the bytes you hash
// are no longer the bytes we signed.
app.post('/postknock', express.raw({ type: 'application/json' }), (req, res) => {
const expected = 'sha256=' + crypto
.createHmac('sha256', process.env.POSTKNOCK_WEBHOOK_SECRET)
.update(req.body)
.digest('hex');
const got = req.get('X-PostKnock-Signature') || '';
const ok = expected.length === got.length &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(got));
if (!ok) return res.status(401).send('bad signature');
const event = JSON.parse(req.body);
// Dedupe on event.id — a retry reuses it, a new event never does.
console.log(event.type, event.data);
res.sendStatus(200);
});
Python (Flask)
import hmac, hashlib, os, json
from flask import request, abort
@app.post("/postknock")
def postknock():
raw = request.get_data() # raw bytes, before any parsing
expected = "sha256=" + hmac.new(
os.environ["POSTKNOCK_WEBHOOK_SECRET"].encode(), raw, hashlib.sha256
).hexdigest()
if not hmac.compare_digest(expected, request.headers.get("X-PostKnock-Signature", "")):
abort(401)
event = json.loads(raw)
# Dedupe on event["id"].
return "", 200
Connect an automation platform
None of these need a PostKnock-specific app. Each one gives you a URL that accepts a JSON POST; create a Generic webhook in PostKnock and paste it in. From there you can branch on type and push into anything the platform connects to.
Make.com
Add a Webhooks › Custom webhook module as the trigger, select Add to mint a URL, then copy it. Paste it into PostKnock, fire a test, and Make will capture the payload structure so later modules see every field by name.
n8n
Start a workflow with the Webhook node, method POST. Use the test URL while you build, then switch to the production URL and activate the workflow — a common trip-up is leaving a test URL in PostKnock after going live.
Pipedream
Create a workflow with an HTTP / Webhook trigger. Pipedream generates an endpoint immediately; paste it into PostKnock and send a test to populate the event shape for the steps that follow.
Zapier
Use Webhooks by Zapier with the Catch Hook trigger and copy the URL it gives you. Note that Catch Hook is a premium Zapier trigger on some plans. PostKnock also has a native Zapier action for pushing contacts into PostKnock — this is the other direction.
Questions
What happens if my endpoint is down? +
How do I stop duplicate processing? +
id. It is a UUID unique to that event and that subscription, so a retry of the same delivery carries the same id and a genuinely new event never reuses one.Can I tell a replay from a fresh event? +
occurred_at lives inside the signed body rather than in a header, so once you have verified the signature you can trust the timestamp and reject anything older than your own tolerance.How many webhooks can I create? +
Does a failing webhook affect my mail? +
Is the payload the same for Slack, Teams and Discord? +
Direct mail your other tools can see
Send postcards and follow-up calls from PostKnock, and stream every delivery, scan and call outcome straight into the tools your team already watches.
Start freeOutbound webhooks are included with PostKnock Pro.