Skip to main content
This guide walks through the complete contract a shipping app honours end-to-end — install handoff, quoting live rates at checkout, fulfillment service registration, receiving the orders/create webhook with shipping_lines[], pushing to your carrier API, and writing tracking back so order.status auto-advances. If you’re building a real-world shipping integration (Shiprocket-style domestic, or a cross-border courier), this is the full surface.

The full chain

The platform implements a Carrier-Service-equivalent contract: live quotes at checkout, orders/create webhook with carrier identity on shipping_lines[].source/code, and a back-pushed fulfillment that flips order status. Every step below is wired the same way for every shipping app — Shiprocket, ShipGlobal, or your own.

Architecture at a glance

1. OAuth install handoff

After the merchant clicks Install in the app store, the platform redirects to your app’s /auth URL with an HMAC-signed querystring. See Install handoff for the full HMAC verification recipe. Inside your /auth handler, exchange the authorization code for an access token by POSTing to the OAuth token endpoint:
Responses use the platform envelope { status, state, message, data } — the token pair lives under data. Cache access_token keyed by storeId; every API call below sends it as the Authorization: Bearer header. When it expires, POST the same endpoint again with grant_type=refresh_token and your refresh_token.

2. Register as a fulfillment service

In the same /auth handler, register your app as a fulfillment service so the platform knows tracking is supported and attributes shipments back to your app. This is idempotent — safe to call on every install or reinstall. First check whether you’ve already registered (so reinstalls don’t create duplicates), then create the service if it’s missing:
POST /api/v1/fulfillment_services.json requires the write_orders scope; reading the list requires read_orders. Writing the metafield requires write_metafields. See Fulfillment Services.

3. Push tracking after AWB assignment

When the merchant clicks Push to <Your App> in the admin order page (or your webhook auto-pushes), your app:
  1. Calls your courier’s external API to create the shipment + assign a tracking number (AWB).
  2. POSTs the resulting tracking number + courier name back to the platform via POST /api/v1/orders/:orderId/fulfillments.json.
That single call:
  1. Appends a new entry to the order’s fulfillments array.
  2. Recomputes coverage and updates order.status:
    • 100% covered → shipped (or delivered if status was set to delivered).
    • Partially covered → partial.
  3. Fires the fulfillments/create webhook to every subscriber.

4. Multi-package orders

For orders that ship in multiple packages, POST to the same orders/:orderId/fulfillments.json route once per package and pass the line_items subset that went in that package. The platform sums quantities per line_item.id across all success-state fulfillments to compute coverage.
Omitting line_items is the back-compat shortcut for single-shipment orders: the fulfillment is treated as covering everything, so a single POST flips order.status straight to shipped.

5. Order lifecycle

The auto-bump rule: Pending / open / error / failure / cancelled fulfillment statuses never mutate order.status — only success (or shipped / delivered) do.

6. Admin UI

The merchant’s order detail page in the platform admin renders one package card per fulfillment, showing the courier name, tracking number (with copy button), tracking link, and the items in that package. The order status badge reflects the lifecycle above:
  • Paid (blue) — no shipments yet.
  • Partial (purple) — some packages out.
  • Shipped (green) — every item covered.
You don’t need to render this UI in your app — the platform handles it automatically once you’ve posted the fulfillment.

7. Optional: track status updates

Carriers expose tracking webhooks (or you can poll). When your app receives a status update from the carrier, mirror it to the platform via PUT /api/v1/fulfillments/:id.json so the order page reflects the latest status:
Setting status: 'delivered' will bump order.status to delivered provided no manual override has been applied.

Full reference