Skip to content

Verify a webhook

React to events - order.confirmed, invoice.issued, stock.changed, order.shipped, product.updated - instead of polling. Every delivery is HMAC-signed; verify the signature over the exact bytes you received before you parse the body, so a forged request never reaches your handler.

What you need

  • A secret key with the storefront:webhooks scope (subscriptions are a privileged write).
  • A public HTTPS endpoint Harmon can POST to.

1. Subscribe

Create a subscription with your endpoint and the events you care about. The response reveals a signing_secret once - store it immediately; you can't read it back.

curl -s -X POST "https://api.harmon.example/v1/webhooks" \
  -H "Authorization: Bearer hk_test_sec_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "endpoint_url": "https://store.example.com/harmon/webhook",
    "event_types": ["order.confirmed", "invoice.issued", "stock.changed"]
  }'
# The response reveals "signing_secret" ONCE - store it now.

Test the wiring any time with POST /v1/webhooks/{subscription_id}/ping - it sends a signed test delivery to your endpoint.

2. Verify on receipt

Each delivery carries three headers:

X-Harmon-Event:     order.confirmed
X-Harmon-Delivery:  <uuid>
X-Harmon-Signature: sha256=<hex hmac>

X-Harmon-Signature is an HMAC-SHA256 of the exact request-body bytes, keyed by your signing_secret. Recompute it over the raw bytes - don't re-serialize the JSON first, or the bytes (and the HMAC) won't match - and reject on a mismatch before doing anything else.

import crypto from "node:crypto";
import express from "express";

const app = express();

// Capture the RAW bytes - do NOT let a JSON parser run first.
app.post(
  "/harmon/webhook",
  express.raw({ type: "*/*" }),
  (req, res) => {
    const raw = req.body; // Buffer of the exact received bytes
    const expected =
      "sha256=" +
      crypto.createHmac("sha256", SIGNING_SECRET).update(raw).digest("hex");
    const got = req.get("X-Harmon-Signature") ?? "";

    const a = Buffer.from(expected);
    const b = Buffer.from(got);
    if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
      return res.sendStatus(401); // not from Harmon - reject before parsing
    }

    const event = JSON.parse(raw.toString("utf8"));
    handle(req.get("X-Harmon-Event"), event);
    res.sendStatus(200);
  },
);

Verify before you parse

A request that fails verification is not from Harmon. Compute the expected signature and reject the delivery before you decode the body. Use a constant-time compare (timingSafeEqual / hmac.compare_digest / hash_equals) so you leak no timing oracle on the secret.

Want to experiment without writing a receiver? The Verify webhook signatures guide has an interactive playground: paste a secret and a body, compute the signature, then tamper a byte and watch verification flip to Invalid.

Next steps

In the API Reference

Open these operations in the interactive reference (with a Try it console):

Built on the Harmon platform — the storefront API for merchants.