marketplace-fulfillment

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Fulfillment, 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
    POST /api/oms/pvt/orders/{marketplaceOrderId}/invoice
    (VTEX marketplace order ID in the path — not the seller’s internal order number)
  • 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回调
  • 通过
    POST /api/oms/pvt/orders/{marketplaceOrderId}/invoice
    发送发票通知(路径中为VTEX marketplace订单ID — 而非卖家内部订单号)
  • 通过
    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 (
    POST /pvt/orders/{sellerOrderId}/fulfill
    ). Only begin fulfillment after receiving this callback.
  • Send invoices via
    POST /api/oms/pvt/orders/{orderId}/invoice
    . Required fields:
    type
    ,
    invoiceNumber
    ,
    invoiceValue
    (in cents),
    issuanceDate
    , and
    items
    array.
  • Use
    type: "Output"
    for sales invoices (shipment) and
    type: "Input"
    for return invoices.
  • Send tracking information separately after the carrier provides it, using
    PATCH /api/oms/pvt/orders/{orderId}/invoice/{invoiceNumber}
    . Do not hardcode placeholder tracking values in the initial invoice.
  • For split shipments, send one invoice per package with only the items in that package. Each
    invoiceValue
    must reflect only its items.
  • 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
type
,
invoiceNumber
,
invoiceValue
,
issuanceDate
, and
items
array. The
invoiceValue
MUST be in cents. The
items
array MUST match the items in the order.
Why this matters
Missing required fields cause the API to reject the invoice with 400 Bad Request, leaving the order stuck in "handling" status. Incorrect
invoiceValue
(e.g., using dollars instead of cents) causes financial discrepancies in marketplace reconciliation.
Detection
If you see an invoice notification payload missing
invoiceNumber
,
invoiceValue
,
issuanceDate
, or
items
→ warn about missing required fields. If
invoiceValue
appears to be in dollars (e.g.,
99.90
instead of
9990
) → warn about cents conversion.
Correct
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
  });
}

发票通知必须包含
type
invoiceNumber
invoiceValue
issuanceDate
items
数组。
invoiceValue
的单位必须为分。
items
数组必须与订单中的商品匹配。
重要性
缺少必填字段会导致API以400 Bad Request拒绝发票,使订单卡在“处理中”状态。
invoiceValue
格式错误(例如使用美元而非分)会导致marketplace对账时出现财务差异。
检测方式
如果发现发票通知负载缺少
invoiceNumber
invoiceValue
issuanceDate
items
→ 警告缺少必填字段。如果
invoiceValue
看起来是美元(例如
99.90
而非
9990
)→ 警告需要转换为分。
正确示例
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
PATCH /api/oms/pvt/orders/{orderId}/invoice/{invoiceNumber}
to add tracking to an existing invoice.
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
invoiceValue
MUST reflect only the items in that particular shipment.
Why 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 (
    type: "Input"
    ). The Cancel Order API will reject the request.
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
    后再发送发票。
  • 未使用退货发票处理已开票订单的取消:卖家尝试直接调用取消订单端点取消已开票订单,而未先发送退货发票。订单处于“已开票”状态后,必须先发送退货发票(
    type: "Input"
    )才能取消。取消订单API会拒绝该请求。
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
    invoiceValue
    in cents (not dollars)?
  • 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 (
    type: "Input"
    ) first?
  • Does the fulfillment simulation endpoint respond within 2.5 seconds?
  • 卖家是否仅在收到Authorize Fulfillment回调后才开始履约?
  • 发票负载是否包含所有必填字段(
    type
    invoiceNumber
    invoiceValue
    issuanceDate
    items
    )?
  • invoiceValue
    的单位是否为分(而非美元)?
  • 跟踪信息是否在承运商提供真实数据后单独发送(而非硬编码占位符)?
  • 拆分发货时,每个发票是否仅包含对应包裹的商品和价值?
  • 已开票订单的取消是否先通过退货发票(
    type: "Input"
    )处理?
  • 履约模拟端点是否在2.5秒内响应?

Reference

参考资料