marketplace-order-hook

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Order Integration & Webhooks

订单集成与Webhook

Overview

概述

What this skill covers: VTEX order Feed v3 and Hook configuration for marketplace integrations, including webhook setup, order status lifecycle, payload structure, authentication validation, and idempotent event processing.
When to use it: When building an integration that needs to react to order status changes in a VTEX marketplace — such as syncing orders to an ERP, triggering fulfillment workflows, or sending notifications to external systems.
What you'll learn:
  • How to configure Feed v3 and Hook for order updates
  • The difference between Feed (pull) and Hook (push) and when to use each
  • How to validate webhook authentication and process events idempotently
  • How to handle the complete order status lifecycle
本技能涵盖内容:用于市场集成的VTEX订单Feed v3和Hook配置,包括Webhook设置、订单状态生命周期、负载结构、身份验证以及幂等事件处理。
适用场景:当您需要构建一个能响应VTEX市场中订单状态变化的集成时,例如将订单同步到ERP、触发履约工作流或向外部系统发送通知。
您将学到
  • 如何为订单更新配置Feed v3和Hook
  • Feed(拉取)与Hook(推送)的区别及适用场景
  • 如何验证Webhook身份并实现事件的幂等处理
  • 如何处理完整的订单状态生命周期

Key Concepts

核心概念

Essential knowledge before implementation:
实施前需掌握的关键知识

Concept 1: Feed vs. Hook

概念1:Feed与Hook的对比

VTEX provides two mechanisms for consuming order updates:
  • Feed (pull model): A queue of order update events. Your integration periodically calls
    GET /api/orders/feed
    to retrieve events, processes them, then commits them via
    POST /api/orders/feed
    . You control the pace.
  • Hook (push model): VTEX sends order update events to your endpoint via POST whenever an event matches your filter. Your endpoint must respond with HTTP 200 within 5000ms.
Key differences:
FeedHook
ModelPull (active)Push (reactive)
ScalabilityYou control volumeMust handle any volume
ReliabilityEvents persist in queueMust be always available
Best forERPs with limited throughputHigh-performance middleware
Each appKey can configure only one feed and one hook. Different users sharing the same appKey access the same feed/hook.
VTEX提供两种方式来获取订单更新:
  • Feed(拉取模式):订单更新事件队列。您的集成需定期调用
    GET /api/orders/feed
    获取事件,处理完成后通过
    POST /api/orders/feed
    提交。您可以控制处理节奏。
  • Hook(推送模式):当事件匹配您设置的过滤器时,VTEX会通过POST请求将订单更新事件发送到您的端点。您的端点必须在5000ms内返回HTTP 200响应。
主要差异:
FeedHook
模式主动拉取被动推送
扩展性您控制处理量必须能处理任意量级的请求
可靠性事件持久化在队列中端点必须始终可用
最佳适用场景吞吐量有限的ERP系统高性能中间件
每个appKey只能配置一个Feed和一个Hook。共享同一appKey的不同用户将访问同一个Feed/Hook。

Concept 2: Filter Types

概念2:过滤器类型

Both Feed and Hook support two filter types:
FromWorkflow — Filter by order status changes only:
json
{
  "filter": {
    "type": "FromWorkflow",
    "status": ["ready-for-handling", "handling", "invoiced", "cancel"]
  }
}
FromOrders — Filter by any order property using JSONata expressions:
json
{
  "filter": {
    "type": "FromOrders",
    "expression": "status = \"ready-for-handling\" and salesChannel = \"2\"",
    "disableSingleFire": false
  }
}
The two filter types are mutually exclusive. Using both in the same configuration request returns
409 Conflict
.
Feed和Hook均支持两种过滤器类型:
FromWorkflow — 仅按订单状态变化过滤:
json
{
  "filter": {
    "type": "FromWorkflow",
    "status": ["ready-for-handling", "handling", "invoiced", "cancel"]
  }
}
FromOrders — 使用JSONata表达式按任意订单属性过滤:
json
{
  "filter": {
    "type": "FromOrders",
    "expression": "status = \"ready-for-handling\" and salesChannel = \"2\"",
    "disableSingleFire": false
  }
}
这两种过滤器类型互斥。在同一配置请求中同时使用两者会返回
409 Conflict
错误。

Concept 3: Hook Notification Payload

概念3:Hook通知负载

When a Hook fires, VTEX sends a POST to your endpoint with this structure:
json
{
  "Domain": "Marketplace",
  "OrderId": "v40484048naf-01",
  "State": "payment-approved",
  "LastChange": "2019-07-29T23:17:30.0617185Z",
  "Origin": {
    "Account": "accountABC",
    "Key": "vtexappkey-keyEDF"
  }
}
The payload contains only the order ID and state — not the full order data. Your integration must call
GET /api/oms/pvt/orders/{orderId}
to retrieve complete order details.
Architecture/Data Flow:
text
VTEX OMS                           Your Integration
   │                                       │
   │── Order status change ──────────────▶│  (Hook POST to your URL)
   │                                       │── Validate auth headers
   │                                       │── Check idempotency (orderId + State)
   │◀── GET /api/oms/pvt/orders/{id} ─────│  (Fetch full order)
   │── Full order data ──────────────────▶│
   │                                       │── Process order
   │◀── HTTP 200 ─────────────────────────│  (Must respond within 5000ms)
当Hook触发时,VTEX会向您的端点发送如下结构的POST请求:
json
{
  "Domain": "Marketplace",
  "OrderId": "v40484048naf-01",
  "State": "payment-approved",
  "LastChange": "2019-07-29T23:17:30.0617185Z",
  "Origin": {
    "Account": "accountABC",
    "Key": "vtexappkey-keyEDF"
  }
}
该负载仅包含订单ID和状态,不包含完整订单数据。您的集成需调用
GET /api/oms/pvt/orders/{orderId}
来获取完整的订单详情。
架构/数据流
text
VTEX OMS                           您的集成系统
   │                                       │
   │── 订单状态变化 ──────────────▶│  (Hook向您的URL发送POST请求)
   │                                       │── 验证身份验证头
   │                                       │── 检查幂等性(orderId + State)
   │◀── GET /api/oms/pvt/orders/{id} ─────│  (获取完整订单数据)
   │── 完整订单数据 ──────────────────▶│
   │                                       │── 处理订单
   │◀── HTTP 200 ─────────────────────────│  (必须在5000ms内响应)

Constraints

约束条件

Rules that MUST be followed to avoid failures, security issues, or platform incompatibilities.
为避免失败、安全问题或平台兼容性问题,必须遵守以下规则

Constraint: Validate Webhook Authentication

约束:验证Webhook身份

Rule: Your hook endpoint MUST validate the authentication headers sent by VTEX before processing any event. The
Origin.Account
and
Origin.Key
fields in the payload must match your expected values.
Why: Without auth validation, any actor can send fake order events to your endpoint, triggering unauthorized fulfillment actions, data corruption, or financial losses.
Detection: If you see a hook endpoint handler that processes events without checking
Origin.Account
,
Origin.Key
, or custom headers → STOP and add authentication validation.
CORRECT:
typescript
import { RequestHandler } from "express";

interface HookPayload {
  Domain: string;
  OrderId: string;
  State: string;
  LastChange: string;
  Origin: {
    Account: string;
    Key: string;
  };
}

interface HookConfig {
  expectedAccount: string;
  expectedAppKey: string;
  customHeaderKey: string;
  customHeaderValue: string;
}

function createHookHandler(config: HookConfig): RequestHandler {
  return async (req, res) => {
    const payload: HookPayload = req.body;

    // Handle VTEX ping during hook configuration
    if (payload && "hookConfig" in payload) {
      res.status(200).json({ success: true });
      return;
    }

    // Validate Origin credentials
    if (
      payload.Origin?.Account !== config.expectedAccount ||
      payload.Origin?.Key !== config.expectedAppKey
    ) {
      console.error("Unauthorized hook event", {
        receivedAccount: payload.Origin?.Account,
        receivedKey: payload.Origin?.Key,
      });
      res.status(401).json({ error: "Unauthorized" });
      return;
    }

    // Validate custom header (configured during hook setup)
    if (req.headers[config.customHeaderKey.toLowerCase()] !== config.customHeaderValue) {
      console.error("Invalid custom header");
      res.status(401).json({ error: "Unauthorized" });
      return;
    }

    // Process the event
    await processOrderEvent(payload);
    res.status(200).json({ success: true });
  };
}

async function processOrderEvent(payload: HookPayload): Promise<void> {
  console.log(`Processing order ${payload.OrderId} in state ${payload.State}`);
}
WRONG:
typescript
// WRONG: No authentication validation — accepts events from anyone
const unsafeHookHandler: RequestHandler = async (req, res) => {
  const payload: HookPayload = req.body;

  // Directly processing without checking Origin or headers
  // Any actor can POST fake events and trigger unauthorized actions
  await processOrderEvent(payload);
  res.status(200).json({ success: true });
};

规则:在处理任何事件之前,您的Hook端点必须验证VTEX发送的身份验证头。负载中的
Origin.Account
Origin.Key
字段必须与您预期的值匹配。
原因:如果不进行身份验证,任何主体都可以向您的端点发送伪造的订单事件,触发未授权的履约操作、数据损坏或财务损失。
检测方式:如果您发现Hook端点处理器在处理事件前未检查
Origin.Account
Origin.Key
或自定义头,请立即停止并添加身份验证逻辑。
正确示例
typescript
import { RequestHandler } from "express";

interface HookPayload {
  Domain: string;
  OrderId: string;
  State: string;
  LastChange: string;
  Origin: {
    Account: string;
    Key: string;
  };
}

interface HookConfig {
  expectedAccount: string;
  expectedAppKey: string;
  customHeaderKey: string;
  customHeaderValue: string;
}

function createHookHandler(config: HookConfig): RequestHandler {
  return async (req, res) => {
    const payload: HookPayload = req.body;

    // 处理Hook配置期间的VTEX ping请求
    if (payload && "hookConfig" in payload) {
      res.status(200).json({ success: true });
      return;
    }

    // 验证Origin凭证
    if (
      payload.Origin?.Account !== config.expectedAccount ||
      payload.Origin?.Key !== config.expectedAppKey
    ) {
      console.error("未授权的Hook事件", {
        receivedAccount: payload.Origin?.Account,
        receivedKey: payload.Origin?.Key,
      });
      res.status(401).json({ error: "Unauthorized" });
      return;
    }

    // 验证自定义头(在Hook设置期间配置)
    if (req.headers[config.customHeaderKey.toLowerCase()] !== config.customHeaderValue) {
      console.error("无效的自定义头");
      res.status(401).json({ error: "Unauthorized" });
      return;
    }

    // 处理事件
    await processOrderEvent(payload);
    res.status(200).json({ success: true });
  };
}

async function processOrderEvent(payload: HookPayload): Promise<void> {
  console.log(`Processing order ${payload.OrderId} in state ${payload.State}`);
}
错误示例
typescript
// 错误示例:无身份验证验证 — 接受来自任何主体的事件
const unsafeHookHandler: RequestHandler = async (req, res) => {
  const payload: HookPayload = req.body;

  // 直接处理事件,未检查Origin或头信息
  // 任何主体都可以发送伪造事件并触发未授权操作
  await processOrderEvent(payload);
  res.status(200).json({ success: true });
};

Constraint: Process Events Idempotently

约束:幂等处理事件

Rule: Your integration MUST process order events idempotently. Use the combination of
OrderId
+
State
+
LastChange
as a deduplication key to prevent duplicate processing.
Why: VTEX may deliver the same hook notification multiple times (at-least-once delivery). Without idempotency, duplicate processing can result in double fulfillment, duplicate invoices, or inconsistent state.
Detection: If you see an order event handler without an
orderId
duplicate check or deduplication mechanism → warn about idempotency. If the handler directly mutates state without checking if the event was already processed → warn.
CORRECT:
typescript
interface ProcessedEvent {
  orderId: string;
  state: string;
  lastChange: string;
  processedAt: Date;
}

// In-memory store for example — use Redis or database in production
const processedEvents = new Map<string, ProcessedEvent>();

function buildDeduplicationKey(payload: HookPayload): string {
  return `${payload.OrderId}:${payload.State}:${payload.LastChange}`;
}

async function idempotentProcessEvent(payload: HookPayload): Promise<boolean> {
  const deduplicationKey = buildDeduplicationKey(payload);

  // Check if this exact event was already processed
  if (processedEvents.has(deduplicationKey)) {
    console.log(`Event already processed: ${deduplicationKey}`);
    return false; // Skip — already handled
  }

  // Mark as processing (with TTL in production)
  processedEvents.set(deduplicationKey, {
    orderId: payload.OrderId,
    state: payload.State,
    lastChange: payload.LastChange,
    processedAt: new Date(),
  });

  try {
    await handleOrderStateChange(payload.OrderId, payload.State);
    return true;
  } catch (error) {
    // Remove from processed set so it can be retried
    processedEvents.delete(deduplicationKey);
    throw error;
  }
}

async function handleOrderStateChange(orderId: string, state: string): Promise<void> {
  switch (state) {
    case "ready-for-handling":
      await startOrderFulfillment(orderId);
      break;
    case "handling":
      await updateOrderInERP(orderId, "in_progress");
      break;
    case "invoiced":
      await confirmOrderShipped(orderId);
      break;
    case "cancel":
      await cancelOrderInERP(orderId);
      break;
    default:
      console.log(`Unhandled state: ${state} for order ${orderId}`);
  }
}

async function startOrderFulfillment(orderId: string): Promise<void> {
  console.log(`Starting fulfillment for ${orderId}`);
}

async function updateOrderInERP(orderId: string, status: string): Promise<void> {
  console.log(`Updating ERP: ${orderId}${status}`);
}

async function confirmOrderShipped(orderId: string): Promise<void> {
  console.log(`Confirming shipment for ${orderId}`);
}

async function cancelOrderInERP(orderId: string): Promise<void> {
  console.log(`Canceling order ${orderId} in ERP`);
}
WRONG:
typescript
// WRONG: No deduplication — processes every event even if already handled
async function processWithoutIdempotency(payload: HookPayload): Promise<void> {
  // If VTEX sends the same event twice, this creates duplicate records
  await database.insert("fulfillment_tasks", {
    orderId: payload.OrderId,
    state: payload.State,
    createdAt: new Date(),
  });

  // Duplicate fulfillment task created — items may ship twice
  await triggerFulfillment(payload.OrderId);
}

async function triggerFulfillment(orderId: string): Promise<void> {
  console.log(`Fulfilling ${orderId}`);
}

const database = {
  insert: async (table: string, data: Record<string, unknown>) => {
    console.log(`Inserting into ${table}:`, data);
  },
};

规则:您的集成必须对订单事件进行幂等处理。使用
OrderId
+
State
+
LastChange
的组合作为去重键,以避免重复处理。
原因:VTEX可能会多次发送同一个Hook通知(至少一次交付机制)。如果不做幂等处理,重复处理可能导致重复履约、重复开票或状态不一致。
检测方式:如果您发现订单事件处理器没有
orderId
重复检查或去重机制,请提示幂等性问题。如果处理器在未检查事件是否已处理的情况下直接修改状态,请发出警告。
正确示例
typescript
interface ProcessedEvent {
  orderId: string;
  state: string;
  lastChange: string;
  processedAt: Date;
}

// 示例使用内存存储 — 生产环境请使用Redis或数据库
const processedEvents = new Map<string, ProcessedEvent>();

function buildDeduplicationKey(payload: HookPayload): string {
  return `${payload.OrderId}:${payload.State}:${payload.LastChange}`;
}

async function idempotentProcessEvent(payload: HookPayload): Promise<boolean> {
  const deduplicationKey = buildDeduplicationKey(payload);

  // 检查该事件是否已处理
  if (processedEvents.has(deduplicationKey)) {
    console.log(`事件已处理: ${deduplicationKey}`);
    return false; // 跳过 — 已处理
  }

  // 标记为处理中(生产环境需设置TTL)
  processedEvents.set(deduplicationKey, {
    orderId: payload.OrderId,
    state: payload.State,
    lastChange: payload.LastChange,
    processedAt: new Date(),
  });

  try {
    await handleOrderStateChange(payload.OrderId, payload.State);
    return true;
  } catch (error) {
    // 从已处理集合中移除,以便重试
    processedEvents.delete(deduplicationKey);
    throw error;
  }
}

async function handleOrderStateChange(orderId: string, state: string): Promise<void> {
  switch (state) {
    case "ready-for-handling":
      await startOrderFulfillment(orderId);
      break;
    case "handling":
      await updateOrderInERP(orderId, "in_progress");
      break;
    case "invoiced":
      await confirmOrderShipped(orderId);
      break;
    case "cancel":
      await cancelOrderInERP(orderId);
      break;
    default:
      console.log(`未处理状态: ${state} for order ${orderId}`);
  }
}

async function startOrderFulfillment(orderId: string): Promise<void> {
  console.log(`Starting fulfillment for ${orderId}`);
}

async function updateOrderInERP(orderId: string, status: string): Promise<void> {
  console.log(`Updating ERP: ${orderId}${status}`);
}

async function confirmOrderShipped(orderId: string): Promise<void> {
  console.log(`Confirming shipment for ${orderId}`);
}

async function cancelOrderInERP(orderId: string): Promise<void> {
  console.log(`Canceling order ${orderId} in ERP`);
}
错误示例
typescript
// 错误示例:无去重机制 — 即使事件已处理仍会重复处理
async function processWithoutIdempotency(payload: HookPayload): Promise<void> {
  // 如果VTEX多次发送同一事件,会创建重复记录
  await database.insert("fulfillment_tasks", {
    orderId: payload.OrderId,
    state: payload.State,
    createdAt: new Date(),
  });

  // 创建了重复的履约任务 — 商品可能会被重复发货
  await triggerFulfillment(payload.OrderId);
}

async function triggerFulfillment(orderId: string): Promise<void> {
  console.log(`Fulfilling ${orderId}`);
}

const database = {
  insert: async (table: string, data: Record<string, unknown>) => {
    console.log(`Inserting into ${table}:`, data);
  },
};

Constraint: Handle All Order Statuses

约束:处理所有订单状态

Rule: Your integration MUST handle all possible order statuses, including
Status Null
. Unrecognized statuses must be logged but not crash the integration.
Why: VTEX documents warn that
Status Null
may be unidentified and end up being mapped as another status, potentially leading to errors. Missing a status in your handler can cause orders to get stuck or lost.
Detection: If you see a status handler that only covers 2-3 statuses without a default/fallback case → warn about incomplete status handling.
CORRECT:
typescript
type OrderStatus =
  | "order-created"
  | "order-completed"
  | "on-order-completed"
  | "payment-pending"
  | "waiting-for-order-authorization"
  | "approve-payment"
  | "payment-approved"
  | "payment-denied"
  | "request-cancel"
  | "waiting-for-seller-decision"
  | "authorize-fulfillment"
  | "order-create-error"
  | "order-creation-error"
  | "window-to-cancel"
  | "ready-for-handling"
  | "start-handling"
  | "handling"
  | "invoice-after-cancellation-deny"
  | "order-accepted"
  | "invoiced"
  | "cancel"
  | "canceled";

async function handleAllStatuses(orderId: string, state: string): Promise<void> {
  switch (state) {
    case "ready-for-handling":
    case "start-handling":
      await notifyWarehouse(orderId, "prepare");
      break;

    case "handling":
      await updateFulfillmentStatus(orderId, "in_progress");
      break;

    case "invoiced":
      await markAsShipped(orderId);
      break;

    case "cancel":
    case "canceled":
    case "request-cancel":
      await handleCancellation(orderId, state);
      break;

    case "payment-approved":
      await confirmPaymentReceived(orderId);
      break;

    case "payment-denied":
      await handlePaymentFailure(orderId);
      break;

    default:
      // CRITICAL: Log unknown statuses instead of crashing
      console.warn(`Unknown or unhandled order status: "${state}" for order ${orderId}`);
      await logUnhandledStatus(orderId, state);
      break;
  }
}

async function notifyWarehouse(orderId: string, action: string): Promise<void> {
  console.log(`Warehouse notification: ${orderId}${action}`);
}
async function updateFulfillmentStatus(orderId: string, status: string): Promise<void> {
  console.log(`Fulfillment status: ${orderId}${status}`);
}
async function markAsShipped(orderId: string): Promise<void> {
  console.log(`Shipped: ${orderId}`);
}
async function handleCancellation(orderId: string, state: string): Promise<void> {
  console.log(`Cancellation: ${orderId} (${state})`);
}
async function confirmPaymentReceived(orderId: string): Promise<void> {
  console.log(`Payment received: ${orderId}`);
}
async function handlePaymentFailure(orderId: string): Promise<void> {
  console.log(`Payment failed: ${orderId}`);
}
async function logUnhandledStatus(orderId: string, state: string): Promise<void> {
  console.log(`UNHANDLED: ${orderId}${state}`);
}
WRONG:
typescript
// WRONG: Only handles 2 statuses, no fallback for unknown statuses
async function incompleteHandler(orderId: string, state: string): Promise<void> {
  if (state === "ready-for-handling") {
    await startOrderFulfillment(orderId);
  } else if (state === "invoiced") {
    await confirmOrderShipped(orderId);
  }
  // All other statuses silently ignored — orders get lost
  // "cancel" events never processed — canceled orders still ship
  // "Status Null" could be misinterpreted
}
规则:您的集成必须处理所有可能的订单状态,包括
Status Null
。对于未识别的状态,必须记录日志但不能导致集成崩溃。
原因:VTEX文档提示
Status Null
可能是未识别状态,可能会被映射为其他状态,从而导致错误。如果您的处理器遗漏了某个状态,可能会导致订单停滞或丢失。
检测方式:如果您发现状态处理器仅涵盖2-3种状态且没有默认/回退逻辑,请提示状态处理不完整。
正确示例
typescript
type OrderStatus =
  | "order-created"
  | "order-completed"
  | "on-order-completed"
  | "payment-pending"
  | "waiting-for-order-authorization"
  | "approve-payment"
  | "payment-approved"
  | "payment-denied"
  | "request-cancel"
  | "waiting-for-seller-decision"
  | "authorize-fulfillment"
  | "order-create-error"
  | "order-creation-error"
  | "window-to-cancel"
  | "ready-for-handling"
  | "start-handling"
  | "handling"
  | "invoice-after-cancellation-deny"
  | "order-accepted"
  | "invoiced"
  | "cancel"
  | "canceled";

async function handleAllStatuses(orderId: string, state: string): Promise<void> {
  switch (state) {
    case "ready-for-handling":
    case "start-handling":
      await notifyWarehouse(orderId, "prepare");
      break;

    case "handling":
      await updateFulfillmentStatus(orderId, "in_progress");
      break;

    case "invoiced":
      await markAsShipped(orderId);
      break;

    case "cancel":
    case "canceled":
    case "request-cancel":
      await handleCancellation(orderId, state);
      break;

    case "payment-approved":
      await confirmPaymentReceived(orderId);
      break;

    case "payment-denied":
      await handlePaymentFailure(orderId);
      break;

    default:
      // 关键:记录未知状态,而非崩溃
      console.warn(`未知或未处理的订单状态: "${state}" for order ${orderId}`);
      await logUnhandledStatus(orderId, state);
      break;
  }
}

async function notifyWarehouse(orderId: string, action: string): Promise<void> {
  console.log(`Warehouse notification: ${orderId}${action}`);
}
async function updateFulfillmentStatus(orderId: string, status: string): Promise<void> {
  console.log(`Fulfillment status: ${orderId}${status}`);
}
async function markAsShipped(orderId: string): Promise<void> {
  console.log(`Shipped: ${orderId}`);
}
async function handleCancellation(orderId: string, state: string): Promise<void> {
  console.log(`Cancellation: ${orderId} (${state})`);
}
async function confirmPaymentReceived(orderId: string): Promise<void> {
  console.log(`Payment received: ${orderId}`);
}
async function handlePaymentFailure(orderId: string): Promise<void> {
  console.log(`Payment failed: ${orderId}`);
}
async function logUnhandledStatus(orderId: string, state: string): Promise<void> {
  console.log(`UNHANDLED: ${orderId}${state}`);
}
错误示例
typescript
// 错误示例:仅处理2种状态,无未知状态回退逻辑
async function incompleteHandler(orderId: string, state: string): Promise<void> {
  if (state === "ready-for-handling") {
    await startOrderFulfillment(orderId);
  } else if (state === "invoiced") {
    await confirmOrderShipped(orderId);
  }
  // 所有其他状态被静默忽略 — 订单会丢失
  // "cancel"事件从未被处理 — 已取消的订单仍会发货
  // "Status Null"可能会被误解
}

Implementation Pattern

实现模式

The canonical, recommended way to implement order integration.
推荐的标准订单集成实现方式

Step 1: Configure the Hook

步骤1:配置Hook

Set up the hook with appropriate filters and your endpoint URL.
typescript
import axios, { AxiosInstance } from "axios";

interface HookSetupConfig {
  accountName: string;
  appKey: string;
  appToken: string;
  hookUrl: string;
  hookHeaderKey: string;
  hookHeaderValue: string;
  filterStatuses: string[];
}

async function configureOrderHook(config: HookSetupConfig): Promise<void> {
  const client: AxiosInstance = axios.create({
    baseURL: `https://${config.accountName}.vtexcommercestable.com.br`,
    headers: {
      "Content-Type": "application/json",
      "X-VTEX-API-AppKey": config.appKey,
      "X-VTEX-API-AppToken": config.appToken,
    },
  });

  const hookConfig = {
    filter: {
      type: "FromWorkflow",
      status: config.filterStatuses,
    },
    hook: {
      url: config.hookUrl,
      headers: {
        [config.hookHeaderKey]: config.hookHeaderValue,
      },
    },
  };

  await client.post("/api/orders/hook/config", hookConfig);
  console.log("Hook configured successfully");
}

// Example usage:
await configureOrderHook({
  accountName: "mymarketplace",
  appKey: process.env.VTEX_APP_KEY!,
  appToken: process.env.VTEX_APP_TOKEN!,
  hookUrl: "https://my-integration.example.com/vtex/order-hook",
  hookHeaderKey: "X-Integration-Secret",
  hookHeaderValue: process.env.HOOK_SECRET!,
  filterStatuses: [
    "ready-for-handling",
    "start-handling",
    "handling",
    "invoiced",
    "cancel",
  ],
});
设置带有合适过滤器和您的端点URL的Hook。
typescript
import axios, { AxiosInstance } from "axios";

interface HookSetupConfig {
  accountName: string;
  appKey: string;
  appToken: string;
  hookUrl: string;
  hookHeaderKey: string;
  hookHeaderValue: string;
  filterStatuses: string[];
}

async function configureOrderHook(config: HookSetupConfig): Promise<void> {
  const client: AxiosInstance = axios.create({
    baseURL: `https://${config.accountName}.vtexcommercestable.com.br`,
    headers: {
      "Content-Type": "application/json",
      "X-VTEX-API-AppKey": config.appKey,
      "X-VTEX-API-AppToken": config.appToken,
    },
  });

  const hookConfig = {
    filter: {
      type: "FromWorkflow",
      status: config.filterStatuses,
    },
    hook: {
      url: config.hookUrl,
      headers: {
        [config.hookHeaderKey]: config.hookHeaderValue,
      },
    },
  };

  await client.post("/api/orders/hook/config", hookConfig);
  console.log("Hook配置成功");
}

// 示例用法:
await configureOrderHook({
  accountName: "mymarketplace",
  appKey: process.env.VTEX_APP_KEY!,
  appToken: process.env.VTEX_APP_TOKEN!,
  hookUrl: "https://my-integration.example.com/vtex/order-hook",
  hookHeaderKey: "X-Integration-Secret",
  hookHeaderValue: process.env.HOOK_SECRET!,
  filterStatuses: [
    "ready-for-handling",
    "start-handling",
    "handling",
    "invoiced",
    "cancel",
  ],
});

Step 2: Build the Hook Endpoint with Auth and Idempotency

步骤2:构建带有身份验证和幂等逻辑的Hook端点

typescript
import express from "express";

const app = express();
app.use(express.json());

const hookConfig: HookConfig = {
  expectedAccount: process.env.VTEX_ACCOUNT_NAME!,
  expectedAppKey: process.env.VTEX_APP_KEY!,
  customHeaderKey: "X-Integration-Secret",
  customHeaderValue: process.env.HOOK_SECRET!,
};

app.post("/vtex/order-hook", createHookHandler(hookConfig));

// The createHookHandler and idempotentProcessEvent functions
// from the Constraints section above handle auth + deduplication
typescript
import express from "express";

const app = express();
app.use(express.json());

const hookConfig: HookConfig = {
  expectedAccount: process.env.VTEX_ACCOUNT_NAME!,
  expectedAppKey: process.env.VTEX_APP_KEY!,
  customHeaderKey: "X-Integration-Secret",
  customHeaderValue: process.env.HOOK_SECRET!,
};

app.post("/vtex/order-hook", createHookHandler(hookConfig));

// 上述约束条件部分的createHookHandler和idempotentProcessEvent函数
// 负责处理身份验证和去重逻辑

Step 3: Fetch Full Order Data and Process

步骤3:获取完整订单数据并处理

After receiving the hook notification, fetch the complete order data for processing.
typescript
interface VtexOrder {
  orderId: string;
  status: string;
  items: Array<{
    id: string;
    productId: string;
    name: string;
    quantity: number;
    price: number;
    sellingPrice: number;
  }>;
  clientProfileData: {
    email: string;
    firstName: string;
    lastName: string;
    document: string;
  };
  shippingData: {
    address: {
      postalCode: string;
      city: string;
      state: string;
      country: string;
      street: string;
      number: string;
    };
    logisticsInfo: Array<{
      itemIndex: number;
      selectedSla: string;
      shippingEstimate: string;
    }>;
  };
  totals: Array<{
    id: string;
    name: string;
    value: number;
  }>;
  value: number;
}

async function fetchAndProcessOrder(
  client: AxiosInstance,
  orderId: string,
  state: string
): Promise<void> {
  const response = await client.get<VtexOrder>(
    `/api/oms/pvt/orders/${orderId}`
  );
  const order = response.data;

  switch (state) {
    case "ready-for-handling":
      await createFulfillmentTask({
        orderId: order.orderId,
        items: order.items.map((item) => ({
          skuId: item.id,
          name: item.name,
          quantity: item.quantity,
        })),
        shippingAddress: order.shippingData.address,
        estimatedDelivery: order.shippingData.logisticsInfo[0]?.shippingEstimate,
      });
      break;

    case "cancel":
      await cancelFulfillmentTask(order.orderId);
      break;

    default:
      console.log(`Order ${orderId}: state=${state}, no action needed`);
  }
}

async function createFulfillmentTask(task: Record<string, unknown>): Promise<void> {
  console.log("Creating fulfillment task:", task);
}

async function cancelFulfillmentTask(orderId: string): Promise<void> {
  console.log("Canceling fulfillment task:", orderId);
}
收到Hook通知后,获取完整的订单数据进行处理。
typescript
interface VtexOrder {
  orderId: string;
  status: string;
  items: Array<{
    id: string;
    productId: string;
    name: string;
    quantity: number;
    price: number;
    sellingPrice: number;
  }>;
  clientProfileData: {
    email: string;
    firstName: string;
    lastName: string;
    document: string;
  };
  shippingData: {
    address: {
      postalCode: string;
      city: string;
      state: string;
      country: string;
      street: string;
      number: string;
    };
    logisticsInfo: Array<{
      itemIndex: number;
      selectedSla: string;
      shippingEstimate: string;
    }>;
  };
  totals: Array<{
    id: string;
    name: string;
    value: number;
  }>;
  value: number;
}

async function fetchAndProcessOrder(
  client: AxiosInstance,
  orderId: string,
  state: string
): Promise<void> {
  const response = await client.get<VtexOrder>(
    `/api/oms/pvt/orders/${orderId}`
  );
  const order = response.data;

  switch (state) {
    case "ready-for-handling":
      await createFulfillmentTask({
        orderId: order.orderId,
        items: order.items.map((item) => ({
          skuId: item.id,
          name: item.name,
          quantity: item.quantity,
        })),
        shippingAddress: order.shippingData.address,
        estimatedDelivery: order.shippingData.logisticsInfo[0]?.shippingEstimate,
      });
      break;

    case "cancel":
      await cancelFulfillmentTask(order.orderId);
      break;

    default:
      console.log(`订单 ${orderId}: 状态=${state}, 无需操作`);
  }
}

async function createFulfillmentTask(task: Record<string, unknown>): Promise<void> {
  console.log("创建履约任务:", task);
}

async function cancelFulfillmentTask(orderId: string): Promise<void> {
  console.log("取消履约任务:", orderId);
}

Step 4: Implement Feed as Fallback

步骤4:实现Feed作为备用方案

Use Feed v3 as a backup to catch any events the hook might miss during downtime.
typescript
async function pollFeedAsBackup(client: AxiosInstance): Promise<void> {
  const feedResponse = await client.get<Array<{
    eventId: string;
    handle: string;
    domain: string;
    state: string;
    orderId: string;
    lastChange: string;
  }>>("/api/orders/feed");

  const events = feedResponse.data;

  if (events.length === 0) {
    return; // No events in queue
  }

  const handlesToCommit: string[] = [];

  for (const event of events) {
    try {
      await fetchAndProcessOrder(client, event.orderId, event.state);
      handlesToCommit.push(event.handle);
    } catch (error) {
      console.error(`Failed to process feed event for ${event.orderId}:`, error);
      // Don't commit failed events — they'll return to the queue after visibility timeout
    }
  }

  // Commit successfully processed events
  if (handlesToCommit.length > 0) {
    await client.post("/api/orders/feed", {
      handles: handlesToCommit,
    });
  }
}

// Run feed polling on a schedule (e.g., every 2 minutes)
setInterval(async () => {
  try {
    const client = createVtexClient();
    await pollFeedAsBackup(client);
  } catch (error) {
    console.error("Feed polling error:", error);
  }
}, 120000); // 2 minutes

function createVtexClient(): AxiosInstance {
  return axios.create({
    baseURL: `https://${process.env.VTEX_ACCOUNT_NAME}.vtexcommercestable.com.br`,
    headers: {
      "X-VTEX-API-AppKey": process.env.VTEX_APP_KEY!,
      "X-VTEX-API-AppToken": process.env.VTEX_APP_TOKEN!,
    },
  });
}
使用Feed v3作为备用方案,以捕获Hook在停机期间可能错过的任何事件。
typescript
async function pollFeedAsBackup(client: AxiosInstance): Promise<void> {
  const feedResponse = await client.get<Array<{
    eventId: string;
    handle: string;
    domain: string;
    state: string;
    orderId: string;
    lastChange: string;
  }>>("/api/orders/feed");

  const events = feedResponse.data;

  if (events.length === 0) {
    return; // 队列中无事件
  }

  const handlesToCommit: string[] = [];

  for (const event of events) {
    try {
      await fetchAndProcessOrder(client, event.orderId, event.state);
      handlesToCommit.push(event.handle);
    } catch (error) {
      console.error(`处理订单 ${event.orderId} 的Feed事件失败:`, error);
      // 不要提交失败的事件 — 可见性超时后会返回队列
    }
  }

  // 提交处理成功的事件
  if (handlesToCommit.length > 0) {
    await client.post("/api/orders/feed", {
      handles: handlesToCommit,
    });
  }
}

// 定期运行Feed轮询(例如,每2分钟一次)
setInterval(async () => {
  try {
    const client = createVtexClient();
    await pollFeedAsBackup(client);
  } catch (error) {
    console.error("Feed轮询错误:", error);
  }
}, 120000); // 2分钟

function createVtexClient(): AxiosInstance {
  return axios.create({
    baseURL: `https://${process.env.VTEX_ACCOUNT_NAME}.vtexcommercestable.com.br`,
    headers: {
      "X-VTEX-API-AppKey": process.env.VTEX_APP_KEY!,
      "X-VTEX-API-AppToken": process.env.VTEX_APP_TOKEN!,
    },
  });
}

Complete Example

完整示例

typescript
import express from "express";
import axios, { AxiosInstance } from "axios";

// 1. Configure hook
async function setupIntegration(): Promise<void> {
  await configureOrderHook({
    accountName: process.env.VTEX_ACCOUNT_NAME!,
    appKey: process.env.VTEX_APP_KEY!,
    appToken: process.env.VTEX_APP_TOKEN!,
    hookUrl: `${process.env.BASE_URL}/vtex/order-hook`,
    hookHeaderKey: "X-Integration-Secret",
    hookHeaderValue: process.env.HOOK_SECRET!,
    filterStatuses: [
      "ready-for-handling",
      "handling",
      "invoiced",
      "cancel",
    ],
  });
}

// 2. Start webhook server
const app = express();
app.use(express.json());

const hookHandler = createHookHandler({
  expectedAccount: process.env.VTEX_ACCOUNT_NAME!,
  expectedAppKey: process.env.VTEX_APP_KEY!,
  customHeaderKey: "X-Integration-Secret",
  customHeaderValue: process.env.HOOK_SECRET!,
});

app.post("/vtex/order-hook", hookHandler);

// 3. Health check for VTEX ping
app.get("/health", (_req, res) => res.status(200).json({ status: "ok" }));

// 4. Start feed polling as backup
setInterval(async () => {
  try {
    const client = createVtexClient();
    await pollFeedAsBackup(client);
  } catch (error) {
    console.error("Feed backup polling error:", error);
  }
}, 120000);

app.listen(3000, () => {
  console.log("Order integration running on port 3000");
  setupIntegration().catch(console.error);
});
typescript
import express from "express";
import axios, { AxiosInstance } from "axios";

// 1. 配置Hook
async function setupIntegration(): Promise<void> {
  await configureOrderHook({
    accountName: process.env.VTEX_ACCOUNT_NAME!,
    appKey: process.env.VTEX_APP_KEY!,
    appToken: process.env.VTEX_APP_TOKEN!,
    hookUrl: `${process.env.BASE_URL}/vtex/order-hook`,
    hookHeaderKey: "X-Integration-Secret",
    hookHeaderValue: process.env.HOOK_SECRET!,
    filterStatuses: [
      "ready-for-handling",
      "handling",
      "invoiced",
      "cancel",
    ],
  });
}

// 2. 启动Webhook服务器
const app = express();
app.use(express.json());

const hookHandler = createHookHandler({
  expectedAccount: process.env.VTEX_ACCOUNT_NAME!,
  expectedAppKey: process.env.VTEX_APP_KEY!,
  customHeaderKey: "X-Integration-Secret",
  customHeaderValue: process.env.HOOK_SECRET!,
});

app.post("/vtex/order-hook", hookHandler);

// 3. 健康检查接口,用于VTEX ping
app.get("/health", (_req, res) => res.status(200).json({ status: "ok" }));

// 4. 启动Feed轮询作为备用
setInterval(async () => {
  try {
    const client = createVtexClient();
    await pollFeedAsBackup(client);
  } catch (error) {
    console.error("Feed备用轮询错误:", error);
  }
}, 120000);

app.listen(3000, () => {
  console.log("订单集成服务运行在端口3000");
  setupIntegration().catch(console.error);
});

Anti-Patterns

反模式

Common mistakes developers make and how to fix them.
开发者常犯的错误及修复方法

Anti-Pattern: Using List Orders API Instead of Feed/Hook

反模式:使用List Orders API而非Feed/Hook

What happens: Developers poll
GET /api/oms/pvt/orders
with status filters to detect order changes instead of using Feed v3 or Hook.
Why it fails: The List Orders API depends on indexing, which can lag behind real-time updates. It's slower, less reliable, and more likely to hit rate limits when polled frequently. Feed v3 runs before indexing and doesn't depend on it.
Fix: Migrate to Feed v3 or Hook for order change detection. Use List Orders only for ad-hoc queries.
typescript
// Correct: Use Feed v3 to consume order updates
async function consumeOrderFeed(client: AxiosInstance): Promise<void> {
  const response = await client.get("/api/orders/feed");
  const events = response.data;

  for (const event of events) {
    await processOrderEvent({
      Domain: "Marketplace",
      OrderId: event.orderId,
      State: event.state,
      LastChange: event.lastChange,
      Origin: { Account: "", Key: "" },
    });
  }

  // Commit processed events
  const handles = events.map((e: { handle: string }) => e.handle);
  if (handles.length > 0) {
    await client.post("/api/orders/feed", { handles });
  }
}

问题表现:开发者轮询
GET /api/oms/pvt/orders
并使用状态过滤器来检测订单变化,而非使用Feed v3或Hook。
失败原因:List Orders API依赖索引,可能滞后于实时更新。当频繁轮询时,速度更慢、可靠性更低,且更容易触发速率限制。Feed v3在索引前运行,不依赖索引机制。
修复方法:迁移到Feed v3或Hook来检测订单变化。仅将List Orders用于临时查询。
typescript
// 正确方式:使用Feed v3消费订单更新
async function consumeOrderFeed(client: AxiosInstance): Promise<void> {
  const response = await client.get("/api/orders/feed");
  const events = response.data;

  for (const event of events) {
    await processOrderEvent({
      Domain: "Marketplace",
      OrderId: event.orderId,
      State: event.state,
      LastChange: event.lastChange,
      Origin: { Account: "", Key: "" },
    });
  }

  // 提交已处理的事件
  const handles = events.map((e: { handle: string }) => e.handle);
  if (handles.length > 0) {
    await client.post("/api/orders/feed", { handles });
  }
}

Anti-Pattern: Blocking Hook Response with Long Processing

反模式:长时间处理阻塞Hook响应

What happens: The hook endpoint performs all order processing synchronously before responding to VTEX.
Why it fails: VTEX requires the hook endpoint to respond with HTTP 200 within 5000ms. If processing takes longer (e.g., ERP sync, complex database writes), VTEX considers the delivery failed and retries with increasing delays. Repeated failures can lead to hook deactivation.
Fix: Acknowledge the event immediately, then process asynchronously via a queue.
typescript
import { RequestHandler } from "express";

// Correct: Acknowledge immediately, process async
const asyncHookHandler: RequestHandler = async (req, res) => {
  const payload: HookPayload = req.body;

  // Validate auth (fast operation)
  if (!validateAuth(payload, req.headers)) {
    res.status(401).json({ error: "Unauthorized" });
    return;
  }

  // Enqueue for async processing (fast operation)
  await enqueueOrderEvent(payload);

  // Respond immediately — well within 5000ms
  res.status(200).json({ received: true });
};

function validateAuth(
  payload: HookPayload,
  headers: Record<string, unknown>
): boolean {
  return (
    payload.Origin?.Account === process.env.VTEX_ACCOUNT_NAME &&
    headers["x-integration-secret"] === process.env.HOOK_SECRET
  );
}

async function enqueueOrderEvent(payload: HookPayload): Promise<void> {
  // Use a message queue (SQS, RabbitMQ, Redis, etc.)
  console.log(`Enqueued order event: ${payload.OrderId}`);
}
问题表现:Hook端点在响应VTEX之前同步执行所有订单处理逻辑。
失败原因:VTEX要求Hook端点在5000ms内返回HTTP 200响应。如果处理时间过长(例如ERP同步、复杂数据库写入),VTEX会认为交付失败并以递增延迟重试。多次失败会导致Hook被停用。
修复方法:立即确认事件已接收,然后通过队列异步处理。
typescript
import { RequestHandler } from "express";

// 正确方式:立即确认,异步处理
const asyncHookHandler: RequestHandler = async (req, res) => {
  const payload: HookPayload = req.body;

  // 快速验证身份
  if (!validateAuth(payload, req.headers)) {
    res.status(401).json({ error: "Unauthorized" });
    return;
  }

  // 加入队列异步处理(快速操作)
  await enqueueOrderEvent(payload);

  // 立即响应 — 远低于5000ms限制
  res.status(200).json({ received: true });
};

function validateAuth(
  payload: HookPayload,
  headers: Record<string, unknown>
): boolean {
  return (
    payload.Origin?.Account === process.env.VTEX_ACCOUNT_NAME &&
    headers["x-integration-secret"] === process.env.HOOK_SECRET
  );
}

async function enqueueOrderEvent(payload: HookPayload): Promise<void> {
  // 使用消息队列(SQS、RabbitMQ、Redis等)
  console.log(`已加入队列的订单事件: ${payload.OrderId}`);
}

Reference

参考资料

Links to VTEX documentation and related resources.
VTEX文档及相关资源链接