Skip to content
Last updated

Legacy API Migration

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.

Key ID Translations

orderKey → external_shipment_id

The v1 orderKey field maps directly to v2's external_shipment_id:

v1 Fieldv2 FieldMigration Notes
orderKeyexternal_shipment_idSame concept, just renamed

v1 Example:

{
  "orderKey": "MY-ORDER-123",
  "orderNumber": "ORD-123"
}

v2 Equivalent:

{
  "external_shipment_id": "MY-ORDER-123",
  "shipment_number": "ORD-123"
}

Internal ID Prefixes

All v2 internal IDs use the se- prefix:

v1 Fieldv1 Valuev2 Fieldv2 Value
orderId12345678shipment_idse-12345678
shipmentId87654321label_idse-87654321
Always Prepend se-

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"

Status Value Mapping

v2 uses different status values than v1:

v1 orderStatusv2 shipment_statusNotes
awaiting_paymentpending⚠️ Many-to-one mapping
awaiting_shipmentpending⚠️ Many-to-one mapping
on_holdpending⚠️ Many-to-one mapping
(no v1 equivalent)processingv2/ShipEngine only - transient status during async label creation
shippedlabel_purchasedDifferent terminology
cancelledcancelledSame value
Loss of Status Granularity

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.

About the processing Status

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.

Legacy ShipStation UI v2 Behavior

If you're using the legacy ShipStation UI alongside the v2 API, there's an important behavior to understand.

Shipments Appear as Orders in UI

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 Expected Behavior

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

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_id field 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_id may be null or auto-populated depending on context
When to Use create_sales_order

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.

Migration Checklist

1. Update Field Names

// ❌ 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"
}

2. Add se- Prefix to IDs

// v1 to v2 ID conversion helper
function convertV1IdsToV2(v1Record) {
  return {
    shipment_id: `se-${v1Record.orderId}`,
    label_id: v1Record.shipmentId ? `se-${v1Record.shipmentId}` : null
  };
}

3. Handle Status Mapping

// 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';
}

4. Store Original v1 Statuses (if needed)

{
  "external_shipment_id": "ORDER-123",
  "shipment_status": "pending",
  "metadata": {
    "legacy_v1_status": "awaiting_payment"  // Preserve original status
  }
}

5. Update API Endpoints

v1 Endpointv2 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/createorderPOST /v2/shipments
POST /orders/createlabelfororderPOST /v2/labels/shipment/{shipment_id}

Common Migration Pitfalls

Pitfall 1: Forgetting se- Prefix

// ❌ Wrong - Using numeric ID directly
const response = await fetch(`/v2/shipments/${12345678}`);

// ✅ Correct - Adding se- prefix
const response = await fetch(`/v2/shipments/se-${12345678}`);

Pitfall 2: Assuming Status Equivalence

// ❌ 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;

Pitfall 3: Not Understanding UI Behavior

// 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!
});