Signature Verification
All webhook requests are signed using HMAC-SHA256. Always verify the signature to ensure the request is from Esca.
Signature Format
The signature is included in the X-Esca-Webhook-Signature header:
X-Esca-Webhook-Signature: t=1705574400,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
| Component | Description |
|---|---|
t |
Unix timestamp when the webhook was sent |
v1 |
HMAC-SHA256 signature |
Verification Steps
- Extract the timestamp (
t) and signature (v1) from the header - Construct the signed payload:
{timestamp}.{JSON payload} - Compute the expected signature using HMAC-SHA256 with your webhook secret
- Compare signatures using a timing-safe comparison
- Optionally, reject requests with timestamps older than 5 minutes (replay attack protection)
Node.js Example
const crypto = require('crypto');
function verifyWebhookSignature(payload, signature, secret, toleranceSeconds = 300) {
// Parse the signature header
const parts = signature.split(',').reduce((acc, part) => {
const [key, value] = part.split('=');
acc[key] = value;
return acc;
}, {});
const timestamp = parseInt(parts.t, 10);
const receivedSignature = parts.v1;
// Check timestamp tolerance (prevent replay attacks)
const currentTime = Math.floor(Date.now() / 1000);
if (Math.abs(currentTime - timestamp) > toleranceSeconds) {
throw new Error('Webhook timestamp too old');
}
// Calculate expected signature
const signedPayload = `${timestamp}.${JSON.stringify(payload)}`;
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
// Timing-safe comparison
const sigBuffer = Buffer.from(receivedSignature, 'hex');
const expectedBuffer = Buffer.from(expectedSignature, 'hex');
if (sigBuffer.length !== expectedBuffer.length) {
throw new Error('Invalid webhook signature');
}
if (!crypto.timingSafeEqual(sigBuffer, expectedBuffer)) {
throw new Error('Invalid webhook signature');
}
return true;
}
// Express.js endpoint example
app.post('/webhooks/esca', express.json(), (req, res) => {
const signature = req.headers['x-esca-webhook-signature'];
const secret = process.env.ESCA_WEBHOOK_SECRET;
try {
verifyWebhookSignature(req.body, signature, secret);
// Process the webhook
const event = req.body;
console.log(`Received ${event.type} event:`, event.data);
// Handle different event types
switch (event.type) {
case 'virtual_account.credited':
// Handle deposit
break;
case 'transfer.completed':
// Handle successful transfer
break;
case 'transfer.failed':
// Handle failed transfer
break;
}
res.status(200).json({ received: true });
} catch (err) {
console.error('Webhook verification failed:', err.message);
res.status(400).json({ error: 'Invalid signature' });
}
});
Python Example
import hmac
import hashlib
import time
import json
def verify_webhook_signature(payload, signature, secret, tolerance_seconds=300):
# Parse the signature header
parts = dict(part.split('=') for part in signature.split(','))
timestamp = int(parts['t'])
received_signature = parts['v1']
# Check timestamp tolerance
current_time = int(time.time())
if abs(current_time - timestamp) > tolerance_seconds:
raise ValueError('Webhook timestamp too old')
# Calculate expected signature
signed_payload = f"{timestamp}.{json.dumps(payload, separators=(',', ':'))}"
expected_signature = hmac.new(
secret.encode('utf-8'),
signed_payload.encode('utf-8'),
hashlib.sha256
).hexdigest()
# Timing-safe comparison
if not hmac.compare_digest(received_signature, expected_signature):
raise ValueError('Invalid webhook signature')
return True
# Flask endpoint example
@app.route('/webhooks/esca', methods=['POST'])
def handle_webhook():
signature = request.headers.get('X-Esca-Webhook-Signature')
secret = os.environ.get('ESCA_WEBHOOK_SECRET')
try:
verify_webhook_signature(request.json, signature, secret)
event = request.json
print(f"Received {event['type']} event:", event['data'])
return jsonify({'received': True}), 200
except ValueError as e:
return jsonify({'error': str(e)}), 400
Common Verification Errors
| Error | Cause | Solution |
|---|---|---|
| Timestamp too old | Request took too long or replay attack | Check server time sync, increase tolerance if needed |
| Invalid signature | Wrong secret or payload modified | Verify you're using the correct webhook secret |
| Buffer length mismatch | Corrupted signature | Ensure header is not truncated |