Skip to main content

Documentation Index

Fetch the complete documentation index at: https://developers.bloobank.com/llms.txt

Use this file to discover all available pages before exploring further.

Proposed scheme. The signature format below follows industry conventions (Stripe, GitHub). Concrete header names and the HMAC secret rotation flow will be confirmed with the BlooBank platform team before this scheme exits “Proposed” status.
Every webhook delivery is signed. Verify the signature before processing the payload — an unverified webhook is an attack surface.

The scheme

HMAC-SHA256 over a deterministic signed payload, transmitted in the X-Bloobank-Signature header.

The signed payload

{timestamp}.{rawBody}
FieldSource
{timestamp}The value of the X-Bloobank-Timestamp header (Unix epoch in milliseconds).
{rawBody}The exact raw bytes of the request body.
The . is the literal ASCII period separator. The whole string is UTF-8 encoded.

The signature header

X-Bloobank-Signature carries one or more signatures, comma-separated, each prefixed with a version label:
X-Bloobank-Signature: t=1736553600123,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
ElementMeaning
t=...Unix epoch in milliseconds — mirrors X-Bloobank-Timestamp.
v1=...HMAC-SHA256 hex digest, signed with the webhook secret.
Future scheme versions may add v2=... for migration; your verifier should accept any version it understands and reject the delivery if none match.

The webhook secret

The webhook signing secret is generated by BlooBank when you configure an endpoint. It is a high-entropy random string (≥ 32 bytes, base64-encoded) shown once in the Dashboard. Copy it into your secret manager and never expose it. Each endpoint configuration has its own secret. Rotating the secret produces a new value while the old one is briefly accepted in parallel — see Rotation below.

Verification — the algorithm

1

Read the headers and raw body

Capture X-Bloobank-Timestamp, X-Bloobank-Signature, and the raw bytes of the request body — do not re-parse and re-serialize.
2

Parse the signature header

Split by ,, then by = for each part. Extract the t= timestamp and every vN= signature.
3

Verify the timestamp

The header timestamp must be within ±5 minutes of your server’s current time. Reject older deliveries — they are stale (probably replays).
4

Compute the expected HMAC

expected = HMAC_SHA256_hex(secret, "{timestamp}.{rawBody}")
5

Constant-time compare

Compare each vN from the header to your expected using a constant-time comparison function (crypto.timingSafeEqual in Node, hmac.compare_digest in Python, etc.). If any matches, the signature is valid.
6

Reject otherwise

Return 401 Unauthorized (without a body). Do not log the body, do not process the event.

Code

import crypto from 'node:crypto';

const FIVE_MINUTES_MS = 5 * 60 * 1000;

function verifyWebhook(rawBody, headers, secret) {
  const sigHeader = headers['x-bloobank-signature'];
  if (!sigHeader) throw new Error('Missing signature header');

  // Parse "t=...,v1=...,v1=..."
  const parts = Object.fromEntries(
    sigHeader.split(',').map(p => {
      const [k, v] = p.split('=', 2);
      return [k.trim(), v.trim()];
    })
  );
  // Multiple v1 signatures are possible (during rotation); collect all
  const v1Sigs = sigHeader
    .split(',')
    .filter(p => p.trim().startsWith('v1='))
    .map(p => p.split('=', 2)[1].trim());

  const ts = Number(parts.t);
  if (!Number.isFinite(ts)) throw new Error('Invalid timestamp');
  if (Math.abs(Date.now() - ts) > FIVE_MINUTES_MS) {
    throw new Error('Timestamp outside ±5 minutes — likely replay');
  }

  const signedPayload = `${parts.t}.${rawBody}`;
  const expected = crypto.createHmac('sha256', secret)
                         .update(signedPayload)
                         .digest('hex');

  const expectedBuf = Buffer.from(expected, 'hex');
  const ok = v1Sigs.some(sig => {
    const sigBuf = Buffer.from(sig, 'hex');
    return sigBuf.length === expectedBuf.length
        && crypto.timingSafeEqual(sigBuf, expectedBuf);
  });
  if (!ok) throw new Error('Signature mismatch');

  return JSON.parse(rawBody);
}

// Express example
app.post('/webhooks/bloobank', express.raw({ type: 'application/json' }), (req, res) => {
  try {
    const event = verifyWebhook(req.body.toString('utf8'), req.headers, process.env.BLOOBANK_WEBHOOK_SECRET);
    enqueueAsync(event);            // do real work elsewhere
    res.status(200).end();
  } catch (err) {
    console.error('Webhook verification failed:', err);
    res.status(401).end();
  }
});

Rotation

Webhook secrets should be rotated periodically. The flow:
1

Request rotation in the Dashboard

Generates a new secret. Both old and new secrets are valid for a configurable overlap window (default 24 hours).
2

During the overlap, BlooBank signs with both

Deliveries during the window carry two v1=... signatures — one signed with the old secret, one with the new. Your verifier accepts either.
3

Update your secret manager

Replace the old secret with the new one before the overlap window ends.
4

After the window expires, the old secret is invalidated

Only the new secret signs subsequent deliveries.
To support rotation client-side without downtime, your verifier should accept multiple secrets — try each, accept on first match. During steady-state operation, you hold one secret; during rotation, two.

What can go wrong

FailureCauseAction
Signature mismatchBody re-parsed and re-serialized between read and verify.Pass the raw bytes to the verifier. Do not let middleware decode the JSON before you verify.
Timestamp driftServer clock more than ±5 minutes off.Enable NTP.
Stale replay attackAttacker captures a delivery and resends hours later.The ±5-minute timestamp check rejects this.
Constant-time bypassUsing == or string.equals.Always use the platform’s constant-time compare.
Secret leaked in logsIncluding the secret in error messages.Never log the secret; log only the result (ok/fail).

Next

Retry & delivery

The delivery contract.

Best practices

Idempotency, ordering, deduplication.

Payment events

Event catalog.