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";

  // Generate expected signature
  const expectedSignature = createHmac("sha256", webhookSecret)
    .update(stringify(data))
    .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 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 and encode as JSON
ksort($eventData);
$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);

📌 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.