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.

Included with Pro. Outbound webhooks, including the Slack, Teams and Discord formats, are part of PostKnock Pro. You can set up webhooks from Admin › Webhooks once you are on a Pro plan.

Set up a webhook

About five minutes, and you never leave the Admin screen.

  1. 1

    Open Admin › Webhooks in PostKnock

    Sign in, open Admin, then the Webhooks tab. Select New webhook.

  2. 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. 3

    Choose the events you care about

    Tick individual events, or subscribe to everything. Most teams start with postcard.delivered and contact.responded — the two that say a mailer is working.

  4. 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. 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. 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? +
PostKnock retries with exponential backoff — 10 attempts spread from about a minute out to eight hours, roughly 15 hours of cover in total. Every attempt is recorded in the delivery log with the status code and response body, and you can force a retry yourself once the endpoint is fixed.
How do I stop duplicate processing? +
Dedupe on the envelope’s 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? +
Yes. 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? +
Up to 25 per account. Each one multiplies the traffic a single event generates, so if you are fanning out to many destinations it is usually better to point one webhook at an automation platform and branch there.
Does a failing webhook affect my mail? +
No. Webhook delivery runs outside the sending pipeline. A webhook that errors, times out or 500s cannot stop a postcard going out or a call being scheduled.
Is the payload the same for Slack, Teams and Discord? +
No — those three get a message formatted for that product instead of the raw JSON envelope, so it reads properly in a channel. Choose the Generic format when you want the JSON documented on this page.

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 free

Outbound webhooks are included with PostKnock Pro.