Loading...
Loading...
Apply when implementing fulfillment, invoice, or tracking logic for VTEX marketplace seller connectors. Covers the External Seller fulfillment protocol: fulfillment simulation (checkout and indexation), order placement with reservation id, order dispatch (authorize fulfillment), OMS invoice and tracking APIs, and partial invoicing. Use for seller-side services that must answer within the simulation SLA and integrate with VTEX marketplace order management.
npx skill4agent add vtex/skills marketplace-fulfillmentPOST /pvt/orderForms/simulationPOST /pvt/ordersorderIdPOST /pvt/orders/{sellerOrderId}/fulfillorderIdPOST /api/oms/pvt/orders/{marketplaceOrderId}/invoicePATCH /api/oms/pvt/orders/{marketplaceOrderId}/invoice/{invoiceNumber}marketplace-catalog-syncmarketplace-order-hookmarketplace-rate-limitingPOST /pvt/orderForms/simulationitemsitemspostalCodecountryclientProfileDatashippingDataselectedSlaitemslogisticsInfopostalCodecountryallowMultipleDeliveriestrueitems[].iditems[].sellerlogisticsInfo[]itemIndexslas[]deliveryChannels[]shipsTostockBalancepriceshippingEstimateshippingEstimateDate5bd30mpickupStoreInfofriendlyNamePOST /pvt/ordersorderIdsellerOrderIdPOST /pvt/orders/{sellerOrderId}/fulfillsellerOrderIdorderIdmarketplaceOrderIdmarketplaceOrderGroupdatemarketplaceOrderIdorderIdreceiptPOST /api/oms/pvt/orders/{marketplaceOrderId}/invoicetypeinvoiceNumberinvoiceValueissuanceDateitemsmarketplaceOrderIdtype: "Output"type: "Input"PATCH /api/oms/pvt/orders/{marketplaceOrderId}/invoice/{invoiceNumber}invoiceValuetype: "Input"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} ─────────▶│{orderId}/api/oms/pvt/orders/{orderId}/...orderIdPOST /pvt/ordersmarketplaceOrderIdPOST /pvt/ordersorderIdPOST .../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/simulationitemspostalCodeclientProfileDatashippingDatalogisticsInfoslasallowMultipleDeliverieslogisticsInfoitemslogisticsInfoshippingData// 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 });orderIdPOST /pvt/ordersorderIdsellerOrderIdPOST /pvt/orders/{sellerOrderId}/fulfillorderIdorderIdapp.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
});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,
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
}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,
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/simulationitems[]idsellerpostalCodeclientProfileDatalogisticsInfoitemIndexslaspickupStoreInfodeliveryChannelsstockBalancecountrypostalCodeallowMultipleDeliveriesimport { 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/ordersorderIdfollowUpEmailorderIdsellerOrderIdPOST /pvt/orders/{sellerOrderId}/fulfillimport { 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}`);
}logisticsInfologisticsInfoorderIdorderIdorderIdorderIdmarketplaceOrderId/api/oms/pvt/orders/...POST /pvt/orders/{sellerOrderId}/fulfilltype: "Input"// 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" }
);
}POST /pvt/orderForms/simulationlogisticsInfoslasPOST /pvt/ordersorderIdPOST /pvt/orders/{sellerOrderId}/fulfillmarketplaceOrderIdmarketplaceOrderGroupsellerOrderId/invoicePATCH .../invoicemarketplaceOrderIdtypeinvoiceNumberinvoiceValueissuanceDateitemsinvoiceValuetype: "Input"POSTorderIdPATCHvtex-apps/external-seller-example