Skip to content
Need help with our api? Ask in Community
Wholesale Orders Packed Outside ShipHero

Wholesale Orders Packed Outside ShipHero

This complete flow leverages our Public API to enable third-party systems to fully automate B2B fulfillment, drastically reducing manual operational time while keeping all ShipHero data perfectly accurate and in real-time sync.

When handling Wholesale Orders that are packed outside ShipHero, the Public API allows you to manage the entire flow — from creation to fulfillment — programmatically, without touching the ShipHero UI.

Below you’ll find each step in order, exactly as tested.

Warning

To fulfill with is_packed_externally: true (packed externally), the order can’t be from SPS, there must be no valid shipping labels, the wholesale order status must be packing, and the packing layout must be 100% empty. If you instead packed in ShipHero (steps 9–12), fulfill with is_packed_externally: false — see step 13.

Statuses and transitions

Before diving into the flow, it’s important to understand how wholesale order statuses work in ShipHero. Each wholesale order goes through a series of well-defined states —from creation and inventory reservation to final fulfillment— that reflect its progress through warehouse operations. Transitions between these states are governed by strict business rules: inventory must be allocated before picking starts, packing can only occur after picking completes, and Fulfilled marks the point of no return when inventory is deducted and stores are notified.

1. Create the Wholesale Order

First, create a wholesale order.
You must include a valid SKU that already exists in your account.

mutation {
  wholesale_order_create(
    data: {
      partner_order_id: "WHOLESALE-1004"
      shop_name: "My B2B Channel"
      order_number: "OR-04"
      tags: ["wholesale"]
      shipping_address: {
        first_name: "Customer"
        last_name: "Wholesale"
        address1: "123 Street"
        city: "Santa Fe"
        state: "SF"
        zip: "3000"
        country: "AR"
      }
      line_items: [
        {
          sku: "WHITE-TSHIRT-M"
          quantity: 12
          price: "15.00"
          partner_line_item_id: "LINE-WHITE-TSHIRT-M-04"
        }
      ]
      allow_partial: false
      picking_flow: DESKTOP
    }
  ) {
    request_id
    wholesale_order {
      id
      picking_flow
      order {
        id
        order_number    
        fulfillment_status
      }
    }
  }
}

In this example we’re using manual picking (picking_flow: DESKTOP).

Note

picking_flow and shipping_option default from your account’s wholesale configuration (typically FREIGHT / MOBILE). Values you pass to wholesale_order_create may be overridden once inventory is allocated. To confirm what actually stuck, query the order:

query {
  wholesale_order(order_id: "T3JkZXI6Njg2MzAwMDEz") {
    data { id status picking_flow shipping_option }
  }
}

If you need different values (e.g. DESKTOP / COURIER), set them while the order is still in default status — before allocating (step 4):

mutation { wholesale_order_update_picking_flow(
  data: { order_id: "T3JkZXI6Njg2MzAwMDEz", picking_flow: DESKTOP }
) { request_id wholesale_order { id picking_flow } } }

mutation { wholesale_order_update(
  data: { order_id: "T3JkZXI6Njg2MzAwMDEz", shipping_option: COURIER }
) { request_id wholesale_order { id shipping_option } } }

wholesale_order_update_picking_flow only works in default status. The picking flow you end up with matters downstream: only the DESKTOP flow transitions the order ready_to_pick → picking when the picking sheet is printed (step 6), which wholesale_order_transfer_picks_to_staging (step 7) requires. For accounts that always need the same values, ask your ShipHero contact to set the wholesale config defaults so every order is created correctly.

2. Get Available Staging Locations

To see which staging bins are available, use:

query {
  wholesale_staging_location_candidates(warehouse_id: "V2FyZWhvdXNlOjE1MDU2") {
    request_id
    data {
      edges {
        node {
          id
          name
        }
      }
    }
  }
}

Example result:

{
  "data": {
    "wholesale_staging_location_candidates": {
      "data": {
        "edges": [
          { "node": { "id": "QmluOjI0MTYxMjY5", "name": "EAD_II_11" } }
        ]
      }
    }
  }
}

Note

This query only lists bins that are eligible to be assigned: flagged as staging (is_staging + is_cart + sellable, non-pickable), currently empty, and not already assigned to another wholesale order. You can’t reuse a staging location that’s already in use — assigning one returns “You cannot select a staging location that is already in use.”

No candidates returned? That means none exist yet, or they’re all in use or non-empty. To proceed, either:

  • Create one with location_create (staging: true) — see Staging Locations, or
  • Free one up by completing/canceling the wholesale orders currently holding them, or empty a bin that has the right flags but still has leftover stock.

3. Assign a Staging Location

Assign one of the free locations from the list:

mutation {
  wholesale_order_update_staging_location(
    data: {
      order_id: "T3JkZXI6Njg2MzAwMDEz"   # new order id for OR-04
      staging_location_id: "QmluOjI0MTYxMjY5" # EAD_II_11
    }
  ) {
    request_id
    wholesale_order {
      id
      staging_location_id
      status
      order { id order_number }
    }
  }
}

4. Allocate Inventory for Picking

Now allocate inventory for this order.

mutation {
  wholesale_order_auto_allocate_for_picking(
    data: {
      order_id: "T3JkZXI6Njg2MzAwMDEz"
      prioritized_location_ids: ["V2FyZWhvdXNlOjE1MDU2"]
    }
  ) {
    request_id
    complexity
    line_items {
      line_item_id
      quantity
      product { sku }
      allocations {
        __typename
      }
    }
  }
}

A successful allocation will show "__typename": "AllocatedAllocationType".

5. Mark as Ready to Pick

After allocation:

mutation {
  wholesale_set_as_ready_to_pick(
    data: { order_id: "T3JkZXI6Njg2MzAwMDEz" }
  ) {
    request_id
    ok
  }
}

The order now moves from defaultready_to_pick.

6. Print the Manual Picking Sheet

Generate a PDF with manual picking instructions:

mutation {
  wholesale_order_print_manual_picking_sheet(
    data: { order_id: "T3JkZXI6Njg2MzAwMDEz" }
  ) {
    request_id
    picking_sheet_url
  }
}

Response example:

{
  "data": {
    "wholesale_order_print_manual_picking_sheet": {
      "picking_sheet_url": "https://d2s9s6xmfnfu1v.cloudfront.net/wholesale/picking_slips/2025-10-10/picking_slip_686300013.pdf"
    }
  }
}

This PDF can be printed and handed to warehouse staff for manual picking.

7. Transfer Picks to Staging

After picking is done, move items into staging.

mutation {
  wholesale_order_transfer_picks_to_staging(
    data: { order_id: "T3JkZXI6Njg2MzAwMDEz" }
  ) {
    request_id
    wholesale_order {
      id
      status
      staging_location_id
      order { id order_number }
    }
  }
}

Status should now read packing.

8. Inspect the Staged Manifest (Optional)

Once picks are in staging, you can query the staged inventory manifest to see exactly what is sitting in the staging location for this order, broken down per line item by SKU, lot, container type (eaches vs cases), quantity, and bin. This is especially useful when your packing is driven by an external system: it tells you which lots were allocated, what’s available to pack, and gives you the IDs you need to build the wholesale_order_import_packing_layout payload in the next step.

The manifest is exposed as a staged_inventory field on each wholesale_line_item:

query {
  wholesale_order(order_id: "T3JkZXI6Njg2MzAwMDEz") {
    request_id
    data {
      id
      status
      wholesale_line_items {
        edges {
          node {
            id
            staged_inventory {
              line_item_id
              sku
              quantity
              case_type
              lot_id
              lot_name
              lot_expiration_date
              location_id
              location_name
            }
          }
        }
      }
    }
  }
}

Example response:

{
  "data": {
    "wholesale_order": {
      "data": {
        "wholesale_line_items": {
          "edges": [
            {
              "node": {
                "id": "V2hvbGVzYWxlTGluZUl0ZW06MTIz",
                "staged_inventory": [
                  {
                    "line_item_id": "TGluZUl0ZW06OTg3Ng==",
                    "sku": "WHITE-TSHIRT-M",
                    "quantity": 12,
                    "case_type": null,
                    "lot_id": "TG90OjQzMg==",
                    "lot_name": "LOT-2026-04",
                    "lot_expiration_date": "2027-04-01T00:00:00",
                    "location_id": "QmluOjI0MTYxMjY5",
                    "location_name": "EAD_II_11"
                  }
                ]
              }
            }
          ]
        }
      }
    }
  }
}

Notes on the response:

  • Only items still pending packing are returned. Once a row is fully packed, it disappears from the manifest — so an empty list for a line item means everything staged has already been packed.
  • case_type is null for loose eaches and set (for example, CASE, INNER_CASE, MASTER_CASE) when the staged unit is a product case.
  • lot_id, lot_name, and lot_expiration_date are only populated for lot-tracked products.
  • line_item_id is the order line item GID. You can pass this value directly into the order_line_item_id field of the packing_data JSON in step 9 — no translation needed.

Tip

Treat the manifest as the source of truth when assembling the import payload: the SKUs, quantities, and line_item_id values it returns are the same ones the import mutation expects.

9. Import Packing Layout (Optional)

If your packing is managed by an external system and you want to reflect the full container structure (pallets, packages, case packs, and line items) in ShipHero before fulfilling, you can import the packing layout at this point.

The order must be in packing status (after step 7) and must not have an existing packing configuration.

mutation ImportPackingLayout($data: WholesaleOrderImportPackingLayoutInput!) {
  wholesale_order_import_packing_layout(data: $data) {
    request_id
    complexity
    wholesale_order {
      id
      status
    }
  }
}

Variables:

{
  "data": {
    "order_id": "T3JkZXI6Njg2MzAwMDEz",
    "packing_data": {
      "containers": [
        {
          "type": "Pallet",
          "details": {
            "weight_in_oz": 1600.0,
            "custom_box_id": "QXZhaWxhYmxlQm94OjEyMzQ1Ng==",
            "container_number": 1
          },
          "line_items": [
            {
              "order_line_item_id": "TGluZUl0ZW06OTg3Ng==",
              "quantity": 12
            }
          ]
        }
      ]
    }
  }
}

This example assumes the order uses the FREIGHT shipping option. For a COURIER order, use a top-level Package instead and follow the container rules in the detailed flow.

For the full JSON schema, container types, numbering rules, and validation details, see Import Packing Layout.

Note

This step is optional, and it’s the fork in the flow — the two fulfillment paths are mutually exclusive:

  • If you import a packing layout (this step), the order gets a packing configuration, so wholesale_order_fulfill with is_packed_externally: true is no longer valid. Continue with wholesale_order_mark_as_packed (step 10) and the label steps, then fulfill with is_packed_externally: false (step 13).
  • If you skip this step (empty packing configuration), skip steps 10–12 too and fulfill with is_packed_externally: true (step 13) — use that path when you externally print your own labels and don’t need SSCC barcodes / GS1 labels generated by ShipHero.

10. Mark as Packed (Optional)

Only mandatory if you completed step 9. After the packing layout is imported, this step advances the order from packing to packed and lets ShipHero finish processing the imported layout so the order is ready for the downstream label and BOL flow:

mutation MarkAsPacked {
  wholesale_order_mark_as_packed(
    data: { order_id: "T3JkZXI6Njg2MzAwMDEz" }
  ) {
    request_id
    complexity
    ok
  }
}

Expected response:

{
  "data": {
    "wholesale_order_mark_as_packed": {
      "ok": true
    }
  }
}

What happens under the hood:

  • LPNs are created for the imported layout. A layout imported in step 9 starts without License Plate Numbers (LPNs). At mark_as_packed time, ShipHero creates an LPN for each container (pallet, package, case pack) that doesn’t already have one, and moves the corresponding inventory out of the staging bin and into those containers.
  • SSCC-18 barcodes are seeded for trading partners that require GS1 labeling, so the later label-generation step can render them onto the GS1 labels. If you already provided sscc_barcode values when importing the packing layout in step 9, those are preserved — ShipHero only fills in barcodes for containers that don’t have one.

ok returns true when the order transitions to packed. If allocated items are still unpacked, ok is false and the order stays in packing — resolve the remaining items and call the mutation again.

Note

This step is part of the imported-layout path (see the fork described in step 9). Once you’ve imported a layout, fulfill with is_packed_externally: false after completing the label steps below.

11. Generate Labels (Optional)

After the order is packed (step 10), you can have ShipHero generate the shipping labels — including GS1/carton labels for trading partners that require them — instead of printing your own externally. Label generation runs asynchronously:

mutation GenerateLabels {
  wholesale_order_generate_labels(
    data: {
      order_id: "T3JkZXI6Njg2MzAwMDEz"
      generate_carton_labels: true
    }
  ) {
    request_id
    complexity
    wholesale_order {
      id
      status
    }
  }
}
FieldTypeRequiredDescription
order_idStringYesThe order ID associated with the wholesale order.
generate_carton_labelsBooleanNo (default true)Whether to also generate the matching carton/container (GS1) labels.

Note

For courier orders, set the shipping carrier and shipping method on the order before calling this mutation. Freight orders don’t require them.

Because generation is asynchronous, the mutation kicks off the work and transitions the order to generating_labels. Poll the wholesale order status until it becomes:

  • ready_to_print — labels generated successfully; continue to step 12.
  • label_error — generation failed; inspect the order’s status_message, fix the cause, and retry.
query {
  wholesale_order(order_id: "T3JkZXI6Njg2MzAwMDEz") {
    request_id
    data {
      id
      status
      status_message
    }
  }
}

12. Print Labels (Optional)

Once the order reaches ready_to_print (step 11), retrieve the shipping labels PDF. This marks the order’s labels as printed and advances it toward fulfillment:

mutation PrintLabels {
  wholesale_order_print_labels(
    data: { order_id: "T3JkZXI6Njg2MzAwMDEz" }
  ) {
    request_id
    complexity
    labels_url
  }
}

Expected response:

{
  "data": {
    "wholesale_order_print_labels": {
      "labels_url": "https://d2s9s6xmfnfu1v.cloudfront.net/wholesale/labels/686300013/labels.pdf"
    }
  }
}

Behavior worth knowing:

  • labels_url is the URL of the shipping labels PDF. It can be null if the order is printable but the PDF isn’t available yet.
  • The order must be in ready_to_print, warehouse_completed, or fulfilled status. Once every package has a printed label, the order advances to warehouse_completed.

Note

Steps 11 and 12 are the ShipHero-generated label path. If you packed externally, you’ll have skipped steps 9–12 entirely and go straight to fulfillment with is_packed_externally: true.

13. Fulfill the Order

Finally, mark the order as fulfilled. The value of is_packed_externally depends on where the order was packed:

  • Packed externally — you packed the order outside ShipHero and skipped the packing layout import (steps 9–12). Use is_packed_externally: true. This requires the order to still be in packing status, with no ShipHero-generated shipping labels and an empty packing configuration.
  • Packed in ShipHero — you imported the packing layout and marked it as packed (steps 9–10), optionally generating and printing labels through ShipHero (steps 11–12), so the order has a packing configuration and is no longer in packing status. Use is_packed_externally: false.
mutation {
  wholesale_order_fulfill(
    data: {
      order_id: "T3JkZXI6Njg2MzAwMDEz"
      is_packed_externally: true   # or false if you packed in ShipHero (steps 9–12)
    }
  ) {
    request_id
    wholesale_order {
      id
      status
    }
  }
}

Expected response:

{
  "data": {
    "wholesale_order_fulfill": {
      "wholesale_order": {
        "status": "fulfilled"
      }
    }
  }
}

The order is now fully fulfilled, just like an internal ShipHero order.

14. Verify the Order Status

Check the final state:

query {
  wholesale_order(order_id: "T3JkZXI6Njg2MzAwMDEz") {
    request_id
    data {
      id
      status
      picking_flow
      staging_location_id
      order {
        order_number
        fulfillment_status
      }
    }
  }
}

Summary Table

StepMutationDescription
1wholesale_order_createCreate wholesale order (picking_flow: DESKTOP)
2wholesale_staging_location_candidatesGet staging locations
3wholesale_order_update_staging_locationAssign staging bin
4wholesale_order_auto_allocate_for_pickingAllocate inventory
5wholesale_set_as_ready_to_pickMark ready to pick
6wholesale_order_print_manual_picking_sheetPrint picking PDF
7wholesale_order_transfer_picks_to_stagingMove to staging
8wholesale_order (staged_inventory)Inspect the staged manifest (optional)
9wholesale_order_import_packing_layoutImport packing layout (optional)
10wholesale_order_mark_as_packedMark as packed (optional, requires step 9)
11wholesale_order_generate_labelsGenerate shipping/GS1 labels (optional, async)
12wholesale_order_print_labelsRetrieve shipping labels PDF (optional)
13wholesale_order_fulfillFulfill (is_packed_externally: true when packed externally, false when packed in ShipHero via steps 9–12)
14wholesale_orderVerify final status