National Commerce Docs
ShipHero Handler

Webhook Processing

The full pipeline behind POST /process_shiphero_shipment, from brand lookup to zone-stamped shipments.

Everything the worker does happens inside one endpoint: POST /process_shiphero_shipment?brand={n8n_id}. This page walks the pipeline in order. The whole thing lives in src/index.ts.

The payload

ShipHero POSTs a JSON body with two parts:

  • fulfillment holds shipment-level fields: shipment_id, order_number, warehouse_id, created_at, and a shipping_address.
  • packages is an array of boxes. Each package has a shipping_label (tracking number, service name, cost), dimensions, weight, and a line_items array (line_item_id, sku, quantity).

If packages is empty the worker returns 200 { message: "No packages to process" } and stops.

The pipeline at a glance

Step by step

1. Brand lookup

The brand query parameter is required. Without it the worker returns 400. It looks up the Brands row by n8n_id; a miss returns 404. The brand row carries the shiphero_access_token used for every ShipHero GraphQL call downstream.

2. Warehouse lookup

The Warehouses table is queried by Warehouse_ID = fulfillment.warehouse_id. A miss returns 404. The warehouse supplies the zip_code used later for the zone calculation.

3. Quantity verification

Once per webhook, the worker asks the ShipHero API what the order's line items actually contain, because the webhook payload sometimes reports quantity: 1 for a line that shipped more. This is a fail-open safety net; any problem falls back to the webhook's own numbers. See Quantity Verification for the full detail.

4. Shipments and line items

For each package:

  • The worker looks for an existing Shipments row by Tracking_ID. If one exists it is reused; otherwise a new row is inserted with the box details: Shipment_Name, Service_Level, Tracking_ID, Box_Name, Box_Weight__oz_, Label_Cost, Box_Length / Width / Height, Shipment_Time__Shiphero_, Warehouses_id, and brand.
  • For each line item it looks for an existing Shipment Line Items row by Line_Item_Id + shipment_tracking_id, inserting one if missing with the resolved SKU_Quantity, Shipments_id, and brand.

5. SKU linking (two-tier lookup)

For every line item the worker resolves a product SKU:

  1. It queries Product SKUs by Product_SKU + Brands_id.
  2. On a miss it POSTs { sku, brand_id, sku_type: "product" } to the n8n webhook at SHIPHERO_SKU_RETRIEVAL_URL, which resolves or creates the SKU (this is the SKU Information Handler flow).
  3. If a SKU is found from either source and the line item is not already linked, the worker sets product_sku. If the line item is already linked to a different SKU it logs a warning for manual review rather than overwriting.

6. Orders and stores

The worker looks for an Orders row by Order_Number.

If the order does not exist, it fetches the full order from the ShipHero GraphQL API (shop_name, order_date, shipping_address, and the money fields), then:

  • Looks up or creates a Stores row from shop_name + brand.
  • Inserts the order with Total_Price, Total_Tax, Total_Discounts, Subtotal, the store, and a derived shipping_proceeds value.

How shipping proceeds is derived

shipping_proceeds is computed inline as total_price − total_tax + total_discounts − subtotal, rounded to three decimals. It is stored on the order, not recomputed by the database.

If the order already exists but has no store, the worker re-fetches shop_name from ShipHero, links (or creates) the store, and updates the order.

7. Customer linking

If the order has no linked customer, the worker looks in End Customer (Amazon buyer) by Full_Name + Address_1. On a miss it fetches the shipping address from ShipHero, derives the state from the ZIP code when the state is blank (see State derivation), inserts the customer, and links it to the order.

8. Shipping zone

With an order and shipments in hand, the worker calculates the shipping zone from the warehouse ZIP and the customer's ZIP, then stamps the result onto the shipments. The Shipping Zones page covers the API call and the status codes.

9. Linking and the independent zone write

Order, store, and zone data are linked onto every shipment. Database updates here are wrapped in a retry with exponential backoff (three attempts).

A separate zone-update block then runs unconditionally, writing the zone data to all shipments again. This exists because an earlier bug skipped zone data whenever a shipment was already linked to an order. The standalone block guarantees the zone is always persisted. (That bug is why the October 2 backfill was needed.)

The endpoint returns 200 { success: true, shipments: [{ id, orders_id }] }. Any thrown error is caught and returned as 500.

Idempotency

Every insert is guarded by a lookup first: shipments by tracking number, line items by ID + tracking, orders by number, customers by name + address, stores by shop name. Replaying the same webhook links records rather than duplicating them, so ShipHero retries and manual replays are safe.

Observability

The worker logs verbosely with emoji-prefixed messages so runs are easy to filter in the Cloudflare dashboard:

PrefixMeaning
📦Package / shipment processing
🔍Lookups and searches
Success
Errors
⚠️Warnings
🔄Updates / linking
📍Zone calculations

Errors are also captured in Sentry. See Configuration & Deployment.

On this page