Signature Verification
Every webhook delivery is signed with your webhook's signing_secret using HMAC-SHA256. This lets you verify the payload was sent by OlaClick and hasn't been tampered with.
How it works
OlaClick computes an HMAC-SHA256 hash of the raw request body using your signing_secret and sends it in the X-OlaClick-Signature header:
X-OlaClick-Signature: sha256=<hex-encoded-hmac>
To verify:
- Read the
X-OlaClick-Signatureheader from the request - Compute HMAC-SHA256 of the raw body using your
signing_secret - Compare using a timing-safe function
Where to get the signing secret
The signing_secret is set when a webhook configuration is created. For connectors, OlaClick uses the connector's OAuth client_id as the signing secret (set automatically during the connection setup). For API-created webhooks, you provide it via Create Webhook.
Implementation
- Node.js
- Python
- Go
- PHP
- Ruby
const crypto = require('crypto');
function verifySignature(rawBody, signatureHeader, signingSecret) {
if (typeof signatureHeader !== 'string') return false;
const expected = Buffer.from(
'sha256=' + crypto
.createHmac('sha256', signingSecret)
.update(rawBody)
.digest('hex'),
);
const received = Buffer.from(signatureHeader);
return (
expected.length === received.length &&
crypto.timingSafeEqual(expected, received)
);
}
// In your handler:
app.post('/webhooks/olaclick', (req, res) => {
const isValid = verifySignature(
req.rawBody, // raw string, NOT parsed JSON
req.headers['x-olaclick-signature'],
process.env.WEBHOOK_SIGNING_SECRET,
);
if (!isValid) {
return res.status(401).json({ error: 'invalid_signature' });
}
// Process event...
res.status(200).json({ status: 'received' });
});
import hmac
import hashlib
def verify_signature(
raw_body: bytes,
signature_header: str,
signing_secret: str,
) -> bool:
if not signature_header:
return False
expected = 'sha256=' + hmac.new(
signing_secret.encode(),
raw_body,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, signature_header)
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
)
func verifySignature(rawBody []byte, signatureHeader, signingSecret string) bool {
mac := hmac.New(sha256.New, []byte(signingSecret))
mac.Write(rawBody)
expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(expected), []byte(signatureHeader))
}
function verifySignature(
string $rawBody,
string $signatureHeader,
string $signingSecret
): bool {
$expected = 'sha256=' . hash_hmac('sha256', $rawBody, $signingSecret);
return hash_equals($expected, $signatureHeader);
}
require 'openssl'
def verify_signature(raw_body, signature_header, signing_secret)
return false unless signature_header
expected = 'sha256=' + OpenSSL::HMAC.hexdigest(
'SHA256', signing_secret, raw_body
)
Rack::Utils.secure_compare(expected, signature_header)
end
Important
- Always use the raw request body string — not a re-serialized version of the parsed JSON. Re-serializing can change key order or whitespace, breaking the signature.
- Always use a timing-safe comparison (
timingSafeEqual,compare_digest,hash_equals,secure_compare) to prevent timing attacks. - Reject requests with missing or invalid signatures — never process an unverified payload.