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.complaint or contact.unsubscribed events?
  • 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_digest(received.encode(), secret.encode()). Encode both sides to bytes; passing str raises 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), default POST. 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 example GET).
  • No event field: there is no Plunk-defined event name on the wire. The trigger (email.bounce and so on) only selects the workflow. The examples infer the kind from the documented fields of event (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 2xx quickly and make your handler idempotent.
  • Header values and the URL support {{variable}}, but verification needs a static value.

Trigger Events

TriggerFires When
email.sentAn email was sent
email.deliveryAn email was delivered
email.openA contact opened an email
email.clickA contact clicked a link
email.bounceAn email bounced
email.complaintA contact marked an email as spam
email.receivedAn email arrived at your verified domain
contact.subscribedA contact subscribed
contact.unsubscribedA contact unsubscribed
segment.<name>.entry / .exitA 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


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 11, 2026

View on GitHub →