# 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)

```javascript
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/](https://github.com/hookdeck/webhook-skills/tree/main/skills/omnisend-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/omnisend-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/omnisend-webhooks/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` value | Where it comes from |
| --- | --- |
| `subscribed` | Value hard-coded in the guide's example body (trigger: `subscribed to marketing`) |
| `placed order` | Illustrative: Omnisend's docs use this as a trigger name, so it arrives only if your template sets it |
| anything else | Whatever 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

```bash
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):

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

```

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

## Resources

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/omnisend-webhooks/references/overview.md) - What Omnisend automation webhooks are, payload shape
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/omnisend-webhooks/references/setup.md) - Add the send webhook action and the secret header
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/omnisend-webhooks/references/verification.md) - Header check details, gotchas, debugging 401s
* [examples/](https://github.com/hookdeck/webhook-skills/tree/main/skills/omnisend-webhooks/examples/) - Runnable Express, Next.js and FastAPI handlers with tests