Migrating from ShipStation Companion API (v1) to v2 requires understanding key differences in how orders, shipments, and identifiers work. This guide focuses on the practical migration steps and gotchas.
The v1 orderKey field maps directly to v2's external_shipment_id:
| v1 Field | v2 Field | Migration Notes |
|---|---|---|
orderKey | external_shipment_id | Same concept, just renamed |
v1 Example:
{
"orderKey": "MY-ORDER-123",
"orderNumber": "ORD-123"
}v2 Equivalent:
{
"external_shipment_id": "MY-ORDER-123",
"shipment_number": "ORD-123"
}All v2 internal IDs use the se- prefix:
| v1 Field | v1 Value | v2 Field | v2 Value |
|---|---|---|---|
orderId | 12345678 | shipment_id | se-12345678 |
shipmentId | 87654321 | label_id | se-87654321 |
When migrating ID references from v1 to v2, you must prepend se- to the numeric ID:
- v1
orderId:12345678 - v2
shipment_id:se-12345678
JavaScript Example:
// Converting v1 orderId to v2 shipment_id
const v1OrderId = 12345678;
const v2ShipmentId = `se-${v1OrderId}`; // "se-12345678"v2 uses different status values than v1:
| v1 orderStatus | v2 shipment_status | Notes |
|---|---|---|
awaiting_payment | pending | ⚠️ Many-to-one mapping |
awaiting_shipment | pending | ⚠️ Many-to-one mapping |
on_hold | pending | ⚠️ Many-to-one mapping |
| (no v1 equivalent) | processing | v2/ShipEngine only - transient status during async label creation |
shipped | label_purchased | Different terminology |
cancelled | cancelled | Same value |
Critical Issue: Three distinct v1 statuses (awaiting_payment, awaiting_shipment, on_hold) all map to a single v2 status (pending).
This means you cannot determine the specific v1 status from a v2 pending status without storing additional metadata in your system.
Workaround: If you need to preserve v1 status granularity, store the original v1 status in custom metadata fields or your own database.
The processing status is a transient state that only exists in v2/ShipEngine during async label creation. When you trigger label creation for a shipment, it briefly enters processing status before transitioning to label_purchased.
If you query a shipment and see processing, wait a few seconds and query again - it should have transitioned to its final state.
This status did not exist in v1.
If you're using the legacy ShipStation UI alongside the v2 API, there's an important behavior to understand.
When you create a shipment via the v2 API, it will appear as an "order" in the ShipStation UI, even if you do NOT pass create_sales_order: true.
Why This Happens:
The legacy ShipStation UI v2 system still uses the "order" terminology and treats API-created shipments as orders for display purposes. This is a backwards-compatibility feature.
What You'll See:
API: POST /v2/shipments
{
"external_shipment_id": "ORDER-123",
// create_sales_order not specified or false
...
}
ShipStation UI: Shows this as an "Order" with number "ORDER-123"This is not a bug. The ShipStation UI v2 displays API-created shipments as orders to maintain consistency with how users expect the UI to work. The underlying data model is still a v2 shipment.
The create_sales_order flag has a specific purpose:
{
"external_shipment_id": "ORDER-123",
"create_sales_order": true, // Creates a sales order record
...
}When create_sales_order is true:
- Creates both a shipment AND a separate sales order record in the UI system
- Populates
external_order_idfield automatically - Links the shipment to the sales order
When create_sales_order is false or omitted:
- Creates only a shipment (which still appears as an "order" in UI)
- No separate sales order record is created
external_order_idmay be null or auto-populated depending on context
Use create_sales_order: true when:
- You want to create a full sales order record in the ShipStation UI system
- You're integrating with sales channel imports
- You need the sales order/shipment separation in the UI
For most API-only integrations, you can omit this flag.
// ❌ v1 field names
{
orderKey: "ORDER-123",
orderNumber: "ORD-123",
orderDate: "2024-06-15T10:00:00Z"
}
// ✅ v2 field names
{
external_shipment_id: "ORDER-123",
shipment_number: "ORD-123",
created_at: "2024-06-15T10:00:00Z"
}// v1 to v2 ID conversion helper
function convertV1IdsToV2(v1Record) {
return {
shipment_id: `se-${v1Record.orderId}`,
label_id: v1Record.shipmentId ? `se-${v1Record.shipmentId}` : null
};
}// v1 to v2 status conversion
const statusMap = {
'awaiting_payment': 'pending',
'awaiting_shipment': 'pending',
'on_hold': 'pending',
'shipped': 'label_purchased',
'cancelled': 'cancelled'
};
function convertStatus(v1Status) {
return statusMap[v1Status] || 'pending';
}{
"external_shipment_id": "ORDER-123",
"shipment_status": "pending",
"metadata": {
"legacy_v1_status": "awaiting_payment" // Preserve original status
}
}| v1 Endpoint | v2 Endpoint |
|---|---|
GET /orders/{orderId} | GET /v2/shipments/{shipment_id} |
GET /orders?orderNumber={num} | GET /v2/shipments?shipment_number={num} |
GET /shipments/{shipmentId} | GET /v2/labels/{label_id} |
POST /orders/createorder | POST /v2/shipments |
POST /orders/createlabelfororder | POST /v2/labels/shipment/{shipment_id} |
// ❌ Wrong - Using numeric ID directly
const response = await fetch(`/v2/shipments/${12345678}`);
// ✅ Correct - Adding se- prefix
const response = await fetch(`/v2/shipments/se-${12345678}`);// ❌ Wrong - Assuming you can reverse-map from v2 to v1
const v2Status = 'pending';
// Can't tell if this was awaiting_payment, awaiting_shipment, or on_hold!
// ✅ Correct - Store original status if you need it
const metadata = shipment.metadata?.legacy_v1_status;// This WILL show up in ShipStation UI as an "order"
// even though create_sales_order is false
await createShipment({
external_shipment_id: "ORDER-123",
create_sales_order: false // Still appears in UI!
});- External Identifiers Guide - Deep dive on
external_shipment_idvs v1orderKey - Shipment Numbers & Uniqueness - Understanding
shipment_numbervs v1orderNumber - Understanding Orders & Shipments - Core v2 concepts
- Products and Plans - Terminology Reference - Complete field mapping table