Skip to main content

Shopify webhooks

The delivery endpoint​

All topics except app/scopes_update are delivered to:

POST /webhooks/events

app/scopes_update has its own subscription at /webhooks/app/scopes_update.

Why a sub-path, and not /webhooks

The reverse proxy in front of the app declares its webhook location with a trailing slash. An nginx prefix location ending in / matches only /webhooks/… — a request to the bare /webhooks gets nginx's implicit 301 redirect to the slashed form.

Shopify requires a 2xx. It does not follow redirects, and after roughly 24 hours of failures it prunes the subscription entirely. A configuration that declared bare /webhooks therefore delivered nothing, on every topic, while looking correct in the app manifest.

uri = "/webhooks/" does not fix it either — Shopify strips the trailing slash when it registers the subscription. A sub-path is the fix: sub-paths are never redirected, which is why /webhooks/app/scopes_update always worked.

If you self-host, verify the endpoint directly rather than reasoning from database state:

curl -i -X POST https://<your-app-host>/webhooks/events
# expect 400 (missing HMAC) — a 301 or 404 means delivery is broken

What Sumeru subscribes to​

27 topics, declared in the app configuration and approved by the merchant at OAuth. Changing the list requires shopify app deploy; adding a topic that needs a new scope also requires every existing install to re-consent.

Topic catalog​

Customer events​

TopicUse
customers/createNew customer → create Customer 360 row
customers/updateUpdate Customer 360

Order events​

TopicUse
orders/createNew order → attribution, journey enrolment, tax computation
orders/updatedStatus and content changes; tax recompute with supersession
orders/fulfilledTrigger review-request journey
orders/paidPost-purchase upsell, affiliate and referral attribution
refunds/createCredit-note tax computation, commission and reward clawback

Product / inventory​

TopicUse
products/createSync to multichannel feeds
products/updateSync changes
products/deleteRemove from feeds
inventory_levels/updateReal-time stock sync to marketplaces

Cart events​

TopicUse
carts/createTrack for cart-recovery
carts/updateUpdate tracking
checkouts/createPre-purchase attribution touchpoint
checkouts/updateCart-abandonment trigger

Subscriptions​

TopicUse
subscription_contracts/createSubscription MRR in the Revenue Hub
subscription_contracts/updateContract changes, churn
subscription_billing_attempts/successRecognised recurring revenue
subscription_billing_attempts/failureDunning

Theme events​

TopicUse
themes/publishCorrelate a theme publish with an SEO crawl regression
themes/updateCorrelate a live-theme edit with an SEO crawl regression

Both require read_themes — see Authentication & scopes. themes/update also fires for draft and development themes, which do not change the live site; the handler filters on the main theme role, so only real live-site changes reach the incident timeline.

App lifecycle​

TopicUse
app/uninstalledTrigger 30-day data retention countdown
app_subscriptions/updatePlan-tier change
app/scopes_updateScope grant changed — delivered to its own endpoint

GDPR (mandatory)​

TopicUse
customers/data_requestExport customer data
customers/redactErase customer data
shop/redactErase entire shop's data (after uninstall + 30d)

HMAC verification​

Shopify signs every webhook with HMAC-SHA256:

Header: X-Shopify-Hmac-Sha256: <base64>
Body: raw JSON

Verification:

import crypto from 'node:crypto';

function verifyShopifyWebhook(rawBody, hmacHeader, sharedSecret) {
const computed = crypto
.createHmac('sha256', sharedSecret)
.update(rawBody)
.digest('base64');
try {
return crypto.timingSafeEqual(
Buffer.from(hmacHeader),
Buffer.from(computed)
);
} catch {
return false;
}
}

Fail = 401, no processing.

Payload examples​

orders/create (excerpt)​

{
"id": 5234567890,
"email": "jane@example.com",
"created_at": "2026-05-10T14:23:00Z",
"total_price": "67.50",
"currency": "USD",
"customer": {
"id": 9876543210,
"email": "jane@example.com"
},
"line_items": [ ... ]
}

Full Shopify payload schemas: Shopify Webhook Reference.

GDPR webhook contract​

Shopify mandates 3 GDPR webhooks for every app:

customers/data_request — export​

When fired:

  1. Sumeru collects all customer data (Customer 360, events, messages, attributions, opt-ins)
  2. Generates JSON export
  3. Posts back to merchant's contact email or data_request_url per Shopify's contract
  4. Logs to GdprRequest with status=completed

SLA per Shopify: 30 days. Sumeru typically delivers within 1 hour.

customers/redact — erase​

When fired:

  1. Run erase pipeline
  2. PII nulled in Customer 360
  3. Identity links deleted
  4. Active journeys exited
  5. Audit log written

SLA per Shopify: 30 days. Sumeru typically processes immediately.

shop/redact — full data erase​

Fired 30 days after app uninstall. Removes all shop data permanently.

Idempotency​

Each Shopify webhook has unique X-Shopify-Webhook-Id header. Sumeru uses this for dedup.

Retry behavior​

Shopify retries failed webhooks up to 48 hours with exponential backoff. Sumeru acks within 2 sec to avoid unnecessary retries.

Common gotchas​

"Not receiving customers/redact webhooks." Verify Sumeru is registered in your Shopify GDPR webhook section. Re-install if missing.

"Order webhook arrived after Sumeru was already in maintenance." Shopify will retry; on next online cycle, queue drains.

See also​