Optionally verify that a webhook post genuinely came from SharpSports and was not modified in transit.
Purpose
Your webhook endpoint is a publicly reachable url, so anyone who discovers it could post data to it that looks like it came from SharpSports. HMAC verification lets you confirm two things before you act on a webhook:
- Authenticity — the post was sent by SharpSports, because only SharpSports and you know the shared secret.
- Integrity — the body was not modified in transit, because the signature is computed over the exact bytes we sent.
HMAC verification is optional. Webhooks are delivered over HTTPS and will work without it. We recommend implementing it for any production integration.
How it works
Every webhook we post includes two headers:
| Header | Description |
|---|---|
Hook-HMAC | The base64 encoded HMAC-SHA256 signature of the raw request body. |
Hook-Subscription | The uuid of the subscription that produced this post. Use it to look up the right secret when you have more than one subscription. |
The signature is computed as:
base64( HMAC-SHA256( hmac_secret, raw_request_body ) )
Where hmac_secret is the secret for that subscription. Retrieve it from the list subscriptions endpoint, which also returns hmac_digest — currently always sha256.
To verify a post, recompute the signature yourself and compare it to the Hook-HMAC header. If they match, the post is genuine.
You must sign the raw request body exactly as received. Parsing the JSON and re-serializing it will change the bytes (key order, whitespace) and produce a different signature. Most frameworks require an explicit option to retain the raw body — for exampleexpress.json({ verify: (req, res, buf) => { req.rawBody = buf } })or NestJS'srawBody: true.
Example
const crypto = require('crypto');
function verifyWebhook(rawBody, hmacHeader, hmacSecret) {
// rawBody must be the exact bytes received (a Buffer), not a re-serialized object
const computed = crypto
.createHmac('sha256', hmacSecret)
.update(rawBody)
.digest('base64');
const a = Buffer.from(computed);
const b = Buffer.from(hmacHeader || '');
// lengths must match before timingSafeEqual, and a constant time
// comparison avoids leaking the signature through response timing
return a.length === b.length && crypto.timingSafeEqual(a, b);
}Wired up in Express:
const express = require('express');
const app = express();
// retain the raw body so the signature can be verified
app.use(express.json({
verify: (req, res, buf) => { req.rawBody = buf; },
}));
app.post('/webhooks/sharpsports', (req, res) => {
const isValid = verifyWebhook(
req.rawBody,
req.get('Hook-HMAC'),
process.env.SHARPSPORTS_HMAC_SECRET,
);
if (!isValid) return res.sendStatus(401);
// signature verified - safe to process req.body
console.log(req.body.event);
return res.sendStatus(200);
});
Treathmac_secretas a credential. Store it in your secret manager or environment, never in client side code or version control.