Multi-Channel Fulfillment with 3PLGuys
Selling across multiple channels — Shopify, WooCommerce, Amazon, wholesale, your own website — creates fulfillment complexity. With the 3PLGuys API, you can route orders from every channel to a single warehouse and keep inventory accurate across all of them.
Why Multi-Channel Fulfillment?
Without a unified system:
- Inventory is fragmented across platforms
- Overselling happens when stock updates lag
- Each channel has its own fulfillment workflow
- Scaling means more manual work
With 3PLGuys as your central fulfillment layer:
- One inventory pool — all channels draw from the same stock
- One API — every order becomes a Pick & Pack shipment
- Real-time sync — push stock levels to all channels from one source
- Scale effortlessly — adding a channel is just another webhook handler
Architecture
The order router is your middleware — a server that:
- Receives orders from all sales channels
- Normalizes them into a common format
- Creates 3PLGuys Pick & Pack shipments
- Tracks fulfillment and updates each channel
Unified Order Format
Normalize orders from any channel into a standard shape:
interface UnifiedOrder {channel: "shopify" | "woocommerce" | "amazon" | "direct";channelOrderId: string;customer: {name: string;company?: string;address1: string;address2?: string;city: string;state: string;zip: string;phone?: string;};items: Array<{sku: string;quantity: number;}>;notes?: string;}
function fromShopify(order) {const addr = order.shipping_address;return {channel: "shopify",channelOrderId: String(order.id),customer: {name: `${addr.first_name} ${addr.last_name}`,company: addr.company,address1: addr.address1,address2: addr.address2,city: addr.city,state: addr.province_code,zip: addr.zip,phone: addr.phone,},items: order.line_items.map(i => ({ sku: i.sku, quantity: i.quantity })),notes: `Shopify #${order.order_number}`,};}function fromWooCommerce(order) {const addr = order.shipping;return {channel: "woocommerce",channelOrderId: String(order.id),customer: {name: `${addr.first_name} ${addr.last_name}`,company: addr.company,address1: addr.address_1,address2: addr.address_2,city: addr.city,state: addr.state,zip: addr.postcode,phone: order.billing.phone,},items: order.line_items.map(i => ({ sku: i.sku, quantity: i.quantity })),notes: `WooCommerce #${order.number}`,};}function fromAmazon(order, items) {return {channel: "amazon",channelOrderId: order.AmazonOrderId,customer: {name: order.ShippingAddress.Name,address1: order.ShippingAddress.AddressLine1,address2: order.ShippingAddress.AddressLine2,city: order.ShippingAddress.City,state: order.ShippingAddress.StateOrRegion,zip: order.ShippingAddress.PostalCode,phone: order.ShippingAddress.Phone,},items: items.map(i => ({ sku: i.SellerSKU, quantity: i.QuantityOrdered })),notes: `Amazon ${order.AmazonOrderId}`,};}
Fulfillment Engine
A single function handles fulfillment for all channels:
const TPL_BASE = "https://api.3plguys.com/v0";const tplAuth = { Authorization: `Bearer ${tplToken}` };async function fulfillOrder(order) {// 1. Resolve SKUs to 3PLGuys product IDsconst items = order.items.map(item => {const productId = skuMap[item.sku];if (!productId) throw new Error(`Unknown SKU: ${item.sku}`);return { productId, quantity: item.quantity };});// 2. Create Pick & Pack draftconst draft = await fetch(`${TPL_BASE}/shipments/pnp`, {method: "POST",headers: { ...tplAuth, "Content-Type": "application/json" },body: JSON.stringify({ warehouseId: "1" }),}).then(r => r.json());// 3. Set shipping addressawait fetch(`${TPL_BASE}/shipments/pnp/${draft.id}/shipping-address`, {method: "PUT",headers: { ...tplAuth, "Content-Type": "application/json" },body: JSON.stringify(order.customer),});// 4. Set itemsawait fetch(`${TPL_BASE}/shipments/pnp/${draft.id}/items`, {method: "PUT",headers: { ...tplAuth, "Content-Type": "application/json" },body: JSON.stringify({ items }),});// 5. Set notes with channel referenceawait fetch(`${TPL_BASE}/shipments/pnp/${draft.id}/notes`, {method: "PUT",headers: { ...tplAuth, "Content-Type": "application/json" },body: JSON.stringify({ notes: order.notes }),});// 6. Submitconst submitted = await fetch(`${TPL_BASE}/shipments/pnp/${draft.id}/submit`,{ method: "POST", headers: tplAuth }).then(r => r.json());// 7. Store mappingawait db.orders.create({channel: order.channel,channelOrderId: order.channelOrderId,tplShipmentId: submitted.id,status: "submitted",});return submitted;}
Unified Inventory Sync
Push a single source of truth to all channels simultaneously:
async function syncInventoryToAllChannels() {// Fetch 3PLGuys stock levels (single source of truth)const levels = await fetch(`${TPL_BASE}/inventory/products/breakdown?take=50`,{ headers: tplAuth }).then(r => r.json());const updates = levels.map(p => ({sku: p.sku,productId: p.id,available: p.quantity,}));// Sync to all channels in parallelawait Promise.all([syncToShopify(updates),syncToWooCommerce(updates),syncToAmazon(updates),]);console.log(`Synced ${updates.length} products to all channels`);}async function syncToShopify(updates) {for (const item of updates) {const inventoryItemId = shopifyMap[item.sku];if (!inventoryItemId) continue;await fetch(`https://${SHOP}.myshopify.com/admin/api/2024-01/inventory_levels/set.json`,{method: "POST",headers: {"X-Shopify-Access-Token": SHOPIFY_TOKEN,"Content-Type": "application/json",},body: JSON.stringify({location_id: SHOPIFY_LOCATION,inventory_item_id: inventoryItemId,available: item.available,}),});}}async function syncToWooCommerce(updates) {const batch = updates.filter(p => wooMap[p.sku]).map(p => ({id: wooMap[p.sku],stock_quantity: p.available,manage_stock: true,}));for (let i = 0; i < batch.length; i += 100) {await fetch(`https://store.com/wp-json/wc/v3/products/batch`, {method: "POST",headers: {Authorization: `Basic ${btoa(`${WC_KEY}:${WC_SECRET}`)}`,"Content-Type": "application/json",},body: JSON.stringify({ update: batch.slice(i, i + 100) }),});}}// Run every 10 minutessetInterval(syncInventoryToAllChannels, 10 * 60 * 1000);
Fulfillment Status Tracker
Poll 3PLGuys and update each channel when orders are shipped:
async function trackAllOrders() {const pending = await db.orders.findAll({where: { status: "submitted" },});for (const order of pending) {const shipment = await fetch(`${TPL_BASE}/shipments/${order.tplShipmentId}`,{ headers: tplAuth }).then(r => r.json());if (shipment.status === "forwarded") {// Update the originating channelswitch (order.channel) {case "shopify":await markShopifyFulfilled(order.channelOrderId);break;case "woocommerce":await markWooCompleted(order.channelOrderId);break;case "amazon":await confirmAmazonShipment(order.channelOrderId);break;}await db.orders.update({ status: "fulfilled" },{ where: { id: order.id } });}}}setInterval(trackAllOrders, 5 * 60 * 1000);
Key Benefits
| Benefit | Without 3PLGuys | With 3PLGuys |
|---|---|---|
| Inventory | Fragmented per channel | Single pool, synced everywhere |
| Overselling | Common — stock updates lag | Prevented — real-time sync from one source |
| New channel | New fulfillment workflow | Just add a webhook adapter |
| Visibility | Check each platform | One API, one dashboard |
| Scaling | More manual work per channel | Same automation handles all volume |
Getting started
Start with your highest-volume channel first. Get the webhook → fulfillment → status update loop working end-to-end, then add channels one at a time. See the detailed guides for Shopify and WooCommerce.
Next Steps
- Connecting Shopify to 3PLGuys — Detailed Shopify integration guide
- Connecting WooCommerce to 3PLGuys — Detailed WooCommerce integration guide
- Syncing Inventory — Keep stock levels accurate across all channels
- Building AI Integrations — Use AI agents to automate fulfillment decisions