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