Security
Authentication
Two directions, two different credentials. Never use one in place of the other.
Direction | Credential | Carried in |
|---|---|---|
You → BLOY (manage webhook subscriptions) | Public API key |
|
BLOY → you (event deliveries) | Subscription signing secret |
|
Calling the BLOY API
Authorization: Bearer YOUR_PUBLIC_API_KEYYou will find the key in the BLOY app in your Shopify admin; contact BLOY support if you cannot locate it. Each of these returns 403 Forbidden: header missing or malformed, key not matching a shop, or a plan that does not include the Webhook API.
Signing secrets
Each subscription gets its own 256-bit signing secret (64 hex characters). BLOY signs every delivery to that endpoint with it, so you can prove a request came from BLOY and was not altered in transit. Secrets are per-subscription: one leaked endpoint does not affect the others, and each rotates independently.
The secret is returned once — as signingSecret from Create Subscription and Rotate Signing Secret. Nothing reveals it afterwards and List Subscriptions never returns it, so put it in a secret manager or environment variable straight away. Lose it and the only way back is to rotate. Never commit it, log it, or send it to a browser.
Verifying a delivery
expected = hex( HMAC_SHA256(signing_secret, raw_request_body) )Compare expected with the X-Bloy-Hmac-Sha256 header using a constant-time comparison, and reject with 401 if they differ.
Use the raw body. The signature covers the exact bytes BLOY sent. A framework that parses the JSON and re-serialises it reorders keys or changes whitespace, and verification fails. Capture the body before any body-parsing middleware touches it — this is the most common cause of verification failures.
const crypto = require('crypto');
function verify(rawBody, headerSignature, secret) {
const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
const a = Buffer.from(expected, 'utf8');
const b = Buffer.from(headerSignature || '', 'utf8');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
Other languages need the same two primitives: hash_hmac + hash_equals in PHP, hmac.new(...).hexdigest() + hmac.compare_digest in Python.
Replay protection The signature covers the body only — it carries no timestamp, so a captured request stays valid indefinitely if someone can replay it at your endpoint. Reject event ids you have already processed; the idempotency record described in Handle duplicate events closes this gap. Serve your endpoint over HTTPS so requests cannot be captured in the first place.
Rotate your signing secret
Rotate periodically, and immediately if you suspect a leak. Call Rotate Signing Secret: BLOY issues a new secret, returns it once.
The grace period is exactly the window in which a leaked secret still produces valid signatures, so pick the shortest one you can deploy within. 24 is the ceiling; longer is not accepted.
When there is a grace period, every delivery in it carries two signatures — X-Bloy-Hmac-Sha256 from the new secret, X-Bloy-Hmac-Sha256-Previous from the old one — so you can deploy without dropping events. Accept either while you migrate:
const ok = [req.get('X-Bloy-Hmac-Sha256'), req.get('X-Bloy-Hmac-Sha256-Previous')]
.filter(Boolean)
.some((sig) => verify(req.body, sig, SIGNING_SECRET));
Rotate, deploy the new secret, confirm deliveries verify against X-Bloy-Hmac-Sha256, then drop the fallback once previousSecretValidUntil has passed. Rotating again before the window closes makes the secret you just replaced the new "previous" one and restarts the 24 hours — only ever two secrets are valid at a time.
Updated on: 27/08/2026
Thank you!
