Skip to main content
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.

HTTP Headers

Each webhook includes the following HTTP headers that must be used during the signature verification process:
finventi-signature-N
string
required
The webhook signature (where N is the version number, currently 1)
finventi-signature-timestamp
string
required
A UNIX timestamp (UTC) indicating when the webhook was sent
finventi-receiver-tenant-id
string
required
The tenant ID for the webhook recipient

Public Key for Webhook Verification

Use the following public keys to verify webhook signatures:
  • Sandbox
  • Production
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:
1

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
2

Hash the concatenated string

Hash the concatenated string using the SHA-256 algorithm.
3

Decode the signature

Base64 decode the received finventi-signature-1 header to obtain the signature bytes.
4

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.
  1. Store the public key securely - Keep the public key in your configuration management system
  2. Verify timestamps - Check that the timestamp is recent to prevent replay attacks
  3. Use HTTPS - Always expose your webhook endpoints over HTTPS
  4. Log verification failures - Monitor and log all signature verification failures
  5. Handle version rotation - Be prepared to support multiple signature versions during key rotation periods