Webhook signing and callbacks
Webhook requests (payment status, consent status, job status) are signed so you can verify they come from FinqLink. You can also restrict incoming requests to our callback IPs.
Headers
| `x-signature` | JWS compact string (header.payload.signature, Base64URL). The signed payload is the raw JSON body of the request. |
| `x-signature-kid` | Key ID of the key used to sign; use it to select the key from the JWKS for verification. |
How to verify
- Fetch the public keys:
GET {base_url}/.well-known/jwks.json(use your environment base URL). Response shape:{"keys": [ { "kty", "kid", "alg", "use", "n", "e" }, ... ]}. - From the webhook request: read the
x-signature-kidandx-signatureheaders and the raw request body (UTF-8 JSON). - Find the key in
keyswith matchingkid. Verify the JWS inx-signature: the JWS payload must equal the raw body, and the signature must verify with that key (RS256 or ES256 per key’salg). Reject ifkidis unknown, signature is invalid, or payload does not match body.
You can implement this with standard JWS/JWK libraries in any language (e.g. jose in Node.js, JOSE in Elixir).
Key rotation policy
Signing keys are rotated automatically at a regular interval. The current key is used to sign new webhook requests and remains in use for approximately 90 days until the next rotation. When a new key is introduced, the previous key stays published in JWKS so you can continue to verify webhooks that were signed with it (for example, requests sent just before or during the rotation). The previous key remains available in JWKS for approximately 90 days after it is replaced. After that, it is removed from JWKS and webhooks signed with it can no longer be verified—treat any webhook whose x-signature-kid is not present in JWKS as invalid.
JWKS endpoint and key versions
GET /.well-known/jwks.json returns the public keys you need to verify webhook signatures. The endpoint always lists exactly two keys: the current key (used for new webhooks) and the previous key (used for webhooks sent before the latest rotation). Keys are ordered newest first. Use x-signature-kid from each webhook to pick the right key for verification. If you receive a webhook with a kid that is not in the JWKS response, do not trust it (it may be a replay or from an outdated integration).
JavaScript verification example (Express + jose)
This example uses Express with a raw body parser and the jose library to verify the webhook signature against the remote JWKS. The signed payload must match the raw request body byte-for-byte, so do not use express.json() for the webhook route—use express.raw().
import express from "express";
import { compactVerify, createRemoteJWKSet } from "jose";
const app = express();
// Capture raw body as Buffer
app.use(express.raw({ type: "application/json" }));
const BASE_URL = process.env.FINQWARE_BASE_URL; // e.g. https://sandbox-pay.finqware.com
const JWKS = createRemoteJWKSet(new URL(`${BASE_URL}/.well-known/jwks.json`));
app.post("/webhook", async (req, res) => {
try {
const sig = req.header("x-signature");
const kidHeader = req.header("x-signature-kid");
if (!sig || !kidHeader) return res.sendStatus(401);
// Verify signature (jose picks key by kid from the JWS header)
const { payload, protectedHeader } = await compactVerify(sig, JWKS);
// Optional defense-in-depth: compare kid values
if (protectedHeader?.kid && protectedHeader.kid !== kidHeader) {
return res.sendStatus(401);
}
// Payload must match raw body exactly
const rawBody = req.body; // Buffer from express.raw
if (Buffer.compare(Buffer.from(payload), rawBody) !== 0) {
return res.sendStatus(401);
}
// At this point: signature valid + body matches
// Now you can JSON.parse(rawBody.toString("utf8")) safely
res.sendStatus(200);
} catch (e) {
res.sendStatus(401);
}
});
app.listen(3000);
Callbacks from fixed IPs
You can restrict your webhook endpoint to accept requests only from FinqLink’s outbound IPs:
| Test | `34.89.170.122` |
| Production | `34.159.239.125` |
Use firewall or load-balancer rules to allow only these source IPs for the path that receives webhooks.
What made this section unhelpful for you?
On this page
- Webhook signing and callbacks