TagPayTagPay

Guides

Verify signatures

Every webhook delivery is signed with HMAC-SHA512 using your endpoint's signing secret. Verifying the signature is the only way to be sure a request came from TagPay and not from an attacker who guessed your URL.

Signing

TagPay computes HMAC-SHA512(raw_body, signing_secret), hex-encodes the result, and ships it in the x-tagpay-signature header. The secret is unique per webhook endpoint — find it in Dashboard → Developers → Webhooks → [your endpoint].

Verifying

Recompute the HMAC over the raw body with your signing secret, then compare in constant time against the header. Constant-time comparison prevents timing-attack leaks of the signature.

Node.js

import crypto from 'crypto';
export function verifyTagPaySignature(
rawBody: string,
signature: string,
secret: string,
): boolean {
const expected = crypto.createHmac('sha512', secret).update(rawBody).digest('hex');
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}

Python

import hmac
import hashlib
def verify_tagpay_signature(raw_body: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(secret.encode(), raw_body, hashlib.sha512).hexdigest()
return hmac.compare_digest(signature, expected)

PHP

<?php
function verify_tagpay_signature(string $rawBody, string $signature, string $secret): bool {
$expected = hash_hmac('sha512', $rawBody, $secret);
return hash_equals($signature, $expected);
}

Examples

Express middleware showing raw-body capture, signature verification, and a fire-and-forget enqueue so the handler can ack the webhook in under 15 seconds:

import express from 'express';
import { verifyTagPaySignature } from './verify';
const app = express();
// IMPORTANT: capture the raw body BEFORE express.json() parses it.
app.post(
'/webhooks/tagpay',
express.raw({ type: 'application/json' }),
(req, res) => {
const signature = req.header('x-tagpay-signature') ?? '';
const rawBody = req.body.toString('utf8');
if (!verifyTagPaySignature(rawBody, signature, process.env.TAGPAY_WEBHOOK_SECRET!)) {
return res.status(401).send('bad signature');
}
const event = JSON.parse(rawBody);
// enqueue, then ack
queue.enqueue(event);
res.status(200).send('ok');
},
);