Connecting Shopify Programmatically
Build your own integration server for full control over Shopify order fulfillment. This guide covers custom SKU mapping, inventory sync, fulfillment callbacks, and error handling.
Looking for the easy way?
If you just need orders to create shipments automatically, use the direct webhook integration instead — no code required.
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