# Guide to Violet Webhooks: Features and Best Practices

[Violet](https://violet.io) is a unified checkout API. It lets a channel, an app or site that sells other merchants' products, take orders and sync catalogues across merchants' ecommerce platforms through one integration. Its webhooks keep the channel up to date as orders ship or are refunded, merchants connect or disconnect, offers change and payouts move.

This guide covers Violet's per-event webhooks, the header-based event metadata, verifying `X-Violet-Hmac` with your app secret, and the pause-and-disable rules that make fast acknowledgement important.

## What are Violet webhooks?

A Violet webhook subscribes one event, such as `ORDER_SHIPPED`, to a `remote_endpoint` URL. You create one per event through the REST API, and several can share a URL. When the event happens, Violet posts the related entity, such as a fully composed `Order` or a `Merchant` object, as the request body.

The body has no envelope and no event field. What happened, and which event this is, travels in `X-Violet-*` headers.

## Violet webhook features

| Feature | Details |
| --- | --- |
| Signature | base64 HMAC-SHA256 |
| Signature header | `x-violet-hmac` |
| Body | JSON |
| Event routing | Header field `X-Violet-Topic` |

Every request carries these default headers, from Violet's [events page](https://docs.violet.io/prism/webhooks/events):

| Header | Meaning |
| --- | --- |
| `X-Violet-Hmac` | The signature |
| `X-Violet-Topic` | The event, such as `ORDER_COMPLETED` |
| `X-Violet-Event-Id` | Unique event ID, for idempotency |
| `X-Violet-Entity-Id` | ID of the related entity |
| `X-Violet-Entity-Length` | Size of the entity in bytes |
| `X-Violet-Webhook-Id` | The webhook subscription |

Some events add `X-Violet-Order-Id`, `X-Violet-Bag-Id` (the bag in the order that changed), `X-Violet-Reason` (why a merchant changed status), `X-Violet-Entity-External-Id` (the payment provider account ID for payout accounts) or `X-Violet-Recovery-Event-Key`. A resent event carries `X-Violet-Event-Id-Source` with the original event ID.

## Events and payloads

Route on `X-Violet-Topic`.

| Event routing | Header field `X-Violet-Topic` |

### All Documented Events (52)

* **Order:** `ORDER_ACCEPTED`, `ORDER_UPDATED`, `ORDER_COMPLETED`, `ORDER_SHIPPED`, `ORDER_DELIVERED`, `ORDER_REFUNDED`, `ORDER_CANCELLED`, `ORDER_FAILED`
* **Merchant:** `MERCHANT_CONNECTED`, `MERCHANT_DISCONNECTED`, `MERCHANT_ENABLED`, `MERCHANT_DISABLED`, `MERCHANT_NEEDS_ATTENTION`, `MERCHANT_COMPLETE`
* **Offer:** `OFFER_CREATED` (deprecated), `OFFER_ADDED`, `OFFER_UPDATED`, `OFFER_REMOVED`, `OFFER_DELETED`
* **Collection:** `COLLECTION_CREATED`, `COLLECTION_UPDATED`, `COLLECTION_REMOVED`, `COLLECTION_OFFERS_UPDATED`
* **Sync:** `PRODUCT_SYNC_STARTED`, `PRODUCT_SYNC_COMPLETED`, `PRODUCT_SYNC_FAILED`, `COLLECTION_SYNC_STARTED`, `COLLECTION_SYNC_COMPLETED`, `COLLECTION_SYNC_FAILED`
* **Payout account:** `MERCHANT_PAYOUT_ACCOUNT_CREATED`, `MERCHANT_PAYOUT_ACCOUNT_REQUIREMENTS_UPDATED`, `MERCHANT_PAYOUT_ACCOUNT_ACTIVATED`, `MERCHANT_PAYOUT_ACCOUNT_DEACTIVATED`, `MERCHANT_PAYOUT_ACCOUNT_DELETED`
* **Payment transaction:** `PAYMENT_TRANSACTION_CAPTURE_STATUS_UPDATED`, `PAYMENT_TRANSACTION_CAPTURE_STATUS_AUTHORIZED`, `PAYMENT_TRANSACTION_CAPTURE_STATUS_CAPTURED`, `PAYMENT_TRANSACTION_CAPTURE_STATUS_REFUNDED`, `PAYMENT_TRANSACTION_CAPTURE_STATUS_PARTIALLY_REFUNDED`, `PAYMENT_TRANSACTION_CAPTURE_STATUS_FAILED`
* **Transfer:** `TRANSFER_SENT`, `TRANSFER_PARTIALLY_SENT`, `TRANSFER_FAILED`, `TRANSFER_UPDATED`, `TRANSFER_REVERSED`, `TRANSFER_PARTIALLY_REVERSED`, `TRANSFER_REVERSAL_FAILED`
* **Recovery case:** the docs list further `RECOVERY_CASE_*` events (see the events page)

The recovery case events are `RECOVERY_CASE_OPENED`, `RECOVERY_CASE_GAP_REMAINING`, `RECOVERY_CASE_RECOVERED` and `RECOVERY_CASE_EXHAUSTED`.

Order events carry the whole order, including all bags and SKUs, so read `X-Violet-Bag-Id` to see which bag changed. `MERCHANT_ENABLED` and `MERCHANT_DISABLED` carry the `Merchant` object, as in this example from Violet's [handling webhooks page](https://docs.violet.io/prism/webhooks/handling-webhooks):

```json
{
  "id": 10189,
  "name": "sample-store",
  "platform": "SHOPIFY",
  "store_url": "sample-store.myshopify.com",
  "custom_store_url": "sample-store.com",
  "status": "ACTIVE",
  "default_currency": "USD",
  "default_country_code": "US",
  "default_state_code": "WA",
  "default_language_code": "EN",
  "commission_rate": 10,
  "distribution_type": "PRIVATE",
  "referral_source": "VIOLET",
  "connection_status": "CONNECTED"
}

```

For merchant status changes, `X-Violet-Reason` may be `APP_INSTALLED_ON_PLATFORM`, `APP_UNINSTALLED_FROM_PLATFORM`, `MERCHANT_PLATFORM_PLAN_INACTIVE`, `MERCHANT_CREDENTIAL_INVALID` or `MERCHANT_PLATFORM_SCOPES_INVALID`.

Bodies always contain the full entity, however large. If you can't take a large body, `X-Violet-Entity-Length` tells you its size and you can fetch the event later by ID.

## Setting up Violet webhooks

1. Find your app secret on the overview page of the channel dashboard. Test Mode and Live Mode have different secrets. Store the one for this environment as `VIOLET_APP_SECRET`.
2. Create a webhook for each event:

```bash
curl -X POST https://sandbox-api.violet.io/v1/events/webhooks \
  -H "X-Violet-App-Id: your-app-id-here" \
  -H "X-Violet-App-Secret: your-app-secret-here" \
  -H "X-Violet-Token: your-token-here" \
  -H "Content-type: application/json" \
  -d '{"event": "ORDER_SHIPPED", "remote_endpoint": "https://example.com/webhooks/violet"}'

```

1. Repeat for each event, using `https://api.violet.io` for Live Mode.

The API can also list, modify, re-activate and remove webhooks, and `POST /v1/events/webhooks/{webhook_id}/headers` adds your own headers to every delivery (see [Managing Webhooks](https://docs.violet.io/prism/webhooks/managing-webhooks)).

To route through Hookdeck, create a Violet source with the app secret and use its URL as each `remote_endpoint`. For local development:

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

```

## Securing Violet webhooks

`X-Violet-Hmac` is the base64 HMAC-SHA256 of the raw body, keyed with the channel's app secret. The secret looks like hex but is used as UTF-8 text, as Violet's Java sample shows with `secret.getBytes(StandardCharsets.UTF_8)`. Violet's Python sample compares with `==`. Use a constant-time comparison instead.

Violet suggests queuing each request, responding, and then validating the HMAC in a worker. Checking the signature before responding, as below, is quick enough to fit in the 10 seconds and keeps unsigned requests out of your queue.

### Node.js verification

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

function decodeBase64(value) {
  if (typeof value !== 'string' || !/^[A-Za-z0-9+/]+={0,2}$/.test(value)) return null;
  const bytes = Buffer.from(value, 'base64');
  return bytes.length > 0 && bytes.toString('base64').replace(/=+$/, '') === value.replace(/=+$/, '')
    ? bytes : null;
}

function verifySignature(rawBody, headers, secret) {
  if (!Buffer.isBuffer(rawBody) || typeof secret !== 'string' || !secret) return false;
  const signature = headers['x-violet-hmac'];
  if (typeof signature !== 'string') return false;
  const key = Buffer.from(secret, 'utf8');
  const message = rawBody;
  const candidates = [signature];
  const expected = crypto.createHmac('sha256', key).update(message).digest();
  return candidates.some((candidate) => {
    const received = decodeBase64(candidate);
    if (!received) return false;
    return received.length === expected.length && crypto.timingSafeEqual(received, expected);
  });
}

```

The handler reads the topic and event ID from the headers and stores the entity under that ID.

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

const app = express();

app.post('/webhooks/violet', express.raw({ type: '*/*' }), async (req, res) => {
  const secret = process.env.VIOLET_APP_SECRET;
  if (!secret) return res.sendStatus(500);
  if (!verifySignature(req.body, req.headers, secret)) return res.sendStatus(401);

  const topic = req.headers['x-violet-topic'];
  const eventId = req.headers['x-violet-event-id'];
  if (typeof topic !== 'string' || typeof eventId !== 'string') return res.sendStatus(400);

  let entity;
  try {
    entity = JSON.parse(req.body.toString('utf8'));
  } catch {
    return res.sendStatus(400);
  }
  if (!entity || typeof entity !== 'object' || Array.isArray(entity)) return res.sendStatus(400);

  try {
    if (await alreadyHandled(eventId)) return res.sendStatus(200);
    await queue.add('violet-event', {
      eventId,
      topic,
      entityId: req.headers['x-violet-entity-id'],
      bagId: req.headers['x-violet-bag-id'],
      reason: req.headers['x-violet-reason'],
      entity,
    });
    await markHandled(eventId);
  } catch {
    return res.sendStatus(503);
  }
  res.sendStatus(200);
});

```

`queue`, `alreadyHandled` and `markHandled` are placeholders for your queue and dedupe storage.

A Hookdeck Violet source verifies requests the same way. Violet gets a `200` back either way, and a mismatch is recorded on the request as failed verification. With no timestamp in Violet's signature, Hookdeck applies no age limit either.

> **Make Violet webhooks production-ready.** [Hookdeck Event Gateway](/event-gateway) acknowledges Violet within its 10-second window, so your outages don't pause or disable the webhook, and holds each order and merchant update until your service is ready.

## Violet webhook limitations and pain points

### Failures pause, then disable, the webhook

**The Problem:** A sustained outage stops delivery, and once the webhook is disabled, events aren't even created.

**Why It Happens:** More than 50 failures in 30 minutes pauses delivery for 1 hour, then 3 hours, then up to 24 hours. After three pauses the webhook is `DISABLED` until you re-activate it, and no events are created in the meantime.

**Workarounds:**

* Queue and respond within 10 seconds, as Violet recommends.
* Act on Violet's emails about pauses, and re-activate disabled webhooks through the API.

**How Hookdeck Can Help:** Hookdeck accepts every request, so Violet never sees failures from your side, and retries your service on its own schedule.

### Events fail after 24 hours

**The Problem:** An event that can't be delivered within a day is marked `FAILED`.

**Why It Happens:** Violet makes 10 attempts with back-off over 24 hours.

**Workarounds:**

* Retrieve missed events by ID through Violet's API.
* Reconcile orders after any long outage.

**How Hookdeck Can Help:** Hookdeck stores each request on arrival, with no 24-hour cut-off for retrying or replaying it to your service.

### The event lives in headers

**The Problem:** Tools that only keep the body lose the event name and ID.

**Why It Happens:** The body is the bare entity, and `X-Violet-Topic` and `X-Violet-Event-Id` carry the metadata.

**Workarounds:**

* Store the headers with the body when you queue a request.
* Route on `X-Violet-Topic`, not on the entity's shape.

**How Hookdeck Can Help:** Hookdeck keeps the original headers on every request, and filters can route on `X-Violet-Topic`.

### One webhook per event, per mode

**The Problem:** A full integration needs dozens of webhooks, in both Test and Live Mode.

**Why It Happens:** Each webhook subscribes to a single event, and the two modes have separate hosts and secrets.

**Workarounds:**

* Script webhook creation through the API.
* Point all of a mode's webhooks at one URL and keep the secret per environment.

**How Hookdeck Can Help:** Use one Hookdeck source per mode, each with its own app secret, and route from there.

## Best practices

### Deduplicate on X-Violet-Event-Id

Violet provides it for idempotency. For resent events, `X-Violet-Event-Id-Source` holds the original ID. See the [idempotency guide](/webhooks/guides/implement-webhook-idempotency).

### Use the bag ID

An order can contain several bags, and `X-Violet-Bag-Id` tells you which one changed.

### Keep merchant state from reasons

Store `X-Violet-Reason` with merchant status changes, so support can see why a merchant was disabled.

### Queue before processing

Violet's own advice is to queue, respond and process in a separate thread. [Processing webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks) explains the pattern.

## Conclusion

Violet posts the full entity for each subscribed event, with the event name and ID in `X-Violet-*` headers and a base64 HMAC-SHA256 keyed by the app secret. Respond within 10 seconds and deduplicate on `X-Violet-Event-Id`, because sustained failures pause and then disable the webhook.

[Hookdeck Event Gateway](https://hookdeck.com) verifies Violet requests, keeps their headers and retries delivery to your services.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and handle Violet webhooks reliably in minutes.