Flinks includes an HMAC-SHA256 signature in webhook headers so you can verify that the payload was sent by Flinks and hasn’t been tampered with.
Prerequisites
- A production, staging, or development instance with webhooks enabled
- HMAC enabled on your instance (contact your Flinks Representative or open a ticket via the Support Portal)
- An HMAC secret key provided by Flinks
How it works
- When Flinks sends a webhook, it computes an HMAC-SHA256 hash of the request body using your shared secret key.
- The resulting signature is included in the webhook request header.
- Your server computes the same hash and compares it to the header value.
- If they match, the webhook is authentic.
Python implementation
import hmac
import hashlib
import base64
import json
def verify_webhook_signature(payload_body, received_signature, secret_key):
"""
Verify the HMAC-SHA256 signature of a Flinks webhook.
Args:
payload_body: The raw request body (bytes)
received_signature: The signature from the webhook header
secret_key: Your HMAC secret key from Flinks
Returns:
True if the signature is valid, False otherwise
"""
# Compute the expected signature
computed_hash = hmac.new(
secret_key.encode('utf-8'),
payload_body,
hashlib.sha256
).digest()
computed_signature = base64.b64encode(computed_hash).decode('utf-8')
# Compare signatures using constant-time comparison
return hmac.compare_digest(computed_signature, received_signature)
Example: Flask webhook endpoint
from flask import Flask, request, jsonify
import hmac
import hashlib
import base64
app = Flask(__name__)
HMAC_SECRET = "your-hmac-secret-key"
@app.route("/webhook", methods=["POST"])
def handle_webhook():
# Get the signature from the request header
received_signature = request.headers.get("X-Flinks-Signature", "")
# Get the raw request body
payload_body = request.get_data()
# Compute the expected signature
computed_hash = hmac.new(
HMAC_SECRET.encode("utf-8"),
payload_body,
hashlib.sha256
).digest()
computed_signature = base64.b64encode(computed_hash).decode("utf-8")
# Verify the signature
if not hmac.compare_digest(computed_signature, received_signature):
return jsonify({"error": "Invalid signature"}), 403
# Process the webhook payload
data = request.get_json()
response_type = data.get("ResponseType")
if response_type == "KYC":
# Handle KYC webhook
pass
elif response_type == "GetAccountsDetail":
# Handle account detail webhook
pass
# Return 200 to acknowledge receipt
return jsonify({"status": "ok"}), 200
Always use constant-time comparison (like hmac.compare_digest) when verifying signatures. Standard string comparison (==) is vulnerable to timing attacks.
Node.js implementation
const crypto = require('crypto');
function verifyWebhookSignature(payloadBody, receivedSignature, secretKey) {
const computedHash = crypto
.createHmac('sha256', secretKey)
.update(payloadBody)
.digest('base64');
// Use timingSafeEqual for constant-time comparison
const expected = Buffer.from(computedHash, 'utf8');
const received = Buffer.from(receivedSignature, 'utf8');
if (expected.length !== received.length) {
return false;
}
return crypto.timingSafeEqual(expected, received);
}
Example: Express webhook endpoint
const express = require('express');
const crypto = require('crypto');
const app = express();
const HMAC_SECRET = 'your-hmac-secret-key';
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const receivedSignature = req.headers['x-flinks-signature'] || '';
const payloadBody = req.body;
// Compute the expected signature
const computedHash = crypto
.createHmac('sha256', HMAC_SECRET)
.update(payloadBody)
.digest('base64');
// Verify using constant-time comparison
const expected = Buffer.from(computedHash, 'utf8');
const received = Buffer.from(receivedSignature, 'utf8');
if (expected.length !== received.length || !crypto.timingSafeEqual(expected, received)) {
return res.status(403).json({ error: 'Invalid signature' });
}
// Process the webhook payload
const data = JSON.parse(payloadBody);
const responseType = data.ResponseType;
if (responseType === 'KYC') {
// Handle KYC webhook
} else if (responseType === 'GetAccountsDetail') {
// Handle account detail webhook
}
// Return 200 to acknowledge receipt
res.status(200).json({ status: 'ok' });
});
Using the Tag parameter with webhooks
The Tag parameter in Flinks Connect allows you to attach custom metadata to a connection, which is then included in the webhook payload. This is useful for correlating webhook data with your internal records without needing to look up the loginId.
Setup
Add the tag parameter to your Flinks Connect iframe URL:
https://[instance]-iframe.private.fin.ag/v2/?tag=userId%3D12345%2Csession%3Dabc&redirectUrl=https://example.com/callback
Webhook payload with Tag
When a webhook is delivered, the Tag value is included in the payload:
{
"Tag": "userId=12345,session=abc",
"ResponseType": "GetAccountsDetail",
"HttpStatusCode": 200,
"Login": {
"Id": "f5d5f008-e529-4714-21c0-08d6abf5bce4"
},
"Accounts": []
}
Do not include personally identifiable information (PII) in tags. Use internal reference IDs instead.
Webhooks as the recommended async pattern
Webhooks are the recommended approach for receiving data from Flinks, rather than polling the API. Benefits include:
- No polling overhead — Receive data as soon as processing completes
- Reduced API calls — Avoid repeated
/GetAccountsDetail calls while waiting for data
- Immediate notification — Get alerted within seconds of data availability
- Retry handling — Flinks retries failed deliveries up to 10 times at 30-minute intervals
For webhook setup instructions, see the Webhooks Introduction.
Verification checklist
- Extract the signature from the webhook request header
- Compute the HMAC-SHA256 hash of the raw request body using your secret key
- Base64-encode the computed hash
- Compare using constant-time comparison
- Return HTTP 200 on success
- Return HTTP 403 on signature mismatch (and log the event for investigation)