DEV Community

Cover image for Reliable Shopify Webhooks: Idempotency, Retries, and Signature Verification
Sumeet Shroff Freelancer
Sumeet Shroff Freelancer

Posted on • Originally published at prateeksha.com

Reliable Shopify Webhooks: Idempotency, Retries, and Signature Verification

Quick summary

  • Verify Shopify webhook HMACs before doing any work.
  • Acknowledge requests quickly and process asynchronously via queues.
  • Use idempotency keys, retry/backoff strategies, and a dead-letter queue (DLQ) to avoid duplicates and data loss.

Why webhook reliability matters

Webhooks drive critical flows: inventory updates, order imports, payment confirmations, and fulfillment. If your receiver drops, delays, or double-processes events you can get missed shipments, duplicate charges, or inconsistent data across systems.

Reliable handling is a combination of:

  • Authenticating the source (HMAC verification).
  • Fast acknowledgement (reduce retries from Shopify).
  • Safe asynchronous processing with idempotency and retries.
  • Observability and operational controls for failures.

Tip: Keep the HTTP response fast (ideally <500ms) and move heavy work to a worker queue.

Signature verification (authenticate the source)

Shopify signs webhooks using HMAC-SHA256. Always validate the signature before enqueuing or processing the payload. Use the raw request body and a timing-safe comparison to prevent replay and spoofing attacks.

Example (Node.js / Express):

// verify-signature.js
const crypto = require('crypto');

function verifyShopifyWebhook(req, shopifySecret) {
  const hmacHeader = req.get('X-Shopify-Hmac-Sha256');
  const rawBody = req.rawBody || Buffer.from(JSON.stringify(req.body));
  const hmac = crypto.createHmac('sha256', shopifySecret).update(rawBody).digest('base64');
  return crypto.timingSafeEqual(Buffer.from(hmac), Buffer.from(hmacHeader));
}

module.exports = { verifyShopifyWebhook };
Enter fullscreen mode Exit fullscreen mode

Notes:

  • Compute HMAC with your app secret against the raw body.
  • Use timing-safe comparisons (e.g., crypto.timingSafeEqual) to avoid leaking verification timing.
  • Reject or log invalid signatures; don’t enqueue them.

Basic webhook receiver pattern

A robust receiver follows a consistent pattern:

  1. Capture raw body and relevant headers.
  2. Verify HMAC signature.
  3. Enqueue the event and return 200 OK quickly.
  4. Process the job asynchronously with idempotency checks and controlled retries.

Example Express route using a Redis-backed queue:

const express = require('express');
const bodyParser = require('body-parser');
const { verifyShopifyWebhook } = require('./verify-signature');
const Queue = require('bull');

const webhookQueue = new Queue('shopify-webhooks', 'redis://127.0.0.1:6379');

const app = express();
app.use(bodyParser.json({ verify: (req, res, buf) => { req.rawBody = buf; } }));

app.post('/webhooks/shopify', async (req, res) => {
  const secret = process.env.SHOPIFY_SECRET;
  if (!verifyShopifyWebhook(req, secret)) {
    return res.status(401).send('Invalid signature');
  }

  // Enqueue quickly
  await webhookQueue.add(req.body, { attempts: 1 });
  res.status(200).send('OK');
});
Enter fullscreen mode Exit fullscreen mode

This separates the HTTP surface (which must be reliable and fast) from the processing surface (which can scale and retry independently).

Idempotency keys and deduplication

Shopify will retry deliveries on transient failures, and networks can duplicate requests. Deduplicate by computing a stable idempotency key for each webhook.

Common key sources:

  • Shopify event ID if present.
  • Resource ID combined with a timestamp or version.
  • A deterministic hash of relevant headers + body.

Pattern:

  • Compute an idempotency key like shopify:{shopId}:webhook:{event_id} or hash(payload).
  • Attempt to claim that key in a fast store (Redis) before processing.
  • If the claim succeeds, proceed and set a TTL (24–72 hours).
  • If the key exists, skip processing and acknowledge success.

Node.js sample:

// idempotency.js
const Redis = require('ioredis');
const redis = new Redis(process.env.REDIS_URL);

async function claimIdempotency(key, ttlSeconds = 86400) {
  const claimed = await redis.set(key, 'processing', 'NX', 'EX', ttlSeconds);
  return claimed === 'OK'; // true if key was set (not present before)
}

async function markDone(key) {
  await redis.set(key, 'done', 'EX', 86400);
}

module.exports = { claimIdempotency, markDone };
Enter fullscreen mode Exit fullscreen mode

Worker-side check:

queue.process(async (job) => {
  const idKey = `shopify:webhook:${job.data.id}`; // choose stable identifier
  const claimed = await claimIdempotency(idKey);
  if (!claimed) return Promise.resolve(); // already processed

  try {
    // do work
    await handleWebhook(job.data);
    await markDone(idKey);
  } catch (err) {
    throw err; // let queue retry or move to DLQ
  }
});
Enter fullscreen mode Exit fullscreen mode

This prevents double-processing when retries arrive while the original job is pending or being retried.

Retries, backoff, and dead-letter patterns

Don’t rely solely on Shopify’s retry schedule for long-running business work. Use your queue’s retry/backoff and DLQ features.

Choose a retry strategy based on failure type:

  • Immediate retries: for tiny transient errors.
  • Exponential backoff: for rate-limited third-party APIs.
  • Rate-limited workers: when you must guarantee throughput to downstream systems.
  • Dead-letter queue: when repeated attempts fail and manual intervention is needed.

Implementation tips:

  • Configure attempts and backoff per job type.
  • After N attempts, move jobs to a DLQ with error metadata.
  • Record last error, attempt count, and origin so operators can triage.

Warning: Never delete failing webhook events silently — push them to DLQ and alert operators.

Observability and monitoring

Track:

  • Webhook endpoint 4xx/5xx rates.
  • Acknowledgement latency.
  • Worker processing latency and success rate.
  • DLQ size and age.

Log structured events (shop ID, webhook ID, attempts) and add traces for workflows spanning services. Alerts should trigger on spikes in 4xx/5xx, DLQ growth, or increased processing lag.

Real-world scenarios

  • Payment duplicates: Teams used a request-hash idempotency key in Redis to avoid a double-capture after a transient 500. DLQ captured the failed item for replay.
  • Order spike: During flash sales, enqueueing plus autoscaled workers and exponential backoff prevented downstream API bans.
  • Key rotation mishap: A staging secret deployed to production caused verification failures. Alerts surfaced the issue quickly; rollback and a DLQ replay fixed missed orders.

Production checklist

  • Verify HMAC signatures with timing-safe comparisons.
  • Respond 200 quickly and enqueue heavy work.
  • Claim idempotency keys in a fast store before processing.
  • Configure queue retries, backoff, and DLQ handling.
  • Log structured events and instrument tracing.
  • Add alerts for endpoint error rates and DLQ growth.
  • Build a safe replay tool for DLQ items.
  • Rotate secrets securely and support multi-key verification during transitions.

Storage comparison for idempotency state

  • Redis: Low latency, TTL support, ideal for short-lived claims.
  • Postgres: Durable and auditable, better for long-term records and complex queries.
  • S3/Blob: High latency, used for mapping large payloads or archival references.

Use Redis for quick dedup checks and a DB for audit trails if needed.

Testing and operational scripts

  • Simulate retries, network errors, and signature failures in staging.
  • Test multi-key verification for secret rotations.
  • Provide a replay CLI/UI that re-validates signatures, rate-limits replays, and requires manual approval.

Short conclusion and CTA

Reliable Shopify webhook handling combines source verification, fast acknowledgement, idempotency, controlled retries, and clear operational tooling. Apply these patterns to reduce duplicates, avoid data loss, and make webhook failures visible and actionable.

Want help implementing a resilient webhook pipeline or reviewing your current setup? Contact us to audit or build a production-ready receiver.

Home: https://prateeksha.com
Blog: https://prateeksha.com/blog
Canonical: https://prateeksha.com/blog/shopify-webhooks-idempotency-signature-verification

Top comments (0)