Live Rate Providers
A live rate provider is an HTTP endpoint your app exposes that the platform calls at checkout to fetch live shipping rates from your carrier. Rates you return render alongside the merchant’s own shipping zones — the customer picks one with a normal radio button, and your app’s chosen carrier identity follows the order through placement, webhook, and fulfillment write-back. Apps register a callback URL in their manifest, return rates on demand, and receiveorders/create webhooks where they recognise
their own picked rates and act on them. Common implementations include
domestic and cross-border courier integrations.
For rate logic that doesn’t need a remote carrier API — e.g. a fixed
surcharge above a cart-value threshold — use a
shipping_rate Function instead. Functions
run server-side in our sandbox; live rate providers run on your
infrastructure.
The Flow
1. Declare in your app manifest
Add aliveRateProviders[] entry to your app.json. The endpoint
template ${APP_URL} is substituted with the value the merchant
configured at install time.
app.json
| Field | Required | Notes |
|---|---|---|
handle | yes | Provider handle within your app. Multiple providers per app allowed (e.g. domestic + international). |
endpoint | yes | URL the dispatcher POSTs to. Template ${APP_URL} resolves to your app’s base URL. |
timeoutMs | optional | Hard cap. Defaults to 5s, max 9s. Slow providers fail open — checkout never breaks. |
description | optional | Shown in the merchant’s installed-apps admin. |
2. Build the /quote endpoint
The dispatcher POSTs a Carrier-Service-shaped body, HMAC-signed with
your app’s clientSecret over the raw bytes:
shop.domainSlug), then call your
carrier and return:
Response body:
| Field | Required | Purpose |
|---|---|---|
service_name | yes | Bold title in the picker (e.g. "Blue Dart Air"). Becomes the rate’s display name. |
service_code | yes | Stable per-courier identifier within your app. Echoed back to you on orders/create so you can map back to your carrier’s courier id. Convention: <app_handle>-<carrier_id> (e.g. acme-1). |
total_price | yes | String. Major currency units (rupees, dollars). |
currency | yes | ISO-4217. Should match the merchant’s base currency unless your app handles FX. |
description | optional | Secondary line in the picker ("3-5 days · DDP available"). |
min_delivery_date / max_delivery_date | optional | ISO-8601. Shown to customers on themes that support delivery-window display. |
3. Verify the HMAC
Use the platform’s shared package:4. Render-side guarantees
Each rate you return is tagged by the platform before it reaches the storefront with:via {Apps.name} label for trust
signalling:
5. Receive the orders/create webhook
Subscribe to orders/create in your manifest (see step 1). When the
customer places an order, the platform POSTs:
shipping_lines[] array is the routing key. Your handler must
inspect shipping_lines[].source and only act when it matches your
app handle:
source-based routing matters. If a merchant installs BOTH
your app and a competitor app, both apps receive the same
orders/create webhook. Without the source check, both apps would
race to push shipments. Inspecting shipping_lines[].source === APP_HANDLE is the contract that makes installed-apps coexist.
Native local pickup is handled by the same
source check. When a
merchant enables in-store pickup on a warehouse (no app required), and
the customer chooses the Pickup tab at checkout, the order’s
shipping_lines[].source is "local_pickup" — never your app handle.
So the find((l) => l.source === APP_HANDLE) guard above already skips
these orders correctly; you don’t need any extra branch. The order also
carries shippingMethod.source === "local_pickup" and
pickupLocationId / pickupLocationName if you need to detect it
explicitly (e.g. to suppress a “shipping” email).6. Optional auto-push without customer pick
If the merchant hasauto_push_enabled set in your config metafield
AND shipping_lines[].source is null (= the customer picked a
merchant zone, not your rate), you can still push if your app handles
ALL of the merchant’s shipping. This is what Flow B looks like.
The shipped Shiprocket app in the repo demonstrates both modes —
see shiprocket/src/server.js
/api/order-create for the source-match + auto-push patterns.
7. Cache, retries, idempotency
| Concern | Platform behaviour | Your app’s responsibility |
|---|---|---|
| Quote cache | 10 minutes, keyed on storeId + destination + items fingerprint | None |
| Quote retry | 3 retries on 5xx / network errors with 250/500/1000 ms backoff | Be idempotent (same request → same rates) |
| Webhook delivery | 3 retries with exponential backoff: 1m / 5m / 15m | Return 2xx within 10s, or the dispatcher retries |
| Webhook signature | X-LMS-Signature: hex(HMAC-SHA256(rawBody, clientSecret)) | Verify before processing |
| AWB duplicate | Platform de-duplicates by tracking_number per order | Don’t worry about double-push from retries |
8. Multi-package support
You can split a single order across multiple shipments. Each call tocreateFulfillment with a different line_items subset becomes a
new Fulfillment row + tracking entry on the order. Order.status
flips to partial until coverage hits 100%, then shipped.
See Create Fulfillment for the
exact body shape and multi-package examples.
Reference
- Fulfillment Services — registering as a tracking provider
- Create Fulfillment — pushing the AWB back
- orders/create webhook payload — full body schema with
shipping_lines[] - shipping_rate Function — for rates that don’t need a remote carrier
- Build a shipping app — end-to-end guide