marketplace-order-hook
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseOrder 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 to retrieve events, processes them, then commits them via
GET /api/orders/feed. You control the pace.POST /api/orders/feed - 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:
| Feed | Hook | |
|---|---|---|
| Model | Pull (active) | Push (reactive) |
| Scalability | You control volume | Must handle any volume |
| Reliability | Events persist in queue | Must be always available |
| Best for | ERPs with limited throughput | High-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响应。
主要差异:
| Feed | Hook | |
|---|---|---|
| 模式 | 主动拉取 | 被动推送 |
| 扩展性 | 您控制处理量 | 必须能处理任意量级的请求 |
| 可靠性 | 事件持久化在队列中 | 端点必须始终可用 |
| 最佳适用场景 | 吞吐量有限的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 ConflictFeed和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 ConflictConcept 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 to retrieve complete order details.
GET /api/oms/pvt/orders/{orderId}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 and fields in the payload must match your expected values.
Origin.AccountOrigin.KeyWhy: 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 , , or custom headers → STOP and add authentication validation.
Origin.AccountOrigin.Key✅ 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.AccountOrigin.Key原因:如果不进行身份验证,任何主体都可以向您的端点发送伪造的订单事件,触发未授权的履约操作、数据损坏或财务损失。
检测方式:如果您发现Hook端点处理器在处理事件前未检查、或自定义头,请立即停止并添加身份验证逻辑。
Origin.AccountOrigin.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 + + as a deduplication key to prevent duplicate processing.
OrderIdStateLastChangeWhy: 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 duplicate check or deduplication mechanism → warn about idempotency. If the handler directly mutates state without checking if the event was already processed → warn.
orderId✅ 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);
},
};规则:您的集成必须对订单事件进行幂等处理。使用 + + 的组合作为去重键,以避免重复处理。
OrderIdStateLastChange原因: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 . Unrecognized statuses must be logged but not crash the integration.
Status NullWhy: VTEX documents warn that 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.
Status NullDetection: 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 + deduplicationtypescript
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 with status filters to detect order changes instead of using Feed v3 or Hook.
GET /api/oms/pvt/ordersWhy 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 });
}
}问题表现:开发者轮询并使用状态过滤器来检测订单变化,而非使用Feed v3或Hook。
GET /api/oms/pvt/orders失败原因: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.
- Feed v3 Guide — Complete guide to Feed and Hook configuration, filter types, and best practices
- Orders API - Feed v3 Endpoints — API reference for feed retrieval and commit
- Hook Configuration API — API reference for creating and updating hook configuration
- Orders Overview — Overview of the VTEX Orders module
- Order Flow and Status — Complete list of order statuses and transitions
- ERP Integration - Set Up Order Integration — Guide for integrating order feed with back-office systems
VTEX文档及相关资源链接。
- Feed v3指南 — Feed和Hook配置、过滤器类型及最佳实践的完整指南
- 订单API - Feed v3端点 — Feed检索和提交的API参考
- Hook配置API — 创建和更新Hook配置的API参考
- 订单概述 — VTEX订单模块概述
- 订单流与状态 — 完整的订单状态及流转列表
- ERP集成 - 设置订单集成 — 将订单Feed与后台系统集成的指南