BCB signs all webhooks and selected API messages to guarantee authenticity, integrity, and replay protection. Two signing methods are supported:
- HMAC-SHA256 – symmetric signing with a shared secret (legacy, still supported)
- RSA-PSS + SHA-256 – asymmetric signing with public/private keys (recommended for new integrations)
Both methods use the same signature string format, same timestamp/nonce headers, and same replay-protection rules.
Both HMAC and RSA sign the same canonical string constructed in this exact order:
{timestamp}{nonce}{method}{path}{body}timestamp: Unix epoch seconds (fromBcb-Timestampheader).nonce: Unique identifier, typically a GUID (fromBcb-Nonceheader).method: HTTP verb in uppercase (GET,POST, etc.).path: The absolute path starting with/, without the query string.- For webhooks: This is the path component of the webhook URL that BCB calls on your server (e.g.,
/webhooks/payments).
- For webhooks: This is the path component of the webhook URL that BCB calls on your server (e.g.,
body: The raw request/response payload.- Use an empty string if there is no body.
To ensure consistent signatures, follow these rules strictly:
- Encoding: Use UTF-8 for all string-to-byte conversions.
- Raw Body: Use the raw bytes exactly as received over the wire.
- Do NOT re-serialize JSON (e.g., changing field order).
- Do NOT normalize whitespace or pretty-print.
- If the body is empty, use an empty string
"".
- Compression: Signature payload is the logical HTTP body after decompression (i.e., the raw UTF-8 string that your application/middleware receives). If BCB sends a gzipped response, your verifier must use the uncompressed content for signature verification.
Bcb-Signature: This header always contains a standard Base64 encoded signature.- JWKS Fields: Fields within the
/.well-known/jwks.json(likenande) are Base64url encoded as per the RFC 7517 spec. - Copy/Paste Warning: Ensure your libraries handle the conversion if you are manually extracting keys.
| Header | Description | Example |
|---|---|---|
Bcb-Signature | Base64-encoded signature | a8f3b2... |
Bcb-Timestamp | Unix epoch seconds | 1702987654 |
Bcb-Nonce | Unique request identifier | abc-123-def-456 |
Bcb-Signature-Version | Indicates algorithm and key version | rsa-v1 |
Note on Bcb-Signature-Version: For RSA, this value serves two purposes:
- It identifies the algorithm used (RSA-PSS + SHA-256).
- It is the literal Key ID (
kid). You must use this value to look up the correct public key in the BCBjwks.jsonendpoint.
Operational Behavior:
- Rotation: When BCB rotates its signing key, the header value will change (e.g., from
rsa-v1torsa-v2). - Grace Period: During rotation, the
jwks.jsonendpoint will publish both keys. - Caching: Clients should cache the JWKS response (5-minute TTL recommended) but must refetch immediately if they receive a message with an unknown
kid.
All signed messages must pass these checks before processing:
- Timestamp tolerance:
|now - timestamp| ≤ 300seconds (5 minutes). - Nonce uniqueness: Reject if
{timestamp}:{nonce}has been seen before (cache for 5 minutes). - Signature validity: Rebuild the signature string and verify against the header.
- BCB and the client share a secret key (provisioned during onboarding).
- Both parties compute:
signature = Base64(HMACSHA256(signature_string, secret)).
const crypto = require('crypto');
function verifyHmacSignature(req, rawBody, sharedSecret) {
const signatureB64 = req.headers['bcb-signature'];
const timestamp = req.headers['bcb-timestamp'];
const nonce = req.headers['bcb-nonce'];
if (!signatureB64 || !timestamp || !nonce) return false;
const method = req.method.toUpperCase();
const path = req.originalUrl.split('?')[0];
const payload = rawBody ?? '';
const signatureString = `${timestamp}${nonce}${method}${path}${payload}`;
const expectedB64 = crypto.createHmac('sha256', sharedSecret)
.update(signatureString, 'utf8')
.digest('base64');
// Safely compare signature bytes
const sig = Buffer.from(signatureB64, 'base64');
const exp = Buffer.from(expectedB64, 'base64');
if (sig.length !== exp.length) return false;
return crypto.timingSafeEqual(sig, exp);
}- Bank → Client: BCB signs with its private key; you verify with BCB's public key (from JWKS).
- Client → Bank: You sign with your private key; BCB verifies with your public key.
- Signing algorithm: RSA-PSS + SHA-256.
GET /v1/.well-known/jwks.jsonAlways use the kid from the Bcb-Signature-Version header to select the correct key from the JWKS response.
const crypto = require('crypto');
async function verifyRsaSignature(req, rawBody, publicKeyPem) {
const signature = req.headers['bcb-signature'];
const timestamp = req.headers['bcb-timestamp'];
const nonce = req.headers['bcb-nonce'];
const method = req.method.toUpperCase();
const path = req.originalUrl.split('?')[0];
const payload = rawBody ?? '';
const signatureString = `${timestamp}${nonce}${method}${path}${payload}`;
const verify = crypto.createVerify('RSA-SHA256');
verify.update(signatureString, 'utf8');
return verify.verify({
key: publicKeyPem,
padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
saltLength: crypto.constants.RSA_PSS_SALTLEN_DIGEST
}, signature, 'base64');
}For instructions on how to rotate your own keys and use the Self-Service API, see the Key Management Guide.