> ## Documentation Index
> Fetch the complete documentation index at: https://docs.launchmystore.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Live Rate Providers

> Quote live shipping rates from your carrier API at checkout — the Carrier Service pattern for shipping apps.

# 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 receive `orders/create` webhooks where they recognise
their own picked rates and act on them. Common implementations include
domestic and cross-border courier integrations.

<Note>
  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](/functions/shipping-rate) instead. Functions
  run server-side in our sandbox; live rate providers run on your
  infrastructure.
</Note>

## The Flow

```mermaid theme={null}
sequenceDiagram
    participant Customer
    participant Checkout
    participant Platform
    participant YourApp
    participant Carrier

    Customer->>Checkout: Types shipping pincode
    Checkout->>Platform: POST /orders/verify-cart
    Note over Platform: Dispatcher looks up every<br/>installed app with<br/>liveRateProviders[]<br/>and calls each in parallel
    Platform->>YourApp: POST /api/quote<br/>(HMAC-signed)
    YourApp->>Carrier: Serviceability call
    Carrier-->>YourApp: Available couriers + prices
    YourApp-->>Platform: { rates: [...] }
    Platform-->>Checkout: customShippingRates[]<br/>(tagged with appHandle)
    Checkout-->>Customer: Picker renders<br/>your rates + merchant zones

    Customer->>Checkout: Picks one rate + Place Order
    Checkout->>Platform: POST /orders/add-client-order<br/>{ customShippingRate: {...} }
    Note over Platform: Persists Order.shippingMethod<br/>= { appHandle, serviceCode, ... }
    Platform->>YourApp: POST orders/create webhook<br/>{ shipping_lines: [{ source, code, ... }] }
    YourApp->>YourApp: Inspect shipping_lines[].source<br/>== my handle?
    YourApp->>Carrier: Create shipment + assign tracking
    Carrier-->>YourApp: AWB / tracking number
    YourApp->>Platform: POST /api/v1/orders/:id/fulfillments.json<br/>{ tracking_number, tracking_company, status:'success' }
    Note over Platform: Order.status flips to 'shipped'<br/>customer email fires
```

## 1. Declare in your app manifest

Add a `liveRateProviders[]` entry to your `app.json`. The endpoint
template `${APP_URL}` is substituted with the value the merchant
configured at install time.

```json app.json theme={null}
{
  "handle": "acme-shipping",
  "name": "Acme Shipping",
  "scopes": [
    "read_orders",
    "write_orders",
    "read_metafields",
    "write_metafields"
  ],
  "extensions": {
    "liveRateProviders": [
      {
        "handle": "acme-rates",
        "endpoint": "${APP_URL}/api/quote",
        "timeoutMs": 5000,
        "description": "Live rates from Acme's carrier API."
      }
    ],
    "webhooks": [
      {
        "topic": "orders/create",
        "endpoint": "${APP_URL}/api/order-create"
      }
    ]
  }
}
```

| 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:

```http theme={null}
POST /api/quote HTTP/1.1
Content-Type:      application/json
X-LMS-Signature:   <hex(HMAC-SHA256(rawBody, clientSecret))>
X-LMS-Request-Id:  d3b8fa2c-...
```

**Request body:**

```json theme={null}
{
  "rate": {
    "origin": { "country": "IN", "postal_code": "110051" },
    "destination": {
      "country":     "IN",
      "country_code": "IN",
      "city":        "Mumbai",
      "province":    "MH",
      "postal_code": "400001",
      "zip":         "400001",
      "cod":         false
    },
    "items": [
      {
        "variant_id": "var_...",
        "product_id": "prod_...",
        "sku":        "ABC-MED",
        "quantity":   1,
        "grams":      1500,
        "price":      499,
        "title":      "Slipper M"
      }
    ],
    "currency": "INR",
    "locale":   "en"
  },
  "shop": {
    "domainSlug": "acme-store",
    "name":       "Acme Store",
    "url":        "acme-store.launchmystore.io"
  },
  "request_id": "d3b8fa2c-..."
}
```

Verify the signature, look up the merchant's per-shop credentials from
your data store (typically keyed on `shop.domainSlug`), then call your
carrier and return:

**Response body:**

```json theme={null}
{
  "rates": [
    {
      "service_name": "Blue Dart Air",
      "service_code": "acme-1",
      "total_price":  "8085",
      "currency":     "INR",
      "description":  "1 days · Blue Dart Air",
      "min_delivery_date": "2026-06-04T00:00:00Z",
      "max_delivery_date": "2026-06-05T00:00:00Z"
    },
    {
      "service_name": "DTDC Surface",
      "service_code": "acme-12",
      "total_price":  "4250",
      "currency":     "INR",
      "description":  "3 days · DTDC Surface"
    }
  ]
}
```

Each rate field:

| 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.                                                                                                                    |

**Fail-open semantics.** Any of: timeout, 5xx, malformed JSON, empty
array — treated as "no rates from this provider" and the checkout
falls back to the merchant's ShippingZones. Your provider failing is
**never** a customer-facing error.

## 3. Verify the HMAC

Use the platform's shared package:

```js theme={null}
import express from 'express';
import { verifyRateRequest } from '@launchmystore/apps-shared';

app.post('/api/quote', express.raw({ type: 'application/json' }), (req, res) => {
  const ok = verifyRateRequest({
    rawBody: req.body.toString('utf8'),
    header:  req.headers['x-lms-signature'],
    clientSecret: process.env.LMS_CLIENT_SECRET,
  });
  if (!ok) return res.status(401).json({ error: 'Invalid signature' });

  const body = JSON.parse(req.body.toString('utf8'));
  const dest = body.rate?.destination || {};
  const items = body.rate?.items || [];
  // ... call your carrier, return rates
  res.json({ rates: [...] });
});
```

Or use the all-in-one middleware:

```js theme={null}
import { carrierServiceMiddleware } from '@launchmystore/apps-shared';

app.post(
  '/api/quote',
  carrierServiceMiddleware({ clientSecret: process.env.LMS_CLIENT_SECRET }),
  async (req, res) => {
    // req.body is already parsed and HMAC-verified.
    res.json({ rates: await myQuoteLogic(req.body) });
  },
);
```

## 4. Render-side guarantees

Each rate you return is tagged by the platform before it reaches the
storefront with:

```js theme={null}
{ ...yourRate,
  appId:      "<your app uuid>",
  appHandle:  "acme-shipping",
  appName:    "Acme Shipping",
  source:     "live_rate_provider"
}
```

The /checkout picker renders **every** app's rate with the same
template, so your service\_name and description appear exactly as you
sent them — alongside a small `via {Apps.name}` label for trust
signalling:

```
○  Blue Dart Air   1 days · via Acme Shipping     ₹8,085.00
○  DTDC Surface    3 days · via Acme Shipping     ₹4,250.00
○  <merchant's own ShippingZone>                  ₹100.00
```

The customer picks one. No code change needed in your app for the
display layer — your rate JSON is what the customer sees.

## 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:

```json theme={null}
{
  "id":           "abc-123",
  "name":         "#A0XJ12K",
  "order_number": "A0XJ12K",
  "email":        "buyer@example.com",
  "financial_status": "pending",
  "currency":     "INR",
  "total_price":  "8160.00",
  "shipping_address": {
    "address1": "1 MG Road", "city": "New Delhi", "province": "DL",
    "country": "India", "zip": "110011", "phone": "9876543210"
  },
  "line_items": [
    { "id": "...", "product_id": "...", "variant_id": "...",
      "title": "Slipper M", "quantity": 1, "price": "75.00" }
  ],
  "shipping_lines": [
    {
      "title":    "Blue Dart Air",
      "code":     "acme-1",
      "source":   "acme-shipping",
      "price":    "8085.00",
      "currency": "INR"
    }
  ]
}
```

<Note>
  The body **is** the order in standard commerce shape — there is no `{ order, orderProducts,
    shop }` wrapper. The sending store is the `X-LMS-Shop-Domain` request header,
  and `shipping_lines[]` (carrying your routing `source`) is preserved at the top
  level.
</Note>

The `shipping_lines[]` array is the routing key. **Your handler must
inspect `shipping_lines[].source` and only act when it matches your
app handle:**

```js theme={null}
import {
  createFulfillment,
} from '@launchmystore/apps-shared';

app.post('/api/order-create', async (req, res) => {
  const order = req.body;                       // the body IS the order
  const shopDomain = req.headers['x-lms-shop-domain'];
  const APP_HANDLE = 'acme-shipping';

  const myLine = (order.shipping_lines || []).find((l) => l.source === APP_HANDLE);
  if (!myLine) {
    // Customer picked someone else's rate (or a merchant zone). Skip.
    return res.json({ ok: true, skipped: 'not-my-rate' });
  }

  // Strip the app prefix to recover your carrier's courier id.
  const myCarrierId = Number(String(myLine.code).replace(/^acme-/, ''));

  // Look up the merchant's per-shop credentials.
  const creds = await loadCreds(shopDomain);

  // Translate to your carrier's API.
  const { awb, courier_name } = await callMyCarrierCreateShipment({
    creds,
    order,
    lineItems: order.line_items,
    courier_id: myCarrierId,
  });

  // Write back the AWB so the platform's Order shows tracking
  // and Order.status flips to 'shipped'.
  await createFulfillment({
    accessToken: await getOAuthTokenFor(shopDomain),
    orderId: order.id,
    trackingNumber:  awb,
    trackingCompany: courier_name,
    trackingUrl:     `https://acme.example/track/${awb}`,
    status:          'success',
  });

  res.json({ ok: true, awb });
});
```

**Why `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.

<Note>
  **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 pickup line's
  `source` is `"local_pickup"`, never your app handle).
</Note>

## 6. Optional auto-push without customer pick

If the merchant has `auto_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](https://github.com/LaunchMyStore/apps)
`/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 to
`createFulfillment` 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](/api-reference/fulfillments/create) for the
exact body shape and multi-package examples.

## Reference

* [Fulfillment Services](/api-reference/fulfillments/services) — registering as a tracking provider
* [Create Fulfillment](/api-reference/fulfillments/create) — pushing the AWB back
* [orders/create webhook payload](/api-reference/webhooks/topics#orders-create) — full body schema with `shipping_lines[]`
* [shipping\_rate Function](/functions/shipping-rate) — for rates that don't need a remote carrier
* [Build a shipping app — end-to-end guide](/guides/shipping-app-integration)
