Webhooks in meCash are signed with HMAC-SHA256 to ensure authenticity and integrity.
This guide shows you how to verify webhook signatures in different programming languages.
Overview
Each webhook request includes a signature in the X-meCash-Signature header. You must verify this signature to ensure the webhook came from meCash and wasn’t tampered with.
X-meCash-Signature: sha256=<signature>
Getting Your Webhook Secret
- Log into your meCash dashboard
- Navigate to Developer → Webhooks
- Find your webhook endpoint
- Copy the webhook secret
Keep your webhook secret secure and never expose it in client-side code.
Verification Process
- Extract the signature from the
X-meCash-Signature header
- Create a payload string from the request body
- Generate HMAC-SHA256 using your webhook secret
- Compare signatures using a constant-time comparison
Code Examples
Node.js
const crypto = require('crypto');
function verifyWebhookSignature(payload, signature, secret) {
// Remove 'sha256=' prefix from signature
const receivedSignature = signature.replace('sha256=', '');
// Create HMAC-SHA256 signature
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(payload, 'utf8')
.digest('hex');
// Use constant-time comparison to prevent timing attacks
return crypto.timingSafeEqual(
Buffer.from(receivedSignature, 'hex'),
Buffer.from(expectedSignature, 'hex')
);
}
// Express.js middleware example
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-mecash-signature'];
const webhookSecret = process.env.MECASH_WEBHOOK_SECRET;
if (!signature) {
return res.status(400).send('Missing signature');
}
const payload = req.body;
const isValid = verifyWebhookSignature(payload, signature, webhookSecret);
if (!isValid) {
return res.status(401).send('Invalid signature');
}
// Process webhook
const event = JSON.parse(payload);
console.log('Received webhook:', event);
res.status(200).send('OK');
});
Python
import hmac
import hashlib
import json
from flask import Flask, request, abort
app = Flask(__name__)
def verify_webhook_signature(payload, signature, secret):
"""Verify webhook signature"""
# Remove 'sha256=' prefix
received_signature = signature.replace('sha256=', '')
# Create expected signature
expected_signature = hmac.new(
secret.encode('utf-8'),
payload,
hashlib.sha256
).hexdigest()
# Use constant-time comparison
return hmac.compare_digest(received_signature, expected_signature)
@app.route('/webhook', methods=['POST'])
def webhook():
signature = request.headers.get('X-meCash-Signature')
webhook_secret = os.getenv('MECASH_WEBHOOK_SECRET')
if not signature:
abort(400, 'Missing signature')
payload = request.get_data()
is_valid = verify_webhook_signature(payload, signature, webhook_secret)
if not is_valid:
abort(401, 'Invalid signature')
# Process webhook
event = json.loads(payload)
print(f'Received webhook: {event}')
return 'OK', 200
PHP
<?php
function verifyWebhookSignature($payload, $signature, $secret) {
// Remove 'sha256=' prefix
$receivedSignature = str_replace('sha256=', '', $signature);
// Create expected signature
$expectedSignature = hash_hmac('sha256', $payload, $secret);
// Use hash_equals for constant-time comparison
return hash_equals($receivedSignature, $expectedSignature);
}
// Handle webhook request
$signature = $_SERVER['HTTP_X_MECASH_SIGNATURE'] ?? '';
$webhookSecret = $_ENV['MECASH_WEBHOOK_SECRET'];
$payload = file_get_contents('php://input');
if (empty($signature)) {
http_response_code(400);
echo 'Missing signature';
exit;
}
$isValid = verifyWebhookSignature($payload, $signature, $webhookSecret);
if (!$isValid) {
http_response_code(401);
echo 'Invalid signature';
exit;
}
// Process webhook
$event = json_decode($payload, true);
echo 'OK';
?>
Ruby
require 'openssl'
require 'sinatra'
require 'json'
def verify_webhook_signature(payload, signature, secret)
# Remove 'sha256=' prefix
received_signature = signature.gsub('sha256=', '')
# Create expected signature
expected_signature = OpenSSL::HMAC.hexdigest(
'sha256',
secret,
payload
)
# Use secure comparison
secure_compare(received_signature, expected_signature)
end
def secure_compare(a, b)
return false if a.length != b.length
result = 0
a.bytes.zip(b.bytes) { |x, y| result |= x ^ y }
result == 0
end
post '/webhook' do
signature = request.env['HTTP_X_MECASH_SIGNATURE']
webhook_secret = ENV['MECASH_WEBHOOK_SECRET']
payload = request.body.read
if signature.nil? || signature.empty?
status 400
return 'Missing signature'
end
unless verify_webhook_signature(payload, signature, webhook_secret)
status 401
return 'Invalid signature'
end
# Process webhook
event = JSON.parse(payload)
puts "Received webhook: #{event}"
'OK'
end
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"net/http"
"strings"
"time"
)
func verifyWebhookSignature(payload, signature, secret string) bool {
// Remove 'sha256=' prefix
receivedSignature := strings.TrimPrefix(signature, "sha256=")
// Create expected signature
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(payload))
expectedSignature := hex.EncodeToString(mac.Sum(nil))
// Use constant-time comparison
return hmac.Equal([]byte(receivedSignature), []byte(expectedSignature))
}
func webhookHandler(w http.ResponseWriter, r *http.Request) {
signature := r.Header.Get("X-meCash-Signature")
webhookSecret := os.Getenv("MECASH_WEBHOOK_SECRET")
if signature == "" {
http.Error(w, "Missing signature", http.StatusBadRequest)
return
}
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "Error reading body", http.StatusBadRequest)
return
}
if !verifyWebhookSignature(string(body), signature, webhookSecret) {
http.Error(w, "Invalid signature", http.StatusUnauthorized)
return
}
// Process webhook
fmt.Println("Received webhook:", string(body))
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
}
func main() {
http.HandleFunc("/webhook", webhookHandler)
http.ListenAndServe(":8080", nil)
}
Testing Webhook Verification
Test Your Implementation
# Test webhook signature verification
curl -X POST http://localhost:8080/webhook \
-H "Content-Type: application/json" \
-H "X-meCash-Signature: sha256=test_signature" \
-d '{"test": "data"}'
Expected Responses
- Missing signature: 400 Bad Request.
- Invalid signature: 401 Unauthorized.
- Valid signature: 200 OK.
Security Best Practices
1. Always Verify Signatures
Never process webhooks without signature verification:
// ❌ Bad: Processing webhook without verification
app.post('/webhook', (req, res) => {
const event = req.body;
// Process webhook without verification
});
// ✅ Good: Verify signature first
app.post('/webhook', (req, res) => {
const signature = req.headers['x-mecash-signature'];
const isValid = verifyWebhookSignature(req.body, signature, secret);
if (!isValid) {
return res.status(401).send('Invalid signature');
}
// Process webhook
});
2. Use Constant-Time Comparison
Always use constant-time comparison to prevent timing attacks:
// ❌ Bad: Using regular string comparison
if (receivedSignature === expectedSignature) {
// Vulnerable to timing attacks
}
// ✅ Good: Using constant-time comparison
if (crypto.timingSafeEqual(
Buffer.from(receivedSignature, 'hex'),
Buffer.from(expectedSignature, 'hex')
)) {
// Secure comparison
}
3. Store Secrets Securely
// ❌ Bad: Hardcoded secret
const webhookSecret = "whsec_1234567890abcdef";
// ✅ Good: Environment variable
const webhookSecret = process.env.MECASH_WEBHOOK_SECRET;
4. Handle Raw Request Body
Make sure to use the raw request body for signature verification:
// Express.js: Use raw middleware
app.use('/webhook', express.raw({ type: 'application/json' }));
// Or use body-parser with raw option
app.use(bodyParser.raw({ type: 'application/json' }));
Common Issues
Issue 1: Signature Mismatch
Problem: Signature verification always fails.
Solutions:
- Check that you’re using the raw request body.
- Verify the webhook secret is correct.
- Ensure you’re removing the ‘sha256=’ prefix.
Problem: X-meCash-Signature header is missing.
Solutions:
- Check your webhook endpoint configuration.
- Verify the header name is correct (case-sensitive).
- Ensure your server is receiving all headers.
Issue 3: Body Parsing Issues
Problem: Request body is modified before verification.
Solutions:
- Use raw body parsing for webhook endpoints.
- Don’t use JSON middleware before signature verification.
- Parse JSON after signature verification.
Monitoring & Debugging
Log Signature Verification
function verifyWebhookSignature(payload, signature, secret) {
const receivedSignature = signature.replace('sha256=', '');
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(payload, 'utf8')
.digest('hex');
console.log('Received signature:', receivedSignature);
console.log('Expected signature:', expectedSignature);
const isValid = crypto.timingSafeEqual(
Buffer.from(receivedSignature, 'hex'),
Buffer.from(expectedSignature, 'hex')
);
console.log('Signature valid:', isValid);
return isValid;
}
Create a simple test endpoint:
app.post('/test-webhook', (req, res) => {
const signature = req.headers['x-mecash-signature'];
const payload = JSON.stringify(req.body);
const secret = process.env.MECASH_WEBHOOK_SECRET;
const isValid = verifyWebhookSignature(payload, signature, secret);
res.json({
signature,
payload,
isValid,
timestamp: new Date().toISOString()
});
});