> ## Knowledge Base Index
> Fetch the complete knowledge base index at: https://docs.bloy.io/sitemap.xml
> Use this file to discover available pages before exploring further.
> Pure-Markdown content can be obtained by appending a '.md' suffix to the content URLs listed in the sitemap (without the trailing slash).

# 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 | `Authorization` header **you** send |
| BLOY → you (event deliveries) | Subscription signing secret | `X-Bloy-Hmac-Sha256` header **BLOY** sends |

###### Calling the BLOY API

```
Authorization: Bearer YOUR_PUBLIC_API_KEY
```

You 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](https://docs.bloy.io/en/article/api-reference-1x15lzo/#2-create-subscription) and [Rotate Signing Secret](https://docs.bloy.io/en/article/api-reference-1x15lzo/#2-rotate-signing-secret). Nothing reveals it afterwards and [List Subscriptions](https://docs.bloy.io/en/article/api-reference-1x15lzo/#2-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](https://docs.bloy.io/en/article/security-bdi9vq/#2-rotate-your-signing-secret). 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](https://file+.vscode-resource.vscode-cdn.net/home/cuongcx/Desktop/project/bloy/bloy_loyalty/shopify-app-loyalty-api/docs/webhook-api/delivery.md#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](https://file+.vscode-resource.vscode-cdn.net/home/cuongcx/Desktop/project/bloy/bloy_loyalty/shopify-app-loyalty-api/docs/webhook-api/api-reference.md#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.