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). 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 http is 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 localhost is refused by name.
  • No redirects. A 3xx answer is a failed delivery, not a second request to a destination the guard never checked.
The URL is checked when you save it and again before every attempt, and the connection is pinned to an address that passed the check so DNS cannot rebind between the two. A save that fails the guard returns 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. Set webhookTopics 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 a POST with a JSON body, and every topic uses the same notify-only envelope:
The body carries no record fields. A webhook body is never the record itself, and no body is ever stored: the delivery log keeps the topic, ids, status and timing only. When an event arrives, re-fetch the record from the API with your own scoped token (see Fetch the current data).

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 both READ_CARTS (so it receives the event) and READ_CART_CONTENTS (so the body carries the cart aggregate):
To read the line items, call 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 ignore webhookTopics 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 need READ_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 the resource + resourceId:
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), 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.
Reject a delivery whose t is more than 300 seconds away from your own clock, in either direction. That tolerance is what makes a replay detectable.
Verify against the raw request body bytes, exactly as received, before any JSON parsing or re-serialization. Re-stringifying the parsed object changes the bytes and breaks verification.
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 2xx within 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 on id).
  • 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 an app_ token.

List deliveries

Filters: 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

The log holds no body, so a replay rebuilds the notify-only envelope from the stored topic, resource, 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

The only test topic is 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 occurredAt if 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.