It's important to always verify the authenticity of webhook requests to ensure they are coming from Paisr. To verify that a webhook was sent by Paisr, each request includes the following headers:
- X-PCB-Signature: A signature of the request payload, used to verify the authenticity of the request.
- X-PCB-Timestamp: The timestamp of the request, used to verify the request was not tampered with.
Other unique headers include:
- User Agent: PCB Events
- Powered By: PAISR Technologies NV
HMAC Verification
The X-PCB-Signature header is an HMAC-SHA256 signature generated using your Webhook Secret. This helps protect your webhook endpoints from spoofed or replayed requests.
To verify the signature:
- Retrieve the X-PCB-Signature and X-PCB-Timestamp headers from the request.
- Compute the HMAC signature using your Webhook Secret and the request body.
- Compare the computed signature to the X-PCB-Signature header.
- If the signatures match, the request is verified as coming from Paisr.
signature = HMAC_SHA256(secret, timestamp + "." + body);The most important step is to always sign and verify the exact raw JSON body of the request. Any re-stringification of the body will alter the payload and invalidate the signature.
Examples
Below are some examples on how to verify a webhook requests in different languages:
Node.js
const crypto = require("crypto");
function verifyWebhook(req, secret) {
const signature = req.header("X-PCB-Signature");
const timestamp = req.header("X-PCB-Timestamp");
const rawBody = req.rawBody; // Ensure middleware captures raw body
const payload = `${timestamp}.${rawBody}`;
const hmac = crypto
.createHmac("sha256", secret)
.update(payload)
.digest("hex");
return crypto.timingSafeEqual(Buffer.from(hmac), Buffer.from(signature));
}Go
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"net/http"
)
func verifyWebhook(r *http.Request, secret []byte, rawBody []byte) bool {
signature := r.Header.Get("X-PCB-Signature")
timestamp := r.Header.Get("X-PCB-Timestamp")
message := append([]byte(timestamp+"."), rawBody...)
mac := hmac.New(sha256.New, secret)
mac.Write(message)
expectedMAC := mac.Sum(nil)
signatureBytes, err := hex.DecodeString(signature)
if err != nil {
return false
}
return hmac.Equal(expectedMAC, signatureBytes)
}Rust
use hmac::{Hmac, Mac};
use sha2::Sha256;
use hex;
type HmacSha256 = Hmac<Sha256>;
fn verify_webhook(raw_body: &str, timestamp: &str, signature: &str, secret: &str) -> bool {
let payload = format!("{}.{}", timestamp, raw_body);
let mut mac = HmacSha256::new_from_slice(secret.as_bytes()).unwrap();
mac.update(payload.as_bytes());
match hex::decode(signature) {
Ok(signature_bytes) => mac.verify_slice(&signature_bytes).is_ok(),
Err(_) => false,
}
}Python
import hmac
import hashlib
def verify_webhook(raw_body: bytes, timestamp: str, signature: str, secret: str) -> bool:
payload = f"{timestamp}.{raw_body.decode('utf-8')}".encode('utf-8')
expected_signature = hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected_signature, signature)PHP
function verifyWebhook($rawBody, $timestamp, $signature, $secret) {
$payload = $timestamp . '.' . $rawBody;
$expected = hash_hmac('sha256', $payload, $secret);
return hash_equals($expected, $signature);
}