Guide to Violet Webhooks: Features and Best Practices
Violet 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:
| 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_, 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:
{
"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_, APP_, MERCHANT_, MERCHANT_ or MERCHANT_.
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
- 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. - Create a webhook for each event:
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"}'
- Repeat for each event, using
https://api.violet.iofor Live Mode.
The API can also list, modify, re-activate and remove webhooks, and POST /v1/events/webhooks/{webhook_ adds your own headers to every delivery (see 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:
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_. 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
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.
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 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.
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 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 verifies Violet requests, keeps their headers and retries delivery to your services.
Get started with Hookdeck for free and handle Violet webhooks reliably in minutes.