# Zernio Webhooks

## When to Use This Skill

* How do I receive Zernio webhooks?
* How do I verify the `X-Zernio-Signature` header?
* How do I handle `post.published`, `post.failed` or `message.received` events?
* Why is my Zernio webhook signature verification failing?
* Why am I receiving the same Zernio event twice?

## Verification (core)

Zernio signs a delivery only when the endpoint has a secret, and **you** supply that secret when you create the endpoint (Zernio never generates one). `X-Zernio-Signature` is the **lowercase hex HMAC-SHA256 of the raw request body** keyed with that secret. `X-Late-Signature` is a legacy alias with the same value. There is no timestamp in the signed content.

Node:

```javascript
const crypto = require('crypto');

function verify(rawBody, signatureHeader, secret) {
  if (!signatureHeader || !secret) return false;
  const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
  try {
    return crypto.timingSafeEqual(Buffer.from(signatureHeader), Buffer.from(expected));
  } catch {
    return false; // different lengths
  }
}
// header: req.headers['x-zernio-signature'] || req.headers['x-late-signature']

```

Python:

```python
import hmac, hashlib

def verify(raw_body: bytes, signature_header: str | None, secret: str) -> bool:
    if not signature_header or not secret:
        return False
    expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(signature_header, expected)

```

Zernio has no SDK helper for verification (the docs show the manual HMAC), so all three examples verify manually. Always set a secret: an endpoint without one receives unsigned deliveries.

> **For complete handlers with route wiring, event dispatch, and tests**, see:
> 
> * [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/zernio-webhooks/examples/express/)
> * [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/zernio-webhooks/examples/nextjs/)
> * [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/zernio-webhooks/examples/fastapi/)

## Common Event Types

| Event | Description |
| --- | --- |
| `post.scheduled` | A post entered the scheduled state |
| `post.published` | The post published on every target platform |
| `post.partial` | Published on some platforms, failed on others |
| `post.failed` | The post failed on every target platform |
| `post.platform.published` | One platform target finished publishing |
| `post.platform.failed` | One platform target failed permanently |
| `message.received` | A new inbox message (DM) arrived |
| `comment.received` | A new comment arrived on a tracked post |
| `account.connected` | An account was connected to a profile |
| `account.disconnected` | A connected account stopped working or was removed |
| `webhook.test` | Sent by `POST /v1/webhooks/test` regardless of subscribed events |

> **For the full event list** (ads, calls, WhatsApp, automations and more), see [Zernio webhooks](https://docs.zernio.com/webhooks) and [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/zernio-webhooks/references/overview.md).

## Important Headers

| Header | Description |
| --- | --- |
| `X-Zernio-Signature` | Lowercase hex HMAC-SHA256 of the raw body (only when the endpoint has a secret) |
| `X-Late-Signature` | Legacy alias, same value |
| `X-Zernio-Event` | Event name, e.g. `post.published` |
| `X-Zernio-Event-Id` | Stable event UUID, identical on retries; use as the dedupe key |
| `X-Late-Event-Id` | Legacy alias of `X-Zernio-Event-Id` |

## Delivery Behavior

* Return any `2xx` within **5 seconds**; persist the event and process it in the background.
* Up to 7 attempts with exponential backoff (10s, 1m40s, 16m40s, 2h46m, then 24h), then a dead-letter queue.
* Delivery is at-least-once, so deduplicate on `payload.id` / `X-Zernio-Event-Id`.
* `payload.id` identifies the event only, never a post or account. Route by `account.accountId` or `profiles[].externalId`.

## Environment Variables

```bash
ZERNIO_WEBHOOK_SECRET=your_webhook_secret   # The secret you sent in POST /v1/webhooks/settings

```

## Local Development

```bash
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 zernio --path /webhooks/zernio

```

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/zernio-webhooks/references/overview.md) - Zernio webhook concepts and event list
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/zernio-webhooks/references/setup.md) - Creating and testing a webhook endpoint
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/zernio-webhooks/references/verification.md) - Signature verification details and gotchas