To ensure the authenticity and integrity of webhooks sent from our system, each webhook includes a digital signature. This signature allows you to verify that the payload has not been altered during transmission and confirms the webhook’s origin.
Overview
Each webhook includes the signature in the finventi-signature-N HTTP header, where N represents the version of the signature. The current version is 1 (finventi-signature-1). When public keys are rotated, new header versions are issued, ensuring backward compatibility for clients until their code is updated.
The signature is verified using the RSASSA-PKCS1-v1.5 algorithm and the appropriate public key.
Each webhook includes the following HTTP headers that must be used during the signature verification process:
The webhook signature (where N is the version number, currently 1)
finventi-signature-timestamp
A UNIX timestamp (UTC) indicating when the webhook was sent
finventi-receiver-tenant-id
The tenant ID for the webhook recipient
Public Key for Webhook Verification
Use the following public keys to verify webhook signatures:
Version - 1 (current)-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvoc7GrFbduCeSVxFPJ3l
a0NRa0caUqBddQAOUxuHTOuShOvdKbxRYc5u1vb9YNLJWjx4XSHESp8Q7oocqXt8
+weBFsk/kAtJ4zjbYPY1PvAOLe+WObdxxZtfwzpwVxbtP6GQk5aUi2HbITe3EDf/
7WEmvnAcWm++Mo6+GSh2Ky1t6o4htrx1lH2gYVg0iRHx1W9lLXjMl/5oLi1C6dtx
TnBmXMlN/NT5YYU4lVlXQBZzS7a8ZgwosfW+v1uCimzbGcWytmmcFISjSNqkYaeg
IXDYwKLwlsWtm975ln6UL20KcSt7ia+Lpuv7cdxJlOY95y0ds/PCw1x0HEPxU+44
swIDAQAB
-----END PUBLIC KEY-----
Signature Verification Process
To verify the authenticity of the webhook, follow these steps:
Concatenate the verification data
Concatenate the following components in the specified order, separated by periods (.):
- The request body (raw JSON)
- The tenant ID from
finventi-receiver-tenant-id header
- The timestamp from
finventi-signature-timestamp header
Example:{"trx_id":10300003,"end_to_end_id":"NOTPROVIDED","type":"Payment","direction":"OUTBOUND","amount":1,"currency":"EUR","status":"Created","updated_at":"2024-09-20T13:46:32.092083Z"}.demo1.1726839992
Hash the concatenated string
Hash the concatenated string using the SHA-256 algorithm.
Decode the signature
Base64 decode the received finventi-signature-1 header to obtain the signature bytes.
Verify the signature
Verify the signature using RSASSA-PKCS1-v1_5 with:
- The hashed data from step 2
- The decoded signature from step 3
- The public key provided above
Code Example (Node.js)
Here’s a complete example of webhook signature verification in Node.js:
const crypto = require('crypto');
function verifyWebhookSignature(body, headers) {
// Extract headers
const signatureBase64 = headers['finventi-signature-1'];
const tenantId = headers['finventi-receiver-tenant-id'];
const timestamp = headers['finventi-signature-timestamp'];
// Public key for sandbox environment
const publicKey = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvoc7GrFbduCeSVxFPJ3l
a0NRa0caUqBddQAOUxuHTOuShOvdKbxRYc5u1vb9YNLJWjx4XSHESp8Q7oocqXt8
+weBFsk/kAtJ4zjbYPY1PvAOLe+WObdxxZtfwzpwVxbtP6GQk5aUi2HbITe3EDf/
7WEmvnAcWm++Mo6+GSh2Ky1t6o4htrx1lH2gYVg0iRHx1W9lLXjMl/5oLi1C6dtx
TnBmXMlN/NT5YYU4lVlXQBZzS7a8ZgwosfW+v1uCimzbGcWytmmcFISjSNqkYaeg
IXDYwKLwlsWtm975ln6UL20KcSt7ia+Lpuv7cdxJlOY95y0ds/PCw1x0HEPxU+44
swIDAQAB
-----END PUBLIC KEY-----`;
// Step 1: Concatenate the data
const dataToVerify = Buffer.from(body + '.' + tenantId + '.' + timestamp);
// Step 2: Hash is done internally by crypto.verify
// Step 3: Decode the signature
const signature = Buffer.from(signatureBase64, 'base64');
// Step 4: Verify the signature
const isVerified = crypto.verify(
"sha256",
dataToVerify,
{
key: publicKey,
padding: crypto.constants.RSA_PKCS1_PADDING,
},
signature
);
return isVerified;
}
// Example usage
const webhookBody = '{"trx_id":10300003,"end_to_end_id":"NOTPROVIDED","type":"Payment","direction":"OUTBOUND","amount":1,"currency":"EUR","status":"Created","updated_at":"2024-09-20T13:46:32.092083Z"}';
const webhookHeaders = {
'finventi-signature-1': 'GtZFu1uNFqOir8eDkar7+d/S+FtwQpk4mPGCuByKhJG29K1u7ynbVhkrDF8c3TqyX9wYHxpOa94FsgW2I4CnLh+B24LqL7WVSuACOL6GoSjfKeXP00NSp0ps8QYbVaJ8Ys6E4FePhp+7piAACkIP5vZ91JCLQ8lz36KRJlOnByQMTBH6j924n1GwZiZfbMojOGmMhLA0h8jWgTIeuvYPswiZXZXp0vpqJfWmdoqiU1ldoausTNFyoVwFuuzkxPv1VHvWeEeWirObUv3wNpyAnLnqomDgR7pe/9dDV0bkq5r0JkRhnbPCEFE/zzHeDgZ957hv8Oq3wJkGJarZ0NvPLw==',
'finventi-receiver-tenant-id': 'demo1',
'finventi-signature-timestamp': '1726839992'
};
const isValid = verifyWebhookSignature(webhookBody, webhookHeaders);
console.log("Verification successful:", isValid);
Express.js Middleware Example
Here’s how to implement webhook verification as Express.js middleware:
const express = require('express');
const crypto = require('crypto');
function webhookVerificationMiddleware(req, res, next) {
// Get raw body
const rawBody = req.rawBody || JSON.stringify(req.body);
// Extract headers
const signatureBase64 = req.headers['finventi-signature-1'];
const tenantId = req.headers['finventi-receiver-tenant-id'];
const timestamp = req.headers['finventi-signature-timestamp'];
if (!signatureBase64 || !tenantId || !timestamp) {
return res.status(401).json({ error: 'Missing required webhook headers' });
}
// Verify signature
const publicKey = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvoc7GrFbduCeSVxFPJ3l
a0NRa0caUqBddQAOUxuHTOuShOvdKbxRYc5u1vb9YNLJWjx4XSHESp8Q7oocqXt8
+weBFsk/kAtJ4zjbYPY1PvAOLe+WObdxxZtfwzpwVxbtP6GQk5aUi2HbITe3EDf/
7WEmvnAcWm++Mo6+GSh2Ky1t6o4htrx1lH2gYVg0iRHx1W9lLXjMl/5oLi1C6dtx
TnBmXMlN/NT5YYU4lVlXQBZzS7a8ZgwosfW+v1uCimzbGcWytmmcFISjSNqkYaeg
IXDYwKLwlsWtm975ln6UL20KcSt7ia+Lpuv7cdxJlOY95y0ds/PCw1x0HEPxU+44
swIDAQAB
-----END PUBLIC KEY-----`;
const dataToVerify = Buffer.from(rawBody + '.' + tenantId + '.' + timestamp);
const signature = Buffer.from(signatureBase64, 'base64');
const isVerified = crypto.verify(
"sha256",
dataToVerify,
{
key: publicKey,
padding: crypto.constants.RSA_PKCS1_PADDING,
},
signature
);
if (!isVerified) {
return res.status(401).json({ error: 'Invalid webhook signature' });
}
next();
}
// Usage
const app = express();
// Important: Capture raw body for signature verification
app.use(express.json({
verify: (req, res, buf) => {
req.rawBody = buf.toString('utf8');
}
}));
app.post('/webhook/payment-status', webhookVerificationMiddleware, (req, res) => {
// Handle verified webhook
console.log('Verified webhook received:', req.body);
res.status(200).send('OK');
});
Security Best Practices
Always verify webhook signatures to ensure the authenticity of incoming webhooks. Never process webhook data without proper verification.
- Store the public key securely - Keep the public key in your configuration management system
- Verify timestamps - Check that the timestamp is recent to prevent replay attacks
- Use HTTPS - Always expose your webhook endpoints over HTTPS
- Log verification failures - Monitor and log all signature verification failures
- Handle version rotation - Be prepared to support multiple signature versions during key rotation periods