Skip to main content

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).
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 with your app token (see 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.
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.

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

The request

Every delivery is a POST with a JSON body:
{
  "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.
FieldDescription
idUnique delivery id. Use it to dedupe — retries reuse the same id.
topicresource/action (see below). The action is best-effort.
resource / actionResource changed, and one of created / updated / deleted.
method / pathThe 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).
resourceIdThe 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.
shopIdThe store this event belongs to.
occurredAtISO-8601 timestamp.

Headers

HeaderValue
X-Salesive-TopicThe event topic, e.g. orders/updated.
X-Salesive-Shop-IdThe store id.
X-Salesive-App-Client-IdYour app’s clientId.
X-Salesive-Event-IdSame as the body id.
X-Salesive-Hmac-SHA256Base64 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.
ResourceRequired scopeExample topics
ordersREAD_ORDERSorders/created, orders/updated
products / foods / servicesREAD_INVENTORYproducts/updated, foods/created
categoriesREAD_CATEGORIEScategories/created
customersREAD_CUSTOMERScustomers/updated
shippingREAD_SHIPPINGshipping/created
discountsREAD_DISCOUNTSdiscounts/created
blogsREAD_BLOGSblogs/updated
notificationsREAD_NOTIFICATIONSnotifications/updated
tasksREAD_TASKStasks/created
notesREAD_NOTESnotes/updated
scriptsREAD_SCRIPTSscripts/created
commentsWRITE_COMMENTScomments/created
shipdayREAD_SHIPDAYshipday/updated
kycREAD_KYCkyc/updated
domainsREAD_DOMAINSdomains/updated
rolesREAD_ROLESroles/updated
payoutsREAD_PAYOUTSpayouts/updated
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.

Fetch the current data

The payload carries no record fields — when you receive an event, read the current state from the Apps API using your app access token and the resource + resourceId:
# 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.
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.

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.
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.
Node.js (Express)
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.