# Violet Webhooks

## When to Use This Skill

* How do I receive Violet webhooks?
* How do I verify the `X-Violet-Hmac` signature on a Violet webhook?
* Why is my Violet webhook signature verification failing?
* How do I handle ORDER_COMPLETED, ORDER_SHIPPED, MERCHANT_CONNECTED or TRANSFER_SENT events?
* Why did Violet pause or disable my webhook?

## Verification (core)

Violet sends `X-Violet-Hmac: <base64>`: the standard, padded base64 of the HMAC-SHA256 of the **raw request body bytes**, keyed with your **app secret** (the same secret you use for API calls) used as UTF-8 — not hex- or base64-decoded. Nothing else is signed (no timestamp or id), and the value has no prefix. Check the header's format, then compare the whole value in constant time.

Node:

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

function verifyVioletSignature(rawBody, header, secret) {
  // 44 chars of padded base64: validate before comparing (never Buffer.from(header, 'base64'))
  if (typeof header !== 'string' || !/^[A-Za-z0-9+/]{43}=$/.test(header)) return false;
  const expected = Buffer.from(crypto.createHmac('sha256', secret).update(rawBody).digest('base64'));
  const received = Buffer.from(header);
  return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}

```

Python:

```python
import base64, hashlib, hmac, re

def verify_violet_signature(raw_body: bytes, header: str | None, secret: str) -> bool:
    if not header or not re.fullmatch(r"[A-Za-z0-9+/]{43}=", header, re.ASCII):
        return False
    digest = hmac.new(secret.encode("utf-8"), raw_body, hashlib.sha256).digest()
    return hmac.compare_digest(header.encode("ascii"), base64.b64encode(digest))

```

Respond **401** when the header is missing, malformed, or doesn't match. Compare the base64 string exactly: a value whose last character carries stray low bits decodes to the same bytes but is not what Violet sends, and Hookdeck's Violet source rejects it too.

> **For complete handlers with route wiring, event dispatch, and tests**, see:
> 
> * [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/violet-webhooks/examples/express/) (use `express.raw({ type: '*/*' })` so the exact bytes are verified for any content type)
> * [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/violet-webhooks/examples/nextjs/) (`await request.arrayBuffer()`)
> * [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/violet-webhooks/examples/fastapi/) (`await request.body()`)

## Dispatch on `X-Violet-Topic`

The body is the entity itself (a full Order, an Offer, a PayoutAccount, a transfer...) with **no event field**, and all `ORDER_*` events send an Order. Only the `X-Violet-Topic` header names the event, so a request without it can't be classified. Parse the body only after verification, and treat every field as untrusted (check types before reading into it).

## Common Event Types (Topics)

| Topic | Description |
| --- | --- |
| `ORDER_ACCEPTED` | Order accepted |
| `ORDER_UPDATED` | Order changed |
| `ORDER_COMPLETED` | Order completed |
| `ORDER_SHIPPED` | Order shipped |
| `ORDER_DELIVERED` | Order delivered |
| `ORDER_REFUNDED` | Order refunded |
| `ORDER_CANCELLED` | Order cancelled |
| `ORDER_FAILED` | Order failed |
| `MERCHANT_CONNECTED` | Merchant connected to your app |
| `MERCHANT_DISCONNECTED` | Merchant disconnected |
| `MERCHANT_ENABLED` / `MERCHANT_DISABLED` | Merchant enabled or disabled |
| `MERCHANT_NEEDS_ATTENTION` | Merchant health check needs attention |
| `OFFER_ADDED` / `OFFER_UPDATED` / `OFFER_REMOVED` / `OFFER_DELETED` | Offer availability and changes |
| `PRODUCT_SYNC_COMPLETED` / `PRODUCT_SYNC_FAILED` | Catalog sync results |
| `TRANSFER_SENT` / `TRANSFER_FAILED` / `TRANSFER_REVERSED` | Payouts to merchants |

> **For the full list of 52 events**, see [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/violet-webhooks/references/overview.md) and the [Violet webhook events docs](https://docs.violet.io/prism/webhooks/events).

## Delivery and Retries

* Respond 2XX within **10 seconds**; queue the work and process it afterwards
* A failed event is retried 10 times with back-off over 24 hours, then marked `FAILED`
* More than 50 failures in 30 minutes pauses the webhook for 1 hour, then 3 hours, then up to 24 hours; if it still fails after three pauses the webhook is `DISABLED` until you re-activate it
* `X-Violet-Event-Id` identifies the event: use it for idempotency. `X-Violet-Event-Id-Source` marks a resend

## Environment Variables

```bash
VIOLET_WEBHOOK_SECRET=your_violet_app_secret   # The app secret, used as UTF-8 bytes

```

## Local Development

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

```

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/violet-webhooks/references/overview.md) - Violet webhook concepts, events, headers, delivery
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/violet-webhooks/references/setup.md) - Registering webhooks through the Violet API
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/violet-webhooks/references/verification.md) - Signature verification details and gotchas