Agent skill
Plunk Webhooks Skill
Receive and authenticate Plunk webhooks. Use when setting up a Plunk workflow Webhook step endpoint, checking the secret header you add to the step, debugging 401s, or handling email.bounce, email.complaint, email.open and contact events forwarded from Plunk workflows.
Install this skill
npx skills add hookdeck/webhook-skills --skill plunk-webhooks
Plunk (the email platform) has no webhook subscriptions of its own. Webhooks come from the Webhook step in a workflow: an event such as email.bounce triggers the workflow, and the step sends an HTTP request to your URL. Requests are not signed. You authenticate them with a secret header that you add to the step yourself.
When to Use This Skill
- How do I receive Plunk webhooks?
- How do I authenticate a Plunk workflow Webhook step?
- Does Plunk sign webhook payloads? (No: use a secret header you add)
- How do I handle
email.bounce,email.complaintorcontact.unsubscribedevents? - Why does my Plunk webhook endpoint return 401?
- Which HTTP methods can a Plunk webhook step use?
Verification (core)
Add a secret header to the step (Headers, as JSON), for example {"Authorization": "Bearer your-shared-secret"}. The header name is yours, so the examples read it from PLUNK_WEBHOOK_HEADER (default Authorization) and the expected value from PLUNK_WEBHOOK_SECRET: the exact, whole header value, including any Bearer prefix you typed. Do not strip or parse a prefix.
const crypto = require('crypto');
function safeEqual(a, b) {
const ba = Buffer.from(String(a), 'utf8');
const bb = Buffer.from(String(b), 'utf8');
if (ba.length !== bb.length) return false; // timingSafeEqual throws on unequal lengths
return crypto.timingSafeEqual(ba, bb);
}
function verifyPlunkRequest(headers, secret = process.env.PLUNK_WEBHOOK_SECRET,
headerName = process.env.PLUNK_WEBHOOK_HEADER || 'Authorization') {
if (!secret) return false; // never accept when unconfigured
const received = headers[headerName.toLowerCase()]; // case-insensitive lookup
return Boolean(received) && safeEqual(received, secret);
}
- Python:
hmac.compare_. Encode both sides to bytes; passingdigest(received.encode(), secret.encode()) strraises on non-ASCII. - Missing header or mismatch: return
401. Check the credential first, for every method, before reading the body. - Read the body raw (
express.raw({ type: '*/*' })) so any content type works, then parse it. - A static shared credential authenticates the sender, not the body, and gives no replay protection (no signature, no timestamp). Use HTTPS, and rotate the credential if it leaks.
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.
Hookdeck Source Setup
In Hookdeck the source type is PLUNK (labelled "Plunk"). It uses API key verification (API Key Header + exact whole API Key). Hookdeck shows this note for the source:
Add a secret header to the webhook step in your workflow and enter it here. Use a static value, not a {{variable}}.
Hookdeck's Plunk source pre-selects POST without managing the method. The webhook step can use GET, POST, PUT, PATCH or DELETE, so allow any other method your step uses on the source too.
Methods, Body and Retries
- Method: a literal verb (
GET,POST,PUT,PATCH,DELETE), defaultPOST. The method is not templated. The examples accept all five on/webhooks/plunk. - Body: with no custom body, the request is JSON
{ contact, workflow, execution, event }. A custom body replaces it entirely. The body may be empty (for exampleGET). - No event field: there is no Plunk-defined event name on the wire. The trigger (
email.bounceand so on) only selects the workflow. The examples infer the kind from the documented fields ofevent(bouncedAt,complainedAt,clickedAt, ...), and treat custom bodies as plain JSON. - No automatic retries: a non-2xx response or a 10 second timeout fails the workflow step. Return
2xxquickly and make your handler idempotent. - Header values and the URL support
{{variable}}, but verification needs a static value.
Trigger Events
| Trigger | Fires When |
|---|---|
email.sent | An email was sent |
email.delivery | An email was delivered |
email.open | A contact opened an email |
email.click | A contact clicked a link |
email.bounce | An email bounced |
email.complaint | A contact marked an email as spam |
email.received | An email arrived at your verified domain |
contact.subscribed | A contact subscribed |
contact.unsubscribed | A contact unsubscribed |
segment.<name>.entry / .exit | A contact entered or left a segment |
Custom events you track through the API can also trigger a workflow.
Environment Variables
PLUNK_WEBHOOK_HEADER=Authorization # the header name you set on the webhook step
PLUNK_WEBHOOK_SECRET=Bearer your-shared-secret # exact, whole header value
Local Development
Plunk can't reach localhost (private addresses are blocked). Tunnel with the Hookdeck CLI (no account required):
npx hookdeck-cli listen 3000 plunk --path /webhooks/plunk
Use the printed URL as the Webhook step URL.
Resources
- references/overview.md - How Plunk webhooks work, trigger events and payload shape
- references/setup.md - Create the workflow and the secret header
- references/verification.md - Secret header check details, gotchas, debugging 401s
- examples/ - Runnable Express, Next.js and FastAPI handlers with tests