Signature Verification
Fireblocks cryptographically signs every webhook, allowing you to verify authenticity and integrity.
Signature methods
| Method | Header | Key Rotation |
|---|
| JWKS | Fireblocks-Webhook-Signature | ✅ Automatic |
| Legacy | Fireblocks-Signature | ❌ Manual |
During migration, both headers are sent with each webhook.
Validating webhooks (JWKS)
How it works
The new signature uses Detached JWS (JSON Web Signature) format:
Fireblocks-Webhook-Signature: “[header]..[signature]”
The kid (key ID) is embedded in the JWS header, allowing automatic key lookup from the JWKS endpoint. The payload is sent separately in the request body (detached format).
JWKS endpoints
Fireblocks publishes public keys in JWKS format for validating webhook signatures. Use the endpoint that matches your workspace’s environment.
{
"keys": [
{
"kty": "RSA",
"kid": "webhook-key-2025-01",
"use": "sig",
"alg": "RS512",
"n": "0vx7agoebGcQemDqTnUt...",
"e": "AQAB"
}
]
}
| Field | Description |
|---|
| kty | Key type. Always RSA for Fireblocks webhook keys. |
| kid | Key ID. Matches the kid header in incoming webhook JWTs. |
| use | Key usage. Always sig (signature verification). |
| alg | Algorithm. Fireblocks uses RS512 (RSA with SHA-512). |
| n | RSA modulus (Base64URL-encoded). |
| e | RSA exponent (Base64URL-encoded). |
Caching behavior
The JWKS endpoint returns caching headers to optimize performance:
- Cache-Control: public, max-age=3600, s-maxage=3600
- Access-Control-Allow-Origin: *
- Content-Type: application/json
Code examples (JWKS)
import { createRemoteJWKSet, compactVerify } from 'jose';
// Initialize JWKS client (caches keys automatically)
const JWKS = createRemoteJWKSet(
new URL('https://keys.fireblocks.io/.well-known/jwks.json')
);
async function verifyWebhookJWKS(
rawBody: Buffer,
jwsSignature: string
): Promise<boolean> {
try {
// Detached JWS format: "header..signature" (no payload)
const [header, , sig] = jwsSignature.split('.');
// Reconstruct full JWS with payload
const payload = Buffer.from(rawBody).toString('base64url');
const fullJws = `${header}.${payload}.${sig}`;
// jose extracts kid from header and fetches correct key from JWKS
await compactVerify(fullJws, JWKS);
return true;
} catch (error) {
console.error('JWKS verification failed:', error);
return false;
}
}
// Usage: pass raw body and Fireblocks-Webhook-Signature header
const isValid = await verifyWebhookJWKS(rawBody, jwsSignatureHeader);
JWKS caching best practices
Cache the JWKS: Most libraries handle this automatically.
Respect Cache-Control: JWKS responses include max-age=3600 (1 hour). Refresh accordingly.
Handle rotation gracefully: Multiple keys will be present; the kid in the JWS header identifies which key to use.
Common pitfalls
| Issue | Solution |
|---|
| Body parsing before verification | Use raw body parser middleware. JSON parsing alters whitespace/ordering. |
| Wrong JWKS URL | Use the correct endpoint for your environment |
| Stale JWKS cache | Refresh the cache if verification fails with an unknown kid |
| Encoding issues | Ensure body is read as raw bytes, not string-converted |
Legacy Static Key Validation
Migration noticeThis method is being replaced by JWKS-based validation. Both headers are sent until March 20th, 2026. New integrations should use JWKS.
How the signature works
Fireblocks signs every webhook event with their private key. The signature is sent in the Fireblocks-Signature HTTP header:
Fireblocks-Signature: Base64(RSA512(WEBHOOK_PRIVATE_KEY, SHA512(eventBody)))
Breakdown:
SHA512(eventBody) — Hash the raw request body with SHA-512
RSA512 Sign — Fireblocks signs the hash with their private RSA key (PKCS#1 v1.5)
Base64 Encode — The signature is base64-encoded for transport
Your job: Verify the signature using Fireblocks’ public key to confirm authenticity.
Public keys by environment
US Mainnet & Testnet
-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA0+6wd9OJQpK60ZI7qnZG
jjQ0wNFUHfRv85Tdyek8+ahlg1Ph8uhwl4N6DZw5LwLXhNjzAbQ8LGPxt36RUZl5
YlxTru0jZNKx5lslR+H4i936A4pKBjgiMmSkVwXD9HcfKHTp70GQ812+J0Fvti/v
4nrrUpc011Wo4F6omt1QcYsi4GTI5OsEbeKQ24BtUd6Z1Nm/EP7PfPxeb4CP8KOH
clM8K7OwBUfWrip8Ptljjz9BNOZUF94iyjJ/BIzGJjyCntho64ehpUYP8UJykLVd
CGcu7sVYWnknf1ZGLuqqZQt4qt7cUUhFGielssZP9N9x7wzaAIFcT3yQ+ELDu1SZ
dE4lZsf2uMyfj58V8GDOLLE233+LRsRbJ083x+e2mW5BdAGtGgQBusFfnmv5Bxqd
HgS55hsna5725/44tvxll261TgQvjGrTxwe7e5Ia3d2Syc+e89mXQaI/+cZnylNP
SwCCvx8mOM847T0XkVRX3ZrwXtHIA25uKsPJzUtksDnAowB91j7RJkjXxJcz3Vh1
4k182UFOTPRW9jzdWNSyWQGl/vpe9oQ4c2Ly15+/toBo4YXJeDdDnZ5c/O+KKadc
IMPBpnPrH/0O97uMPuED+nI6ISGOTMLZo35xJ96gPBwyG5s2QxIkKPXIrhgcgUnk
tSM7QYNhlftT4/yVvYnk0YcCAwEAAQ==
-----END PUBLIC KEY-----
EU & EU2 Mainnet & Testnet
-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA6hLRQL0jPf5OEuaDYGjO
xSyaYIlv08S0+4giiwgKSfV3Onc5hn03mvE0znzaUq2ReSxi9KYDdMYFfzf1uwF7
7kYy2MY0oTYGdQb+PS4Ym4R4tgZ2otuoAXt8YRKq2maWyguFiaowMcYwwAVQv8JB
afIm6Jq1nI6v1mEDVX065ePlBlAt+BGAqr6ahPxnaIz3L4eztpuNrt5nTbSxs7eF
aqQx1p56W1nl3Hl0V3tLkaXbuVtbFNR/mGMInrkPnpsG+mt35b9vmqAOvLPI0Cx1
59uVeEs62Hj1AOCRyT6SuwIaFynRj2KnD42ioQtkodHQ0xDtgdiYGsxuwQ9vTIe7
5oLsL8gBDeX5gdcTfSZhfGjZ7RggLNJ7vCAbYKMuUOdgWVMYnJfrhNLCq3zDSZPO
+H0x5m/Yeq/Hn5o7xCmLNT3qARfwDd5IHfQyXqVYB6TMU75xqH5fdSRw0iMdoPyL
ALnr9/JT0av3qssNMRdWCXr+j9Ys3NkfcbU/a49657mg8e2QGSkl9w39csEKojnr
omUz25szIL8CcXLmc5cAmnimFCe4L7UT4mvVP3+fOo+cbc/82zqA8tsSwd2Y93/6
ueGnNZD9V5rewrKjmdPfrwoI2gntzc8QJUu+nxAWhoqHV91AQeglu6WIF/DiEJC5
WPoNk2SdlAuA6RYmgB2YyikCAwEAAQ==
-----END PUBLIC KEY-----
Developer Sandbox
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApZE6wL2+7P1ohvVYSpCd
gSgtmyGwiLbUC1UoGJhn1zwfY7ZWbNH7Pg8Osk8OzZTZHSG/arcgE8HnGCmGKtbE
QBkf2XlBRBQ01FcCMlZuJQJ3nElCPaMl9N6fq0VKNEIlVSVUpDCgvag5kFhDKS/L
p3YYJLFR46/hDlVLn+vM84diO3xGyMc16YJGNz7Z4jb8dmSZQE5E2XaQMDXW6uxC
c2ChjWJ3X5H70MzRG35JsN0j58SQTwbf4Pxm0aJfhPuaIBn3mJuZL5etsuFihoFG
FDnT+qWRcgD/pRNulBFAFhJeUnFrE4fFTJ1iaHhjBrStBCrxJk6QI0pGznoapTgA
2QIDAQAB
-----END PUBLIC KEY-----
Step-by-step validation process
- Extract the signature from the Fireblocks-Signature header
- Base64 decode the signature to get raw bytes
- Read the raw request body (do NOT parse or modify it)
- Hash the body with SHA-512
- Verify using RSA PKCS#1 v1.5 with the Fireblocks public key
- Accept or reject the webhook based on the verification result
Code examples (Legacy)
import crypto from 'crypto';
// Select the appropriate key for your environment
const FIREBLOCKS_PUBLIC_KEY_US = `-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA0+6wd9OJQpK60ZI7qnZG
jjQ0wNFUHfRv85Tdyek8+ahlg1Ph8uhwl4N6DZw5LwLXhNjzAbQ8LGPxt36RUZl5
YlxTru0jZNKx5lslR+H4i936A4pKBjgiMmSkVwXD9HcfKHTp70GQ812+J0Fvti/v
4nrrUpc011Wo4F6omt1QcYsi4GTI5OsEbeKQ24BtUd6Z1Nm/EP7PfPxeb4CP8KOH
clM8K7OwBUfWrip8Ptljjz9BNOZUF94iyjJ/BIzGJjyCntho64ehpUYP8UJykLVd
CGcu7sVYWnknf1ZGLuqqZQt4qt7cUUhFGielssZP9N9x7wzaAIFcT3yQ+ELDu1SZ
dE4lZsf2uMyfj58V8GDOLLE233+LRsRbJ083x+e2mW5BdAGtGgQBusFfnmv5Bxqd
HgS55hsna5725/44tvxll261TgQvjGrTxwe7e5Ia3d2Syc+e89mXQaI/+cZnylNP
SwCCvx8mOM847T0XkVRX3ZrwXtHIA25uKsPJzUtksDnAowB91j7RJkjXxJcz3Vh1
4k182UFOTPRW9jzdWNSyWQGl/vpe9oQ4c2Ly15+/toBo4YXJeDdDnZ5c/O+KKadc
IMPBpnPrH/0O97uMPuED+nI6ISGOTMLZo35xJ96gPBwyG5s2QxIkKPXIrhgcgUnk
tSM7QYNhlftT4/yVvYnk0YcCAwEAAQ==
-----END PUBLIC KEY-----`;
function verifyWebhookSignature(
rawBody: Buffer,
signature: string,
publicKey: string = FIREBLOCKS_PUBLIC_KEY_US
): boolean {
try {
const verifier = crypto.createVerify('RSA-SHA512');
verifier.update(rawBody);
return verifier.verify(publicKey, signature, 'base64');
} catch (error) {
console.error('Signature verification failed:', error);
return false;
}
}
/**
* IMPORTANT:
* This function requires the **raw request body**.
* Do NOT use express.json() or any body parser that mutates the payload.
*
* Use:
* app.use(express.raw({ type: 'application/json' }))
*
* Otherwise, signature verification will fail.
*/
// Usage: pass raw request body (Buffer) and Fireblocks-Signature header value const isValid = verifyWebhookSignature(rawBody, signatureHeader);