关于 marketplace-fulfillment
面向 VTEX 外部卖家的履约集成技能,涵盖模拟、订单预留与发货,以及通过 OMS 提交发票和物流跟踪。
主要功能
- 01实现履约模拟与物流选项响应
- 02创建订单预留并返回 orderId
- 03处理订单发货授权
- 04提交发票及拆包发票
- 05单独更新物流跟踪信息
适用场景
- 01接入 VTEX 商品索引和结账流程
- 02将市场订单转为卖家预留及正式订单
- 03为发货订单提交发票和跟踪信息
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ vtex/ skills --skill marketplace-fulfillment安装后在 Claude Code 中输入/marketplace-fulfillment确认已加载。
详细介绍
履约、模拟、订单与 OMS 后续处理
适用场景
在构建 外部卖家 集成时使用此技能:VTEX 会将库存可用性、配送、结账模拟和下单请求转发到你的履约基础 URL;发货后,你需要调用市场 OMS API 发送发票和跟踪信息。
- 实现
POST /pvt/orderForms/simulation(索引和/或结账——有或没有客户上下文) - 实现
POST /pvt/orders(下单——创建预留;返回orderId= 你系统中的预留 ID) - 处理
POST /pvt/orders/{sellerOrderId}/fulfill(订单获批后发货——路径使用你在下单时返回的同一个orderId) - 通过
POST /api/oms/pvt/orders/{marketplaceOrderId}/invoice发送发票通知(路径中使用市场订单 ID) - 通过
PATCH /api/oms/pvt/orders/{marketplaceOrderId}/invoice/{invoiceNumber}更新跟踪信息 - 实现拆分发货的部分开票
以下情况不要使用此技能:
- 目录或 SKU 同步(参见
marketplace-catalog-sync) - 通过 Feed/Hook 消费订单事件(参见
marketplace-order-hook) - 通用 API 速率限制(参见
marketplace-rate-limiting)
决策规则
外部卖家协议(在卖家主机上实现)
- 履约模拟 —
POST /pvt/orderForms/simulation。VTEX 会在产品索引和结账期间调用此接口。请求可能仅包含items(以及可选的查询参数),也可能包含完整的结账上下文:items、postalCode、country、clientProfileData、shippingData、selectedSla等。没有邮政编码或客户资料(索引期间的常见情况)时,响应仍必须说明每个商品是否可用。提供完整上下文时,返回items、logisticsInfo(每个请求商品对应一条记录)、postalCode、country,并按协议要求将allowMultipleDeliveries设为true。items[].id是卖家 SKU ID;items[].seller是市场账户上的卖家 ID。 - 响应结构 — 每条
logisticsInfo[]记录都对应一个请求商品(itemIndex)。包含所有配送选项的slas[](适用时包括送货上门和自提点)、含各渠道库存的deliveryChannels[]、shipsTo和stockBalance。SLA 字段包括price(每件商品的配送费,单位为分)、shippingEstimate/shippingEstimateDate(例如5bd、30m)。自提 SLA 必须包含pickupStoreInfo(地址、friendlyName等)。 - SLA — 模拟处理程序必须在 2.5 秒内响应,否则该商品将被视为不可用。
- 下单 — 使用 JSON 数组形式的订单调用
POST /pvt/orders。为每个订单在你的系统中创建一个预留。响应必须与请求结构相同,并为每个订单添加orderId:该值是你的预留 ID,之后协议调用中的sellerOrderId都会使用此值(例如授权履约的路径参数)。 - 订单发货(授权履约) — 市场批准后,VTEX 会调用
POST /pvt/orders/{sellerOrderId}/fulfill,其中sellerOrderId等于你在下单时返回的orderId。请求正文包含marketplaceOrderId和marketplaceOrderGroup。在你的系统中将预留转为正式订单;响应正文包含date、marketplaceOrderId、orderId(卖家引用)和receipt。
OMS API(卖家 → 市场)
- 通过
POST /api/oms/pvt/orders/{marketplaceOrderId}/invoice发送发票。必填字段:type、invoiceNumber、invoiceValue(以分为单位)、issuanceDate和items数组。路径中的marketplaceOrderId是 VTEX 市场订单 ID,而不是你的预留 ID。 - 销售发票(发货)使用
type: "Output";退货发票使用type: "Input"。 - 承运商提供物流跟踪信息后,使用
PATCH /api/oms/pvt/orders/{marketplaceOrderId}/invoice/{invoiceNumber}单独发送。 - 对于拆分发货,每个包裹发送 一张发票,且只包含该包裹中的商品。每张发票的
invoiceValue必须仅反映其中商品的金额。 - 订单开具发票后,必须先发送退货发票(
type: "Input"),才能取消订单。 - 履约模拟端点必须在 2.5 秒内响应,否则商品会被视为不可用。
架构 / 数据流(概览):
VTEX Checkout / indexation External Seller VTEX OMS (marketplace)
│ │ │
│── POST /pvt/orderForms/simulation ▶│ Price, stock, SLAs │
│◀── 200 + items + logisticsInfo ───│ │
│ │ │
│── POST /pvt/orders (array) ───────▶│ Create reservation │
│◀── same + orderId (reservation) ─│ │
│ │ │
│── POST /pvt/orders/{id}/fulfill ──▶│ Commit / pick pack │
│◀── date, marketplaceOrderId, ... ─│ │
│ │── POST .../invoice ────────────────▶│
│ │── PATCH .../invoice/{n} ─────────▶│
硬性约束
约束:OMS 路径中的市场订单 ID
/api/oms/pvt/orders/{orderId}/... 中的任何 {orderId} 都必须是 VTEX 市场订单 ID(OMS),而不是你在 POST /pvt/orders 返回的 orderId(预留 ID)。调用发票或物流跟踪 API 之前,必须先从协议中(下单负载、履约请求体或事件)映射出 marketplaceOrderId。
原因
在 OMS URL 中使用预留 ID,无法匹配市场订单;发票和物流跟踪信息都不会关联到客户订单。
检测
如果 POST /pvt/orders 响应中的 orderId 与 POST .../oms/.../invoice 使用的是同一个变量,且未进行映射 → 停止。
正确
// reservationId from your POST /pvt/orders response; marketplaceOrderId from VTEX payload
await omsClient.post(`/api/oms/pvt/orders/${marketplaceOrderId}/invoice`, payload);
错误
await omsClient.post(`/api/oms/pvt/orders/${reservationId}/invoice`, payload);
约束:履约模拟契约和延迟
卖家必须实现 POST /pvt/orderForms/simulation,为每个请求返回有效的 items 数组。当请求包含结账上下文(例如 postalCode、clientProfileData、shippingData)时,响应必须包含匹配的 logisticsInfo、所有相关模式的 slas(包括提供自提服务时的自提选项),并在需要时设置 allowMultipleDeliveries: true。处理程序必须在 2.5 秒内完成。
原因
logisticsInfo 不完整或缺少 SLA 会导致结账时无法选择配送方式。响应缓慢会导致商品被标记为不可用,并降低转化率。
检测
如果模拟请求包含 shippingData,但响应仅返回带有价格的 items,省略了 logisticsInfo → 警告。如果未启用缓存且 p95 延迟接近 2 秒 → 警告。
正确
// Pseudocode: branch on whether checkout context is present
if (hasCheckoutContext(req.body)) {
return res.json({
country: req.body.country,
items: pricedItems,
logisticsInfo: buildLogisticsPerItem(pricedItems, req.body),
postalCode: req.body.postalCode,
allowMultipleDeliveries: true,
});
}
return res.json({ items: availabilityOnlyItems /* + minimal logistics if required */ });
错误
// WRONG: Full checkout body but response omits logisticsInfo / SLAs
res.json({ items: pricedItemsOnly });
约束:下单必须返回卖家 orderId(预留 ID)
POST /pvt/orders 接受一个订单数组(JSON 数组)。响应必须返回相同的订单,并将每个元素中的 orderId 设置为你的预留标识符(卖家系统)。VTEX 会在 POST /pvt/orders/{sellerOrderId}/fulfill 中将该值用作 sellerOrderId。
原因
省略 orderId 或重复使用虚假的 orderId,会破坏市场订单流程与预留记录之间的关联,并导致派送无法正确路由。
检测
如果处理程序返回 200,但未添加 orderId,或返回单个对象而不是数组 → 警告。
正确
app.post("/pvt/orders", (req, res) => {
const orders = req.body as Array<Record<string, unknown>>;
const out = orders.map((order) => {
const reservationId = createReservation(order);
return { ...order, orderId: reservationId, followUpEmail: "" };
});
res.json(out);
});
错误
app.post("/pvt/orders", (req, res) => {
createReservation(req.body);
res.status(200).send(); // WRONG: missing orderId echo
});
约束:发送包含所有必填字段的正确格式发票
发票通知必须包含 type、invoiceNumber、invoiceValue、issuanceDate 和 items 数组。invoiceValue 必须以分为单位。items 数组必须与订单中的商品一致。
为什么这很重要
缺少必填字段会导致 API 以 400 Bad Request 拒绝发票,使订单卡在“处理中”状态。invoiceValue 不正确(例如使用美元而非分)会导致市场平台对账时出现财务差异。
检测
如果看到发票通知载荷缺少 invoiceNumber、invoiceValue、issuanceDate 或 items → 提醒缺少必填字段。如果 invoiceValue 看起来是以美元为单位(例如,99.90 而不是 9990)→ 提醒进行分的换算。
正确
import axios, { AxiosInstance } from "axios";
interface InvoiceItem {
id: string;
quantity: number;
price: number; // in cents
}
interface InvoicePayload {
type: "Output" | "Input";
invoiceNumber: string;
invoiceValue: number; // total in cents
issuanceDate: string; // ISO 8601
invoiceUrl?: string;
invoiceKey?: string;
courier?: string;
trackingNumber?: string;
trackingUrl?: string;
items: InvoiceItem[];
}
async function sendInvoiceNotification(
client: AxiosInstance,
marketplaceOrderId: string,
invoice: InvoicePayload
): Promise<void> {
// Validate required fields before sending
if (!invoice.invoiceNumber) {
throw new Error("invoiceNumber is required");
}
if (!invoice.invoiceValue || invoice.invoiceValue <= 0) {
throw new Error("invoiceValue must be a positive number in cents");
}
if (!invoice.issuanceDate) {
throw new Error("issuanceDate is required");
}
if (!invoice.items || invoice.items.length === 0) {
throw new Error("items array is required and must not be empty");
}
// Warn if invoiceValue looks like it's in dollars instead of cents
if (invoice.invoiceValue < 100 && invoice.items.length > 0) {
console.warn(
`Warning: invoiceValue ${invoice.invoiceValue} seems very low. ` +
`Ensure it's in cents (e.g., 9990 for $99.90).`
);
}
await client.post(`/api/oms/pvt/orders/${marketplaceOrderId}/invoice`, invoice);
}
// Example usage:
async function invoiceOrder(
client: AxiosInstance,
marketplaceOrderId: string
): Promise<void> {
await sendInvoiceNotification(client, marketplaceOrderId, {
type: "Output",
invoiceNumber: "NFE-2026-001234",
invoiceValue: 15990, // $159.90 in cents
issuanceDate: new Date().toISOString(),
invoiceUrl: "https://invoices.example.com/NFE-2026-001234.pdf",
invoiceKey: "35260614388220000199550010000012341000012348",
items: [
{ id: "123", quantity: 1, price: 9990 },
{ id: "456", quantity: 2, price: 3000 },
],
});
}
错误
// WRONG: Missing required fields, value in dollars instead of cents
async function sendBrokenInvoice(
client: AxiosInstance,
marketplaceOrderId: string
): Promise<void> {
await client.post(`/api/oms/pvt/orders/${marketplaceOrderId}/invoice`, {
// Missing 'type' field — API may reject or default incorrectly
invoiceNumber: "001234",
invoiceValue: 159.9, // WRONG: dollars, not cents — causes financial mismatch
// Missing 'issuanceDate' — API will reject with 400
// Missing 'items' — API cannot match invoice to order items
});
}
约束:发货后及时更新追踪信息
承运商提供追踪信息后,必须立即发送。使用 PATCH /api/oms/pvt/orders/{marketplaceOrderId}/invoice/{invoiceNumber} 将追踪信息添加到现有发票。
为什么这很重要
延迟更新追踪信息会导致客户无法在市场平台上查看配送状态。订单会停留在“已开票”状态,而无法继续进入“配送中”状态,随后也无法进入“已送达”状态。这会产生客户支持工单,并损害卖家的声誉。
检测
如果看到追踪信息被集中起来每天更新,而不是实时发送 → 提醒及时更新追踪信息。如果在承运商尚未提供追踪信息时(使用硬编码/空值)将其包含在初始发票请求中 → 提醒。
正确
interface TrackingUpdate {
courier: string;
trackingNumber: string;
trackingUrl?: string;
isDelivered?: boolean;
}
async function updateOrderTracking(
client: AxiosInstance,
marketplaceOrderId: string,
invoiceNumber: string,
tracking: TrackingUpdate
): Promise<void> {
await client.patch(
`/api/oms/pvt/orders/${marketplaceOrderId}/invoice/${invoiceNumber}`,
tracking
);
}
// Send tracking as soon as carrier provides it
async function onCarrierPickup(
client: AxiosInstance,
marketplaceOrderId: string,
invoiceNumber: string,
carrierData: { name: string; trackingId: string; trackingUrl: string }
): Promise<void> {
await updateOrderTracking(client, marketplaceOrderId, invoiceNumber, {
courier: carrierData.name,
trackingNumber: carrierData.trackingId,
trackingUrl: carrierData.trackingUrl,
});
console.log(
`Tracking updated for marketplace order ${marketplaceOrderId}: ${carrierData.trackingId}`
);
}
// Update delivery status when confirmed
async function onDeliveryConfirmed(
client: AxiosInstance,
marketplaceOrderId: string,
invoiceNumber: string
): Promise<void> {
await updateOrderTracking(client, marketplaceOrderId, invoiceNumber, {
courier: "",
trackingNumber: "",
isDelivered: true,
});
console.log(`Marketplace order ${marketplaceOrderId} marked as delivered`);
}
错误
// WRONG: Sending empty/fake tracking data with the invoice
async function invoiceWithFakeTracking(
client: AxiosInstance,
marketplaceOrderId: string
): Promise<void> {
await client.post(`/api/oms/pvt/orders/${marketplaceOrderId}/invoice`, {
type: "Output",
invoiceNumber: "NFE-001",
invoiceValue: 9990,
issuanceDate: new Date().toISOString(),
items: [{ id: "123", quantity: 1, price: 9990 }],
// WRONG: Hardcoded tracking — carrier hasn't picked up yet
courier: "TBD",
trackingNumber: "PENDING",
trackingUrl: "",
});
// Customer sees "PENDING" as tracking number — useless information
}
约束:处理拆分发货的部分开票
对于分多个包裹发货的订单,每个包裹的货件都必须有单独的发票,且仅包含该包裹中的商品。invoiceValue 必须仅反映该货件中的商品价值。
为什么这很重要
仅发出部分商品时,却为整个订单价值发送单张发票,会导致财务差异。市场平台无法正确对账,订单状态也可能无法正常推进。
检测
如果看到部分发货时仍发送包含整个订单价值的单张发票 → 提醒需要部分开票。如果 items 数组与实际发货商品不一致 → 提醒。
正确
interface OrderItem {
id: string;
name: string;
quantity: number;
price: number; // per unit in cents
}
interface Shipment {
items: OrderItem[];
invoiceNumber: string;
}
async function sendPartialInvoices(
client: AxiosInstance,
marketplaceOrderId: string,
shipments: Shipment[]
): Promise<void> {
for (const shipment of shipments) {
// Calculate value for only the items in this shipment
const shipmentValue = shipment.items.reduce(
(total, item) => total + item.price * item.quantity,
0
);
await sendInvoiceNotification(client, marketplaceOrderId, {
type: "Output",
invoiceNumber: shipment.invoiceNumber,
invoiceValue: shipmentValue,
issuanceDate: new Date().toISOString(),
items: shipment.items.map((item) => ({
id: item.id,
quantity: item.quantity,
price: item.price,
})),
});
console.log(
`Partial invoice ${shipment.invoiceNumber} sent for marketplace order ${marketplaceOrderId}: ` +
`${shipment.items.length} items, value=${shipmentValue}`
);
}
}
// Example: Order with 3 items shipped in 2 packages
await sendPartialInvoices(client, "vtex-marketplace-order-id-12345", [
{
invoiceNumber: "NFE-001-A",
items: [
{ id: "sku-1", name: "Laptop", quantity: 1, price: 250000 },
],
},
{
invoiceNumber: "NFE-001-B",
items: [
{ id: "sku-2", name: "Mouse", quantity: 1, price: 5000 },
{ id: "sku-3", name: "Keyboard", quantity: 1, price: 12000 },
],
},
]);
错误
// WRONG: Sending full order value for partial shipment
async function wrongPartialInvoice(
client: AxiosInstance,
marketplaceOrderId: string,
totalOrderValue: number,
shippedItems: OrderItem[]
): Promise<void> {
await client.post(`/api/oms/pvt/orders/${marketplaceOrderId}/invoice`, {
type: "Output",
invoiceNumber: "NFE-001-A",
invoiceValue: totalOrderValue, // WRONG: Full order value, not partial
issuanceDate: new Date().toISOString(),
items: shippedItems.map((item) => ({
id: item.id,
quantity: item.quantity,
price: item.price,
})),
// invoiceValue doesn't match sum of items — financial mismatch
});
}
首选模式
实现履约模拟
注册 POST /pvt/orderForms/simulation。解析 items[](id = 卖家 SKU,seller = 市场平台上的卖家 ID)。如果请求正文中没有 postalCode / clientProfileData,则将其视为索引请求:返回可用性(如果你的合同要求,也返回最少的物流信息)。如果请求正文包含结账字段,则按 itemIndex 构建 logisticsInfo,填充 slas(配送和到店自提;适用时包含 pickupStoreInfo)、deliveryChannels、stockBalance,设置 country / postalCode,并设置 allowMultipleDeliveries: true。限制 CPU 和 I/O 的使用,确保处理时间低于 2.5 秒。
import { RequestHandler } from "express";
const fulfillmentSimulation: RequestHandler = async (req, res) => {
const body = req.body as Record<string, unknown>;
const items = body.items as Array<{ id: string; quantity: number; seller: string }>;
const hasCheckoutContext = Boolean(body.postalCode && body.shippingData);
const pricedItems = await priceAndStockForItems(items);
if (!hasCheckoutContext) {
res.json({ items: pricedItems, logisticsInfo: minimalLogistics(pricedItems) });
return;
}
res.json({
country: body.country,
postalCode: body.postalCode,
items: pricedItems,
logisticsInfo: buildFullLogistics(pricedItems, body),
allowMultipleDeliveries: true,
});
};
function minimalLogistics(
pricedItems: Array<{ id: string; requestIndex: number }>
): unknown[] {
return pricedItems.map((_, i) => ({
itemIndex: i,
quantity: 1,
shipsTo: ["USA"],
slas: [],
stockBalance: "",
deliveryChannels: [{ id: "delivery", stockBalance: "" }],
}));
}
function buildFullLogistics(
pricedItems: Array<{ id: string; requestIndex: number }>,
_checkoutBody: unknown
): unknown[] {
// Replace with SLA / carrier rules derived from checkoutBody
return pricedItems.map((_, i) => ({
itemIndex: i,
quantity: 1,
shipsTo: ["USA"],
slas: [],
stockBalance: 0,
deliveryChannels: [],
}));
}
async function priceAndStockForItems(
items: Array<{ id: string; quantity: number; seller: string }>
): Promise<Array<Record<string, unknown>>> {
return items.map((item, requestIndex) => ({
id: item.id,
quantity: item.quantity,
seller: item.seller,
measurementUnit: "un",
merchantName: null,
price: 0,
priceTags: [],
priceValidUntil: null,
requestIndex,
unitMultiplier: 1,
attachmentOfferings: [],
}));
}
实现下单(预留)
注册 POST /pvt/orders。请求正文是一个数组。将每个订单保存为预留;响应中返回相同的对象,并添加 orderId(预留键),通常还包括 followUpEmail。VTEX 会将该 orderId 作为 sellerOrderId 传入 POST /pvt/orders/{sellerOrderId}/fulfill。
import { RequestHandler } from "express";
function createReservation(_order: Record<string, unknown>): string {
return `res-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
}
const orderPlacement: RequestHandler = (req, res) => {
const orders = req.body as Array<Record<string, unknown>>;
const out = orders.map((order) => ({
...order,
orderId: createReservation(order),
followUpEmail: "",
}));
res.json(out);
};
实现授权履约端点
支付获批后,市场平台会调用此端点。
import express, { RequestHandler } from "express";
interface FulfillOrderRequest {
marketplaceOrderId: string;
marketplaceOrderGroup: string;
}
interface OrderMapping {
/** Same id returned as orderId from POST /pvt/orders (reservation) */
sellerOrderId: string;
marketplaceOrderId: string;
items: OrderItem[];
status: string;
}
// Store for order mappings — use a real database in production
const orderStore = new Map<string, OrderMapping>();
const authorizeFulfillmentHandler: RequestHandler = async (req, res) => {
const sellerOrderId = req.params.sellerOrderId;
const { marketplaceOrderId, marketplaceOrderGroup }: FulfillOrderRequest = req.body;
console.log(
`Fulfillment authorized: reservation=${sellerOrderId}, marketplaceOrder=${marketplaceOrderId}, group=${marketplaceOrderGroup}`
);
// Store the marketplace order ID mapping
const order = orderStore.get(sellerOrderId);
if (!order) {
res.status(404).json({ error: "Order not found" });
return;
}
order.marketplaceOrderId = marketplaceOrderId;
order.status = "fulfillment_authorized";
orderStore.set(sellerOrderId, order);
// Trigger fulfillment process asynchronously
enqueueFulfillment(sellerOrderId).catch(console.error);
res.status(200).json({
date: new Date().toISOString(),
marketplaceOrderId,
// Echo seller reservation id (same as path param / placement orderId)
orderId: sellerOrderId,
receipt: null,
});
};
async function enqueueFulfillment(sellerOrderId: string): Promise<void> {
console.log(`Enqueued fulfillment for ${sellerOrderId}`);
}
const app = express();
app.use(express.json());
app.post("/pvt/orders/:sellerOrderId/fulfill", authorizeFulfillmentHandler);
履约后发送发票
订单打包完成并生成发票后,发送发票通知。
async function fulfillAndInvoice(
client: AxiosInstance,
order: OrderMapping
): Promise<void> {
// Generate invoice from your invoicing system
const invoice = await generateInvoice(order);
// Send invoice notification to VTEX marketplace
await sendInvoiceNotification(client, order.marketplaceOrderId, {
type: "Output",
invoiceNumber: invoice.number,
invoiceValue: invoice.totalCents,
issuanceDate: invoice.issuedAt.toISOString(),
invoiceUrl: invoice.pdfUrl,
invoiceKey: invoice.accessKey,
items: order.items.map((item) => ({
id: item.id,
quantity: item.quantity,
price: item.price,
})),
});
console.log(
`Invoice ${invoice.number} sent for order ${order.marketplaceOrderId}`
);
}
async function generateInvoice(order: OrderMapping): Promise<{
number: string;
totalCents: number;
issuedAt: Date;
pdfUrl: string;
accessKey: string;
}> {
const totalCents = order.items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
return {
number: `NFE-${Date.now()}`,
totalCents,
issuedAt: new Date(),
pdfUrl: `https://invoices.example.com/NFE-${Date.now()}.pdf`,
accessKey: "35260614388220000199550010000012341000012348",
};
}
承运商取件时发送追踪信息
async function handleCarrierPickup(
client: AxiosInstance,
marketplaceOrderId: string,
invoiceNumber: string,
carrier: { name: string; trackingId: string; trackingUrl: string }
): Promise<void> {
await updateOrderTracking(client, marketplaceOrderId, invoiceNumber, {
courier: carrier.name,
trackingNumber: carrier.trackingId,
trackingUrl: carrier.trackingUrl,
});
console.log(
`Tracking ${carrier.trackingId} sent for marketplace order ${marketplaceOrderId}`
);
}
确认送达
async function handleDeliveryConfirmation(
client: AxiosInstance,
marketplaceOrderId: string,
invoiceNumber: string
): Promise<void> {
await client.patch(
`/api/oms/pvt/orders/${marketplaceOrderId}/invoice/${invoiceNumber}`,
{
isDelivered: true,
courier: "",
trackingNumber: "",
}
);
console.log(`Marketplace order ${marketplaceOrderId} marked as delivered`);
}
完整示例
import axios, { AxiosInstance } from "axios";
function createMarketplaceClient(
accountName: string,
appKey: string,
appToken: string
): AxiosInstance {
return axios.create({
baseURL: `https://${accountName}.vtexcommercestable.com.br`,
headers: {
"Content-Type": "application/json",
"X-VTEX-API-AppKey": appKey,
"X-VTEX-API-AppToken": appToken,
},
timeout: 10000,
});
}
async function completeFulfillmentFlow(
client: AxiosInstance,
order: OrderMapping
): Promise<void> {
// 1. Fulfill and invoice
await fulfillAndInvoice(client, order);
// 2. When carrier picks up, send tracking
const carrierData = await waitForCarrierPickup(order.sellerOrderId);
const invoice = await getLatestInvoice(order.sellerOrderId);
await handleCarrierPickup(
client,
order.marketplaceOrderId,
invoice.number,
carrierData
);
// 3. When delivered, confirm
await waitForDeliveryConfirmation(order.sellerOrderId);
await handleDeliveryConfirmation(
client,
order.marketplaceOrderId,
invoice.number
);
}
async function waitForCarrierPickup(
sellerOrderId: string
): Promise<{ name: string; trackingId: string; trackingUrl: string }> {
// Replace with actual carrier integration
return {
name: "Correios",
trackingId: "BR123456789",
trackingUrl: "https://tracking.example.com/BR123456789",
};
}
async function getLatestInvoice(
sellerOrderId: string
): Promise<{ number: string }> {
// Replace with actual invoice lookup
return { number: `NFE-${sellerOrderId}` };
}
async function waitForDeliveryConfirmation(
sellerOrderId: string
): Promise<void> {
// Replace with actual delivery confirmation logic
console.log(`Waiting for delivery confirmation: ${sellerOrderId}`);
}
常见故障模式
-
将索引模拟当作结账处理。 同一端点在索引期间接收最简请求体(不含客户位置),在结账期间接收完整请求体。对于最简请求返回结账级别的
logisticsInfo可能是多余的工作;对于结账请求仅返回价格而不包含 SLA 和logisticsInfo,则会导致配送方式选择失败。 -
下单时遗漏
orderId。 VTEX 期望响应为一个数组,其中回显的每笔订单都设置了**orderId**,其值为你的预留订单 ID。返回空的 200 响应或缺少orderId,都会导致订单流程停滞。 -
在 OMS 发票 URL 中使用预留订单
orderId。 下单后,调用/api/oms/pvt/orders/...时必须使用协议中的marketplaceOrderId。混淆这两个 ID 会导致 404 或发票/跟踪信息提交静默失败。 -
在履约授权前发送发票。 卖家在订单下达后立即发送发票通知,而此时尚未收到来自市场的 Authorize Fulfillment 回调。付款可能仍处于待处理或审核中。授权前开具发票可能导致发票被拒收,或订单处于不一致状态。只有收到
POST /pvt/orders/{sellerOrderId}/fulfill后才能发送发票。 -
未处理取消订单的退货发票。 卖家尝试直接调用 Cancel Order 端点取消已开具发票的订单,却没有先发送退货发票。订单一旦处于“已开票”状态,若没有退货发票(
type: "Input"),便无法取消。Cancel Order API 会拒绝该请求。
// Correct: Send return invoice before canceling an invoiced order
async function cancelInvoicedOrder(
client: AxiosInstance,
marketplaceOrderId: string,
originalItems: InvoiceItem[],
originalInvoiceValue: number
): Promise<void> {
// Step 1: Send return invoice (type: "Input")
await sendInvoiceNotification(client, marketplaceOrderId, {
type: "Input", // Return invoice
invoiceNumber: `RET-${Date.now()}`,
invoiceValue: originalInvoiceValue,
issuanceDate: new Date().toISOString(),
items: originalItems,
});
// Step 2: Now cancel the order
await client.post(
`/api/marketplace/pvt/orders/${marketplaceOrderId}/cancel`,
{ reason: "Customer requested return" }
);
}
- 履约模拟超过 2.5 秒超时。 卖家的履约模拟端点执行复杂的数据库查询或外部 API 调用,导致响应时间超过限制。VTEX 市场最多等待 2.5 秒以获取履约模拟响应。超时后,该商品会被视为不可用/未激活,因此不会显示在店面或结账页面中。请预缓存价格和库存数据。
审核清单
-
POST /pvt/orderForms/simulation是否同时处理最简请求(索引)和结账格式的请求,并在存在相关上下文时返回logisticsInfo和slas? -
POST /pvt/orders是否接受数组,并为每笔订单返回orderId(预留订单 ID)? -
POST /pvt/orders/{sellerOrderId}/fulfill是否读取marketplaceOrderId和marketplaceOrderGroup,并将sellerOrderId与下单时的预留订单相匹配? - OMS
/invoice和PATCH .../invoice调用是否使用marketplaceOrderId,而非预留订单 ID? - 卖家是否只在收到 Authorize Fulfillment 回调后才开始实际履约?
- 对于 IO/BFF 连接器:缓存和路由选择是否符合 vtex-io 技能中的要求(模拟 SLA、数据范围)?
- 发票请求体是否包含所有必填字段(
type、invoiceNumber、invoiceValue、issuanceDate、items)? -
invoiceValue的单位是否为分(而不是美元)? - 是否在承运商提供真实数据后单独发送跟踪信息(而非使用硬编码的占位内容)?
- 对于拆分发货,每张发票是否仅包含对应包裹的商品和金额?
- 已开票订单的取消是否先通过退货发票(
type: "Input")处理? - 履约模拟端点是否能在 2.5 秒内响应?
参考资料
- External Seller 集成指南 — 端到端卖家连接器(履约 URL、订单、开票)
- Marketplace Protocol — External seller fulfillment — 模拟、下单、授权履约及相关端点
- External Seller Connector — Order invoicing — 何时以及如何向 Marketplace OMS 通知发票
- Order Invoice Notification API — 向 OMS 提交发票的
POST请求(路径中的orderId= Marketplace 订单) - Update Order Tracking API — 对发票中的物流跟踪信息执行
PATCH请求 - Order Flow and Status — 订单状态生命周期
VTEX 还在 vtex-apps/external-seller-example GitHub 仓库中维护着 External Seller 服务的开源参考实现(可用作脚手架;请使其行为与上述官方协议文档保持一致)。