marketplace-fulfillment
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseFulfillment, Invoice & Tracking
履约、发票与跟踪
When this skill applies
适用场景
Use this skill when building a seller integration that needs to send invoice data and tracking information to a VTEX marketplace after fulfilling an order.
- Handling the Authorize Fulfillment callback from the marketplace
- Sending invoice notifications via (VTEX marketplace order ID in the path — not the seller’s internal order number)
POST /api/oms/pvt/orders/{marketplaceOrderId}/invoice - Updating tracking information via
PATCH /api/oms/pvt/orders/{marketplaceOrderId}/invoice/{invoiceNumber} - Implementing partial invoicing for split shipments
Do not use this skill for:
- Catalog or SKU synchronization (see )
marketplace-catalog-sync - Order event consumption via Feed/Hook (see )
marketplace-order-hook - General API rate limiting (see )
marketplace-rate-limiting
当您构建卖家集成系统,需要在订单履约后向VTEX marketplace发送发票数据和跟踪信息时,可使用本技能。
- 处理来自marketplace的Authorize Fulfillment回调
- 通过发送发票通知(路径中为VTEX marketplace订单ID — 而非卖家内部订单号)
POST /api/oms/pvt/orders/{marketplaceOrderId}/invoice - 通过更新跟踪信息
PATCH /api/oms/pvt/orders/{marketplaceOrderId}/invoice/{invoiceNumber} - 为拆分发货实现部分开票
本技能不适用于:
- 商品目录或SKU同步(参见)
marketplace-catalog-sync - 通过Feed/Hook消费订单事件(参见)
marketplace-order-hook - 通用API速率限制(参见)
marketplace-rate-limiting
Decision rules
决策规则
- After payment is approved, the VTEX marketplace sends an Authorize Fulfillment request to the seller's endpoint (). Only begin fulfillment after receiving this callback.
POST /pvt/orders/{sellerOrderId}/fulfill - Send invoices via . Required fields:
POST /api/oms/pvt/orders/{orderId}/invoice,type,invoiceNumber(in cents),invoiceValue, andissuanceDatearray.items - Use for sales invoices (shipment) and
type: "Output"for return invoices.type: "Input" - Send tracking information separately after the carrier provides it, using . Do not hardcode placeholder tracking values in the initial invoice.
PATCH /api/oms/pvt/orders/{orderId}/invoice/{invoiceNumber} - For split shipments, send one invoice per package with only the items in that package. Each must reflect only its items.
invoiceValue - Once an order is invoiced, it cannot be canceled without first sending a return invoice ().
type: "Input" - The fulfillment simulation endpoint must respond within 2.5 seconds or the product is considered unavailable.
Architecture/Data Flow:
text
VTEX Marketplace External Seller
│ │
│── POST /fulfill (auth) ──────────▶│ Payment approved
│ │── Start fulfillment
│ │── Pick, pack, ship
│◀── POST /invoice (invoice) ──────│ Invoice issued
│ (status → invoiced) │
│ │── Carrier picks up
│◀── PATCH /invoice/{num} ─────────│ Tracking number added
│ (status → delivering) │
│ │── Package delivered
│◀── PATCH /invoice/{num} ─────────│ isDelivered: true
│ (status → delivered) │- 付款获批后,VTEX marketplace会向卖家的端点发送Authorize Fulfillment请求()。仅在收到该回调后再开始履约流程。
POST /pvt/orders/{sellerOrderId}/fulfill - 通过发送发票。必填字段:
POST /api/oms/pvt/orders/{orderId}/invoice、type、invoiceNumber(单位为分)、invoiceValue和issuanceDate数组。items - 销售发票(发货)使用,退货发票使用
type: "Output"。type: "Input" - 承运商提供跟踪信息后,单独通过发送跟踪信息。请勿在初始发票中硬编码占位符跟踪值。
PATCH /api/oms/pvt/orders/{orderId}/invoice/{invoiceNumber} - 对于拆分发货,每个包裹必须对应单独的发票,且仅包含该包裹内的商品。必须仅反映该包裹内商品的总价值。
invoiceValue - 订单开票后,必须先发送退货发票()才能取消订单。
type: "Input" - 履约模拟端点必须在2.5秒内响应,否则商品会被视为不可用。
架构/数据流:
text
VTEX Marketplace External Seller
│ │
│── POST /fulfill (auth) ──────────▶│ Payment approved
│ │── Start fulfillment
│ │── Pick, pack, ship
│◀── POST /invoice (invoice) ──────│ Invoice issued
│ (status → invoiced) │
│ │── Carrier picks up
│◀── PATCH /invoice/{num} ─────────│ Tracking number added
│ (status → delivering) │
│ │── Package delivered
│◀── PATCH /invoice/{num} ─────────│ isDelivered: true
│ (status → delivered) │Hard constraints
硬性约束
Constraint: Send Correct Invoice Format with All Required Fields
约束:发送包含所有必填字段的正确发票格式
The invoice notification MUST include , , , , and array. The MUST be in cents. The array MUST match the items in the order.
typeinvoiceNumberinvoiceValueissuanceDateitemsinvoiceValueitemsWhy this matters
Missing required fields cause the API to reject the invoice with 400 Bad Request, leaving the order stuck in "handling" status. Incorrect (e.g., using dollars instead of cents) causes financial discrepancies in marketplace reconciliation.
invoiceValueDetection
If you see an invoice notification payload missing , , , or → warn about missing required fields. If appears to be in dollars (e.g., instead of ) → warn about cents conversion.
invoiceNumberinvoiceValueissuanceDateitemsinvoiceValue99.909990Correct
typescript
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,
orderId: 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/${orderId}/invoice`, invoice);
}
// Example usage:
async function invoiceOrder(client: AxiosInstance, orderId: string): Promise<void> {
await sendInvoiceNotification(client, orderId, {
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
typescript
// WRONG: Missing required fields, value in dollars instead of cents
async function sendBrokenInvoice(
client: AxiosInstance,
orderId: string
): Promise<void> {
await client.post(`/api/oms/pvt/orders/${orderId}/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
});
}发票通知必须包含、、、和数组。的单位必须为分。数组必须与订单中的商品匹配。
typeinvoiceNumberinvoiceValueissuanceDateitemsinvoiceValueitems重要性
缺少必填字段会导致API以400 Bad Request拒绝发票,使订单卡在“处理中”状态。格式错误(例如使用美元而非分)会导致marketplace对账时出现财务差异。
invoiceValue检测方式
如果发现发票通知负载缺少、、或 → 警告缺少必填字段。如果看起来是美元(例如而非)→ 警告需要转换为分。
invoiceNumberinvoiceValueissuanceDateitemsinvoiceValue99.909990正确示例
typescript
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,
orderId: 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/${orderId}/invoice`, invoice);
}
// Example usage:
async function invoiceOrder(client: AxiosInstance, orderId: string): Promise<void> {
await sendInvoiceNotification(client, orderId, {
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 },
],
});
}错误示例
typescript
// WRONG: Missing required fields, value in dollars instead of cents
async function sendBrokenInvoice(
client: AxiosInstance,
orderId: string
): Promise<void> {
await client.post(`/api/oms/pvt/orders/${orderId}/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
});
}Constraint: Update Tracking Promptly After Shipping
约束:发货后及时更新跟踪信息
Tracking information MUST be sent as soon as the carrier provides it. Use to add tracking to an existing invoice.
PATCH /api/oms/pvt/orders/{orderId}/invoice/{invoiceNumber}Why this matters
Late tracking updates prevent customers from seeing shipment status in the marketplace. The order remains in "invoiced" state instead of progressing to "delivering" and then "delivered". This generates customer support tickets and damages seller reputation.
Detection
If you see tracking information being batched for daily updates instead of sent in real-time → warn about prompt tracking updates. If tracking is included in the initial invoice call but the carrier hasn't provided it yet (hardcoded/empty values) → warn.
Correct
typescript
interface TrackingUpdate {
courier: string;
trackingNumber: string;
trackingUrl?: string;
isDelivered?: boolean;
}
async function updateOrderTracking(
client: AxiosInstance,
orderId: string,
invoiceNumber: string,
tracking: TrackingUpdate
): Promise<void> {
await client.patch(
`/api/oms/pvt/orders/${orderId}/invoice/${invoiceNumber}`,
tracking
);
}
// Send tracking as soon as carrier provides it
async function onCarrierPickup(
client: AxiosInstance,
orderId: string,
invoiceNumber: string,
carrierData: { name: string; trackingId: string; trackingUrl: string }
): Promise<void> {
await updateOrderTracking(client, orderId, invoiceNumber, {
courier: carrierData.name,
trackingNumber: carrierData.trackingId,
trackingUrl: carrierData.trackingUrl,
});
console.log(`Tracking updated for order ${orderId}: ${carrierData.trackingId}`);
}
// Update delivery status when confirmed
async function onDeliveryConfirmed(
client: AxiosInstance,
orderId: string,
invoiceNumber: string
): Promise<void> {
await updateOrderTracking(client, orderId, invoiceNumber, {
courier: "",
trackingNumber: "",
isDelivered: true,
});
console.log(`Order ${orderId} marked as delivered`);
}Wrong
typescript
// WRONG: Sending empty/fake tracking data with the invoice
async function invoiceWithFakeTracking(
client: AxiosInstance,
orderId: string
): Promise<void> {
await client.post(`/api/oms/pvt/orders/${orderId}/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
}承运商提供跟踪信息后必须立即发送。使用为已有的发票添加跟踪信息。
PATCH /api/oms/pvt/orders/{orderId}/invoice/{invoiceNumber}重要性
延迟更新跟踪信息会导致客户无法在marketplace查看发货状态。订单会停留在“已开票”状态,无法推进到“配送中”和“已送达”状态。这会引发客户支持工单,损害卖家声誉。
检测方式
如果发现跟踪信息被批量每日更新而非实时发送 → 警告需及时更新跟踪信息。如果初始发票调用中包含跟踪信息,但承运商尚未提供(硬编码/空值)→ 警告。
正确示例
typescript
interface TrackingUpdate {
courier: string;
trackingNumber: string;
trackingUrl?: string;
isDelivered?: boolean;
}
async function updateOrderTracking(
client: AxiosInstance,
orderId: string,
invoiceNumber: string,
tracking: TrackingUpdate
): Promise<void> {
await client.patch(
`/api/oms/pvt/orders/${orderId}/invoice/${invoiceNumber}`,
tracking
);
}
// Send tracking as soon as carrier provides it
async function onCarrierPickup(
client: AxiosInstance,
orderId: string,
invoiceNumber: string,
carrierData: { name: string; trackingId: string; trackingUrl: string }
): Promise<void> {
await updateOrderTracking(client, orderId, invoiceNumber, {
courier: carrierData.name,
trackingNumber: carrierData.trackingId,
trackingUrl: carrierData.trackingUrl,
});
console.log(`Tracking updated for order ${orderId}: ${carrierData.trackingId}`);
}
// Update delivery status when confirmed
async function onDeliveryConfirmed(
client: AxiosInstance,
orderId: string,
invoiceNumber: string
): Promise<void> {
await updateOrderTracking(client, orderId, invoiceNumber, {
courier: "",
trackingNumber: "",
isDelivered: true,
});
console.log(`Order ${orderId} marked as delivered`);
}错误示例
typescript
// WRONG: Sending empty/fake tracking data with the invoice
async function invoiceWithFakeTracking(
client: AxiosInstance,
orderId: string
): Promise<void> {
await client.post(`/api/oms/pvt/orders/${orderId}/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
}Constraint: Handle Partial Invoicing for Split Shipments
约束:处理拆分发货的部分开票
For orders shipped in multiple packages, each shipment MUST have its own invoice with only the items included in that package. The MUST reflect only the items in that particular shipment.
invoiceValueWhy this matters
Sending a single invoice for the full order value when only partial items are shipped causes financial discrepancies. The marketplace cannot reconcile payments correctly, and the order status may not progress properly.
Detection
If you see a single invoice being sent with the full order value for partial shipments → warn about partial invoicing. If the items array doesn't match the actual items being shipped → warn.
Correct
typescript
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,
orderId: 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, orderId, {
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 order ${orderId}: ` +
`${shipment.items.length} items, value=${shipmentValue}`
);
}
}
// Example: Order with 3 items shipped in 2 packages
await sendPartialInvoices(client, "ORD-123", [
{
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
typescript
// WRONG: Sending full order value for partial shipment
async function wrongPartialInvoice(
client: AxiosInstance,
orderId: string,
totalOrderValue: number,
shippedItems: OrderItem[]
): Promise<void> {
await client.post(`/api/oms/pvt/orders/${orderId}/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
});
}对于分多个包裹发货的订单,每个包裹必须有单独的发票,且仅包含该包裹内的商品。必须仅反映该包裹内商品的总价值。
invoiceValue重要性
仅发货部分商品时发送全价发票会导致财务差异。marketplace无法正确对账,订单状态也可能无法正常推进。
检测方式
如果发现拆分发货时发送全价发票 → 警告需要部分开票。如果items数组与实际发货商品不匹配 → 警告。
正确示例
typescript
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,
orderId: 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, orderId, {
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 order ${orderId}: ` +
`${shipment.items.length} items, value=${shipmentValue}`
);
}
}
// Example: Order with 3 items shipped in 2 packages
await sendPartialInvoices(client, "ORD-123", [
{
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 },
],
},
]);错误示例
typescript
// WRONG: Sending full order value for partial shipment
async function wrongPartialInvoice(
client: AxiosInstance,
orderId: string,
totalOrderValue: number,
shippedItems: OrderItem[]
): Promise<void> {
await client.post(`/api/oms/pvt/orders/${orderId}/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
});
}Preferred pattern
推荐模式
Implement the Authorize Fulfillment Endpoint
实现Authorize Fulfillment端点
The marketplace calls this endpoint when payment is approved.
typescript
import express, { RequestHandler } from "express";
interface FulfillOrderRequest {
marketplaceOrderId: string;
}
interface OrderMapping {
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 }: FulfillOrderRequest = req.body;
console.log(
`Fulfillment authorized: seller=${sellerOrderId}, marketplace=${marketplaceOrderId}`
);
// 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,
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);付款获批后,marketplace会调用该端点。
typescript
import express, { RequestHandler } from "express";
interface FulfillOrderRequest {
marketplaceOrderId: string;
}
interface OrderMapping {
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 }: FulfillOrderRequest = req.body;
console.log(
`Fulfillment authorized: seller=${sellerOrderId}, marketplace=${marketplaceOrderId}`
);
// 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,
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);Send Invoice After Fulfillment
履约后发送发票
Once the order is packed and the invoice is generated, send the invoice notification.
typescript
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",
};
}订单打包完成并生成发票后,发送发票通知。
typescript
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",
};
}Send Tracking When Carrier Picks Up
承运商取件时发送跟踪信息
typescript
async function handleCarrierPickup(
client: AxiosInstance,
orderId: string,
invoiceNumber: string,
carrier: { name: string; trackingId: string; trackingUrl: string }
): Promise<void> {
await updateOrderTracking(client, orderId, invoiceNumber, {
courier: carrier.name,
trackingNumber: carrier.trackingId,
trackingUrl: carrier.trackingUrl,
});
console.log(
`Tracking ${carrier.trackingId} sent for order ${orderId}`
);
}typescript
async function handleCarrierPickup(
client: AxiosInstance,
orderId: string,
invoiceNumber: string,
carrier: { name: string; trackingId: string; trackingUrl: string }
): Promise<void> {
await updateOrderTracking(client, orderId, invoiceNumber, {
courier: carrier.name,
trackingNumber: carrier.trackingId,
trackingUrl: carrier.trackingUrl,
});
console.log(
`Tracking ${carrier.trackingId} sent for order ${orderId}`
);
}Confirm Delivery
确认送达
typescript
async function handleDeliveryConfirmation(
client: AxiosInstance,
orderId: string,
invoiceNumber: string
): Promise<void> {
await client.patch(
`/api/oms/pvt/orders/${orderId}/invoice/${invoiceNumber}`,
{
isDelivered: true,
courier: "",
trackingNumber: "",
}
);
console.log(`Order ${orderId} marked as delivered`);
}typescript
async function handleDeliveryConfirmation(
client: AxiosInstance,
orderId: string,
invoiceNumber: string
): Promise<void> {
await client.patch(
`/api/oms/pvt/orders/${orderId}/invoice/${invoiceNumber}`,
{
isDelivered: true,
courier: "",
trackingNumber: "",
}
);
console.log(`Order ${orderId} marked as delivered`);
}Complete Example
完整示例
typescript
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}`);
}typescript
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}`);
}Common failure modes
常见失败模式
-
Sending invoice before fulfillment authorization. The seller sends an invoice notification immediately when the order is placed, before receiving the Authorize Fulfillment callback from the marketplace. Payment may still be pending or under review. Invoicing before authorization can result in the invoice being rejected or the order being in an inconsistent state. Only send the invoice after receiving.
POST /pvt/orders/{sellerOrderId}/fulfill -
Not handling return invoices for cancellation. A seller tries to cancel an invoiced order by calling the Cancel Order endpoint directly without first sending a return invoice. Once an order is in "invoiced" status, it cannot be canceled without a return invoice (). The Cancel Order API will reject the request.
type: "Input"
typescript
// Correct: Send return invoice before canceling an invoiced order
async function cancelInvoicedOrder(
client: AxiosInstance,
orderId: string,
originalItems: InvoiceItem[],
originalInvoiceValue: number
): Promise<void> {
// Step 1: Send return invoice (type: "Input")
await sendInvoiceNotification(client, orderId, {
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/${orderId}/cancel`,
{ reason: "Customer requested return" }
);
}- Fulfillment simulation exceeding the 2.5-second timeout. The seller's fulfillment simulation endpoint performs complex database queries or external API calls that exceed the response time limit. VTEX marketplaces wait a maximum of 2.5 seconds for a fulfillment simulation response. After that, the product is considered unavailable/inactive and won't appear in the storefront or checkout. Pre-cache price and inventory data.
-
在收到履约授权前发送发票:卖家在订单创建后立即发送发票通知,而非收到Authorize Fulfillment回调后。此时付款可能仍在处理或审核中。提前开票会导致发票被拒绝或订单状态不一致。仅在收到后再发送发票。
POST /pvt/orders/{sellerOrderId}/fulfill -
未使用退货发票处理已开票订单的取消:卖家尝试直接调用取消订单端点取消已开票订单,而未先发送退货发票。订单处于“已开票”状态后,必须先发送退货发票()才能取消。取消订单API会拒绝该请求。
type: "Input"
typescript
// Correct: Send return invoice before canceling an invoiced order
async function cancelInvoicedOrder(
client: AxiosInstance,
orderId: string,
originalItems: InvoiceItem[],
originalInvoiceValue: number
): Promise<void> {
// Step 1: Send return invoice (type: "Input")
await sendInvoiceNotification(client, orderId, {
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/${orderId}/cancel`,
{ reason: "Customer requested return" }
);
}- 履约模拟超时超过2.5秒:卖家的履约模拟端点执行复杂的数据库查询或外部API调用,导致响应时间超过限制。VTEX marketplace最多等待2.5秒获取履约模拟响应。超时后,商品会被视为不可用/未激活,不会出现在店铺前台或结账页面。建议预缓存价格和库存数据。
Review checklist
审核清单
- Does the seller only begin fulfillment after receiving the Authorize Fulfillment callback?
- Does the invoice payload include all required fields (,
type,invoiceNumber,invoiceValue,issuanceDate)?items - Is in cents (not dollars)?
invoiceValue - Is tracking sent separately after the carrier provides real data (not hardcoded placeholders)?
- For split shipments, does each invoice cover only its package's items and value?
- Is cancellation of invoiced orders handled via return invoice () first?
type: "Input" - Does the fulfillment simulation endpoint respond within 2.5 seconds?
- 卖家是否仅在收到Authorize Fulfillment回调后才开始履约?
- 发票负载是否包含所有必填字段(、
type、invoiceNumber、invoiceValue、issuanceDate)?items - 的单位是否为分(而非美元)?
invoiceValue - 跟踪信息是否在承运商提供真实数据后单独发送(而非硬编码占位符)?
- 拆分发货时,每个发票是否仅包含对应包裹的商品和价值?
- 已开票订单的取消是否先通过退货发票()处理?
type: "Input" - 履约模拟端点是否在2.5秒内响应?
Reference
参考资料
- External Seller Connector - Order Invoicing — Seller-side invoicing flow in the integration guide
- Order Invoice Notification API — API reference for sending invoice data
- Update Order Tracking API — API reference for adding/updating tracking info
- Sending Invoice and Tracking to Marketplace — Guide for marketplace connector invoice/tracking flow
- Order Flow and Status — Complete order status lifecycle
- Authorize Fulfillment API — API reference for the fulfillment authorization endpoint
- External Seller Connector - Order Invoicing — 集成指南中的卖家端开票流程
- Order Invoice Notification API — 发送发票数据的API参考
- Update Order Tracking API — 添加/更新跟踪信息的API参考
- Sending Invoice and Tracking to Marketplace — marketplace连接器发票/跟踪流程指南
- Order Flow and Status — 完整的订单状态生命周期
- Authorize Fulfillment API — 履约授权端点的API参考