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.
/webhooksThe 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
| Topic | Use |
|---|---|
customers/create | New customer → create Customer 360 row |
customers/update | Update Customer 360 |
Order events
| Topic | Use |
|---|---|
orders/create | New order → attribution, journey enrolment, tax computation |
orders/updated | Status and content changes; tax recompute with supersession |
orders/fulfilled | Trigger review-request journey |
orders/paid | Post-purchase upsell, affiliate and referral attribution |
refunds/create | Credit-note tax computation, commission and reward clawback |
Product / inventory
| Topic | Use |
|---|---|
products/create | Sync to multichannel feeds |
products/update | Sync changes |
products/delete | Remove from feeds |
inventory_levels/update | Real-time stock sync to marketplaces |
Cart events
| Topic | Use |
|---|---|
carts/create | Track for cart-recovery |
carts/update | Update tracking |
checkouts/create | Pre-purchase attribution touchpoint |
checkouts/update | Cart-abandonment trigger |
Subscriptions
| Topic | Use |
|---|---|
subscription_contracts/create | Subscription MRR in the Revenue Hub |
subscription_contracts/update | Contract changes, churn |
subscription_billing_attempts/success | Recognised recurring revenue |
subscription_billing_attempts/failure | Dunning |
Theme events
| Topic | Use |
|---|---|
themes/publish | Correlate a theme publish with an SEO crawl regression |
themes/update | Correlate 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
| Topic | Use |
|---|---|
app/uninstalled | Trigger 30-day data retention countdown |
app_subscriptions/update | Plan-tier change |
app/scopes_update | Scope grant changed — delivered to its own endpoint |
GDPR (mandatory)
| Topic | Use |
|---|---|
customers/data_request | Export customer data |
customers/redact | Erase customer data |
shop/redact | Erase 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:
- Sumeru collects all customer data (Customer 360, events, messages, attributions, opt-ins)
- Generates JSON export
- Posts back to merchant's contact email or
data_request_urlper Shopify's contract - Logs to GdprRequest with status=
completed
SLA per Shopify: 30 days. Sumeru typically delivers within 1 hour.
customers/redact — erase
When fired:
- Run erase pipeline
- PII nulled in Customer 360
- Identity links deleted
- Active journeys exited
- 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.