Shopify is the commerce platform behind millions of stores. This guide uses the community shopify-api-node SDK (maintained by MONEI) to call the Shopify Admin API from a DaloyJS plugin, and shows how to verify Shopify webhooks against the raw request body.
REST is deprecated: prefer GraphQL
Shopify froze the REST Admin API at version 2024-04 and requires new public apps to use the GraphQL Admin API. The REST resources on shopify-api-node (shopify.product.list, shopify.order.create, …) still work against older API versions, but you should write new code against the SDK's shopify.graphql() method. Every example below uses GraphQL.
Pin your apiVersion to a current stable release (Shopify ships a new version every quarter, supports each for 12 months, and lists them on the versioning page). The default in shopify-api-node is the oldest supported stable version, which is usually not what you want.
1. Provision a custom app
In the Shopify admin, open Settings → Apps and sales channels → Develop apps, then create a custom app. The legacy “private apps” flow (API key + password) was removed back in January 2022, don't use the apiKey / password options on the SDK.
Pick the Admin API access scopes your integration actually needs (for example read_products, write_orders). Stay minimal, you can always grant more later.
Install the app on the store. Copy the Admin API access token (starts with shpat_). This is the only credential the SDK needs.
2. Install
ts
pnpm add shopify-api-node
TypeScript users: the package ships its own typings (under types/ in the repo). No @types/shopify-api-node install needed.
The webhook secret is shown when you create a webhook subscription (either in the admin or via GraphQL webhookSubscriptionCreate). Update SHOPIFY_API_VERSION to the latest stable on each Shopify release.
GraphQL pagination uses opaque cursors (endCursor) rather than the RESTnextPageParameters helper. Forward the cursor as a query string to your client.
6. Receive and verify webhooks
Webhook verification
ShopifyDaloyJS routeYour worker
01requestShopifyDaloyJS routePOST /webhooks/shopifyX-Shopify-Hmac-Sha256 over raw body
02noteDaloyJS routeDaloyJS routeCompare HMAC-SHA256 with timingSafeEqualverifyWebhook(headers, rawBody)
03responseDaloyJS routeShopify401 when the signature does not match{ error: 'invalid signature' }
04asyncDaloyJS routeYour workerDedupe on X-Shopify-Webhook-Id, then enqueueack 200 within ~5s
Hash the raw bytes before JSON.parse, reject bad signatures with 401, dedupe on the webhook id, then ack fast and do the heavy work in a background job.
Shopify signs every webhook with X-Shopify-Hmac-Sha256 over the raw body. Skip JSON parsing until the signature matches, and dedupe on X-Shopify-Webhook-Idso retries don't double-process. Use the raw-body helper to get the bytes:
ts
import { z } from "zod";import { readRawBody } from "@daloyjs/core/raw";app.route({ method: "POST", path: "/webhooks/shopify", operationId: "shopifyWebhook", // No body schema - we hash bytes before parsing. responses: { 200: { description: "ack", body: z.object({ ok: z.literal(true) }) }, 401: { description: "bad signature", body: z.object({ error: z.string() }) }, }, handler: async ({ request, state }) => { const raw = await readRawBody(request); const result = state.shopify.verifyWebhook(request.headers, raw); if (!result.ok) { return { status: 401, body: { error: "invalid signature" } }; } // Dedupe before any side effect. if (result.eventId && (await seen(result.eventId))) { return { status: 200, body: { ok: true as const } }; } const payload = JSON.parse(raw.toString("utf8")); switch (result.topic) { case "orders/create": await onOrderCreated(payload); break; case "orders/paid": await onOrderPaid(payload); break; case "app/uninstalled": await onAppUninstalled(result.shop, payload); break; // ... } return { status: 200, body: { ok: true as const } }; },});
Shopify expects a 2xx within ~5 seconds, retries with exponential back-off for up to 48 hours, and disables the subscription after 19 consecutive failures. Do the heavy work in a background job and ack fast.
Rate limits
The GraphQL Admin API uses a calculated cost & leaky-bucket model. Use the maxRetries option (above) so the SDK respects the throttled-cost information that comes back on 429 responses. autoLimit only works for the REST API and only inside a single Node process, skip it for GraphQL or multi-instance deployments and rely on retries instead.
Runtimes
shopify-api-node is built on got v11, which depends on Node's HTTPS module. It runs fine on Node, Bun, AWS Lambda, and any long-running container, but it is not drop-in compatible with Cloudflare Workers. On those runtimes, call the Admin GraphQL endpoint directly with fetch:
Shopify also publishes the official @shopify/shopify-api library, which adds OAuth for public/embedded apps and a built-in webhook registry. Reach for it when you're building a Shopify App Store listing; reach for shopify-api-nodewhen you're building a server-side integration for a single store and want a smaller surface.