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

# Webhook Topics

> Reference list of subscribable webhook topics

# Webhook Topics

Webhook topics notify your app when events occur in the merchant's store.
Subscribe to a topic with [`POST /api/v1/webhooks.json`](/api-reference/webhooks/overview#registering-webhooks)
to receive real-time notifications. (Subscribing requires the `read_shop`
scope — there is no dedicated webhook write scope.)

<Note>
  This is a **static reference page** — there is no `GET .../webhooks/topics.json`
  endpoint to query the list at runtime. The subscribable topics are fixed by
  the API and listed below. Subscribing to a value that is not in this list is
  rejected with a `422` validation error.
</Note>

The topics below are the ones you can subscribe to via
`POST /api/v1/webhooks.json`.

<Note>
  Order, product, customer, collection and refund webhooks are delivered as
  snake\_case bodies (`line_items`, `financial_status`,
  money fields as decimal strings) — the same shape as the corresponding REST
  resource. The contract version is sent in the `X-LMS-Api-Version` header
  (currently `2026-06`), and the sending store is identified by the
  `X-LMS-Shop-Domain` header.
</Note>

## Available Topics

### Orders

| Topic              | Description                                                                                                                                                                                                          |
| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `orders/create`    | Fired when a new order is created. Payload includes a standard `shipping_lines[]` array with `source` (the app handle the customer's shipping rate came from) — see [orders/create payload](#orders-create-payload). |
| `orders/updated`   | Fired when an order is updated. Also fired (alongside `orders/cancelled`) when an order is refunded.                                                                                                                 |
| `orders/paid`      | Fired when an order payment is completed                                                                                                                                                                             |
| `orders/cancelled` | Fired when an order is cancelled                                                                                                                                                                                     |
| `orders/fulfilled` | Fired when all items in an order are fulfilled                                                                                                                                                                       |
| `orders/delete`    | Fired when an order is deleted                                                                                                                                                                                       |

<h4 id="orders-create-payload">
  orders/create payload
</h4>

The body is the order in standard commerce shape (snake\_case, `line_items`,
`financial_status`, money as decimal strings), plus a top-level
`shipping_lines[]` carrying the `source` routing key. The sending store is
identified by the `X-LMS-Shop-Domain` header (the order body has no
`storeId`).

```json theme={null}
{
  "id":                 "5c02f40b-aca3-...",
  "name":               "#A0XJ12K",
  "order_number":       "A0XJ12K",
  "email":              "buyer@example.com",
  "phone":              "9876543210",
  "financial_status":   "pending",
  "fulfillment_status": "unfulfilled",
  "currency":           "INR",
  "total_price":        "8160.00",
  "subtotal_price":     "75.00",
  "shipping_price":     "8085.00",
  "created_at":         "2026-06-01T22:51:50.123Z",
  "customer": {
    "id":         "...",
    "first_name": "Live",
    "last_name":  "Buyer",
    "email":      "buyer@example.com"
  },
  "shipping_address": {
    "address1": "1 MG Road",
    "address2": "Building A",
    "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",
      "sku":        ""
    }
  ],
  "shipping_lines": [
    {
      "title":    "Blue Dart Air",
      "code":     "shiprocket-1",
      "source":   "shiprocket",
      "price":    "8085.00",
      "currency": "INR"
    }
  ]
}
```

<Note>
  **Field notes for order webhooks**

  * Money fields are **decimal strings** (`"49.00"`), not numbers.
  * `line_items[].variant_id` **falls back to `product_id`** for products with no variants.
  * There is no `digital` flag on the order — drive digital/course fulfilment off `line_items[].product_id`.
  * The order body has no `storeId`; the sending store is the `X-LMS-Shop-Domain` request header.
</Note>

**`shipping_lines[].source`** is the routing key for shipping apps.
When a merchant has multiple shipping apps installed, every app
receives every `orders/create` webhook. Each app inspects `source`
against its own handle and only acts when it matches — this is how
your app distinguishes "the customer picked MY rate" from "the
customer picked another app's rate". See
[Live Rate Providers](/extensions/live-rate-providers#receive-the-orders-create-webhook)
for the full handler pattern.

**`source` values:**

* `"<app_handle>"` (e.g. `"shiprocket"`) — customer picked this app's live rate at checkout
* `null` — customer picked a merchant ShippingZone or a local-delivery rule (no app should auto-push)

**`code`** carries your app's `service_code` from the live-rate
response. Convention is `<app_handle>-<carrier_id>` so apps can strip
the prefix to recover their carrier's internal identifier.

### Refunds

| Topic            | Description                                                                                                                                                                                                                                                                                                              |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `refunds/create` | Fired when an order is refunded. A full refund and a POS refund both emit `refunds/create`; a **full refund also flips the order to canceled**, so it additionally emits `orders/updated` + `orders/cancelled`. The body is the refund (snake\_case, `refund_line_items[]`, `transactions[]`, money as decimal strings). |

<Note>
  **Detecting cancellations & refunds reliably.** A merchant *cancel* emits
  `orders/updated` + `orders/cancelled`; a *full refund* emits `refunds/create`

  * `orders/updated` + `orders/cancelled`; a *POS refund* emits
    `refunds/create`. Delivery is at-least-once and unordered, so don't count on
    an exact number of signals — on any cancellation/refund event, re-fetch the
    order ([`GET /api/v1/orders/{id}.json`](/api-reference/orders/get), scope
    `read_orders`) and reconcile against the authoritative `order.status`. Make
    your entitlement changes idempotent.
</Note>

### Products

| Topic             | Description                         |
| ----------------- | ----------------------------------- |
| `products/create` | Fired when a new product is created |
| `products/update` | Fired when a product is updated     |
| `products/delete` | Fired when a product is deleted     |

### Customers

| Topic              | Description                             |
| ------------------ | --------------------------------------- |
| `customers/create` | Fired when a customer record is created |
| `customers/update` | Fired when a customer is updated        |
| `customers/delete` | Fired when a customer is deleted        |

### Inventory

| Topic                     | Description                                           |
| ------------------------- | ----------------------------------------------------- |
| `inventory_levels/update` | Fired when an inventory level changes at any location |

### Fulfillments

| Topic                 | Description                         |
| --------------------- | ----------------------------------- |
| `fulfillments/create` | Fired when a fulfillment is created |
| `fulfillments/update` | Fired when a fulfillment is updated |

### Subscriptions

Recurring product subscriptions sold through **selling plans**. The
underlying recurring billing is handled by the merchant's Stripe
subscription; these topics fire as the Stripe subscription moves through
its lifecycle.

| Topic                          | Description                                                                                                                                            |
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `subscriptions/create`         | Fired on the **first** successful charge — a new subscription has started (Stripe `invoice.payment_succeeded`, `billing_reason: subscription_create`). |
| `subscriptions/renew`          | Fired on each **automatic renewal** charge (Stripe `invoice.payment_succeeded`, `billing_reason: subscription_cycle`).                                 |
| `subscriptions/update`         | Fired when the subscription changes — plan/quantity change, status change, or trial→active (Stripe `customer.subscription.updated`).                   |
| `subscriptions/payment_failed` | Fired when a renewal charge fails (Stripe `invoice.payment_failed`). The subscription enters Stripe's dunning/retry flow.                              |
| `subscriptions/cancelled`      | Fired when the subscription ends — cancelled by the merchant/customer, or expired at period end (Stripe `customer.subscription.deleted`).              |

<Note>
  There is no separate `subscriptions/expired` topic. An expired
  subscription ends as `customer.subscription.deleted` in Stripe and is
  delivered as `subscriptions/cancelled`.
</Note>

#### Lifecycle order

A typical subscription fires these topics in order over its lifetime:

```
checkout ──▶ subscriptions/create        (first charge succeeds, access granted)
         ──▶ subscriptions/update         (e.g. trial → active, plan change)   [0..n]
         ──▶ subscriptions/renew          (each automatic renewal charge)      [0..n]
         ──▶ subscriptions/payment_failed (a renewal charge fails)             [0..n]
         ──▶ subscriptions/cancelled      (ends — cancelled or expired)        (terminal)
```

`subscriptions/renew` and `subscriptions/payment_failed` can repeat for the
life of the subscription. `subscriptions/cancelled` is terminal — no further
events fire for that subscription afterwards.

<h4 id="subscriptions-payload">
  subscription payload
</h4>

All five subscription topics share the same payload shape:

```json theme={null}
{
  "orderId":              "5c02f40b-aca3-...",
  "invoiceId":            "A0XJ12K",
  "storeId":              "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "subscriptionId":       "sub_1QabcdEFgh...",
  "customerId":           "cus_QabcdEF...",
  "customerEmail":        "buyer@example.com",
  "status":               "active",
  "planName":             "Coffee Club — Monthly",
  "amount":               24.0,
  "currency":             "usd",
  "billingInterval":      "month",
  "billingIntervalCount": 1,
  "currentPeriodStart":   1717286400,
  "currentPeriodEnd":     1719878400,
  "trialEnd":             null
}
```

| Field                  | Type           | Description                                                                                                                                       |
| ---------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `orderId`              | string         | The LaunchMyStore order the subscription was created from. Stable across the whole subscription lifecycle — use it as your local correlation key. |
| `invoiceId`            | string         | Human-readable invoice/order reference.                                                                                                           |
| `storeId`              | string         | The merchant store the subscription belongs to.                                                                                                   |
| `subscriptionId`       | string         | The recurring subscription identifier (`sub_…`). Unique per subscription.                                                                         |
| `customerId`           | string         | The billing customer identifier (`cus_…`).                                                                                                        |
| `customerEmail`        | string         | Buyer email — handy for provisioning access on an external system.                                                                                |
| `status`               | string         | Current subscription status (see below).                                                                                                          |
| `planName`             | string         | The plan/product name shown to the buyer.                                                                                                         |
| `amount`               | number         | Charge amount per billing cycle, in major units (e.g. `24.0` = \$24.00).                                                                          |
| `currency`             | string         | ISO-4217 currency code, lowercase.                                                                                                                |
| `billingInterval`      | string         | `day` \| `week` \| `month` \| `year`.                                                                                                             |
| `billingIntervalCount` | number         | Number of intervals between charges (e.g. `3` + `month` = quarterly).                                                                             |
| `currentPeriodStart`   | number         | Unix timestamp (seconds) — start of the current paid period.                                                                                      |
| `currentPeriodEnd`     | number         | Unix timestamp (seconds) — when the next renewal is due.                                                                                          |
| `trialEnd`             | number \| null | Unix timestamp (seconds) when the trial ends, or `null` if no trial.                                                                              |

**`status`** mirrors the underlying subscription status. The value you can
expect per topic:

| Topic                          | Typical `status`                                   |
| ------------------------------ | -------------------------------------------------- |
| `subscriptions/create`         | `active`, or `trialing` if the plan has a trial    |
| `subscriptions/renew`          | `active`                                           |
| `subscriptions/update`         | any — `trialing`, `active`, `past_due`, `canceled` |
| `subscriptions/payment_failed` | `past_due`                                         |
| `subscriptions/cancelled`      | `canceled`                                         |

#### Example handler

Grant access on the first charge and each renewal, pause on a failed
charge, and revoke on cancellation:

```javascript theme={null}
app.post('/webhooks/subscriptions', verifyHmac, (req, res) => {
  const topic = req.headers['x-lms-topic'];
  const s = req.body; // the subscription payload above

  switch (topic) {
    case 'subscriptions/create':
    case 'subscriptions/renew':
      grantAccess(s.customerEmail, { until: s.currentPeriodEnd });
      break;
    case 'subscriptions/payment_failed':
      pauseAccess(s.customerEmail);          // card retry in progress
      break;
    case 'subscriptions/cancelled':
      revokeAccess(s.customerEmail);         // terminal
      break;
    case 'subscriptions/update':
      syncPlan(s.customerEmail, s.planName, s.status);
      break;
  }
  res.sendStatus(200); // ack within 10s
});
```

<Note>
  **Idempotency & ordering:** deliveries are at-least-once and may arrive out
  of order or be retried, so key your handler on `subscriptionId` +
  `currentPeriodStart` and make access changes idempotent. The topic is in the
  `X-LMS-Topic` header; verify the `X-LMS-Hmac-SHA256` signature (see
  [Webhook Verification](/api-reference/webhooks/verification)) before acting.
</Note>

### App Lifecycle

| Topic                      | Description                                                                                                                                                                                                             |
| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `app/uninstalled`          | Fired when your app is uninstalled from a store                                                                                                                                                                         |
| `app_subscriptions/update` | Billing state of your app's installation on a store changed — payment failed (grace window opened), grace expired (install disabled), payment recovered (install re-enabled), or the merchant scheduled a cancellation. |

`app_subscriptions/update` payload:

```json theme={null}
{
  "appId": "uuid",
  "installationId": "uuid",
  "billingStatus": "past_due",
  "status": "active",
  "graceEndsAt": "2026-07-23T10:00:00.000Z",
  "reason": "billing_grace_expired"
}
```

* `billingStatus` — `active` | `trial` | `past_due` | `cancelled` | `free`.
* `status` — the installation state: `active` (app functional) or
  `disabled` (API tokens, session tokens and extensions all stop working).
* `graceEndsAt` — present on payment failure: the app keeps full service
  until this time (7-day grace). A successful retry inside the window
  restores `billingStatus: active` automatically.
* `reason` — present on `billing_grace_expired` (the daily sweep disabled
  the install) and `cancel_scheduled` (merchant cancelled; service
  continues until the paid period ends, then the install is disabled).

<Note>
  A later successful payment (e.g. the merchant re-subscribes or fixes their
  card) re-enables a disabled installation automatically — you'll receive
  another `app_subscriptions/update` with `billingStatus: active`,
  `status: active`. Don't delete merchant data on disable; treat it as a
  pause.
</Note>

## Platform-dispatched topics (no subscription)

The following topics are **not** subscribable via `POST /api/v1/webhooks.json`
— they are delivered automatically to the webhook URLs configured on your
app, independent of any subscription.

### GDPR (Mandatory)

| Topic                    | Description                                                                                                                      |
| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------- |
| `customers/data_request` | Merchant or customer requested their data export. Your app must return all stored personal data for the customer within 30 days. |
| `customers/redact`       | A customer has been deleted; redact (or hash) any data your app stores about them within 30 days.                                |
| `shop/redact`            | A store has uninstalled your app for 48+ hours; permanently delete all merchant data within 30 days.                             |

GDPR webhooks are **mandatory** for app-store listing. See [GDPR compliance](/api-reference/webhooks/gdpr) for the payload contract and deadlines.

## Subscribing to Webhooks

Create a webhook subscription using the API:

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST "https://api.launchmystore.io/api/v1/webhooks.json" \
    -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
    -H "Content-Type: application/json" \
    -d '{
      "topic": "orders/create",
      "callbackUrl": "https://your-app.com/webhooks/orders",
      "format": "json"
    }'
  ```

  ```javascript Node.js theme={null}
  const response = await fetch('https://api.launchmystore.io/api/v1/webhooks.json', {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer YOUR_ACCESS_TOKEN',
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      topic: 'orders/create',
      callbackUrl: 'https://your-app.com/webhooks/orders',
      format: 'json'
    })
  });
  ```
</CodeGroup>

## Webhook Subscription Parameters

<ParamField body="topic" type="string" required>
  The webhook topic to subscribe to. Must be one of the subscribable topics listed above.
</ParamField>

<ParamField body="callbackUrl" type="string" required>
  HTTPS URL where webhook payloads are POSTed.
</ParamField>

<ParamField body="format" type="string" default="json">
  Payload format: `json` (default) or `xml`.
</ParamField>

## Webhook Delivery

| Property         | Value                                          |
| ---------------- | ---------------------------------------------- |
| HTTP Method      | `POST`                                         |
| Content-Type     | `application/json`                             |
| Timeout          | 10 seconds                                     |
| Retry policy     | 3 attempts: 60s → 300s → 900s with ±10% jitter |
| Signature header | `X-LMS-Hmac-SHA256`                            |

Your endpoint must return a `2xx` status code within 10 seconds to acknowledge receipt. Anything else is treated as a failure and retried per the policy above. After 3 failed attempts the delivery is moved to the delivery-log error state — inspect via [Webhook Delivery Logs](/api-reference/webhooks/delivery-logs).

See [Webhook Verification](/api-reference/webhooks/verification) for the HMAC contract.

## Listing Subscriptions

```bash theme={null}
curl -X GET "https://api.launchmystore.io/api/v1/webhooks.json" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
```

## Deleting a Subscription

```bash theme={null}
curl -X DELETE "https://api.launchmystore.io/api/v1/webhooks/{id}.json" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
```

## Secret & lifecycle

* Each subscription has its own HMAC signing secret, returned **once** in the
  `POST /api/v1/webhooks.json` create response. Store it securely — it is
  **not** included when you list subscriptions.
* There is no update or secret-rotation endpoint. To rotate a secret (or
  change the topic / callback URL), **delete the subscription and create a new
  one** (a fresh secret is minted on create).
* Re-registering the same `(topic, callbackUrl)` pair returns `400`
  (duplicate) — delete the existing subscription first.
* Failing subscriptions are **not** auto-disabled or deleted; they stay active
  and keep receiving future events. Each delivery retries 3× (60s → 300s →
  900s); after that only that individual delivery is marked failed in the
  [delivery logs](/api-reference/webhooks/delivery-logs).
* There is no test-delivery/ping endpoint on the app API — trigger a real
  event (e.g. place or cancel a test order) to exercise your handler.
