API Docs

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

Shopify → 3PLGuys Flow
Shopify Order
Customer places order
Your Server
Receives webhook, maps data
3PLGuys PnP
Create draft → submit
Fulfilled
Mark order complete in Shopify

Your integration acts as the bridge:

  1. Shopify sends an order webhook to your server
  2. Your server maps Shopify line items to 3PLGuys product IDs
  3. 3PLGuys fulfills the order via Pick & Pack
  4. Your server polls for forwarded status, 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 catalog
const products = await fetch(
"https://api.3plguys.com/v0/inventory/products?take=50",
{ headers: { Authorization: `Bearer ${tplToken}` } }
).then(r => r.json());
// Build a SKU → productId map
const 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 signature
const 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 asynchronously
await 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 draft
const 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 address
await 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 products
const 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 notes
await 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. Submit
const submitted = await fetch(
`${TPL_BASE}/shipments/pnp/${draft.id}/submit`,
{ method: "POST", headers: tplAuth }
).then(r => r.json());
// Store the mapping: shopifyOrder.id → submitted.id
await 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 levels
const 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 inventory
await 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 minutes
setInterval(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 Shopify
await 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 minutes
setInterval(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 retry
await new Promise(r => setTimeout(r, 2 ** attempt * 1000));
} else if (status >= 500) {
// Server error — retry with backoff
await new Promise(r => setTimeout(r, 2 ** attempt * 1000));
} else {
throw err; // Client error — don't retry
}
}
}
}
ScenarioSolution
SKU not found in mappingLog the unmapped SKU, hold the order for manual review
Insufficient stockCheck stock levels before creating the draft, notify your team
3PLGuys API downQueue the order and retry with exponential backoff
Duplicate webhooksUse 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