Automating SPD Shipments
Create, configure, and submit Small Parcel Delivery shipments through the full lifecycle using the API. SPD is used for sending cartons to Amazon FBA and similar fulfillment centers.
Required Scope
Your OAuth application needs the shipments scope to access these endpoints.
SPD Shipment Lifecycle
Every SPD shipment follows this status progression:
All configuration (items, labels, notes) happens during the draft phase. Once submitted, the shipment is locked.
SPD vs Pick & Pack
SPD ships full cartons only — you select carton types and quantities, and the warehouse ships them as-is. Pick & Pack opens cartons to pick individual product units, repacks them, and ships to a customer address (incurring additional labor fees). SPD deducts inventory immediately on submit, so make sure stock is available. If cancelled, inventory is restored automatically.
Step 1: Create a Draft
/v0/shipments/spdCreate a new SPD draft
curl -X POST https://api.3plguys.com/v0/shipments/spd \-H "Authorization: Bearer YOUR_TOKEN" \-H "Content-Type: application/json" \-d '{ "warehouseId": "1" }'
const res = await fetch("https://api.3plguys.com/v0/shipments/spd", {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/spd",json={"warehouseId": "1"},headers={"Authorization": f"Bearer {token}"},)draft = res.json()print(f"Draft created: {draft['id']}")
{"id": "21","status": "draft","type": "outbound-spd","warehouse": { "id": "1", "name": "Main Warehouse" },"notes": "","createdAt": "2026-03-04T10:00:00.000Z","updatedAt": "2026-03-04T10:00:00.000Z"}
Step 2: Set Items (Carton Types)
SPD items are carton types with counts — not individual products. Each item represents "ship X cartons of this type."
/v0/shipments/spd/:id/itemsSet items — replaces all existing items
curl -X PUT https://api.3plguys.com/v0/shipments/spd/21/items \-H "Authorization: Bearer YOUR_TOKEN" \-H "Content-Type: application/json" \-d '{"items": [{ "cartonTypeId": "1", "cartonCount": 5 },{ "cartonTypeId": "2", "cartonCount": 3 }]}'
await fetch(`https://api.3plguys.com/v0/shipments/spd/${draftId}/items`, {method: "PUT",headers: {Authorization: `Bearer ${token}`,"Content-Type": "application/json",},body: JSON.stringify({items: [{ cartonTypeId: "1", cartonCount: 5 },{ cartonTypeId: "2", cartonCount: 3 },],}),});
[{"id": "16","cartonTypeId": "1","cartonTypeName": "10-Pack Case","productId": "1","sku": "ABC123","productName": "Egg Shells","cartonCount": 5,"unitsPerCarton": 10,"attachments": []},{"id": "17","cartonTypeId": "2","cartonTypeName": "24-Pack Case","productId": "1","sku": "ABC123","productName": "Egg Shells","cartonCount": 3,"unitsPerCarton": 24,"attachments": []}]
Note the id field on each item — you'll need this for uploading labels in the next step.
Step 3: Upload Shipping Labels
Every item must have a shipping label before the shipment can be submitted. Labels are uploaded as raw binary data.
/v0/shipments/spd/:id/items/:itemId/shipping-labelUpload shipping label (binary body)
curl -X PUT \"https://api.3plguys.com/v0/shipments/spd/21/items/16/shipping-label?fileName=label-10pack.pdf" \-H "Authorization: Bearer YOUR_TOKEN" \-H "Content-Type: application/octet-stream" \--data-binary @label-10pack.pdf
import fs from "fs";const labelData = fs.readFileSync("label-10pack.pdf");await fetch(`https://api.3plguys.com/v0/shipments/spd/${draftId}/items/16/shipping-label?fileName=label-10pack.pdf`,{method: "PUT",headers: {Authorization: `Bearer ${token}`,"Content-Type": "application/octet-stream",},body: labelData,});
label_data = open("label-10pack.pdf", "rb").read()res = httpx.put(f"https://api.3plguys.com/v0/shipments/spd/{draft_id}/items/16/shipping-label",params={"fileName": "label-10pack.pdf"},headers={"Authorization": f"Bearer {token}","Content-Type": "application/octet-stream",},content=label_data,)
Repeat for each item. FNSKU labels and custom attachments are optional.
Step 4: Submit
Submitting validates everything and triggers fulfillment. No request body needed.
/v0/shipments/spd/:id/submitValidate labels + stock, deduct inventory, create pending shipment
What happens on submit:
- Validates all items have shipping labels
- Checks stock is available at the warehouse
- Immediately deducts inventory
- Creates a new shipment in
pendingstatus - Deletes the draft
curl -X POST https://api.3plguys.com/v0/shipments/spd/21/submit \-H "Authorization: Bearer YOUR_TOKEN"
const res = await fetch(`https://api.3plguys.com/v0/shipments/spd/${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": "22","status": "pending","type": "outbound-spd","warehouse": { "id": "1", "name": "Main Warehouse" },"notes": "","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 5: Track Status
Poll the shipment to monitor progress through the warehouse:
curl https://api.3plguys.com/v0/shipments/22 \-H "Authorization: Bearer YOUR_TOKEN"
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}}
Complete Example
import fs from "fs";const BASE = "https://api.3plguys.com/v0";const TOKEN = process.env.API_TOKEN;const auth = { Authorization: `Bearer ${TOKEN}` };async function createSpdShipment({ warehouseId, items, labels, notes }) {// 1. Create draftconst draft = await fetch(`${BASE}/shipments/spd`, {method: "POST",headers: { ...auth, "Content-Type": "application/json" },body: JSON.stringify({ warehouseId }),}).then(r => r.json());// 2. Set itemsconst itemRes = await fetch(`${BASE}/shipments/spd/${draft.id}/items`, {method: "PUT",headers: { ...auth, "Content-Type": "application/json" },body: JSON.stringify({ items }),}).then(r => r.json());// 3. Upload labels for each itemfor (const item of itemRes) {const labelFile = labels[item.cartonTypeId];if (!labelFile) throw new Error(`No label for carton type ${item.cartonTypeId}`);await fetch(`${BASE}/shipments/spd/${draft.id}/items/${item.id}/shipping-label?fileName=${labelFile}`,{method: "PUT",headers: { ...auth, "Content-Type": "application/octet-stream" },body: fs.readFileSync(labelFile),});}// 4. Set notes (optional)if (notes) {await fetch(`${BASE}/shipments/spd/${draft.id}/notes`, {method: "PUT",headers: { ...auth, "Content-Type": "application/json" },body: JSON.stringify({ notes }),});}// 5. Submitconst submitted = await fetch(`${BASE}/shipments/spd/${draft.id}/submit`, {method: "POST",headers: auth,}).then(r => r.json());return submitted; // Use submitted.id for tracking}// Usageconst shipment = await createSpdShipment({warehouseId: "1",items: [{ cartonTypeId: "1", cartonCount: 5 },{ cartonTypeId: "2", cartonCount: 3 },],labels: {"1": "label-10pack.pdf","2": "label-24pack.pdf",},notes: "FBA restock Q1",});console.log(`Shipment ${shipment.id} submitted!`);
Error Handling
Common errors during SPD shipment creation:
| Status | Message | Cause |
|---|---|---|
| 400 | Only draft shipments can be edited | Tried to modify items/labels on a non-draft |
| 400 | Only draft shipments can be submitted | Tried to submit a non-draft shipment |
| 400 | Please add at least one item before submitting | Submitted with no items set |
| 400 | Please upload shipping labels for every item | One or more items missing a SHIPPING_LABEL |
| 400 | Not enough cartons of '...' in stock to submit this shipment | Not enough cartons at the warehouse |
| 400 | Only pending shipments can request cancellation | Tried to cancel a non-pending shipment |
| 404 | Carton type not found: | Invalid cartonTypeId in items |
| 404 | Shipment not found | Invalid shipment ID or wrong organization |
Tip
Always check stock levels via /v0/inventory/cartons/breakdown before submitting to avoid insufficient stock errors.
Next Steps
- Automating Pick & Pack Shipments — Fulfill customer orders with individual unit picking
- Syncing Inventory — Keep stock levels accurate before submitting shipments
- Inbound Receiving — Track inbound containers and replenishment shipments
- Multi-Channel Fulfillment — Manage fulfillment across multiple sales channels