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

# Build a shipping app

> End-to-end guide: quote live rates at checkout, receive orders/create webhook, push to your carrier, write fulfillment back, drive the partial → shipped status flow.

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.

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

    Customer->>Checkout: Types pincode
    Checkout->>Platform: POST /orders/verify-cart
    Platform->>YourApp: POST /api/quote (HMAC)
    YourApp->>Carrier: Serviceability call
    Carrier-->>YourApp: Couriers + prices
    YourApp-->>Platform: { rates: [...] }
    Platform-->>Checkout: customShippingRates[]
    Checkout-->>Customer: Picker renders YOUR rates + merchant zones

    Customer->>Checkout: Picks a rate + Place Order
    Checkout->>Platform: POST /orders/add-client-order<br/>{ customShippingRate: {appHandle, serviceCode, ...} }
    Platform->>Platform: Persist Order.shippingMethod
    Platform->>YourApp: POST orders/create webhook<br/>{ shipping_lines: [{ source, code }] }
    YourApp->>YourApp: shipping_lines[].source === my handle?
    YourApp->>Carrier: Create shipment + assign AWB
    Carrier-->>YourApp: AWB / courier_name
    YourApp->>Platform: POST /api/v1/orders/:id/fulfillments.json
    Platform->>Platform: Order.status → 'shipped'<br/>Send customer email
```

## Architecture at a glance

```
┌──────────────┐    OAuth     ┌────────────────┐    POST tracking    ┌──────────────┐
│ Your app     │ ──────────►  │  Platform      │ ◄─────────────────  │ Your app     │
│ (your infra) │              │  /api/v1       │                     │ post-AWB     │
└──────┬───────┘              └────────┬───────┘                     └──────┬───────┘
       │                               │                                    │
       │ /auth handoff                 │ writes order.fulfillments[]       │
       │ POST fulfillment_services.json│ flips order.status                │
       │                               │                                    │
       ▼                               ▼                                    ▼
   serviceId                   fulfillments[] array                Admin order page
   cached in metafields        on the order                        renders package
                                                                   card per shipment
```

## 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](/getting-started/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:

```js theme={null}
const res = await fetch('https://api.launchmystore.io/apps/oauth/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    grant_type: 'authorization_code',
    client_id: CLIENT_ID,
    client_secret: CLIENT_SECRET,
    code: req.query.code,
    state: req.query.state,
    // code_verifier: ... // include if you used PKCE on /authorize
  }),
});
const result = await res.json();
// result.data = { access_token, refresh_token, token_type: 'bearer', expires_in }
const token = result.data;
```

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:

```js theme={null}
const BASE = 'https://api.launchmystore.io';
const auth = { Authorization: `Bearer ${token.access_token}` };
const json = { 'Content-Type': 'application/json', ...auth };

// 1. Look for an existing service with your name.
const listRes = await fetch(`${BASE}/api/v1/fulfillment_services.json`, {
  headers: auth,
});
const list = (await listRes.json()).data?.fulfillment_services || [];
let service = list.find((s) => s.name === 'Acme Shipping');

// 2. Create it only if it doesn't exist (scope: write_orders).
if (!service) {
  const createRes = await fetch(`${BASE}/api/v1/fulfillment_services.json`, {
    method: 'POST',
    headers: json,
    body: JSON.stringify({
      name: 'Acme Shipping',
      callbackUrl: `${APP_URL}/api/tracking`,
      tracking_support: true,
    }),
  });
  service = (await createRes.json()).data?.fulfillment_service;
}
const serviceId = service.id;

// 3. Cache the serviceId on a shop metafield so subsequent fulfillment
//    writes can include it without a round-trip (scope: write_metafields).
await fetch(`${BASE}/api/v1/metafields.json`, {
  method: 'POST',
  headers: json,
  body: JSON.stringify({
    namespace: 'acme_shipping',
    key: 'fulfillment_service_id',
    value: String(serviceId),
    type: 'single_line_text_field',
    ownerType: 'shop',
    ownerId: storeId,
  }),
});
```

`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](/api-reference/fulfillments/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`.

```js theme={null}
// After your courier API returned { awb, courier_name, label_url }.
// POST /api/v1/orders/:orderId/fulfillments.json  (scope: write_orders)
const res = await fetch(
  `${BASE}/api/v1/orders/${orderId}/fulfillments.json`,
  {
    method: 'POST',
    headers: json,
    body: JSON.stringify({
      tracking_number: awb,
      tracking_company: courier_name,
      tracking_url: `https://your-tracking-domain.com/track/${awb}`,
      status: 'success',         // ← triggers the status flip
      service_id: serviceId,     // from step 2
      notify_customer: true,     // optional — send the shipment email
    }),
  },
);
const fulfillment = (await res.json()).data;
```

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.

```js theme={null}
const postFulfillment = (orderId, body) =>
  fetch(`${BASE}/api/v1/orders/${orderId}/fulfillments.json`, {
    method: 'POST',
    headers: json,
    body: JSON.stringify(body),
  });

// Package 1 — 2 of 5 items
await postFulfillment(orderId, {
  tracking_number: 'AWB-PKG-A',
  tracking_company: 'DHL Express',
  tracking_url: 'https://dhl.com/track/AWB-PKG-A',
  status: 'success',
  line_items: [
    { id: 'li_001', quantity: 1 },
    { id: 'li_002', quantity: 1 },
  ],
});
// → order.status flips paid → partial

// Package 2 — remaining 3 items
await postFulfillment(orderId, {
  tracking_number: 'AWB-PKG-B',
  tracking_company: 'FedEx Priority',
  tracking_url: 'https://fedex.com/track/AWB-PKG-B',
  status: 'success',
  line_items: [
    { id: 'li_003', quantity: 1 },
    { id: 'li_004', quantity: 1 },
    { id: 'li_005', quantity: 1 },
  ],
});
// → order.status flips partial → shipped
```

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

```
pending → confirmed → paid → partial → shipped → delivered
                                    (canceled / abandoned as terminal off-ramps)
```

The auto-bump rule:

| State of order | First success fulfillment (full coverage)   | First success fulfillment (partial) |
| -------------- | ------------------------------------------- | ----------------------------------- |
| `pending`      | → `shipped`                                 | → `partial`                         |
| `confirmed`    | → `shipped`                                 | → `partial`                         |
| `paid`         | → `shipped`                                 | → `partial`                         |
| `partial`      | → `shipped` (once final package covers all) | (stays `partial`)                   |
| `shipped`      | (unchanged — manual override respected)     | (unchanged)                         |
| `delivered`    | (unchanged — terminal)                      | (unchanged)                         |
| `canceled`     | (unchanged — terminal)                      | (unchanged)                         |

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:

```js theme={null}
await fetch(
  `${PLATFORM_BASE}/api/v1/fulfillments/${fulfillmentId}.json`,
  {
    method: 'PUT',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${accessToken}`,
    },
    body: JSON.stringify({
      status: 'delivered',
    }),
  },
);
```

Setting `status: 'delivered'` will bump `order.status` to `delivered`
provided no manual override has been applied.

## Full reference

* [Create Fulfillment](/api-reference/fulfillments/create)
* [Update Fulfillment](/api-reference/fulfillments/update)
* [List Fulfillments](/api-reference/fulfillments/list)
* [Delete Fulfillment](/api-reference/fulfillments/delete)
* [Fulfillment Services](/api-reference/fulfillments/services)
* [Install handoff](/getting-started/install-handoff)
