Loading...
Loading...
Apply when implementing fulfillment, invoice, or tracking logic for VTEX marketplace seller connectors. Covers the Order Invoice Notification API, invoice payload structure, tracking updates, partial invoicing for split shipments, and the authorize fulfillment flow. Use for building seller-side order fulfillment that integrates with VTEX marketplace order management including the 2.5s simulation timeout.
npx skill4agent add vtexdocs/ai-skills marketplace-fulfillmentPOST /api/oms/pvt/orders/{marketplaceOrderId}/invoicePATCH /api/oms/pvt/orders/{marketplaceOrderId}/invoice/{invoiceNumber}marketplace-catalog-syncmarketplace-order-hookmarketplace-rate-limitingPOST /pvt/orders/{sellerOrderId}/fulfillPOST /api/oms/pvt/orders/{orderId}/invoicetypeinvoiceNumberinvoiceValueissuanceDateitemstype: "Output"type: "Input"PATCH /api/oms/pvt/orders/{orderId}/invoice/{invoiceNumber}invoiceValuetype: "Input"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) │typeinvoiceNumberinvoiceValueissuanceDateitemsinvoiceValueitemsinvoiceValueinvoiceNumberinvoiceValueissuanceDateitemsinvoiceValue99.909990import 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: 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
});
}PATCH /api/oms/pvt/orders/{orderId}/invoice/{invoiceNumber}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: 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
}invoiceValueinterface 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: 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
});
}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);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,
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}`
);
}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`);
}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}`);
}POST /pvt/orders/{sellerOrderId}/fulfilltype: "Input"// 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" }
);
}typeinvoiceNumberinvoiceValueissuanceDateitemsinvoiceValuetype: "Input"