Appearance
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:webhooksscope (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
- Verify webhook signatures - the playground + the canonical signer in every language.
- Cart → COD checkout - produce the
order.confirmedevent this recipe consumes.
In the API Reference
Open these operations in the interactive reference (with a Try it console):
POST /v1/webhooks- create a subscriptionPOST /v1/webhooks/{subscription_id}/ping- send a signed test delivery