Agent skill

Omnisend Webhooks Skill

Receive and authenticate Omnisend webhooks sent by the automation "send webhook" action. Use when setting up an Omnisend webhook endpoint, checking a shared secret header such as X-Webhook-Secret, debugging 401s, or handling automation payloads built from [[contact.*]] and [[event.raw.*]] tags (for example event "subscribed").

Install this skill

npx skills add hookdeck/webhook-skills --skill omnisend-webhooks


Omnisend (ecommerce email and SMS) sends webhooks from the send webhook action in an automation workflow (sendWebhook block, paid plans only, not the free plan). Each time a contact reaches that block, Omnisend POSTs your JSON body template, with [[contact.*]] and [[event.raw.*]] tags resolved, to your HTTPS callbackUrl.

Requests are not signed. Omnisend lets you add up to 15 custom headers to the action, and the docs recommend a shared secret header such as X-Webhook-Secret. Your endpoint authenticates the request by comparing that header with the value you stored.

When to Use This Skill

  • How do I receive Omnisend webhooks from an automation?
  • How do I verify an Omnisend webhook? (No signature: check a shared secret header)
  • How do I check X-Webhook-Secret on an Omnisend webhook request?
  • How do I handle an Omnisend payload with "event": "subscribed"?
  • Why does my Omnisend webhook endpoint return 401?

Verification (core)

const crypto = require('crypto');

// Hash both sides: equal-length buffers, so timingSafeEqual never throws.
function safeEqual(a, b) {
  const ha = crypto.createHash('sha256').update(String(a)).digest();
  const hb = crypto.createHash('sha256').update(String(b)).digest();
  return crypto.timingSafeEqual(ha, hb);
}

function verifyOmnisendRequest(headers, {
  headerName = process.env.OMNISEND_WEBHOOK_HEADER || 'X-Webhook-Secret',
  secret = process.env.OMNISEND_WEBHOOK_SECRET,
} = {}) {
  if (!secret) return false;                          // never accept when unconfigured
  const received = headers[headerName.toLowerCase()]; // Node lowercases header names
  return typeof received === 'string' && safeEqual(received, secret); // whole value, exact
}
  • The header name is whatever you typed in the webhook action's headers; the examples read it from OMNISEND_WEBHOOK_HEADER (default X-Webhook-Secret). Look it up case-insensitively.
  • Compare the whole header value with OMNISEND_WEBHOOK_SECRET. Do not strip or parse a Bearer prefix: if you typed Bearer abc as the header value, the secret is Bearer abc.
  • Return 401 on a missing header or mismatch, 400 for a body that isn't a JSON object, 200 once handled.
  • Read the body raw (express.raw({ type: '*/*' })) and authenticate before parsing it.
  • A static shared credential authenticates the sender, not the body, and gives no replay protection (no signature, no timestamp). Use HTTPS (Omnisend requires it) and rotate the credential if it leaks.

For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.

Event Types

Omnisend does not define a fixed webhook event schema. The body is your JSON template, so the event name is whatever you put in it. The official guide's example sets "event": "subscribed" by hand for an automation triggered by the subscribed to marketing event.

event valueWhere it comes from
subscribedValue hard-coded in the guide's example body (trigger: subscribed to marketing)
placed orderIllustrative: Omnisend's docs use this as a trigger name, so it arrives only if your template sets it
anything elseWhatever your template sets; use [[event.raw.<property>]] for trigger event data

The examples dispatch on event and log unknown values, so add a case per automation.

Environment Variables

OMNISEND_WEBHOOK_HEADER=X-Webhook-Secret   # header name you set in the webhook action (default)
OMNISEND_WEBHOOK_SECRET=your-shared-secret # exact, whole header value

Local Development

Omnisend can't reach localhost, and callbackUrl must be HTTPS on a public domain. Tunnel with the Hookdeck CLI (no account required):

npx hookdeck-cli listen 3000 omnisend --path /webhooks/omnisend

Use the printed URL as the callbackUrl of the webhook action.

Resources


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 5, 2026

View on GitHub →