Service Webhook Receiver Guide
This guide explains what bbserver sends to your endpoint, how the secret is used, and what your receiver should validate.
What bbserver sends
When a service webhook fires, bbserver sends an HTTPS POST request with:
- A JSON request body
- Metadata headers describing the event and delivery
- An HMAC-SHA256 signature derived from your webhook
secret
The secret is not used to encrypt the request body. It is used only to sign the request so your receiver can verify authenticity and integrity.
Headers
Current outbound headers:
User-Agent: bbserver-service-webhook/1X-BB-Event: certificate.issuedX-BB-Webhook-Id: <webhook_id>X-BB-Delivery-Id: <delivery_id>X-BB-Cert-Id: <cert_id>X-BB-Issue-History-Id: <issue_history_id>X-BB-Timestamp: <unix_ms>X-BB-Signature: sha256=<hex_digest>
Body
Typical request body:
{
"event": "certificate.issued",
"cert_id": 123,
"issue_history_id": 456,
"user_id": 789,
"ts_ms": 1700000000000,
"domain_name": "example.com"
}
Field notes:
event: current event keycert_id: certificate record idissue_history_id: issuance history id for this successful issuanceuser_id: owner of the webhook/certificatets_ms: enqueue-time timestamp in millisecondsdomain_name: primary domain if available; it may be omitted
How the secret is used
When bbserver sends the webhook, it computes:
signing_input = <X-BB-Timestamp> + "." + <raw_request_body>
signature = HMAC-SHA256(secret, signing_input)
Then bbserver sends the result as:
X-BB-Signature: sha256=<hex_digest>
That means:
- The body is plain JSON over HTTPS
- The secret is shared between bbserver and your receiver
- Your receiver should recompute the HMAC and compare it
Receiver verification steps
Your endpoint should:
- Read the raw request body bytes exactly as received.
- Read
X-BB-Timestamp. - Read
X-BB-Signature. - Rebuild the signing input as
<timestamp>.<raw_body>. - Compute
HMAC-SHA256(secret, signing_input). - Hex-encode the digest and compare it against the signature value after the
sha256=prefix. - Reject stale timestamps, for example anything older than 5 minutes.
Important:
- Do not parse and re-stringify JSON before verification.
- Do not trim whitespace or normalize line endings before verification.
- Use a constant-time comparison when checking the signature.
Minimal receiver behavior
If you only want a safe baseline:
- Verify the signature
- Verify the timestamp window
- Check
event === "certificate.issued" - Queue the real work asynchronously
- Return
204quickly
Node.js example
The example below uses Express and verifies the signature against the raw request body before parsing JSON.
const crypto = require('crypto');
const express = require('express');
const app = express();
const webhookSecret = process.env.BB_WEBHOOK_SECRET;
app.post('/bb/webhook', express.raw({ type: '*/*' }), (req, res) => {
const timestamp = req.header('x-bb-timestamp');
const signatureHeader = req.header('x-bb-signature') || '';
if (!timestamp || !signatureHeader.startsWith('sha256=')) {
return res.status(400).send('missing signature headers');
}
const rawBody = Buffer.isBuffer(req.body) ? req.body : Buffer.from(req.body || '');
const signingInput = Buffer.concat([
Buffer.from(String(timestamp), 'utf8'),
Buffer.from('.', 'utf8'),
rawBody,
]);
const expected = crypto
.createHmac('sha256', webhookSecret)
.update(signingInput)
.digest('hex');
const provided = signatureHeader.slice('sha256='.length);
const expectedBuf = Buffer.from(expected, 'hex');
const providedBuf = Buffer.from(provided, 'hex');
if (expectedBuf.length !== providedBuf.length) {
return res.status(401).send('invalid signature');
}
if (!crypto.timingSafeEqual(expectedBuf, providedBuf)) {
return res.status(401).send('invalid signature');
}
const now = Date.now();
const skewMs = Math.abs(now - Number(timestamp));
if (!Number.isFinite(skewMs) || skewMs > 5 * 60 * 1000) {
return res.status(401).send('stale timestamp');
}
let payload;
try {
payload = JSON.parse(rawBody.toString('utf8'));
} catch {
return res.status(400).send('invalid json');
}
if (payload.event !== 'certificate.issued') {
return res.status(400).send('unexpected event');
}
console.log('verified webhook', {
cert_id: payload.cert_id,
issue_history_id: payload.issue_history_id,
user_id: payload.user_id,
domain_name: payload.domain_name,
});
return res.status(204).end();
});
app.listen(3000, () => {
console.log('listening on :3000');
});
Notes:
- Use
express.raw()for this route. If JSON middleware consumes and rewrites the body first, signature verification will fail. - Store the secret outside source control, for example in an environment variable.
- Return
204quickly and hand off slow work to a queue or background job.
What happens if you do not verify
The endpoint will still receive requests and may appear to work, but it becomes an unauthenticated public POST endpoint.
Without verification:
- Anyone who can reach the URL can forge events
- Payload integrity is not protected
- Replay protection is effectively lost
Recommended response behavior
bbserver treats any 2xx response as success.
Retryable responses are:
4084295xx
Recommended receiver behavior:
- Return
204or another2xxas soon as the request is accepted - Perform slow work after the response, usually through a job queue
- Return
4xxonly for real permanent problems such as an invalid signature
Using the payload after verification
The webhook is a notification, not a certificate bundle. After verification, use cert_id to fetch the current certificate through your existing authenticated API flow.