Agent skill
VTEX Webhooks Skill
Receive and authenticate VTEX Orders API hook webhooks. Use when setting up a VTEX order hook endpoint, checking the secret header you add in the hook configuration, answering the hookConfig ping, debugging 401s, or handling order status changes like order-created, payment-approved, ready-for-handling, invoiced and canceled.
Install this skill
npx skills add hookdeck/webhook-skills --skill vtex-webhooks
VTEX's Orders API Hook (part of Feed v3 and Hook) POSTs a small JSON notification to your URL every time an order changes status and matches your filter. Hooks are configured by API, one per appKey. Requests are not signed. VTEX does send whatever you put in the headers object of the hook configuration, so you add a secret header yourself and your endpoint checks it.
When to Use This Skill
- How do I receive VTEX order webhooks?
- How do I authenticate a VTEX hook if there is no signature?
- How do I answer the
{"hookConfig": "ping"}request VTEX sends when I save the hook? - How do I handle
order-created,payment-approved,ready-for-handling,invoicedorcanceled? - Why does my VTEX hook endpoint return 401, or why did VTEX stop sending notifications?
Verification (core)
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 verifyVtexRequest(headers, name = process.env.VTEX_WEBHOOK_HEADER || 'X-Webhook-Secret',
secret = process.env.VTEX_WEBHOOK_SECRET) {
if (!secret) return false; // never accept when unconfigured
const received = headers[name.toLowerCase()]; // Node lower-cases header names
return Boolean(received) && safeEqual(received, secret); // whole value, no Bearer parsing
}
- You choose the header in the hook configuration, e.g.
"headers": {"X-Webhook-Secret": "<secret>"}. The examples read its name fromVTEX_WEBHOOK_HEADER(defaultX-Webhook-Secret) and the exact, whole value fromVTEX_WEBHOOK_SECRET, including anyBearerprefix you typed. Look the header up case-insensitively and compare the whole value; do not strip or parse a prefix. - On setup VTEX POSTs
{"hookConfig": "ping"}and needs a200, or the hook can't be saved. Reply200to the ping before the auth check and do nothing with it (the docs don't say whether the ping carries your headers). - Return
401on a missing or wrong header,400for a body that isn't a JSON object,200once handled (VTEX needs a200within 5000 ms). - Read the body raw (
express.raw({ type: '*/*' })) and parse JSON only after the credential check.
A static shared credential authenticates the sender, not the body, and gives no replay protection (there is no signature and no timestamp). Use HTTPS only, and rotate the credential (re-run the hook configuration call and update VTEX_WEBHOOK_SECRET) if it leaks.
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.
Event Types
The documented notification body is {Domain, OrderId, State, LastChange, Origin: {Account, Key}}. State is the order status (the API status names from VTEX's order flow table); dispatch on it.
State | Meaning |
|---|---|
order-created | The order was created |
on-order-completed | The purchase was processed at checkout and the order data is complete |
payment-pending | Waiting for payment |
payment-approved | Payment approved |
waiting-ffmt-authorization | Waiting for fulfillment authorization |
ready-for-handling | Ready to be handled by the seller |
start-handling | Handling is about to start |
handling | The order is being handled |
invoiced | An invoice was issued |
cancel | Cancellation workflow status |
canceled | The order was canceled |
See VTEX's order flow documentation for the full status list; unknown states should still get a 200.
Environment Variables
VTEX_WEBHOOK_HEADER=X-Webhook-Secret # header name you put in the hook "headers" object
VTEX_WEBHOOK_SECRET=your-secret-value # exact whole header value
Local Development
VTEX can't reach localhost. Tunnel with the Hookdeck CLI (no account required):
npx hookdeck-cli listen 3000 vtex --path /webhooks/vtex
Use the printed URL as hook.url in the VTEX hook configuration.
Resources
- references/overview.md - What VTEX order hooks are, statuses and payload shape
- references/setup.md - Configure the hook by API with a secret header
- references/verification.md - Header check details, gotchas, debugging 401s
- examples/ - Runnable Express, Next.js and FastAPI handlers with tests