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:
fulfillmentholds shipment-level fields:shipment_id,order_number,warehouse_id,created_at, and ashipping_address.packagesis an array of boxes. Each package has ashipping_label(tracking number, service name, cost), dimensions, weight, and aline_itemsarray (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
Shipmentsrow byTracking_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, andbrand. - For each line item it looks for an existing
Shipment Line Itemsrow byLine_Item_Id+shipment_tracking_id, inserting one if missing with the resolvedSKU_Quantity,Shipments_id, andbrand.
5. SKU linking (two-tier lookup)
For every line item the worker resolves a product SKU:
- It queries
Product SKUsbyProduct_SKU+Brands_id. - On a miss it POSTs
{ sku, brand_id, sku_type: "product" }to the n8n webhook atSHIPHERO_SKU_RETRIEVAL_URL, which resolves or creates the SKU (this is the SKU Information Handler flow). - 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
Storesrow fromshop_name+brand. - Inserts the order with
Total_Price,Total_Tax,Total_Discounts,Subtotal, the store, and a derivedshipping_proceedsvalue.
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:
| Prefix | Meaning |
|---|---|
| 📦 | Package / shipment processing |
| 🔍 | Lookups and searches |
| ✅ | Success |
| ❌ | Errors |
| ⚠️ | Warnings |
| 🔄 | Updates / linking |
| 📍 | Zone calculations |
Errors are also captured in Sentry. See Configuration & Deployment.