API Docs

Small Parcel Delivery (SPD)

scope: shipments

Create outbound SPD shipments for Amazon FBA and similar workflows. SPD ships full cartons as-is — unlike Pick & Pack, no cartons are opened or repacked. Select carton types and quantities, upload shipping labels per item, and submit for fulfillment with automatic stock validation and inventory deduction.

Lifecycle

An SPD shipment moves through a defined set of stages. All configuration happens during the draft phase — once submitted, the shipment is locked and handled by the warehouse.

  1. 1
    Create a draft

    Choose a warehouse. The shipment starts in draft status.

  2. 2
    Set items

    Add carton types and counts. Each item represents a carton type with a quantity (e.g. 5 units of "10-Pack Case").

  3. 3
    Upload shipping labels

    Each item must have a shipping label attached before the shipment can be submitted. FNSKU labels and custom attachments are optional.

  4. 4
    Submit

    Validates that all items have shipping labels and sufficient stock exists at the warehouse. On success, inventory is immediately deducted, a new shipment is created in pending status, and the draft is deleted.

  5. 5
    Warehouse processes and ships

    The warehouse picks cartons, applies labels, and ships. Status progresses through processing to forwarded. Track via GET /v0/shipments/:id.

Status Flow

Happy path

draftpendingprocessingforwarded

Cancellation

pendingpending_cancelcancelled

SPD = full cartons only

SPD ships whole cartons as-is. You select carton types and counts — the warehouse ships them without opening. Pick & Pack opens cartons to pick individual units, repacks, and ships to a customer (incurring additional labor fees). SPD deducts inventory immediately upon submission. If cancelled, inventory is restored automatically.

Endpoints

Shipment

POST
/v0/shipments/spd

Create a new SPD draft

DELETE
/v0/shipments/spd/:id

Delete a draft shipment (draft only, 204)

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

Submit shipment — validates labels + stock, deducts inventory (draft → pending)

POST
/v0/shipments/spd/:id/cancel

Request cancellation (pending → pending_cancel)

Notes

GET
/v0/shipments/spd/:id/notes

Get shipment notes

PUT
/v0/shipments/spd/:id/notes

Update shipment notes (any status)

Items

GET
/v0/shipments/spd/:id/items

Get items with carton details and attachments

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

Set items — replaces all existing items (draft only)

Shipping Labels

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

Upload shipping label (binary body, application/octet-stream)

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

Remove shipping label

FNSKU Labels

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

Upload FNSKU label (binary body, optional)

DELETE
/v0/shipments/spd/:id/items/:itemId/fnsku-label

Remove FNSKU label

Shipment Attachments

PUT
/v0/shipments/spd/:id/attachments

Upload custom attachment (binary body)

DELETE
/v0/shipments/spd/:id/attachments/:attachmentId

Remove custom attachment

Create Draft

Creates a new SPD shipment in draft status. You must specify which warehouse will fulfill the shipment.

Request Body

ParameterTypeDescription
warehouseId*stringID of the warehouse that will fulfill this shipment
curl
curl -X POST "https://api.3plguys.com/v0/shipments/spd" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{ "warehouseId": "1" }'
Node.js
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();
Python
res = httpx.post(
"https://api.3plguys.com/v0/shipments/spd",
json={"warehouseId": "1"},
headers={"Authorization": f"Bearer {token}"},
)
draft = res.json()
Response
{
"id": "21",
"status": "draft",
"type": "outbound-spd",
"warehouse": { "id": "1", "name": "Main Warehouse" },
"notes": "",
"invoiceId": null,
"workflowId": null,
"createdAt": "2026-03-04T10:00:00.000Z",
"updatedAt": "2026-03-04T10:00:00.000Z",
}

Set Items

Sets the carton types and quantities for the shipment. This replaces all existing items — pass the full desired item list each time. Any previously uploaded labels for removed items are also deleted. Only works while the shipment is in draft status.

Request Body

ParameterTypeDescription
items*arrayList of carton types and counts to ship
items[].cartonTypeId*stringCarton type ID (must belong to your organization)
items[].cartonCount*numberNumber of cartons of this type to ship (min: 1)
curl
curl -X PUT "https://api.3plguys.com/v0/shipments/spd/21/items" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"items": [
{ "cartonTypeId": "1", "cartonCount": 5 },
{ "cartonTypeId": "2", "cartonCount": 3 }
]
}'
Node.js
const res = 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 },
],
}),
});
const items = await res.json();
Request Body
{
"items": [
{ "cartonTypeId": "1", "cartonCount": 5 },
{ "cartonTypeId": "2", "cartonCount": 3 }
]
}
Response
[
{
"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": []
}
]

Get Items

Retrieve the current items for the shipment, including carton details, product info, and all attached files.

GET /v0/shipments/spd/21/items
[
{
"id": "16",
"cartonTypeId": "1",
"cartonTypeName": "10-Pack Case",
"productId": "1",
"sku": "ABC123",
"productName": "Egg Shells",
"cartonCount": 5,
"unitsPerCarton": 10,
"attachments": [
{
"id": "1",
"type": "SHIPPING_LABEL",
"fileName": "shipping-label-10pack.pdf",
"fileUrl": "https://storage.3plguys.com/..."
}
]
}
]

Item Response Fields

ParameterTypeDescription
id*stringItem ID (used for label upload/remove endpoints)
cartonTypeId*stringCarton type ID
cartonTypeName*stringCarton type name
productId*stringProduct ID of the contents
sku*stringProduct SKU
productName*stringProduct name
cartonCount*numberNumber of cartons of this type
unitsPerCarton*numberProduct units per carton
attachments*arrayFiles attached to this item
attachments[].id*stringAttachment ID
attachments[].type*enumSHIPPING_LABEL or FNSKU_LABEL
attachments[].fileName*stringOriginal file name
attachments[].fileUrl*stringDownload URL for the file

Notes

Free-text notes visible to the warehouse. Notes can be updated at any time regardless of shipment status — unlike items and labels which are locked after submission.

Request Body

ParameterTypeDescription
notes*stringFree-text notes for the warehouse
PUT /v0/shipments/spd/21/notes
{
"notes": "FBA shipment for Q1 restock — handle with care"
}
Response
{
"notes": "FBA shipment for Q1 restock — handle with care"
}

File Upload

Shipping labels, FNSKU labels, and custom attachments are uploaded as raw binary data. The request body is the file content itself — not multipart form data.

Upload Requirements

Header / ParamValue
Content-Typeapplication/octet-stream
fileName (query param)Original file name, e.g. label.pdf
Request bodyRaw binary file content

Upload Shipping Label

Uploads a shipping label for a specific item. Each item can have one shipping label — uploading again replaces the existing one. Every item must have a shipping label before the shipment can be submitted.

curl
curl -X PUT \
"https://api.3plguys.com/v0/shipments/spd/21/items/16/shipping-label?fileName=label.pdf" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/octet-stream" \
--data-binary @label.pdf
Node.js
import fs from "fs";
const labelData = fs.readFileSync("label.pdf");
const res = await fetch(
`https://api.3plguys.com/v0/shipments/spd/${draftId}/items/${itemId}/shipping-label?fileName=label.pdf`,
{
method: "PUT",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/octet-stream",
},
body: labelData,
}
);
const attachment = await res.json();
Python
label_data = open("label.pdf", "rb").read()
res = httpx.put(
f"https://api.3plguys.com/v0/shipments/spd/{draft_id}/items/{item_id}/shipping-label",
params={"fileName": "label.pdf"},
headers={
"Authorization": f"Bearer {token}",
"Content-Type": "application/octet-stream",
},
content=label_data,
)
Response
{
"id": "1",
"fileName": "label.pdf",
"fileUrl": "https://storage.3plguys.com/..."
}

Upload Response Fields

ParameterTypeDescription
id*stringAttachment ID (use for deletion)
fileName*stringFile name as provided in the query parameter
fileUrl*stringURL to download the uploaded file

Remove Shipping Label

Removes the shipping label from an item. Only works while the shipment is in draft status. Returns 204 No Content.

curl — Remove Shipping Label
curl -X DELETE \
"https://api.3plguys.com/v0/shipments/spd/21/items/16/shipping-label" \
-H "Authorization: Bearer <token>"

Upload FNSKU Label

Uploads an FNSKU label for a specific item. Same upload pattern as shipping labels — raw binary body with fileName query parameter. FNSKU labels are optional and not required for submission.

curl — Upload FNSKU Label
curl -X PUT \
"https://api.3plguys.com/v0/shipments/spd/21/items/16/fnsku-label?fileName=fnsku.pdf" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/octet-stream" \
--data-binary @fnsku.pdf

Remove FNSKU Label

Removes the FNSKU label from an item. Returns 204 No Content. Draft only.

Upload Custom Attachment

Uploads a custom attachment to the shipment itself (not a specific item). You can attach multiple files. Custom attachments are optional.

curl — Upload Custom Attachment
curl -X PUT \
"https://api.3plguys.com/v0/shipments/spd/21/attachments?fileName=packing-instructions.pdf" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/octet-stream" \
--data-binary @packing-instructions.pdf
Response
{
"id": "5",
"fileName": "packing-instructions.pdf",
"fileUrl": "https://storage.3plguys.com/..."
}

Remove Custom Attachment

Removes a custom attachment by its ID. Returns 204 No Content.

curl — Remove Custom Attachment
curl -X DELETE \
"https://api.3plguys.com/v0/shipments/spd/21/attachments/5" \
-H "Authorization: Bearer <token>"

Submit

Submitting validates the shipment and triggers fulfillment. This is a multi-step operation:

  1. Validates items exist — the shipment must have at least one item.
  2. Validates shipping labels — every item must have a SHIPPING_LABEL attachment.
  3. Checks stock — verifies there are enough cartons of each type at the warehouse.
  4. Deducts inventory — carton counts are immediately decremented at the warehouse.
  5. Creates new shipment — a new shipment in pending status is created with all items, labels, and attachments copied over.
  6. Deletes the draft — the original draft shipment is removed.

No request body is needed. The response is the newly created shipment.

POST /v0/shipments/spd/21/submit
{
"id": "22",
"status": "pending",
"type": "outbound-spd",
"warehouse": { "id": "1", "name": "Main Warehouse" },
"notes": "FBA shipment for Q1 restock",
"invoiceId": null,
"workflowId": null,
"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, cancellation, or any further API calls.

Cancel

Request cancellation of a submitted shipment. The shipment must be in pending status. This moves the status to pending_cancel. The warehouse will review the request and either confirm the cancellation (restoring inventory) or continue processing.

No request body is needed.

POST /v0/shipments/spd/22/cancel
{
"id": "22",
"status": "pending_cancel",
"type": "outbound-spd",
"warehouse": { "id": "1", "name": "Main Warehouse" },
"notes": "FBA shipment for Q1 restock",
"invoiceId": null,
"workflowId": null,
"createdAt": "2026-03-04T10:05:00.000Z",
"updatedAt": "2026-03-04T10:06:00.000Z",
}

Delete Draft

Permanently deletes a draft SPD shipment, including all items, labels, and attachments. Only works while the shipment is in draft status. Returns 204 No Content on success.

DELETE /v0/shipments/spd/21
204 No Content

Shipment Response Fields

Create, submit, and cancel endpoints return the full shipment object. This follows the same structure as GET /v0/shipments/:id.

ParameterTypeDescription
id*stringUnique shipment ID
status*enumCurrent status: draft, pending, processing, forwarded, pending_cancel, cancelled (see Shipments page for full lifecycle)
type*enumAlways "outbound-spd" for SPD shipments
warehouse*objectWarehouse handling this shipment
warehouse.id*stringWarehouse ID
warehouse.name*stringWarehouse name
notesstringFree-text notes attached to the shipment
invoiceIdstring | nullInvoice ID if the shipment has been invoiced, null otherwise
workflowIdstring | nullWorkflow ID linking grouped shipments, null if not part of a workflow
createdAt*ISO 8601Timestamp when the shipment was created
updatedAt*ISO 8601Timestamp of the last update

Carton details via GET /v0/shipments/:id

The SPD-specific endpoints (create, submit, cancel) return the shipment object without the cartons array. To retrieve carton details for a submitted shipment, use GET /v0/shipments/:id which includes the full cartons breakdown.

Error Responses

400Only draft shipments can be deleted

Returned when trying to delete a non-draft shipment.

400Only draft shipments can be edited

Returned when trying to modify items, labels, or attachments on a non-draft shipment.

400Only draft shipments can be submitted

Returned when trying to submit a shipment that is not in draft status.

400Please add at least one item before submitting

Returned when submitting a shipment with no items set.

400Please upload shipping labels for every item

Returned when submitting and one or more items are missing a SHIPPING_LABEL attachment.

400Not enough cartons of '10-Pack Case' in stock to submit this shipment

Returned when the warehouse does not have enough cartons in stock for the requested quantity. The error message includes the specific product name.

400Only pending shipments can request cancellation

Cancellation is only available for shipments in pending status.

404Carton type not found: <id>

A carton type ID in the items list does not exist or does not belong to your organization.

404Shipment not found

The shipment ID does not exist or does not belong to your organization.

404Item not found

The item ID does not exist or does not belong to this shipment.

SPD vs Pick & Pack

Both shipment types follow the same lifecycle but have key differences in how items and fulfillment work.

Pick & PackSPD
What shipsIndividual units — warehouse opens cartons, picks units, repacksFull cartons — shipped as-is, no opening or repacking
ItemsProducts + unit quantitiesCarton types + carton counts
Shipping addressRequired (ships to customer)Not needed (ships to FBA or similar)
File uploadsNoneShipping labels required, FNSKU + attachments optional
Additional feesPick & pack labor fees per unitNo additional labor — cartons ship whole
Submit validationItems + shipping addressItems + shipping labels + stock check
Inventory deductionWhen warehouse forwardsImmediately on submit

Key points

  • Items and labels can only be modified while the shipment is in draft status.
  • Notes can be updated at any time, regardless of status.
  • Every item must have a shipping label before the shipment can be submitted.
  • Inventory is deducted immediately upon submission — ensure stock is available.
  • After submission, a new shipment ID is returned. The draft is deleted.
  • Submit and cancel endpoints do not require a request body.
  • FNSKU labels and custom attachments are optional.