Gareth Wilson Gareth Wilson

Guide to VTEX Webhooks: Features and Best Practices

Published


VTEX webhooks tell you when an order changes. The Orders API hook calls your URL each time an order moves into a status you care about, such as payment-approved, ready-for-handling or invoiced, so your ERP, warehouse system or middleware can pick it up without polling. The hook is the push counterpart of the Orders feed, which holds the same updates in a queue for you to fetch.

This guide covers how the hook is configured, what a notification contains, how to authenticate it with a header you add yourself, the configuration ping, the retry and expiry rules, and the best practices for running it in production.

What are VTEX webhooks?

VTEX has two ways to follow order updates: Feed v3 and Hook. The feed keeps updates in a queue that your integration reads and commits. The hook sends each update to a URL you give it as soon as it happens. Both use the same filter, so the choice is between pulling at your own pace and being pushed to as updates happen.

The hook has no settings page in the VTEX Admin. You create it with a call to the Orders API, and each appKey can have exactly one. Configuring a hook again with the same appKey replaces the existing one, so if several integrations share a key, they share a hook too. VTEX recommends one hook per appKey per user.

VTEX doesn't sign hook notifications. The hook configuration has a headers object that VTEX describes as the credentials to use when calling your endpoint, so you add a secret header there and your receiver checks it.

VTEX webhook features

FeatureDetails
ConfigurationAPI only: Create or update hook configuration on the Orders API
Hooks per keyOne per appKey. Configuring again replaces it
FilterFromWorkflow (a list of statuses) or FromOrders (a JSONata expression)
MethodPOST with a JSON body
Event fieldState, the order's new status
AuthenticationHeaders you set in hook.headers
SignatureNone
Setup checkA {"hookConfig": "ping"} request that must get a 200
Response200 within 5,000 ms
RetriesIntervals from an internal geometric progression
ExpiryRemoved after three days with no notifications
Source IPsNot published

Order statuses and the payload

There's no separate event type. A notification carries the order's new status in State, and the values are VTEX's order statuses. Common ones include:

StateMeaning
order-createdThe order was created
payment-pendingWaiting for payment
payment-approvedPayment was approved
waiting-ffmt-authorizationWaiting for fulfilment authorisation
ready-for-handlingReady for the seller to handle
start-handlingHandling is about to start
handlingThe order is being handled
invoicedAn invoice was issued
cancelThe order entered the cancellation flow
canceledThe order was cancelled

The full list is in VTEX's order flow documentation, and it differs slightly between marketplace and seller flows, so handle values you don't recognise without failing. If you use a FromOrders filter, a notification can fire on a change that isn't a status change, and State then holds the unchanged status.

The body is deliberately small:

{
  "Domain": "Marketplace",
  "OrderId": "v40484048naf-01",
  "State": "payment-approved",
  "LastChange": "2019-07-29T23:17:30.0617185Z",
  "Origin": {
    "Account": "accountABC",
    "Key": "vtexappkey-keyEDF"
  }
}

OrderId identifies the order, LastChange is when it changed, and Origin names the account and the appKey the hook belongs to. Nothing else about the order is included, so fetch it from the Orders API when you need the items, customer or totals. Origin.Key is an identifier, not a secret.

See VTEX webhook payloads in action. Inspect and replay sample VTEX webhook payloads in Hookdeck Inspect, with no account or setup required.

Setting up VTEX webhooks

You'll need an API key (appKey and appToken) whose role includes the Feed v3 and Hook Admin resource in Order Management, and an endpoint that's already running, because VTEX checks it while saving.

Generate a long random value for your secret header:

openssl rand -hex 32

Then call Create or update hook configuration with a filter and the hook details:

curl -X POST "https://{accountName}.{environment}.com.br/api/orders/hook/config" \
  -H "Content-Type: application/json" \
  -H "X-VTEX-API-AppKey: $VTEX_APP_KEY" \
  -H "X-VTEX-API-AppToken: $VTEX_APP_TOKEN" \
  -d '{
    "filter": {
      "type": "FromWorkflow",
      "status": ["payment-approved", "ready-for-handling", "invoiced", "canceled"]
    },
    "hook": {
      "url": "https://example.com/webhooks/vtex",
      "headers": { "X-Webhook-Secret": "<your random value>" }
    }
  }'

FromWorkflow fires when an order moves into one of the listed statuses, and leaving the filter out sends every status change. FromOrders takes a JSONata expression instead, so you can filter on any field of the order document, with disableSingleFire controlling whether an order can match more than once. The two filter types can't be combined; sending fields from both returns a 409.

When you save, VTEX sends a POST with the body {"hookConfig": "ping"} to your URL. If it doesn't get a 200, the configuration call returns 400 and the hook isn't saved. VTEX's docs don't say whether the ping carries the headers from hook.headers, so don't rely on it being authenticated: answer it with a 200 and do nothing else with it.

Once the hook is saved, keep an eye on it. A hook with no notifications for three days is removed, and you have to configure it again. A narrow filter on a quiet store can hit that limit, so check it with Get hook configuration and recreate it when it's gone.

For local development, use the Hookdeck CLI: hookdeck listen 3000 vtex --path /webhooks/vtex gives you a public HTTPS URL that forwards to your local server, plus a web UI for inspecting and replaying deliveries, with no account required. Use the generated URL as hook.url.

With Hookdeck. Create a VTEX source in Hookdeck and enter the Header Name and Header Value exactly as they appear in hook.headers, including any Bearer prefix. Use the source URL as hook.url. Hookdeck responds to the configuration ping with a 200 whether or not it carries your header, so VTEX can save the hook. If the ping arrives without the header, it shows in Hookdeck as a request that failed verification. If it arrives with it, it's delivered to your destination like any other request, so your handler should still acknowledge {"hookConfig": "ping"} and ignore it.

Securing VTEX webhooks

Check the header first. Look it up case-insensitively, compare the whole value in constant time, and check lengths first because crypto.timingSafeEqual throws on buffers of different lengths. The one request you let through without the header is the configuration ping, and only to answer it.

const crypto = require('crypto');
const express = require('express');

const app = express();

function safeEqual(a, b) {
  const x = Buffer.from(String(a), 'utf8');
  const y = Buffer.from(String(b), 'utf8');
  return x.length === y.length && crypto.timingSafeEqual(x, y);
}

function verifyVtexRequest(headers) {
  const secret = process.env.VTEX_WEBHOOK_SECRET;
  if (!secret) return false; // fail closed when unconfigured
  const name = (process.env.VTEX_WEBHOOK_HEADER || 'X-Webhook-Secret').toLowerCase();
  const received = headers[name];
  return typeof received === 'string' && safeEqual(received, secret);
}

function parseJson(buffer) {
  try {
    return JSON.parse(buffer.toString('utf8'));
  } catch {
    return null;
  }
}

app.post('/webhooks/vtex', express.raw({ type: '*/*' }), (req, res) => {
  const body = parseJson(req.body);

  // The configuration ping may or may not carry your header. Answer it and stop.
  if (body && body.hookConfig === 'ping') {
    return res.sendStatus(200);
  }

  if (!verifyVtexRequest(req.headers)) {
    return res.sendStatus(401);
  }
  if (!body || typeof body.OrderId !== 'string') {
    return res.sendStatus(400);
  }

  res.sendStatus(200); // VTEX wants a 200 within 5,000 ms

  // Queue the work: fetch body.OrderId from the Orders API and act on body.State.
});

The same check in Python:

import hmac
import os


def verify_vtex_request(headers) -> bool:
    secret = os.environ.get("VTEX_WEBHOOK_SECRET")
    if not secret:
        return False
    name = os.environ.get("VTEX_WEBHOOK_HEADER", "X-Webhook-Secret")
    received = headers.get(name)  # framework header maps are case-insensitive
    if not received:
        return False
    return hmac.compare_digest(received.encode("utf-8"), secret.encode("utf-8"))

A static header authenticates the sender, but it doesn't protect the body, and a captured request can be replayed because nothing is signed or timestamped. Use HTTPS for hook.url and keep the value out of logs. Because the body only points at an order, treat the Orders API as the source of truth: fetch the order and check its status before acting on anything with financial consequences.

To rotate the value, call the hook configuration endpoint again with the new header and update your receiver at the same moment. Notifications sent with the old value in between will fail and be retried.

Make VTEX webhooks production-ready. Hookdeck Event Gateway checks your header on every notification, answers VTEX with a 200 as soon as it's stored, and feeds your ERP at a rate it can handle.

VTEX webhook limitations and pain points

A five-second response window

The Problem: Notifications fail when your endpoint is slow, even though it would have processed them.

Why It Happens: VTEX expects a 200 within 5,000 ms. Anything slower, or any other status, counts as a failed notification. The docs say "status 200" rather than any 2xx, so don't answer with 202 or 204.

Workarounds:

  • Acknowledge with 200 as soon as the request is authenticated and stored.
  • Fetch the order and update downstream systems in a background job.

How Hookdeck Can Help: Hookdeck replies to VTEX with a 200 as soon as the notification is stored, then delivers it to your service with its own delivery timeout and automatic retries, so a slow handler doesn't turn into failed notifications at VTEX.

Sales peaks

The Problem: A promotion or Black Friday floods your middleware or ERP with notifications.

Why It Happens: The hook is reactive. It sends every matching update as it happens, and VTEX's own docs warn that an integration that can't scale with peaks can stop the store handling orders properly. They suggest middleware that buffers notifications and passes them on at the rate your ERP can take, or using the feed as a backup.

Workarounds:

  • Put a queue between the hook and your ERP.
  • Keep the feed configured as a fallback you can drain when the hook path is struggling.

How Hookdeck Can Help: Hookdeck can be that buffer. Set a max delivery rate on the destination and notifications queue in Hookdeck during a peak, then reach your ERP at a steady rate. Issues alert you when deliveries start failing.

Hooks expire, and there's only one per key

The Problem: Notifications stop and nothing tells you why.

Why It Happens: A hook with no notifications for three days is removed. And because each appKey has one hook, configuring a hook for a second integration with the same key silently replaces the first.

Workarounds:

  • Use a separate appKey for each integration.
  • Check the hook with Get hook configuration on a schedule and recreate it when it's missing.
  • Keep the filter broad enough that a quiet weekend doesn't leave it idle for three days.

How Hookdeck Can Help: Hookdeck can't keep a hook alive, but every notification is stored with a timestamp, so a gap in arrivals is easy to spot. Because Hookdeck can fan one source out to several destinations, several services can share one hook without needing a key each.

No signature

The Problem: Anyone who knows the URL and the header value can post a fake order update.

Why It Happens: VTEX's only authentication option for the hook is the static headers you configure. It publishes no source IP addresses either.

Workarounds:

  • Use a long random value, HTTPS, and rotate it if it leaks.
  • Fetch the order from the API before acting on the notification.

How Hookdeck Can Help: Hookdeck's VTEX source compares the whole header value with the one you paste. A request without a matching header still gets a 200 response, but Hookdeck marks it as failed verification and creates no event, so it never reaches your destination.

Best practices

Use the notification as a trigger

The body only says which order changed and to what. Fetch the order from the Orders API for anything you act on.

Make handlers idempotent

Retries can deliver the same notification more than once. Use OrderId, State and LastChange together as a key and skip repeats. Hookdeck's deduplication can drop exact duplicates before they reach you. See our guide to webhook idempotency.

Don't rely on order of arrival

Retries can make an older update arrive after a newer one. Compare LastChange with what you've stored, or read the order's current status from the API, before moving an order backwards.

Filter at the source

Use a FromWorkflow status list or a FromOrders expression so VTEX only sends what you need. Test JSONata expressions against a real order before you put them in the hook.

Acknowledge fast

Return 200 before calling the Orders API or your ERP, and do that work in the background. See why to process webhooks asynchronously.

Conclusion

The VTEX Orders API hook sends a small, unsigned JSON notification each time an order moves into a status that matches your filter. Authenticate it with a secret header you add to hook.headers, answer the configuration ping with a 200, route on State, fetch the order for details, and respond within five seconds. Watch for the three-day expiry and the one-hook-per-key rule, since either can stop notifications without warning.

Hookdeck Event Gateway checks the header, answers VTEX straight away, and buffers peaks so your ERP receives orders at a pace it can handle.

Get started with Hookdeck for free and handle VTEX webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.