Overview
Instead of polling, your app can receive webhooks — Salesive sends an HTTPPOST 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.
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). When you save it, the console shows your app’s signing secret (whsec_…),
which you use to verify deliveries.
The URL must pass the same guard Salesive applies on every delivery:
- HTTPS only. Plain
httpis refused. - No credentials in the URL (
user:password@host). - No private destinations. The hostname must resolve only to public addresses. Loopback,
private, link local, carrier grade NAT, multicast, documentation, benchmarking and reserved
ranges are refused for IPv4 and IPv6, and
localhostis refused by name. - No redirects. A
3xxanswer is a failed delivery, not a second request to a destination the guard never checked.
400 with the reason.
One webhook URL serves all installations of your app, unless you publish
releases that pin different installs to different snapshots. Each delivery tells
you which store it’s for (
shopId in the body and the X-Salesive-Shop-Id header), so route by
that.Choose the topics you receive
By default an app receives every topic its scopes allow. SetwebhookTopics on your app (the
field accepted by PUT /api/v1/apps/developer/:id) to a list such as
["orders/created", "orders/fulfilled", "inventory/updated"] to receive only those. An empty or
missing list means every topic. Each entry is lowercase resource/action (letters, digits, _ and
-), and the list holds at most 100 topics; anything else is a 400.
Five topics cannot be opted out of, because they are lifecycle or legal notices rather than feature
events: app/uninstalled, app/test, customers/redact, shop/redact and
customers/data_request. They are delivered even when they are not in webhookTopics. See
Data protection.
webhookTopics is part of a release snapshot. If you publish releases, changing
the list on the app does not reach installs pinned to an older release until you publish a new one.Payload shape
Every delivery is aPOST with a JSON body, and every topic uses the same notify-only
envelope:
Per-topic additions
A few topics add a small, fixed field on top of the envelope. Nothing else is ever added, and no topic ever carries the record itself.The
cart aggregate is computed per installation from the scopes that installation holds. Two
apps on the same store can therefore receive the same event with different bodies, each signed
with its own secret. An install without READ_CART_CONTENTS receives the envelope without
cart.Example: a cart event with the aggregate
This install holds bothREAD_CARTS (so it receives the event) and READ_CART_CONTENTS (so the
body carries the cart aggregate):
GET /carts/66b1f0a3c2d4e5f6a7b8c9d0 with your app token. See
Carts.
Headers
Topics
topic is resource/action. For admin events the action is created (POST), updated
(PUT/PATCH), or deleted (DELETE). Storefront events (customer activity on the live store) carry
their own action (signed_in, placed, paid, and so on). You receive a topic only if your
installation holds the scope below, except the mandatory lifecycle and data protection topics, which
are always delivered.
Admin events (dashboard / API changes)
Digital stores. The action comes from the HTTP method, so some sub-actions do not read
naturally: granting, extending or revoking buyer access (all
POST) arrive as
digital-access/created, and so does a bulk delete of products (digital-products/created).
Saving the all-access pass is digital-products/updated with no resourceId. Re-fetch with
List buyer access or
Get the all-access pass to see what
changed. Access that is granted automatically when a buyer pays is not published as a
digital-access event.Metafields. Each delivery covers one metafield, so a batch write arrives as one delivery per
row, and the body adds
metafield: { ownerType, ownerId, namespace, key }. Changes in an app’s
own namespace go to that app only. See Metafields.shop/updated fires when the merchant changes the store profile, business details, location,
app info or published website config. resourceId is the store id and a section field says which
part changed (profile, business, location, app, website_config); read the values from
GET /app/context, which every installed app can call. It rides
READ_SETTINGS rather than a scope of its own because the profile is already readable without a
scope. Changes to store settings keep their own settings/updated topic; the two are never sent for
the same change.
Order, stock and lifecycle events
These describe what happened rather than which route was called, so they fire however the change was made: the dashboard, another app, Ola, or a payment.
A status change sends
orders/fulfilled or orders/refunded once: saving an order that is
already refunded again sends nothing. The route that made the change still sends its own
orders/updated.
Data protection topics
These are legal notices, not feature events. They cannot be opted out of: they ignorewebhookTopics and are sent to every active installation of the shop that has a webhook URL. What
to do when one arrives, and the 30 day expectation, is in
Data protection.
The customer topics carry the customer id in
resourceId; shop/redact is shop wide and uses the
shop id. None of them carry the customer’s fields.
Storefront events (live shopper activity)
These fire from the storefront when a shopper acts on the live store.orders/placed and orders/paid are distinct from the admin orders/created/orders/updated.
A single storefront checkout fires orders/placed (then later orders/paid) — never the admin
topics. Use the resourceId (the order id) with READ_ORDERS to fetch the order via the Apps API.Cart events fire only for signed-in shoppers. A guest (ghost) cart is not delivered: the
storefront re-owns that cart to the real user when they sign in, so sending both would count the
same cart twice. The abandonment job applies the same rule.
READ_CARTS on its own is notify-only: the envelope carries the customer id as resourceId
and the shopId, but no cart contents. Add READ_CART_CONTENTS to fetch the cart with
GET /carts/:customerId and to receive the cart aggregate on cart events. The fetch returns
the line items; the event never does. See Carts.
Wishlist events carry the wishlist id, fetchable with GET /wishlist/:id. See
Wishlists.Events fire for successful changes regardless of who made them — a merchant editing an order in
the dashboard, another app, or a shopper on the storefront. Dedupe on
id and treat delivery as
at-least-once.Billing events (charges and subscriptions)
Fired by Salesive when a payment your app asked for changes state. Unlike the topics above they are sent only to the app that made the charge, and needREAD_CHARGES (to ask for charges at all
you also need WRITE_CHARGES, so an app that wants these events requests both).
resourceId is the charge or subscription id. Subscription events caused by a payment also carry
a chargeId. See Payments.
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 theresource + resourceId:
- 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), re-list the affected resource to re-sync the
collection.Verify the signature
Every delivery carries two signatures. Verify the newer one; the older one stays for receivers that have not moved yet.X-Salesive-Signature (current). The header is t=<unix seconds>,v1=<signature>, where the
signature is the base64 HMAC-SHA256 of the string <t>.<raw body>, keyed with your app’s
signing secret. The timestamp is part of the signed string, so an attacker cannot re-date a captured
request.
Node.js (Express)
X-Salesive-Hmac-SHA256 (older). The base64 HMAC-SHA256 of the raw body, with no timestamp. It
is still sent on every delivery so existing receivers keep working. It cannot detect a replay, so
move to X-Salesive-Signature when you can.
Respond & retries
- Acknowledge fast. Return a
2xxwithin 10 seconds. Do heavy work asynchronously. Anything else, including a timeout, is a failed attempt. - Retries. A failed attempt gets four retries, five attempts in total, with exponential backoff
starting at 60 seconds and capped at one hour. Retries reuse the same event
id, so make your handler idempotent (dedupe onid). - No retry for a bad URL. A URL that fails the guard is a permanent failure: it is logged once and not retried. A DNS failure stays retryable, because it may clear on its own.
- At-least-once. You may occasionally receive a duplicate; never assume exactly-once.
Delivery log, replay and test events
Every attempt is logged for 14 days: the first try, each retry, each replay and each test. One row per attempt holds the topic, time, attempt number, duration, the HTTP status your endpoint answered (or a short error such as a timeout), the environment and the URL without its query string. Nothing secret is stored: no request or response body, no headers, no signature and no secret. Open your app in Apps > Developer and scroll to Webhook deliveries, or use the developer endpoints below. They require your merchant session, never anapp_ token.
List deliveries
topic (for example orders/fulfilled), status (failed or delivered), limit (1
to 100, default 50) and cursor (the next value from the previous page; the older before name
is also accepted). The response carries items, retentionDays and next.
Replay one delivery
resourceId and shop. It gets a new event id, so it is a new delivery rather than a
byte-for-byte copy: a dedupe check keyed on the original id will not suppress it. The replay goes
to the installation’s current URL and is signed with the current secret for that environment, with
one attempt and no retries. A replay is dropped, not retried, when the installation is gone or
inactive, the app has no webhook URL for its environment, or the app’s webhookTopics no longer
includes the topic: the queue call still answers queued, but nothing is sent. You can replay up
to 30 deliveries an hour. resend is an accepted older name for the same operation.
Send a test event
app/test. The event carries test: true, the installation id as
resourceId and no store record. The app needs a webhook URL and at least one active installation;
pass installationId to choose one, otherwise the most recent active installation is used. One
attempt, no retries, up to 10 test events an hour. See
Webhook deliveries for the response shapes.
Best practices
- Verify the signature on every request; reject unsigned, stale 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
occurredAtif you need to reason about sequence. - Handle the mandatory topics (
app/uninstalled,app/test,customers/redact,shop/redact,customers/data_request) even if you subscribe to a short list. See Data protection.

