Connecting Shopify to 3PLGuys
Route Shopify orders directly to your 3PLGuys warehouse for automated fulfillment. This guide covers syncing inventory to Shopify, receiving orders via webhooks, and creating Pick & Pack shipments automatically.
Prerequisites
You need a 3PLGuys OAuth application with shipments and inventory scopes, plus a Shopify app with read_orders, write_fulfillments, and write_inventory scopes.
Architecture Overview
Your integration acts as the bridge:
- Shopify sends an order webhook to your server
- Your server maps Shopify line items to 3PLGuys product IDs
- 3PLGuys fulfills the order via Pick & Pack
- Your server polls for
forwardedstatus, then marks the Shopify order as fulfilled
Step 1: Map Products
Before processing orders, create a mapping between Shopify variant IDs and 3PLGuys product IDs. You can store this in a database or config file.
// Fetch your 3PLGuys product catalogconst products = await fetch("https://api.3plguys.com/v0/inventory/products?take=50",{ headers: { Authorization: `Bearer ${tplToken}` } }).then(r => r.json());// Build a SKU → productId mapconst skuMap = {};for (const product of products) {skuMap[product.sku] = product.id;}// Now when a Shopify order comes in, match by SKU:// shopifyItem.sku → skuMap[shopifyItem.sku] → 3PLGuys productId
SKU matching
The simplest approach is matching by SKU — keep your Shopify variant SKUs identical to your 3PLGuys product SKUs. This avoids maintaining a separate mapping table.
Step 2: Handle Shopify Order Webhooks
Register a Shopify webhook for orders/create events. When an order comes in, create a Pick & Pack shipment.
import express from "express";import crypto from "crypto";const app = express();app.use(express.raw({ type: "application/json" }));const SHOPIFY_SECRET = process.env.SHOPIFY_WEBHOOK_SECRET;app.post("/webhooks/shopify/orders", async (req, res) => {// Verify webhook signatureconst hmac = crypto.createHmac("sha256", SHOPIFY_SECRET).update(req.body).digest("base64");if (hmac !== req.headers["x-shopify-hmac-sha256"]) {return res.status(401).send("Invalid signature");}const order = JSON.parse(req.body);res.status(200).send("OK"); // Respond quickly// Process asynchronouslyawait fulfillOrder(order);});
Step 3: Create Pick & Pack Shipment
Map the Shopify order data to a 3PLGuys Pick & Pack draft:
const TPL_BASE = "https://api.3plguys.com/v0";const tplAuth = { Authorization: `Bearer ${tplToken}` };async function fulfillOrder(shopifyOrder) {const address = shopifyOrder.shipping_address;// 1. Create 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());// 2. Set shipping addressawait fetch(`${TPL_BASE}/shipments/pnp/${draft.id}/shipping-address`, {method: "PUT",headers: { ...tplAuth, "Content-Type": "application/json" },body: JSON.stringify({name: `${address.first_name} ${address.last_name}`,company: address.company || undefined,address1: address.address1,address2: address.address2 || undefined,city: address.city,state: address.province_code,zip: address.zip,phone: address.phone || undefined,}),});// 3. Map Shopify line items to 3PLGuys productsconst items = shopifyOrder.line_items.map(item => ({productId: skuMap[item.sku],quantity: item.quantity,}));await fetch(`${TPL_BASE}/shipments/pnp/${draft.id}/items`, {method: "PUT",headers: { ...tplAuth, "Content-Type": "application/json" },body: JSON.stringify({ items }),});// 4. Add order reference as notesawait fetch(`${TPL_BASE}/shipments/pnp/${draft.id}/notes`, {method: "PUT",headers: { ...tplAuth, "Content-Type": "application/json" },body: JSON.stringify({notes: `Shopify Order #${shopifyOrder.order_number}`,}),});// 5. Submitconst submitted = await fetch(`${TPL_BASE}/shipments/pnp/${draft.id}/submit`,{ method: "POST", headers: tplAuth }).then(r => r.json());// Store the mapping: shopifyOrder.id → submitted.idawait saveOrderMapping(shopifyOrder.id, submitted.id);return submitted;}
Step 4: Sync Inventory to Shopify
Keep Shopify stock levels in sync with your 3PLGuys warehouse inventory by polling the stock levels endpoint:
async function syncInventoryToShopify() {// Fetch 3PLGuys stock levelsconst levels = await fetch(`${TPL_BASE}/inventory/products/breakdown?take=50`,{ headers: tplAuth }).then(r => r.json());for (const product of levels) {const shopifyInventoryItemId = productToShopifyMap[product.id];if (!shopifyInventoryItemId) continue;// Update Shopify inventoryawait fetch(`https://${SHOPIFY_STORE}.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_ID,inventory_item_id: shopifyInventoryItemId,available: product.quantity,}),});}}// Run every 15 minutessetInterval(syncInventoryToShopify, 15 * 60 * 1000);
Delta sync
For large catalogs, use the updatedSince parameter on the product breakdown endpoint to only fetch products that changed since your last sync. See the Syncing Inventory guide.
Step 5: Mark Orders as Fulfilled
Poll 3PLGuys for shipment status updates and mark Shopify orders as fulfilled when the warehouse ships:
async function checkAndFulfill() {const pendingOrders = await getPendingOrderMappings();for (const { shopifyOrderId, tplShipmentId } of pendingOrders) {const shipment = await fetch(`${TPL_BASE}/shipments/${tplShipmentId}`,{ headers: tplAuth }).then(r => r.json());if (shipment.status === "forwarded") {// Mark as fulfilled in Shopifyawait fetch(`https://${SHOPIFY_STORE}.myshopify.com/admin/api/2024-01/orders/${shopifyOrderId}/fulfillments.json`,{method: "POST",headers: {"X-Shopify-Access-Token": SHOPIFY_TOKEN,"Content-Type": "application/json",},body: JSON.stringify({fulfillment: {location_id: SHOPIFY_LOCATION_ID,notify_customer: true,},}),});await markOrderComplete(shopifyOrderId);}}}// Poll every 5 minutessetInterval(checkAndFulfill, 5 * 60 * 1000);
Error Handling
Build resilience into your integration:
async function withRetry(fn, maxRetries = 3) {for (let attempt = 0; attempt <= maxRetries; attempt++) {try {return await fn();} catch (err) {if (attempt === maxRetries) throw err;const status = err.response?.status;if (status === 429) {// Rate limited — wait and retryawait new Promise(r => setTimeout(r, 2 ** attempt * 1000));} else if (status >= 500) {// Server error — retry with backoffawait new Promise(r => setTimeout(r, 2 ** attempt * 1000));} else {throw err; // Client error — don't retry}}}}
| Scenario | Solution |
|---|---|
| SKU not found in mapping | Log the unmapped SKU, hold the order for manual review |
| Insufficient stock | Check stock levels before creating the draft, notify your team |
| 3PLGuys API down | Queue the order and retry with exponential backoff |
| Duplicate webhooks | Use Shopify's order ID as an idempotency key |
Production checklist
- Store the Shopify order ID → 3PLGuys shipment ID mapping in a database
- Handle webhook retries (Shopify sends the same event multiple times)
- Add logging for every API call for debugging
- Monitor for failed orders with alerts
- Test the full flow in the 3PLGuys sandbox environment first
Next Steps
- Connecting WooCommerce to 3PLGuys — Same pattern for WooCommerce stores
- Multi-Channel Fulfillment — Manage Shopify alongside other sales channels
- Automating Pick & Pack — Deep dive into the Pick & Pack API
- Syncing Inventory — Advanced polling and delta sync strategies
- Building AI Integrations — Automate fulfillment decisions with AI agents