Verify the Webhook Token
Token verification applies to destinations registered through
POST /partner/webhook/subscription — the
real-estate route, carrying new_lead and pre_approve.
BNPL partner? Your destinations are signed with HMAC instead. Go to Verify HMAC Signatures.
Every delivery to your destination carries a shared secret in a header. You compare it against the value you stored when you registered, and reject the request if it does not match. That is the whole mechanism.
The header​
bancame-webhook-secret: wh_production_0123456789abcdefghijklmnopqrstuvwxyz
The value is the token returned — once — in the 201 response when the destination was created.
There is no bancame-signature header on these deliveries, and nothing to hash. If you are
reading examples that compute an HMAC over the request body, those belong to the BNPL
integration and do not apply here.
Verifying​
All you have to do is compare the header against the value you stored. There is no signature to compute and no hashing involved, which means you may not need to write any code at all.
If your platform can check a header for you​
Most automation and integration platforms verify a static header natively. If yours does, use it — it is the intended path for this integration, and it is the reason the token scheme exists alongside HMAC.
In n8n, the Webhook node supports Header Auth: create a Header Auth credential with the name
bancame-webhook-secret and your token as the value, and select it on the node. Anything arriving
without that exact header is rejected before your workflow runs. Make, Zapier and most API
gateways have an equivalent.
This is worth knowing before you plan the work: a real-estate webhook integration can be stood up in a no-code tool with no verification code to maintain.
If you are writing the comparison yourself​
Prefer a constant-time comparison. The risk it removes is small in this setting — an attacker would have to measure nanosecond differences across the network, against your own endpoint — but it costs you three lines and removes the question entirely.
const crypto = require("crypto");
function verifyWebhookToken(receivedToken, expectedToken) {
if (typeof receivedToken !== "string" || receivedToken.length === 0) {
return false;
}
const received = Buffer.from(receivedToken);
const expected = Buffer.from(expectedToken);
// timingSafeEqual throws when the lengths differ, so check first.
// Skipping this turns a malformed token into a 500 instead of a clean reject.
if (received.length !== expected.length) {
return false;
}
return crypto.timingSafeEqual(received, expected);
}
// Usage in Express.js
app.post("/webhooks/bancame", express.json(), (req, res) => {
const received = req.headers["bancame-webhook-secret"];
const expected = process.env.BANCAME_WEBHOOK_SECRET;
if (!verifyWebhookToken(received, expected)) {
return res.status(401).send("Invalid token");
}
// Acknowledge immediately, process afterwards
res.status(200).send("Received");
setImmediate(() => handleWebhook(req.body));
});
A plain equality check​
if (req.headers["bancame-webhook-secret"] !== process.env.BANCAME_WEBHOOK_SECRET) {
return res.status(401).send("Invalid token");
}
This is acceptable, but it is not what we would recommend. It does reject every wrong token, and the timing attack it leaves open is not realistically exploitable over a network against a webhook receiver. Use it if a constant-time primitive is genuinely out of reach in your stack — not as the default.
What is not acceptable in any of the three variants: skipping the check, comparing only a prefix of the token, or logging the header while you debug.
Unlike HMAC, the token does not care how your framework parses the body. There is no raw-body requirement and no key-ordering concern — the two most common sources of signature-verification bugs simply do not exist here.
There is no timestamp on the wire​
This is the one thing the token gives up relative to HMAC, and it is worth knowing.
The HMAC scheme sends t=<iso timestamp> alongside the signature, which lets a receiver reject
anything too old. Token deliveries carry no timestamp, so that check is not available to you.
Your replay defence is eventId instead:
- It is unique per event and stable across the retry, so it identifies the event rather than the attempt.
- Recording processed ids and skipping repeats gives you idempotency and replay protection at the same time.
async function handleWebhook(event) {
const { eventId, data } = event;
if (await alreadyProcessed(eventId)) {
return; // replay or retry — already handled
}
await process(data);
await markProcessed(eventId);
}
Keep processed ids long enough to outlive any plausible replay, not merely long enough to cover the retry.
Which scheme a destination uses, and why it never changes​
The scheme is fixed by the endpoint the destination was registered through, and is a property of the destination — not of your partner account, and not of the event type.
| Registered via | Scheme | Header |
|---|---|---|
POST /partner/webhook | HMAC | bancame-signature |
POST /partner/webhook/subscription | Token | bancame-webhook-secret |
- A destination verifies everything it receives the same way. One subscribed to both
new_leadandpre_approveuses one verifier, never two. - It cannot be changed after creation. No call flips a destination between schemes, so a working integration will not silently start failing verification. To use the other scheme, register a second destination through the other endpoint and delete the first.
- Reconciling does not change it. Re-posting a URL you already registered leaves the scheme
untouched, and posting an HMAC destination's URL to the subscription endpoint is refused with
409 ERR_PARTNER_WEBHOOK_SCHEME_IS_NOT_TOKENrather than migrated.
If you are unsure which scheme a destination uses,
GET /partner/webhook reports verificationScheme for each one.
Handling the token​
- Store it as a secret — environment variable or secret manager, never in source control.
- It is shown once, in the
201that created the destination.GET /partner/webhooknever returns it. If you lose it, delete the destination and register again to get a new one. - Verify before processing, and reject with
401rather than doing partial work. - Use HTTPS, which banca.me requires at registration anyway. Unlike a signature, the token is the credential itself: anything that can read the header can impersonate us to you.
Because the credential travels in a header on every delivery, a proxy, APM agent or request logger that captures headers will capture the token too. Check that your ingress does not log it.
Next Steps​
- The events you will receive, field by field: Event Catalog
- Timeouts and retries, which differ per event: Best Practices