Shopify Apps skill for Codex

Admin GraphQL, verified webhooks, safe deploys. Codex builds Shopify apps the way Shopify expects in 2025-2026: configuration in shopify.app.toml, managed installation with session tokens, and Admin GraphQL on a pinned quarterly version. Every mutation checks userErrors, syncs respect cost-based throttling or switch to bulk operations, and webhooks are HMAC-verified on raw bytes with compliance topics in place. Deploys and billing changes wait for explicit approval.

  • Integrations
  • $shopify-apps
  • 199 lines
  • 3 files
  • ~2,150 tokens
  • MIT
  • Updated
  • Editor's pick

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- shopify-apps

Installs to ~/.agents/skills/shopify-apps/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $shopify-apps, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Builds and maintains Shopify apps with Shopify CLI and the React Router app template - shopify.app.toml config, managed installation and session-token auth, Admin GraphQL API queries and mutations with userErrors and cost-based throttling, bulk operations, webhooks with HMAC verification and mandatory compliance topics, and app extensions (Functions, checkout UI, theme app extensions). Use when asked to build a Shopify app, sync products, orders or inventory, add or debug Shopify webhooks, migrate off the REST Admin API, or write a Shopify Function or checkout extension.

Use it when

  • You are building or maintaining a Shopify app with Shopify CLI
  • Products, orders, inventory or metafields must sync through the Admin GraphQL API
  • Shopify webhooks need verification, deduplication or compliance handling
  • You need a Shopify Function, checkout UI extension or theme app extension

Use something else when

How to check the result

Run shopify app dev against a dev store, trigger each subscribed webhook topic and confirm HMAC passes, and log throttleStatus during a sync to confirm no THROTTLED errors.

The mistake it prevents

Treating a 200 response from a GraphQL mutation as success without reading userErrors, so failed writes go unnoticed.

~/.agents/skills/shopify-apps/SKILL.md

--- name: shopify-apps description: Builds and maintains Shopify apps with Shopify CLI and the React Router app template - shopify.app.toml config, managed installation and… license: MIT ---

Done well means: the app is configured as code in shopify.app.toml, talks to a pinned Admin GraphQL API version, checks userErrors on every mutation, respects query cost, verifies every webhook HMAC against the raw body, implements the three compliance webhooks, and never deploys or charges merchants without explicit confirmation.

Inspect first

- [ ] Read AGENTS.md, package.json and shopify.app.toml (client_id, scopes, webhooks, api_version)
- [ ] Note the template (React Router is current; older apps use Remix) and the session storage
- [ ] Check for REST Admin calls (/admin/api/<v>/*.json): new public apps must use GraphQL
- [ ] Find every webhook handler and confirm HMAC verification happens before parsing
- [ ] Confirm which store is the dev store; never point experiments at a live merchant store

Shopify CLI needs network and a Partner/Dev Dashboard login; in an offline Codex sandbox, prepare the changes and ask the user to run CLI commands.

CLI loop

shopify app init                       # scaffold (choose the React Router template)
shopify app dev                        # tunnel + dev store install + hot reload
shopify app generate extension         # Functions, checkout UI, theme app extension...
shopify app function run               # run a Function locally against input JSON
shopify app deploy                     # creates and releases an app version: confirm first

shopify app deploy pushes config and extensions to every store that has the app. Treat it as a production release: show the user the diff of shopify.app.toml and extensions and get a clear yes.

Configuration as code

client_id = "..."
name = "Stock Sync"
application_url = "https://app.example.com"
embedded = true

[access_scopes]
scopes = "read_products,write_inventory,read_orders"

[webhooks]
api_version = "2025-10"

  [[webhooks.subscriptions]]
  topics = ["app/uninstalled", "app/scopes_update"]
  uri = "/webhooks/app"

  [[webhooks.subscriptions]]
  topics = ["products/update"]
  uri = "/webhooks/products"

  [[webhooks.subscriptions]]
  compliance_topics = ["customers/data_request", "customers/redact", "shop/redact"]
  uri = "/webhooks/compliance"
  • Scopes are least privilege; every extra scope is shown to the merchant and reviewed in app review. Customer and order fields with personal data need protected customer data access approved in the dashboard, or fields come back null/denied.
  • Managed installation: Shopify installs with the scopes in the TOML; embedded apps get a session token from App Bridge and exchange it for an access token (token exchange). The template’s authenticate.admin(request) does this.
  • API versions are quarterly (YYYY-MM) and supported for about 12 months. Pin one version for both GraphQL calls and webhooks; upgrade deliberately each quarter after reading the changelog.

Admin GraphQL

// app/routes/app.products.tsx (React Router template)
import { authenticate } from "../shopify.server";

export async function loader({ request }: { request: Request }) {
  const { admin } = await authenticate.admin(request);
  const res = await admin.graphql(
    `#graphql
    query Products($first: Int!, $after: String) {
      products(first: $first, after: $after, query: "status:active") {
        nodes { id title handle variants(first: 10) { nodes { id sku inventoryQuantity } } }
        pageInfo { hasNextPage endCursor }
      }
    }`,
    { variables: { first: 50, after: null } },
  );
  const { data } = await res.json();
  return data.products;
}

Mutations return HTTP 200 even when they fail. Always read userErrors:

const res = await admin.graphql(
  `#graphql
  mutation SetMeta($metafields: [MetafieldsSetInput!]!) {
    metafieldsSet(metafields: $metafields) {
      metafields { id key }
      userErrors { field message code }
    }
  }`,
  { variables: { metafields: [{ ownerId: productGid, namespace: "custom", key: "erp_id",
                                type: "single_line_text_field", value: "ERP-8812" }] } },
);
const { data } = await res.json();
if (data.metafieldsSet.userErrors.length) throw new Error(JSON.stringify(data.metafieldsSet.userErrors));
NeedUse
Create or fully sync a product with variantsproductSet (declarative; good for ERP syncs)
Add or edit variantsproductVariantsBulkCreate / productVariantsBulkUpdate
Set inventoryinventorySetQuantities (per location, with reason)
Custom dataMetafields (metafieldsSet, up to 25 per call) or metaobjects
Read thousands of recordsbulkOperationRunQuery, then download the JSONL from url
Write thousands of recordsbulkOperationRunMutation with a staged JSONL upload

Rate limits

GraphQL is cost-based: each query has a calculated cost and the shop’s bucket refills at a plan-dependent rate. Read extensions.cost.throttleStatus (currentlyAvailable, restoreRate) and sleep before the bucket empties. A single query cannot exceed 1,000 points; request smaller pages or fewer nested connections. On THROTTLED errors, wait (requested - available) / restoreRate seconds and retry.

IDs are global IDs (gid://shopify/Product/123). Money values are decimal strings with a currency; never parse them into floats for arithmetic.

Webhooks

import crypto from "node:crypto";

export function verifyShopifyHmac(rawBody: Buffer, hmacHeader: string, secret: string) {
  const digest = crypto.createHmac("sha256", secret).update(rawBody).digest("base64");
  const a = Buffer.from(digest), b = Buffer.from(hmacHeader ?? "");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

The template’s authenticate.webhook(request) verifies for you; custom servers must read the raw body before any JSON middleware. Rules:

  • Respond 200 within about 5 seconds; enqueue work. Shopify retries failed deliveries with backoff and removes subscriptions that keep failing.
  • Deduplicate by X-Shopify-Webhook-Id; ordering is not guaranteed, so compare updated_at before overwriting newer data.
  • Payload shape follows the webhook api_version; changing it is a breaking change for handlers.
  • Compliance topics are mandatory for public apps: answer data requests and actually delete data on customers/redact and shop/redact.
  • app/uninstalled: delete sessions and stop jobs for that shop.

To debug a failing signature from a captured delivery, run python3 scripts/verify_webhook_hmac.py body.json <hmac-header> <secret-env-var-name>; it reports whether the raw bytes verify and common causes when they do not. Trigger test events with shopify app webhook trigger --topic products/update --address <url>.

Extensions

GoalExtensionNote
Custom discounts, shipping/payment rules, cart validationShopify Functions (Rust or JS compiled to Wasm)Run on Shopify, strict instruction and size limits; test with shopify app function run
UI in checkout or thank-you/order status pagesCheckout UI extensionCheckout steps beyond thank-you/order status require Plus
Storefront widgetsTheme app extension (app blocks, app embeds)Replaces ScriptTag injection; merchants place blocks in the theme editor
Actions inside the adminAdmin UI extensions

Functions get only the data their input query requests; keep the query minimal to stay under limits.

Billing

Charges use appSubscriptionCreate or appPurchaseOneTimeCreate, returning a confirmationUrl the merchant must approve. Use test: true on dev stores. Changing plans or prices for live merchants needs explicit confirmation from the user.

Gotchas

  • Inventory lives per location (InventoryLevel); inventoryQuantity on variants is a total.
  • Product search query: syntax is eventually consistent; do not read-after-write through it.
  • Online vs offline tokens: background jobs need offline tokens; online tokens expire with the user session.
  • Embedded apps must not set cookies for auth in iframes; use session tokens.
  • Ignoring app/scopes_update leaves stored scopes stale after a merchant approves new ones.

Verification

  • shopify app dev on a dev store; exercise each route and mutation, confirm userErrors empty.
  • Trigger each subscribed webhook topic; confirm 200, HMAC verified, idempotent on replay.
  • Log throttleStatus during a bulk sync and confirm no THROTTLED errors at steady state.
  • Before deploy, list scopes and webhook topics in the TOML and get the user’s approval.

What the zip contains

Codex reads bundled files only when a step points to them. Every one is shown here, so you can review it before installing.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "Shopify Apps"
  short_description: "Admin GraphQL, verified webhooks, safe deploys"
  default_prompt: "$shopify-apps Build a Shopify app that syncs inventory from our ERP every 15 minutes"
scripts/verify_webhook_hmac.py67 lines, 2.4 KB
#!/usr/bin/env python3
"""Verify a captured Shopify webhook delivery against its X-Shopify-Hmac-Sha256 header.

Usage:
    python3 verify_webhook_hmac.py <raw-body-file> <hmac-header-value> <SECRET_ENV_VAR>

The secret is read from the named environment variable so it never appears in shell history.
The body file must contain the exact bytes Shopify sent (not re-serialised JSON).
Exit code 0 when the signature verifies, 1 when it does not, 2 on usage errors.
"""
import base64
import hashlib
import hmac
import json
import os
import sys


def digest(secret: str, body: bytes) -> str:
    return base64.b64encode(hmac.new(secret.encode(), body, hashlib.sha256).digest()).decode()


def main() -> int:
    if len(sys.argv) != 4:
        print(__doc__.strip())
        return 2
    path, header, env_name = sys.argv[1:]
    secret = os.environ.get(env_name)
    if not secret:
        print(f"error: environment variable {env_name} is empty or unset")
        return 2
    try:
        body = open(path, "rb").read()
    except OSError as exc:
        print(f"error: cannot read {path}: {exc}")
        return 2

    expected = digest(secret, body)
    if hmac.compare_digest(expected, header.strip()):
        print(f"OK: signature verifies for {len(body)} bytes")
        return 0

    print("FAIL: signature does not match")
    print(f"  body bytes: {len(body)}")
    hints = []
    if body.endswith(b"\n"):
        trimmed = body.rstrip(b"\r\n")
        if hmac.compare_digest(digest(secret, trimmed), header.strip()):
            hints.append("matches once the trailing newline is removed: the capture added a newline")
    try:
        reserialised = json.dumps(json.loads(body), separators=(",", ":")).encode()
        if reserialised != body:
            hints.append("make sure this file is the raw request body; a framework that parsed and "
                         "re-serialised the JSON changes whitespace and key order and breaks the HMAC")
    except ValueError:
        hints.append("body is not valid JSON: the capture may be truncated or decoded")
    if b"\r\n" in body:
        hints.append("body contains CRLF line endings: it was likely altered by a tool or editor")
    hints.append("check you are using the app's client secret (API secret key), not an access token")
    hints.append("if the app's secret was rotated recently, try the previous secret")
    for h in hints:
        print(f"  hint: {h}")
    return 1


if __name__ == "__main__":
    sys.exit(main())

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc
Just launched

Codex Skills Pro is live

79 premium skills for Codex, each with scripts, references and an evidence report that proves the result.

Bought one by one$882.21
All 79 skills$49
94% off

That's $0.62 a skill, paid once.

Get all 79 for $49Or pick single skills from $4.99 →

Secure checkout by Creem