Automating Pick & Pack Shipments
Pick & Pack shipments are customer orders where the warehouse opens cartons, picks individual product units, repacks them, and ships to a customer address. This guide walks through the full lifecycle via the API.
Required Scope
Your OAuth application needs the shipments scope to access these endpoints.
Pick & Pack Lifecycle
Every Pick & Pack shipment follows this status progression:
All configuration (shipping address, items, notes) happens during the draft phase. Once submitted, address and items are locked.
Pick & Pack vs SPD
Pick & Pack opens cartons to pick individual product units for customer orders — you specify products and quantities, provide a shipping address, and the warehouse handles packing and shipping. Per-unit labor fees apply. Inventory is deducted when the warehouse marks the order as forwarded. SPD ships full cartons as-is without opening — you provide carton types, quantities, and shipping labels. Use the SPD guide for that workflow.
Step 1: Create a Draft
/v0/shipments/pnpCreate a new Pick & Pack draft
curl -X POST https://api.3plguys.com/v0/shipments/pnp \-H "Authorization: Bearer YOUR_TOKEN" \-H "Content-Type: application/json" \-d '{ "warehouseId": "1" }'
const res = await fetch("https://api.3plguys.com/v0/shipments/pnp", {method: "POST",headers: {Authorization: `Bearer ${token}`,"Content-Type": "application/json",},body: JSON.stringify({ warehouseId: "1" }),});const draft = await res.json();console.log(`Draft created: ${draft.id}`);
res = httpx.post("https://api.3plguys.com/v0/shipments/pnp",json={"warehouseId": "1"},headers={"Authorization": f"Bearer {token}"},)draft = res.json()print(f"Draft created: {draft['id']}")
{"id": "15","status": "draft","type": "outbound-pickpack","warehouse": { "id": "1", "name": "Main Warehouse" },"notes": "","createdAt": "2026-03-04T10:00:00.000Z","updatedAt": "2026-03-04T10:00:00.000Z"}
Step 2: Set Shipping Address
The customer's delivery address is required before submitting. It can only be set while the shipment is in draft status.
/v0/shipments/pnp/:id/shipping-addressSet or update the shipping address
curl -X PUT https://api.3plguys.com/v0/shipments/pnp/15/shipping-address \-H "Authorization: Bearer YOUR_TOKEN" \-H "Content-Type: application/json" \-d '{"name": "John Doe","company": "Acme Corp","address1": "123 Main St","address2": "Suite 100","city": "Los Angeles","state": "CA","zip": "90001","phone": "555-1234"}'
await fetch(`https://api.3plguys.com/v0/shipments/pnp/${draftId}/shipping-address`, {method: "PUT",headers: {Authorization: `Bearer ${token}`,"Content-Type": "application/json",},body: JSON.stringify({name: "John Doe",company: "Acme Corp",address1: "123 Main St",address2: "Suite 100",city: "Los Angeles",state: "CA",zip: "90001",phone: "555-1234",}),});
res = httpx.put(f"https://api.3plguys.com/v0/shipments/pnp/{draft_id}/shipping-address",json={"name": "John Doe","company": "Acme Corp","address1": "123 Main St","address2": "Suite 100","city": "Los Angeles","state": "CA","zip": "90001","phone": "555-1234",},headers={"Authorization": f"Bearer {token}"},)
All fields are optional per-request (for partial updates), but name, address1, city, state, and zip must be set before submitting. Optional: company, address2, phone.
Step 3: Set Order Items
Order items specify which products and how many units to ship. This replaces all existing items on the draft.
/v0/shipments/pnp/:id/itemsSet order items — replaces all existing items
curl -X PUT https://api.3plguys.com/v0/shipments/pnp/15/items \-H "Authorization: Bearer YOUR_TOKEN" \-H "Content-Type: application/json" \-d '{"items": [{ "productId": "1", "quantity": 50 },{ "productId": "2", "quantity": 100 }]}'
const items = await fetch(`https://api.3plguys.com/v0/shipments/pnp/${draftId}/items`,{method: "PUT",headers: {Authorization: `Bearer ${token}`,"Content-Type": "application/json",},body: JSON.stringify({items: [{ productId: "1", quantity: 50 },{ productId: "2", quantity: 100 },],}),}).then(r => r.json());
[{"productId": "1","sku": "ABC123","productName": "Egg Shells","quantity": 50},{"productId": "2","sku": "DEF456","productName": "Bubble Wrap","quantity": 100}]
Products, not cartons
Pick & Pack items are products with unit quantities — the warehouse decides which cartons to open. This is different from SPD, where you specify carton types and counts.
Step 4: Add Notes (Optional)
Free-text notes visible to the warehouse team. Notes can be updated at any time, even after submission.
/v0/shipments/pnp/:id/notesSet or update shipment notes
curl -X PUT https://api.3plguys.com/v0/shipments/pnp/15/notes \-H "Authorization: Bearer YOUR_TOKEN" \-H "Content-Type: application/json" \-d '{ "notes": "Handle with care — fragile items inside" }'
Step 5: Submit
Submitting validates the shipping address and items, then creates a new shipment in pending status. No request body needed.
/v0/shipments/pnp/:id/submitValidate address + items, create pending shipment
What happens on submit:
- Validates a complete shipping address (name, address1, city, state, zip)
- Validates at least one order item exists
- Creates a new shipment in
pendingstatus - Deletes the draft
- Generates a packing plan
curl -X POST https://api.3plguys.com/v0/shipments/pnp/15/submit \-H "Authorization: Bearer YOUR_TOKEN"
const res = await fetch(`https://api.3plguys.com/v0/shipments/pnp/${draftId}/submit`,{method: "POST",headers: { Authorization: `Bearer ${token}` },});const submitted = await res.json();// IMPORTANT: Use the NEW ID from the response, not the draft IDconsole.log(`Submitted! New shipment ID: ${submitted.id}`);
{"id": "16","status": "pending","type": "outbound-pickpack","warehouse": { "id": "1", "name": "Main Warehouse" },"notes": "Handle with care — fragile items inside","createdAt": "2026-03-04T10:05:00.000Z","updatedAt": "2026-03-04T10:05:00.000Z"}
New ID after submit
Submitting creates a brand new shipment and deletes the draft. The response contains the new shipment ID. Always use the returned ID for tracking and cancellation.
Step 6: Track Status
Poll the shipment to monitor progress through the warehouse:
async function waitForShipment(shipmentId, token) {const terminal = ["forwarded", "cancelled"];while (true) {const res = await fetch(`https://api.3plguys.com/v0/shipments/${shipmentId}`,{ headers: { Authorization: `Bearer ${token}` } });const data = await res.json();console.log(`Status: ${data.status}`);if (terminal.includes(data.status)) return data;await new Promise(r => setTimeout(r, 60_000)); // Check every minute}}
Cancel a Shipment
If the warehouse hasn't started processing, you can request cancellation. Only available for shipments in pending status.
/v0/shipments/pnp/:id/cancelRequest cancellation (pending → pending_cancel)
curl -X POST https://api.3plguys.com/v0/shipments/pnp/16/cancel \-H "Authorization: Bearer YOUR_TOKEN"
The shipment moves to pending_cancel. The warehouse reviews the request and either confirms cancellation (cancelled) or continues processing.
Complete Example
const BASE = "https://api.3plguys.com/v0";const TOKEN = process.env.API_TOKEN;const auth = { Authorization: `Bearer ${TOKEN}` };async function createPickPackOrder({ warehouseId, address, items, notes }) {// 1. Create draftconst draft = await fetch(`${BASE}/shipments/pnp`, {method: "POST",headers: { ...auth, "Content-Type": "application/json" },body: JSON.stringify({ warehouseId }),}).then(r => r.json());// 2. Set shipping addressawait fetch(`${BASE}/shipments/pnp/${draft.id}/shipping-address`, {method: "PUT",headers: { ...auth, "Content-Type": "application/json" },body: JSON.stringify(address),});// 3. Set order itemsawait fetch(`${BASE}/shipments/pnp/${draft.id}/items`, {method: "PUT",headers: { ...auth, "Content-Type": "application/json" },body: JSON.stringify({ items }),});// 4. Set notes (optional)if (notes) {await fetch(`${BASE}/shipments/pnp/${draft.id}/notes`, {method: "PUT",headers: { ...auth, "Content-Type": "application/json" },body: JSON.stringify({ notes }),});}// 5. Submitconst submitted = await fetch(`${BASE}/shipments/pnp/${draft.id}/submit`, {method: "POST",headers: auth,}).then(r => r.json());return submitted; // Use submitted.id for tracking}// Usageconst shipment = await createPickPackOrder({warehouseId: "1",address: {name: "Jane Smith",address1: "456 Oak Ave",city: "San Francisco",state: "CA",zip: "94102",phone: "555-9876",},items: [{ productId: "1", quantity: 2 },{ productId: "2", quantity: 5 },],notes: "Gift order — include tissue paper",});console.log(`Order ${shipment.id} submitted!`);
Error Handling
Common errors during Pick & Pack shipment creation:
| Status | Message | Cause |
|---|---|---|
| 400 | Only draft shipments can be edited | Tried to modify items/address on a non-draft |
| 400 | Only draft shipments can be submitted | Tried to submit a non-draft shipment |
| 400 | Please provide a complete shipping address before submitting | Submitted without name, address1, city, state, or zip |
| 400 | Please add at least one item before submitting | Submitted with no order items |
| 400 | Only pending shipments can request cancellation | Tried to cancel a non-pending shipment |
| 400 | Only draft shipments can be deleted | Tried to delete a non-draft shipment |
| 404 | Shipment not found | Invalid shipment ID or wrong organization |
| 404 | Warehouse not found | Invalid or archived warehouse ID |
| 404 | Product not found: | Invalid, archived, or wrong-org product ID |
Key Differences from SPD
- Pick & Pack requires a shipping address — SPD requires shipping labels
- Pick & Pack items are products + quantities — SPD items are carton types + counts
- Pick & Pack inventory is deducted on forwarded — SPD deducts on submit
- Pick & Pack incurs per-unit labor fees for picking and repacking
Next Steps
- Connecting Shopify to 3PLGuys — Route Shopify orders to Pick & Pack automatically
- Connecting WooCommerce to 3PLGuys — WooCommerce order fulfillment via the API
- Multi-Channel Fulfillment — Fulfill orders from multiple channels through one warehouse
- Automating SPD Shipments — Ship full cartons with the SPD workflow
- Syncing Inventory — Keep stock levels accurate across all systems