From shopify-pack
Registers Shopify webhooks via GraphQL, configures GDPR subscriptions in shopify.app.toml, and implements handlers with HMAC verification for events and App Store compliance.
npx claudepluginhub jeremylongshore/claude-code-plugins-plus-skills --plugin shopify-packThis skill is limited to using the following tools:
Register webhooks via GraphQL, handle events with HMAC verification, and implement the mandatory GDPR compliance webhooks required for Shopify App Store submission.
Guides Next.js Cache Components and Partial Prerendering (PPR) with cacheComponents enabled. Implements 'use cache', cacheLife(), cacheTag(), revalidateTag(), static/dynamic optimization, and cache debugging.
Guides building MCP servers enabling LLMs to interact with external services via tools. Covers best practices, TypeScript/Node (MCP SDK), Python (FastMCP).
Generates original PNG/PDF visual art via design philosophy manifestos for posters, graphics, and static designs on user request.
Register webhooks via GraphQL, handle events with HMAC verification, and implement the mandatory GDPR compliance webhooks required for Shopify App Store submission.
shopify app dev tunnel for local)// Register a webhook subscription
const REGISTER_WEBHOOK = `
mutation webhookSubscriptionCreate($topic: WebhookSubscriptionTopic!, $webhookSubscription: WebhookSubscriptionInput!) {
webhookSubscriptionCreate(topic: $topic, webhookSubscription: $webhookSubscription) {
webhookSubscription {
id
topic
endpoint {
... on WebhookHttpEndpoint {
callbackUrl
}
}
format
}
userErrors { field message }
}
}
`;
// Common webhook topics
const topics = [
"ORDERS_CREATE",
"ORDERS_UPDATED",
"ORDERS_PAID",
"ORDERS_FULFILLED",
"PRODUCTS_CREATE",
"PRODUCTS_UPDATE",
"PRODUCTS_DELETE",
"CUSTOMERS_CREATE",
"CUSTOMERS_UPDATE",
"APP_UNINSTALLED",
"INVENTORY_LEVELS_UPDATE",
];
for (const topic of topics) {
await client.request(REGISTER_WEBHOOK, {
variables: {
topic,
webhookSubscription: {
callbackUrl: "https://your-app.example.com/webhooks",
format: "JSON",
},
},
});
}
Required for App Store submission. These are configured in shopify.app.toml, not via API:
# shopify.app.toml
[webhooks]
api_version = "2024-10"
# MANDATORY: customers/data_request
[[webhooks.subscriptions]]
topics = ["customers/data_request"]
uri = "/webhooks/gdpr/data-request"
# MANDATORY: customers/redact
[[webhooks.subscriptions]]
topics = ["customers/redact"]
uri = "/webhooks/gdpr/customers-redact"
# MANDATORY: shop/redact
[[webhooks.subscriptions]]
topics = ["shop/redact"]
uri = "/webhooks/gdpr/shop-redact"
// Mandatory GDPR handlers — your app will be REJECTED without these
// 1. Customer Data Request — merchant forwards customer's data request
app.post("/webhooks/gdpr/data-request", rawBodyParser, async (req, res) => {
if (!verifyShopifyWebhook(req.body, req.headers["x-shopify-hmac-sha256"]!, SECRET)) {
return res.status(401).send("Unauthorized");
}
const payload = JSON.parse(req.body.toString());
// payload shape:
// {
// "shop_id": 12345,
// "shop_domain": "store.myshopify.com",
// "orders_requested": [123, 456],
// "customer": { "id": 789, "email": "customer@example.com", "phone": "+1234567890" },
// "data_request": { "id": 101112 }
// }
// Collect all data you have for this customer
const customerData = await collectCustomerData(payload.customer.id);
await sendDataToMerchant(payload.shop_domain, customerData);
res.status(200).send("OK");
});
// 2. Customer Redact — delete customer's personal data
app.post("/webhooks/gdpr/customers-redact", rawBodyParser, async (req, res) => {
if (!verifyShopifyWebhook(req.body, req.headers["x-shopify-hmac-sha256"]!, SECRET)) {
return res.status(401).send("Unauthorized");
}
const payload = JSON.parse(req.body.toString());
// payload shape:
// {
// "shop_id": 12345,
// "shop_domain": "store.myshopify.com",
// "customer": { "id": 789, "email": "customer@example.com", "phone": "+1234567890" },
// "orders_to_redact": [123, 456]
// }
await deleteCustomerData(payload.customer.id);
await deleteOrderData(payload.orders_to_redact);
res.status(200).send("OK");
});
// 3. Shop Redact — 48 hours after app uninstall, delete ALL shop data
app.post("/webhooks/gdpr/shop-redact", rawBodyParser, async (req, res) => {
if (!verifyShopifyWebhook(req.body, req.headers["x-shopify-hmac-sha256"]!, SECRET)) {
return res.status(401).send("Unauthorized");
}
const payload = JSON.parse(req.body.toString());
// { "shop_id": 12345, "shop_domain": "store.myshopify.com" }
await deleteAllShopData(payload.shop_id);
res.status(200).send("OK");
});
import crypto from "crypto";
import express from "express";
type WebhookTopic =
| "orders/create"
| "orders/updated"
| "orders/paid"
| "products/create"
| "products/update"
| "products/delete"
| "app/uninstalled"
| "inventory_levels/update";
const handlers: Record<WebhookTopic, (shop: string, payload: any) => Promise<void>> = {
"orders/create": async (shop, payload) => {
console.log(`New order ${payload.name} from ${shop}: $${payload.total_price}`);
// payload has: id, name, email, total_price, line_items[], shipping_address, etc.
},
"orders/paid": async (shop, payload) => {
console.log(`Order ${payload.name} paid: ${payload.financial_status}`);
},
"products/update": async (shop, payload) => {
console.log(`Product updated: ${payload.title} (${payload.id})`);
// Sync to your catalog
},
"products/delete": async (shop, payload) => {
console.log(`Product deleted: ${payload.id}`);
// Remove from your catalog
},
"app/uninstalled": async (shop, payload) => {
console.log(`App uninstalled from ${shop}`);
// Clean up session, disable features, prepare for shop/redact
},
};
app.post(
"/webhooks",
express.raw({ type: "application/json" }),
async (req, res) => {
const hmac = req.headers["x-shopify-hmac-sha256"] as string;
const topic = req.headers["x-shopify-topic"] as WebhookTopic;
const shop = req.headers["x-shopify-shop-domain"] as string;
if (!verifyShopifyWebhook(req.body, hmac, process.env.SHOPIFY_API_SECRET!)) {
return res.status(401).send("Invalid HMAC");
}
// Respond immediately — Shopify requires 200 within 5 seconds
res.status(200).send("OK");
// Process asynchronously
const payload = JSON.parse(req.body.toString());
const handler = handlers[topic];
if (handler) {
try {
await handler(shop, payload);
} catch (err) {
console.error(`Webhook handler failed for ${topic}:`, err);
// Shopify will retry failed webhooks (no 200 response)
}
} else {
console.log(`No handler for topic: ${topic}`);
}
}
);
// Query all webhook subscriptions
const LIST_WEBHOOKS = `{
webhookSubscriptions(first: 50) {
edges {
node {
id
topic
endpoint {
... on WebhookHttpEndpoint { callbackUrl }
}
format
createdAt
}
}
}
}`;
// Delete a webhook
const DELETE_WEBHOOK = `
mutation webhookSubscriptionDelete($id: ID!) {
webhookSubscriptionDelete(id: $id) {
deletedWebhookSubscriptionId
userErrors { field message }
}
}
`;
| Issue | Cause | Solution |
|---|---|---|
| Webhook delivery fails | Endpoint not reachable | Ensure HTTPS, check tunnel is running |
| HMAC validation fails | Wrong API secret | Verify SHOPIFY_API_SECRET in Partner Dashboard |
| Webhook not received | Topic not registered | Check webhookSubscriptions query |
| App Store rejection | Missing GDPR webhooks | Implement all 3 mandatory handlers |
| Duplicate events | Shopify retries on timeout | Add idempotency with webhook ID tracking |
| Timeout errors | Handler takes > 5 seconds | Respond 200 immediately, process async |
# Use Shopify CLI to trigger test webhooks
shopify app webhook trigger --topic orders/create --address http://localhost:3000/webhooks
# Or use curl with a test payload
curl -X POST http://localhost:3000/webhooks \
-H "Content-Type: application/json" \
-H "X-Shopify-Topic: orders/create" \
-H "X-Shopify-Shop-Domain: test.myshopify.com" \
-H "X-Shopify-Hmac-Sha256: $(echo -n '{"test":true}' | openssl dgst -sha256 -hmac "$SHOPIFY_API_SECRET" -binary | base64)" \
-d '{"test":true}'
For performance optimization, see shopify-performance-tuning.