Integrations

Shopify webhooks

Point Shopify's webhooks at a Hookie endpoint: Hookie checks X-Shopify-Hmac-Sha256 before anything is stored, files orders by topic, and forwards them to your service signed, retried and replayable.

Build this freeNo credit card · Free plan included

Set it up

  1. Create an endpoint. In the console, open a project and add a webhook endpoint (or create it over the API, the CLI or MCP). Copy its URL, https://app.hookie.ai/{workspace}/{project}/{webhook}.
  2. Point Shopify at it. In Shopify admin → Settings → Notifications → Webhooks → Create webhook (format JSON), or your app's webhook subscriptions, paste the URL and pick the events you want, for example orders/create, orders/paid, customers/create, app/uninstalled.
  3. Turn on signature verification. Edit the endpoint, set Verification to Shopify, and paste your app's client secret, or the signing secret Shopify shows under Settings → Notifications → Webhooks. From then on a request without a valid X-Shopify-Hmac-Sha256 gets a 401 and nothing is stored or counted.
  4. Forward it to your service. Add a destination: any HTTPS URL, Slack, S3-compatible storage, SQS or Pub/Sub. Each delivery is signed, retried for about a day, and replayable.

Suggested routing

Each endpoint files what it receives into one dataset, through its own criteria. The Shopify preset suggests these; leave the criteria empty to keep every event in one dataset instead.

EndpointCriteriaDataset
Paid ordersfinancial_status = paidshopify_orders

What Hookie checks

Shopify sends the base64 HMAC-SHA256 of the raw body, keyed with the secret. Hookie recomputes it and compares in constant time. Shopify signs no timestamp, so there is no age check.

An event the criteria do not match is still answered 201 and kept as a submission, marked filtered, so you can see what was turned away and why.

Verify what Hookie forwards

Your service should check Hookie's signature the same way Hookie checks Shopify's. Every delivery to an HTTPS destination carries Hookie-Signature, keyed with that destination's signing secret (whsec_…, which an owner or admin can reveal on the destination). It also carries the Standard Webhooks headers, so any Standard Webhooks library verifies it too.

import crypto from "node:crypto";

// Hookie-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>">
// keyed with your destination's signing secret (whsec_...). During a secret
// rotation it carries one v1 per secret, so accept the request if ANY matches.
export function verifyHookie(rawBody, header, secret, toleranceSeconds = 300) {
  if (!header) return false;
  const parts = header.split(",").map((p) => p.trim().split("="));
  const t = parts.find(([k]) => k === "t")?.[1];
  const signatures = parts.filter(([k]) => k === "v1").map(([, v]) => v);
  if (!t || !signatures.length) return false;
  if (Math.abs(Date.now() / 1000 - Number(t)) > toleranceSeconds) return false;
  const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  return signatures.some((s) => s.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(s), Buffer.from(expected)));
}

Wired into an Express receiver, with the raw body the signature covers:

import express from "express";
import { verifyHookie } from "./verify-hookie.js";

const app = express();

// The RAW body: a signature is over the exact bytes sent, not re-serialised JSON.
app.post("/hooks/hookie", express.raw({ type: "*/*" }), (req, res) => {
  const body = req.body.toString("utf8");
  if (!verifyHookie(body, req.get("Hookie-Signature"), process.env.HOOKIE_SIGNING_SECRET)) {
    return res.sendStatus(401);
  }
  const event = JSON.parse(body);
  // Retries repeat Hookie-Delivery-Id; dedupe the same event on Hookie-Event-Id.
  console.log(req.get("Hookie-Event-Id"), event);
  res.sendStatus(200);
});

app.listen(3000);

Check Shopify's signature yourself

Hookie does this for you on ingest. If you also receive Shopify directly somewhere else, this is the same check in plain Node, with no dependencies:

import crypto from "node:crypto";

// X-Shopify-Hmac-Sha256: <base64 HMAC-SHA256 of the raw body>
export function verifyShopify(rawBody, header, secret) {
  if (!header) return false;
  const expected = crypto.createHmac("sha256", secret).update(rawBody).digest("base64");
  const given = Buffer.from(header.trim());
  return given.length === expected.length && crypto.timingSafeEqual(given, Buffer.from(expected));
}

Next: the quickstart, pricing, or how Hookie compares with Hookdeck, Svix and Formspree. Create a free endpoint and send Shopify a test event.