Webhook Signature Verification
Breeze signs every webhook event sent to your server using an HMAC-SHA256 signature. You should always verify this signature before trusting the webhook payload.
🚩 How Signature Verification Works
Step 1: Sort Data
Sort all keys in the webhook data payload alphabetically in ascending order and stringify them as JSON.
For example, given this webhook data:
{
"amount": 500,
"clientReferenceId": "testClientReferenceId",
"currency": "USD",
"pageId": "page_8d78100e18bc825f",
"status": "PAID"
}Sorted and stringified, the JSON string is:
{"amount":500,"clientReferenceId":"testClientReferenceId","currency":"USD","pageId":"page_8d78100e18bc825f","status":"PAID"}
Step 2: Generate Signature
Using your webhook secret, create an HMAC-SHA256 hash of this JSON string, then encode it to Base64.
For example, if your webhook secret is testwebhooksecret, your resulting signature is: afZiTJOjqNBTWTLVuP4/bhY1dwUNxo+P8z1Rb1fUPSU=
✅ Examples
💡 Don’t see your preferred language here? Just let us know at [email protected]—we’ll gladly help you set it up!
import stringify from "json-stable-stringify";
import { createHmac, timingSafeEqual } from "crypto";
// Express webhook handler
app.post("/webhook", (request, response) => {
const { type, signature, data } = request.body;
// Your webhook secret from dashboard.breeze.cash
const webhookSecret = "whook_sk_xxxxxx";
// Sort and stringify the JSON string
const webhookData = stringify(data);
if (!webhookData) {
console.error("Invalid webhook data!");
return response.status(400).send();
}
// Generate expected signature
const expectedSignature = createHmac("sha256", webhookSecret)
.update(webhookData)
.digest("base64");
// Securely compare signatures using timingSafeEqual
const signatureIsValid = timingSafeEqual(
Buffer.from(signature, "utf8"),
Buffer.from(expectedSignature, "utf8")
);
if (!signatureIsValid) {
console.error("Invalid webhook signature!");
return response.status(400).send();
}
// Handle the webhook payload
console.log(`type: ${type}`);
console.log(`data:`, data);
// Acknowledge successful processing to Breeze
response.status(200).send();
});import base64
import json
import hmac
import hashlib
from flask import Flask, request, abort
app = Flask(__name__)
# Your webhook secret from dashboard.breeze.cash
WEBHOOK_SECRET = 'whook_sk_xxxxxx'
def verify_signature(data, provided_signature, secret):
sorted_data = json.dumps(data, separators=(',', ':'), sort_keys=True)
expected_hmac = hmac.new(
secret.encode('utf-8'),
sorted_data.encode('utf-8'),
hashlib.sha256
).digest()
expected_signature = base64.b64encode(expected_hmac).decode('utf-8')
# Securely compare signatures to prevent timing attacks
return hmac.compare_digest(provided_signature, expected_signature)
@app.route('/webhook', methods=['POST'])
def webhook():
payload = request.get_json()
data = payload.get('data', {})
signature = payload.get('signature', '')
event_type = payload.get('type', '')
if not verify_signature(data, signature, WEBHOOK_SECRET):
print('Invalid webhook signature!')
abort(400)
# Signature is valid—process webhook event
print(f"Event Type: {event_type}")
print(f"Data: {data}")
# Acknowledge successful processing to Breeze
return '', 200
if __name__ == '__main__':
app.run(port=5000)<?php
// Your webhook secret from dashboard.breeze.cash
$webhookSecret = 'whook_sk_xxxxxx';
// Retrieve the raw POST body
$payload = file_get_contents('php://input');
$data = json_decode($payload, true);
// Extract the signature and data from the payload
$signature = $data['signature'] ?? '';
$eventType = $data['type'] ?? '';
$eventData = $data['data'] ?? [];
// Sort keys in $eventData
function ksort_recursive(array &$array): void {
foreach ($array as &$value) {
if (is_array($value)) {
ksort_recursive($value);
}
}
ksort($array);
}
ksort_recursive($eventData);
// Encode as JSON
$sortedJson = json_encode($eventData, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
// Compute HMAC SHA256 and Base64 encode it
$expectedSignature = base64_encode(hash_hmac('sha256', $sortedJson, $webhookSecret, true));
// Securely compare signatures
if (!hash_equals($expectedSignature, $signature)) {
error_log('Invalid webhook signature!');
http_response_code(400);
exit;
}
// Handle webhook event
error_log("Webhook received: Type: $eventType");
error_log("Data: " . print_r($eventData, true));
// Respond with HTTP 200 to acknowledge webhook
http_response_code(200);import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
import java.util.Map;
import java.util.TreeMap;
import com.google.gson.Gson;
import spark.Spark;
public class WebhookHandler {
private static final String WEBHOOK_SECRET = "whook_sk_xxxxxx";
private static class Payload {
public Map<String, Object> data;
public String signature;
public String type;
}
public static void main(String[] args) {
Spark.post("/webhook", (request, response) -> {
String body = request.body();
Gson gson = new Gson();
Payload payload = gson.fromJson(body, Payload.class);
if (payload == null || payload.data == null || payload.signature == null) {
response.status(400);
return "Invalid payload format: expected 'data' object and 'signature' string.";
}
if (!verifySignature(payload.data, payload.signature, WEBHOOK_SECRET)) {
response.status(400);
return "Invalid webhook signature!";
}
// Handle the webhook payload
System.out.println("Type: " + payload.type);
System.out.println("Data: " + payload.data);
// Acknowledge successful processing to Breeze
response.status(200);
return "Success";
});
}
private static boolean verifySignature(Map<String, Object> data, String providedSignature, String secret) throws Exception {
Object sortedData = deepSort(data);
String sortedJson = new Gson().toJson(sortedData);
Mac sha256Hmac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
sha256Hmac.init(secretKey);
byte[] hash = sha256Hmac.doFinal(sortedJson.getBytes(StandardCharsets.UTF_8));
String expectedSignature = Base64.getEncoder().encodeToString(hash);
return timingSafeEquals(expectedSignature, providedSignature);
}
private static boolean timingSafeEquals(String a, String b) {
if (a == null || b == null) {
return false;
}
byte[] aBytes = a.getBytes(StandardCharsets.UTF_8);
byte[] bBytes = b.getBytes(StandardCharsets.UTF_8);
if (aBytes.length != bBytes.length) {
return false;
}
int result = 0;
for (int i = 0; i < aBytes.length; i++) {
result |= aBytes[i] ^ bBytes[i];
}
return result == 0;
}
private static Object deepSort(Object value) {
if (value instanceof Map<?, ?> map) {
TreeMap<String, Object> sorted = new TreeMap<>();
for (Map.Entry<?, ?> entry : map.entrySet()) {
sorted.put(String.valueOf(entry.getKey()), deepSort(entry.getValue()));
}
return sorted;
}
if (value instanceof Iterable<?> iterable) {
java.util.List<Object> list = new java.util.ArrayList<>();
for (Object item : iterable) {
list.add(deepSort(item));
}
return list;
}
return value;
}
}📌 Important Notes
- Always compare signatures securely.
- Your webhook handler should respond with HTTP status 200 after successfully processing the webhook. Otherwise, Breeze will retry the webhook.
- Never expose your webhook secret publicly.
Updated about 2 months ago
