API Docs

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:

Happy Path
Draft
Create & configure
Pending
Submitted, awaiting warehouse
Processing
Being packed
Forwarded
Shipped out
Cancellation Path
Pending
Submitted
Pending Cancel
Cancel requested
Cancelled
Inventory restored

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

POST
/v0/shipments/spd

Create 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."

PUT
/v0/shipments/spd/:id/items

Set 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.

PUT
/v0/shipments/spd/:id/items/:itemId/shipping-label

Upload 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.

POST
/v0/shipments/spd/:id/submit

Validate labels + stock, deduct inventory, create pending shipment

What happens on submit:

  1. Validates all items have shipping labels
  2. Checks stock is available at the warehouse
  3. Immediately deducts inventory
  4. Creates a new shipment in pending status
  5. 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 ID
console.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 draft
const draft = await fetch(`${BASE}/shipments/spd`, {
method: "POST",
headers: { ...auth, "Content-Type": "application/json" },
body: JSON.stringify({ warehouseId }),
}).then(r => r.json());
// 2. Set items
const 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 item
for (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. Submit
const submitted = await fetch(`${BASE}/shipments/spd/${draft.id}/submit`, {
method: "POST",
headers: auth,
}).then(r => r.json());
return submitted; // Use submitted.id for tracking
}
// Usage
const 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:

StatusMessageCause
400Only draft shipments can be editedTried to modify items/labels on a non-draft
400Only draft shipments can be submittedTried to submit a non-draft shipment
400Please add at least one item before submittingSubmitted with no items set
400Please upload shipping labels for every itemOne or more items missing a SHIPPING_LABEL
400Not enough cartons of '...' in stock to submit this shipmentNot enough cartons at the warehouse
400Only pending shipments can request cancellationTried to cancel a non-pending shipment
404Carton type not found: Invalid cartonTypeId in items
404Shipment not foundInvalid shipment ID or wrong organization

Tip

Always check stock levels via /v0/inventory/cartons/breakdown before submitting to avoid insufficient stock errors.


Next Steps