Signature Verification
Verify webhook authenticity using HMAC-SHA256 signatures
Why Verify?
Always verify webhook signatures before processing. This ensures the webhook genuinely originated from EPaySe and hasn't been tampered with.
Authenticity
Confirm webhooks are from EPaySe, not a malicious actor attempting to spoof events
Integrity
Ensure the payload hasn't been modified in transit between EPaySe and your server
Replay Protection
Timestamp validation prevents attackers from replaying old webhook requests
Verification Methods
EPaySe supports 6 verification methods. Choose based on your security requirements.
| Method | Security | Description |
|---|---|---|
HMAC-SHA256Recommended | Cryptographic signature verification - most secure option | |
Bearer Token | OAuth-style token authentication via Authorization header | |
API Key | Simple header-based authentication with X-API-Key | |
Basic Auth | HTTP Basic authentication with username and password | |
Custom Header | User-defined header name and value for custom auth schemes | |
None | No verification - only use for testing in sandbox environment |
HMAC-SHA256 Verification
HMAC-SHA256 provides the strongest security by signing the payload with a shared secret.
5 Verification Steps
Extract Headers
Get X-Webhook-Signature and X-Webhook-Timestamp from the request headers
Validate Timestamp
Ensure timestamp is within 5 minutes of current time to prevent replay attacks
Build Signed Payload
Concatenate timestamp and raw body with a period: timestamp.body
Compute Signature
Generate expected signature: HMAC-SHA256(secret, signedPayload)
Timing-Safe Compare
Use timing-safe comparison to prevent timing attacks
Code Examples
<?php
// Webhook Signature Verification - PHP
function verifyWebhookSignature(Request $request): bool
{
// Step 1: Get headers
$signature = $request->header('X-Webhook-Signature');
$timestamp = $request->header('X-Webhook-Timestamp');
if (!$signature || !$timestamp) {
return false;
}
// Step 2: Check timestamp (prevent replay attacks)
$tolerance = 300; // 5 minutes
if (abs(time() - (int)$timestamp) > $tolerance) {
return false;
}
// Step 3: Build signed payload
$payload = $request->getContent();
$signedPayload = $timestamp . '.' . $payload;
// Step 4: Compute expected signature
$secret = config('services.epayse.webhook_secret');
$expectedSignature = hash_hmac('sha256', $signedPayload, $secret);
// Step 5: Compare signatures (timing-safe)
return hash_equals($expectedSignature, $signature);
}
// Usage in controller
public function handleWebhook(Request $request)
{
if (!verifyWebhookSignature($request)) {
return response('Invalid signature', 401);
}
$event = json_decode($request->getContent(), true);
switch ($event['type']) {
case 'payment.succeeded':
// Handle successful payment
break;
case 'refund.completed':
// Handle refund
break;
}
return response('OK', 200);
}Alternative Methods
If HMAC-SHA256 doesn't fit your use case, you can use simpler verification methods.
Bearer Token
Verify the token in the Authorization header. The token is configured in your dashboard.
// Bearer Token verification
app.post('/webhooks/epayse', (req, res) => {
const authHeader = req.headers['authorization'];
const expectedToken = process.env.WEBHOOK_BEARER_TOKEN;
if (authHeader !== `Bearer ${expectedToken}`) {
return res.status(401).send('Unauthorized');
}
// Process webhook...
res.status(200).send('OK');
});API Key
Verify the X-API-Key header value. Simpler but less secure than HMAC.
// API Key verification
app.post('/webhooks/epayse', (req, res) => {
const apiKey = req.headers['x-api-key'];
const expectedKey = process.env.WEBHOOK_API_KEY;
if (apiKey !== expectedKey) {
return res.status(401).send('Unauthorized');
}
// Process webhook...
res.status(200).send('OK');
});Basic Auth
Verify HTTP Basic Auth credentials. Suitable for integration with legacy systems.
// Basic Auth verification
app.post('/webhooks/epayse', (req, res) => {
const authHeader = req.headers['authorization'];
if (!authHeader || !authHeader.startsWith('Basic ')) {
return res.status(401).send('Unauthorized');
}
const credentials = Buffer.from(
authHeader.slice(6),
'base64'
).toString();
const [username, password] = credentials.split(':');
if (username !== process.env.WEBHOOK_USER ||
password !== process.env.WEBHOOK_PASSWORD) {
return res.status(401).send('Unauthorized');
}
// Process webhook...
res.status(200).send('OK');
});Security Best Practices
Timing-Safe Comparison
Use hash_equals (PHP), timingSafeEqual (Node.js), or compare_digest (Python) to prevent timing attacks.
Timestamp Validation
Reject webhooks with timestamps older than 5 minutes to prevent replay attacks.
Rotate Secrets Periodically
Rotate your webhook secret every 90 days or immediately if compromised.
Always Use HTTPS
Webhook endpoints must use HTTPS. HTTP endpoints will be rejected.
Troubleshooting
Common webhook verification issues and how to resolve them
Signature Mismatch
Using parsed JSON instead of raw body, or incorrect encoding.
Solution:
Ensure you're using the raw request body and UTF-8 encoding for all strings.
Timestamp Expired
Server clock is out of sync or webhook was delayed.
Solution:
Sync your server clock with NTP and increase tolerance if needed.
Encoding Issues
Special characters or Unicode not handled correctly.
Solution:
Ensure all strings use consistent UTF-8 encoding.
Wrong Secret
Using incorrect webhook secret or secret was rotated.
Solution:
Check the secret in your dashboard and update your code.
