Shopify App Security Best Practices for 2026
An app that fails Shopify's security review gets pulled — and when a public app gets pulled, every merchant using it loses functionality instantly. Beyond the review gate, a data breach in an app handling customer PII lands the merchant in GDPR exposure and you in a support nightmare. Security in a Shopify app is not optional hardening; it is load-bearing infrastructure from day one.
This post covers the concrete requirements: what Shopify mandates, what the App Store review team actually checks, and what the Protected Customer Data approval process demands before you can touch a customer's name or email.
TL;DR
- Never trust a client-supplied
shopparameter. Verify every request with HMAC or a session token from App Bridge — never with something the browser sent you. - Mandatory compliance webhooks (
customers/data_request,customers/redact,shop/redact) are a hard gate for App Store listing. Missing one gets the app rejected every time. - Protected Customer Data (PCD) access requires explicit Partner Dashboard approval; unapproved fields return
nullat runtime, not an error your tests will catch. - Offline tokens store in encrypted server-side storage only. They must never reach the browser.
- Least-privilege scopes mean requesting only the permissions the app needs today, not the ones it might need someday.
The OAuth flow: where most apps make their first mistake
Shopify uses the OAuth 2.0 authorization-code grant. The flow is standard; the mistakes are not.
When Shopify redirects to your app's install URL, it includes shop, hmac, timestamp, and code as query parameters. Step zero is HMAC verification. Remove the hmac parameter from the query string, sort the remaining parameters alphabetically, URL-encode them, and compute an HMAC-SHA256 digest using your app's client secret. If your digest does not match the hmac Shopify sent, reject the request outright. Do not proceed to exchange the code for a token.
The state nonce matters too. Generate a cryptographically random value before the redirect to Shopify, store it in a server-side session, and verify it matches the state Shopify returns. This prevents CSRF attacks against your install flow — a vector that gets exploited in the wild.
After verification, exchange the code for a token using a server-to-server POST (never from the browser). The token you receive is either online or offline, and this distinction shapes your entire session architecture.
Online vs offline access tokens: pick the right one per use case
Online tokens are scoped to a specific staff member's session and expire when they log out or after a short TTL. Use them for embedded admin UI operations — actions the merchant takes manually while your app is open.
Offline tokens persist until the merchant uninstalls the app. Use them for background jobs, webhooks, scheduled syncs, and anything that runs without a user present.
The security mistake we see most often: storing the offline token in a cookie or passing it back to the frontend so the JavaScript layer can make API calls directly. Never do this. Offline tokens belong in encrypted server-side storage (a database field encrypted at rest, or a secrets manager). The frontend talks to your backend; your backend talks to Shopify.
For embedded apps in 2026, the recommended authentication path is the token exchange flow: App Bridge generates a short-lived session token (a JWT, valid for one minute), your backend verifies its signature against your client secret, and exchanges it for the access token you need. No redirects, no pop-ups, and the browser never holds a long-lived credential.
Session tokens and App Bridge: never trust what the browser tells you
If your app is embedded in the Shopify Admin, App Bridge is the authentication layer. App Bridge generates a signed JWT session token for each request. Your backend must verify this token before acting on it.
The verification steps:
- Decode the JWT header and payload.
- Verify the signature using your app's client secret (HMAC-SHA256).
- Check the
issclaim — it must behttps://<shop>.myshopify.com/admin. - Check the
destclaim matches the expected shop domain. - Check
exp— session tokens expire after 60 seconds.
The attack you are defending against: a bad actor crafts a request with ?shop=legitimate-store.myshopify.com appended to fool your backend into treating it as a valid install. If you extract shop from the query string without verifying a token or HMAC, you have a shop-parameter injection vulnerability. It is a documented attack class with real exploits. Verify, do not assume.
HMAC verification on webhooks
Every webhook Shopify sends includes an X-Shopify-Hmac-SHA256 header. This is a base64-encoded HMAC-SHA256 digest of the raw request body, signed with your app's client secret.
Your webhook handler must:
- Read the raw request body before any JSON parsing.
- Compute HMAC-SHA256 of that raw body using your client secret.
- Base64-encode your digest.
- Compare it (using a constant-time comparison function) to the header value.
- Return
401if they do not match. Never process an unverified webhook payload.
This applies to every webhook topic — including the three mandatory compliance webhooks below. A webhook handler that skips HMAC verification is an open endpoint any attacker can POST to.
The three mandatory compliance webhooks
These are not optional. A public app missing any one of them fails App Store review on first pass — consistently. The Shopify compliance webhooks documentation describes all three topics.
| Webhook topic | When Shopify sends it | What your app must do |
|---|---|---|
customers/data_request |
A customer requests to see their data | Compile all stored data for that customer (identified by customer.id, customer.email, customer.phone, and associated order IDs) and send it to the store owner. |
customers/redact |
A customer requests data deletion | Permanently delete all data you hold for that customer, unless you have a legal obligation to retain it. |
shop/redact |
48 hours after a merchant uninstalls your app | Delete all data associated with that shop (shop_id, shop_domain) from your database. No residual records. |
All three handlers must: accept a POST with Content-Type: application/json, verify the X-Shopify-Hmac-SHA256 header, and return a 2xx status to confirm receipt. Shopify logs delivery attempts; failed or non-responding handlers are visible during review.
One practical note: Shopify sends the shop/redact webhook 48 hours after uninstall, not immediately. Design your data-retention logic accordingly — do not purge data on the uninstall webhook alone, as merchants sometimes reinstall quickly and expect their configuration to survive.
Protected Customer Data: getting access approved
The PCD framework divides customer data into access levels.
Level 1 covers customer data that excludes personally identifying fields (name, email, phone, address). Public apps can access this after completing a data-protection review in the Partner Dashboard.
Level 2 covers the identifying fields: name, address, email, phone number. This requires a stricter review. Shopify's approval criterion: the requested data is the minimum required for the app's stated functionality. Data minimization is the test. "We might need it later" does not pass.
Level 2 also carries operational requirements:
- Encryption at rest and in transit (not just TLS for transit — database-level encryption for PII fields).
- Encrypted backups.
- Separate production and test environments — no real customer data in staging.
- Access logging for staff who can view PII.
- A documented incident response plan.
To request access: Partner Dashboard → Apps → your app → API access requests → Protected customer data access. Select the specific fields, provide the business justification for each, and submit. Unapproved fields do not throw errors at runtime — they return null. This means an app without approval can pass all its own tests on dev data, then silently receive nulls in production when merchants install it.
Submit the PCD request before you start building features that depend on those fields. The review takes time, and approval is not guaranteed on first submission.
Least-privilege access scopes
Request the minimum set of API scopes required for the app's current functionality. Not what you might build later. Not what a comparable app uses. What this app needs now.
Every additional scope is an attack surface. If your app reads product data, request read_products. If it never writes orders, do not request write_orders. Scope creep increases the blast radius of a compromised token and signals over-reach to the App Store review team.
Audit your scope list before submission. We have seen apps list a dozen scopes because the scaffold template had them — and then use three. Review teams notice.
Secure secret storage and API-version pinning
Two controls that are easy to get right and embarrassing to get wrong:
Client secrets and access tokens belong in environment variables or a secrets manager (AWS Secrets Manager, HashiCorp Vault, GCP Secret Manager). Never commit them to a repository. Never embed them in client-side JavaScript bundles. A single console.log of a token in development that makes it into a public repo is a full incident.
API version pinning. Shopify releases a new API version quarterly and deprecates old ones on a rolling basis. Pin your app to a specific version (2026-01, 2026-04) and schedule deliberate upgrades. Unpinned or unstable versions are not for production. Breaking changes in an unpinned version will surface in the worst way: a production outage after a Shopify platform update.
Rate limits and abuse handling
The Shopify Admin GraphQL API uses a cost-based bucket system. Each query consumes points based on its complexity; the bucket refills over time. Hitting the limit returns a throttle response and headers showing your current bucket state.
Build retry logic with exponential backoff for all API calls, and respect any Retry-After guidance exactly — retrying immediately after a throttle keeps you throttled. For bulk work (syncing thousands of orders or products), use Shopify's Bulk Operations API instead of paginated loops; it is designed for scale. We cover the cost model in detail in Shopify GraphQL API best practices.
On the inbound side: your webhook endpoints, install handlers, and API endpoints are public URLs. Implement basic request validation (HMAC on webhooks, session-token verification on Admin UI calls) and consider rate-limiting your own endpoints against repeat-request attacks. The OWASP API Security Top 10 is a useful checklist for the endpoints you expose.
The complete security and compliance checklist
Use this before every App Store submission and any major release.
| Area | Control | Required for public app? |
|---|---|---|
| OAuth | HMAC verification on install callback | Yes |
| OAuth | State nonce to prevent CSRF on install | Yes |
| OAuth | Server-to-server token exchange only | Yes |
| Session auth | App Bridge session tokens verified by backend | Yes (embedded apps) |
| Session auth | shop param never trusted without verification |
Yes |
| Webhooks | HMAC verified on every inbound webhook | Yes |
| Webhooks | customers/data_request handler live |
Yes — review gate |
| Webhooks | customers/redact handler live |
Yes — review gate |
| Webhooks | shop/redact handler live |
Yes — review gate |
| Tokens | Online tokens used for user-session operations | Best practice |
| Tokens | Offline tokens server-side, encrypted at rest | Yes |
| Tokens | No tokens in client-side JS or cookies | Yes |
| PCD | Level 1/2 access requested before build | Yes (if applicable) |
| PCD | PII fields encrypted at rest | Level 2 requirement |
| PCD | Separate prod/test environments | Level 2 requirement |
| Scopes | Minimum required scopes only | Yes |
| Secrets | Client secret in env vars / secrets manager | Yes |
| Secrets | No secrets in git history | Yes |
| API | Version pinned (not unstable) |
Yes |
| Rate limits | Retry logic with exponential backoff | Best practice |
| Rate limits | Bulk Operations API for large data sets | Best practice |
FAQ
Do custom apps need the mandatory compliance webhooks? Custom apps — installed directly by one merchant via a link, not listed on the App Store — are not subject to App Store review and therefore are not gated on the compliance webhooks. That said, if the custom app handles customer PII, implementing them is the correct approach for GDPR compliance regardless of review status. The compliance gap between custom and public apps is discussed in Custom vs Public Shopify Apps.
What happens if my PCD access request is denied?
Denied fields return null in API responses rather than throwing an error. Your app continues to function; it just cannot access those specific fields. You can address the denial by clarifying your business justification and resubmitting, or by redesigning the feature to work without the denied field. Shopify's approval criterion is always the same: is this the minimum data required?
How do I test the compliance webhooks before submission?
Shopify provides a way to simulate the compliance webhooks from the Partner Dashboard (Apps → your app → compliance webhooks). Use these to confirm your endpoints are reachable, return 2xx, and correctly verify the HMAC. Log the payloads during testing so you can inspect the shape of what Shopify sends.
What is the state nonce and why does it matter?
Before redirecting the merchant to Shopify's authorization URL, generate a random string (cryptographically random), store it server-side, and append it as the state parameter. When Shopify redirects back, verify the returned state matches. Without this, an attacker can trick a logged-in merchant into installing your app on a shop they don't control — a classic OAuth CSRF attack. The OWASP API Security guidance covers this attack class.
Does HMAC verification protect against replay attacks?
Partly. Shopify's install callbacks include a timestamp parameter; reject any request where the timestamp is older than a few minutes. This limits the replay window without requiring a nonce store. Webhook HMAC verification alone does not prevent replays — if replay protection matters for a specific webhook, implement idempotency keys on your handler.
How much does compliance work add to the build? It depends on whether your app needs Protected Customer Data and how many webhooks you're registering. For a straightforward app with no PII handling, proper OAuth, HMAC verification, and the three mandatory compliance webhooks add roughly one to two weeks to a build. Needing Level 2 PCD access and the associated infrastructure (encrypted storage, access logging, incident response documentation) adds more. The Shopify App Development Cost post has phase-by-phase figures.
Build it right the first time. The security controls above are the foundation the rest of the app sits on — OAuth done wrong means a compromised token; compliance webhooks missing means an immediate review rejection. First Bridge Consulting builds Shopify apps with all of this wired in from the start, not bolted on at the end. Get a working budget range from our Shopify App Cost Estimator, or get in touch for a scoped proposal.