Skip to content

Shopify app development

shopify-app-development

Build a Shopify app with the configured Admin API version, session-token auth for embedded admin requests, and HMAC-verified webhooks. Use when creating or changing a Shopify app, Admin GraphQL call, app proxy, or Shopify webhook handler.

Version 0.3.2  ·  MIT  ·  Source  ·  Rule

Install this skill

Copy the skill and its rule into your project:

npx @sadrmanesh/commerce-agent-skills add shopify-app-development

The skill goes to .agents/skills/ and the rule to .agents/rules/. See Command line for other directories.

Use this skill for a Shopify app. Payment capture inside a shop you own is the payment skill. Webhook durability is the order and webhook skill. Storefront browsing without an admin token is the headless storefront skill.

Shopify's developer docs are the source for request authentication and webhook delivery. Re-check them when an API version or header name is load-bearing. This skill records the contracts those docs describe.

Procedure

  1. Keep app configuration in shopify.app.toml, including the API version and app-specific webhook subscriptions. Do not hard-code an API version in a handler. The X-Shopify-API-Version header says which version serialized that delivery, and the app configuration selects it.
  2. Authenticate every request before using a shop domain from the query, body, or header.
  3. Call the GraphQL Admin API for new admin work. Do not start a new REST Admin API client.
  4. Ask for the narrowest access scopes the mutation needs.
  5. Verify HTTPS webhooks with the app client secret over the raw body, then dedupe on X-Shopify-Webhook-Id.

Header and auth details are in the webhook and auth notes. Worked examples and checks are in evals/tasks.json.

Which credential

Request Proof
Embedded app in the Shopify admin Session token (ID token) validated for signature, expiry, audience, and destination.
HTTPS webhook HMAC-SHA256 of the raw body, Base64, in X-Shopify-Hmac-Sha256, keyed with the app client secret. Compare header names case-insensitively.
App proxy from a storefront App proxy signature. It proves the shop, not the person. Do not change shop settings from an app proxy route.
Checkout or customer-account UI extension That extension's session token.
Browser call that skips the app proxy Anonymous. It may read public data. It must not mutate the shop.

Reject a request that fails its check. Do not fall back to a shop parameter.

Webhooks

Compute Base64 HMAC-SHA256 over the raw body with the client secret. If a JSON parser runs first, the signature will not match. Return 401 when the signature does not match.

X-Shopify-Webhook-Id identifies one delivery. Store it and skip a repeat. X-Shopify-Event-Id is shared by deliveries from the same merchant action and is not a substitute for the delivery id when you are deduping retries of one POST.

Topic and shop-domain headers are untrusted until the HMAC matches. App-specific subscriptions in shopify.app.toml are kept in sync by Shopify. Use webhookSubscriptionCreate when the subscription has to be shop-specific. You cannot set the webhook API version on that mutation; it follows the app's webhook API version.

After the app client secret rotates, Shopify can keep signing with the previous secret for up to an hour. Accept that window only if you still have the previous secret stored for verification.

Google Cloud Pub/Sub and Amazon EventBridge deliveries do not use this HMAC header. Do not pretend an HTTPS verifier applies to those transports.

Admin writes

  • Send the shop domain you got from the verified token, not from a form field.
  • Treat Admin GraphQL user errors and top-level errors as separate. A 200 response can still contain userErrors.
  • Write webhooks and billing changes so a second delivery does not create a second subscription or a second charge.
  • Do not log the access token, the client secret, or the raw HMAC.

Gotchas

  • express.json() before verification replaces the raw body. Capture the bytes first.
  • A shop domain check must be anchored. example.myshopify.com.attacker.example is not a shop domain.
  • OAuth state has to be bound to the browser that started the install and checked on return.
  • Scopes are not implied by installation. A missing scope is a failed call, not a reason to use a broader token you stored earlier.

Verification

  • A webhook with a changed byte returns 401 and writes nothing.
  • The same X-Shopify-Webhook-Id applied twice creates one record.
  • An embedded request with no valid session token does not run a mutation.
  • An app proxy request cannot change shop settings.

Evaluation

Worked examples and the checks they must satisfy are in evals/tasks.json.

References

Shopify auth and webhooks

These notes follow Shopify's app authentication and webhook delivery docs. Header names are case-insensitive on the wire.

HTTPS webhook headers

Header Use
X-Shopify-Topic Topic, such as products/update. Untrusted until the HMAC matches.
X-Shopify-Hmac-Sha256 Base64 HMAC-SHA256 of the raw body using the app client secret.
X-Shopify-Shop-Domain myshopify.com domain. Untrusted until the HMAC matches.
X-Shopify-API-Version Version used to serialize the payload.
X-Shopify-Webhook-Id Unique per delivery. Dedupe retries with this.
X-Shopify-Triggered-At When Shopify triggered the delivery.
X-Shopify-Event-Id Shared by deliveries from the same merchant action.

Manual check:

import { createHmac, timingSafeEqual } from "node:crypto";

export function shopifyWebhookValid(secret, rawBody, header) {
  const digest = createHmac("sha256", secret).update(rawBody).digest();
  const received = Buffer.from(String(header), "base64");
  if (digest.length !== received.length) {
    return false;
  }
  return timingSafeEqual(digest, received);
}

Compare the decoded digest bytes. A string compare of the Base64 text can disagree on padding. Reject when header is missing.

Session tokens

For an embedded admin request, validate the ID token's signature, expiry, audience, and destination. The destination shop is the shop you may call. A shop query parameter is not that proof.

App configuration

Prefer app-specific webhook subscriptions in shopify.app.toml when every install should receive the same topics. Use the Admin GraphQL webhookSubscriptionCreate mutation for a subscription that differs per shop. The webhook API version is the app's configured version in both cases.

Shop domain

Accept a shop domain only when it matches a pattern anchored at both ends, such as a single label plus .myshopify.com. Do not use a substring or a suffix check that allows an extra domain on the right.

Evaluation

Fixtures from evals/tasks.json. See evaluation fixtures for how the checks work.

verify-webhook

Verify a Shopify HTTPS webhook with the app client secret before reading the topic or shop domain.

Must include: X-Shopify-Hmac-Sha256, raw body

Must not include: express.json()

Worked example

Compute the HMAC over the raw body with the app client secret and compare it to X-Shopify-Hmac-Sha256.

Reject the request when the digest does not match. Read X-Shopify-Topic only after that check.

dedupe-delivery

Process a retried Shopify order webhook once using the delivery id Shopify sends.

Must include: X-Shopify-Webhook-Id

Must not include: shop query parameter

Worked example

Store X-Shopify-Webhook-Id after the HMAC matches.

If that id is already in the inbox, return 200 and do not create another order record. Authorize the handler with the verified HMAC, not with an unverified shop field.