# 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`, `invoiced` or `canceled`?
* Why does my VTEX hook endpoint return 401, or why did VTEX stop sending notifications?

## Verification (core)

```javascript
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 from `VTEX_WEBHOOK_HEADER` (default `X-Webhook-Secret`) and the
  **exact, whole value** from `VTEX_WEBHOOK_SECRET`, including any `Bearer ` prefix 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 a `200`, or the hook can't be saved. Reply
  `200` to the ping **before** the auth check and do nothing with it (the docs don't say whether
  the ping carries your headers).
* Return `401` on a missing or wrong header, `400` for a body that isn't a JSON object, `200` once
  handled (VTEX needs a `200` within 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/](https://github.com/hookdeck/webhook-skills/tree/main/skills/vtex-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/vtex-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/vtex-webhooks/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

```bash
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):

```bash
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](https://github.com/hookdeck/webhook-skills/blob/main/skills/vtex-webhooks/references/overview.md) - What VTEX order hooks are, statuses and payload shape
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/vtex-webhooks/references/setup.md) - Configure the hook by API with a secret header
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/vtex-webhooks/references/verification.md) - Header check details, gotchas, debugging 401s
* [examples/](https://github.com/hookdeck/webhook-skills/tree/main/skills/vtex-webhooks/examples/) - Runnable Express, Next.js and FastAPI handlers with tests