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

# Webhooks

> Get lightweight, signed event notifications in real time, then fetch the current data from the API with your app token — webhooks never carry the record itself.

## Overview

Instead of polling, your app can receive **webhooks** — Salesive sends an HTTP `POST` to your
endpoint whenever something changes on a store your app is installed on (an order is created, a
product updated, and so on).

<Note>
  **A webhook is a notification, not the data.** A delivery tells you *what* changed — the
  resource, the action, and the affected record's id — but it does **not** include the record's
  fields. Fetch the current state yourself from the [Apps API](/apps-api/introduction) with your app
  token (see [Fetch the current data](#fetch-the-current-data)). Reading it back through your own
  scoped token means you only ever receive data your permissions allow, and you always act on the
  latest state rather than a possibly-stale snapshot.
</Note>

Webhooks are **permission-gated**: you only receive events for a resource if the merchant granted
your app the scope that governs it (e.g. you get `orders/*` events only if the installation has
`READ_ORDERS`). See [Scopes & permissions](/apps/scopes-permissions).

## Enable webhooks

Set a **Webhook URL** on your app in the dashboard **Developer console** (it's optional — leave it
blank to disable). The URL must be HTTPS and publicly reachable. When you save it, the console
shows your app's **signing secret** (`whsec_…`) — you'll use it to verify deliveries.

<Note>
  One webhook URL serves all installations of your app. Each delivery tells you which store it's
  for (`shopId` in the body and the `X-Salesive-Shop-Id` header), so route by that.
</Note>

## The request

Every delivery is a `POST` with a JSON body:

```json theme={null}
{
  "id": "8f1c2e3a-...-9b0d",
  "topic": "orders/updated",
  "resource": "orders",
  "action": "updated",
  "method": "PUT",
  "path": "/api/v1/orders/66b1f0a3c2d4e5f6a7b8c9d0",
  "resourceId": "66b1f0a3c2d4e5f6a7b8c9d0",
  "shopId": "6680aabbccddeeff00112200",
  "occurredAt": "2026-06-28T18:42:11.000Z"
}
```

There is **no record data in the payload** — only the identifiers you need to
[fetch it](#fetch-the-current-data).

| Field                 | Description                                                                                                                                              |
| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`                  | Unique delivery id. **Use it to dedupe** — retries reuse the same id.                                                                                    |
| `topic`               | `resource/action` (see below). The `action` is best-effort.                                                                                              |
| `resource` / `action` | Resource changed, and one of `created` / `updated` / `deleted`.                                                                                          |
| `method` / `path`     | The exact API call that triggered the event — use these to disambiguate bulk or sub-actions (the `topic` won't capture, e.g., a bulk price update).      |
| `resourceId`          | The affected record's id — present on creates (the new record), updates and deletes. May be absent for bulk/sub-actions; fall back to `method` + `path`. |
| `shopId`              | The store this event belongs to.                                                                                                                         |
| `occurredAt`          | ISO-8601 timestamp.                                                                                                                                      |

### Headers

| Header                     | Value                                                     |
| -------------------------- | --------------------------------------------------------- |
| `X-Salesive-Topic`         | The event topic, e.g. `orders/updated`.                   |
| `X-Salesive-Shop-Id`       | The store id.                                             |
| `X-Salesive-App-Client-Id` | Your app's `clientId`.                                    |
| `X-Salesive-Event-Id`      | Same as the body `id`.                                    |
| `X-Salesive-Hmac-SHA256`   | Base64 HMAC-SHA256 signature of the raw body (see below). |

### Topics

`topic` is `resource/action`, where `action` is `created` (POST), `updated` (PUT/PATCH), or
`deleted` (DELETE). You receive a resource's events only if your installation holds the scope below.

| Resource                          | Required scope       | Example topics                      |
| --------------------------------- | -------------------- | ----------------------------------- |
| `orders`                          | `READ_ORDERS`        | `orders/created`, `orders/updated`  |
| `products` / `foods` / `services` | `READ_INVENTORY`     | `products/updated`, `foods/created` |
| `categories`                      | `READ_CATEGORIES`    | `categories/created`                |
| `customers`                       | `READ_CUSTOMERS`     | `customers/updated`                 |
| `shipping`                        | `READ_SHIPPING`      | `shipping/created`                  |
| `discounts`                       | `READ_DISCOUNTS`     | `discounts/created`                 |
| `blogs`                           | `READ_BLOGS`         | `blogs/updated`                     |
| `notifications`                   | `READ_NOTIFICATIONS` | `notifications/updated`             |
| `tasks`                           | `READ_TASKS`         | `tasks/created`                     |
| `notes`                           | `READ_NOTES`         | `notes/updated`                     |
| `scripts`                         | `READ_SCRIPTS`       | `scripts/created`                   |
| `comments`                        | `WRITE_COMMENTS`     | `comments/created`                  |
| `shipday`                         | `READ_SHIPDAY`       | `shipday/updated`                   |
| `kyc`                             | `READ_KYC`           | `kyc/updated`                       |
| `domains`                         | `READ_DOMAINS`       | `domains/updated`                   |
| `roles`                           | `READ_ROLES`         | `roles/updated`                     |
| `payouts`                         | `READ_PAYOUTS`       | `payouts/updated`                   |

<Note>
  Events fire for successful changes regardless of who made them — a merchant editing an order in
  the dashboard, another app, or your own app. Dedupe on `id` and treat delivery as at-least-once.
</Note>

## Fetch the current data

The payload carries no record fields — when you receive an event, read the current state from the
[Apps API](/apps-api/introduction) using your app access token and the `resource` + `resourceId`:

```bash theme={null}
# e.g. an orders/updated event with resourceId 66b1f0a3c2d4e5f6a7b8c9d0
curl https://api.salesive.com/api/v1/orders/66b1f0a3c2d4e5f6a7b8c9d0 \
  -H "Authorization: Bearer app_<your-app-access-token>"
```

Re-fetching is deliberate, and safer than a fat payload:

* **You only ever see data you're entitled to.** The record comes back through *your* scoped token,
  so the webhook can never hand you fields your permissions don't cover.
* **You always act on the latest state**, not a snapshot that may be stale by the time you process it.

<Note>
  On a `deleted` event the record is gone — don't re-fetch; act on `resource` + `resourceId`. When
  `resourceId` is absent (bulk or sub-actions like `POST /products/bulk/price`), use `method` +
  `path` to decide what to re-sync — typically re-list the affected collection.
</Note>

## Verify the signature

Every delivery is signed so you can trust it came from Salesive and wasn't altered. Compute the
**base64 HMAC-SHA256 of the raw request body** using your app's signing secret, and compare it to
the `X-Salesive-Hmac-SHA256` header with a constant-time check.

<Warning>
  Verify against the **raw** request body bytes — exactly as received, before any JSON parsing or
  re-serialization. Re-stringifying the parsed object will change the bytes and break verification.
</Warning>

```js Node.js (Express) theme={null}
import express from "express";
import crypto from "crypto";

const app = express();
const SIGNING_SECRET = process.env.SALESIVE_WEBHOOK_SECRET; // whsec_...

// Capture the RAW body for signature verification.
app.post(
  "/webhooks/salesive",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const signature = req.get("X-Salesive-Hmac-SHA256") || "";
    const expected = crypto
      .createHmac("sha256", SIGNING_SECRET)
      .update(req.body) // req.body is a Buffer (the raw bytes)
      .digest("base64");

    const ok =
      signature.length === expected.length &&
      crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));

    if (!ok) return res.status(401).send("invalid signature");

    const event = JSON.parse(req.body.toString("utf8"));
    // TODO: enqueue and process async; respond fast.
    console.log(event.topic, event.shopId, event.id);

    res.sendStatus(200);
  },
);
```

## Respond & retries

* **Acknowledge fast.** Return a `2xx` within a few seconds. Do heavy work asynchronously — Salesive
  treats anything other than `2xx` (or a timeout) as a failed delivery.
* **Retries.** A failed delivery is retried with exponential backoff for several attempts before it's
  dropped. Because retries reuse the same `id`, make your handler **idempotent** (dedupe on `id`).
* **At-least-once.** You may occasionally receive a duplicate; never assume exactly-once.

## Best practices

* Verify the signature on every request; reject unsigned or mismatched deliveries.
* Process asynchronously (queue the event, ack immediately).
* Scope your handling to the `shopId` — a delivery only ever concerns one store.
* Don't depend on ordering; use `occurredAt` if you need to reason about sequence.
