EPaySe
Security
Recommended

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.

MethodSecurityDescription
HMAC-SHA256
Recommended
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

1

Extract Headers

Get X-Webhook-Signature and X-Webhook-Timestamp from the request headers

2

Validate Timestamp

Ensure timestamp is within 5 minutes of current time to prevent replay attacks

3

Build Signed Payload

Concatenate timestamp and raw body with a period: timestamp.body

4

Compute Signature

Generate expected signature: HMAC-SHA256(secret, signedPayload)

5

Timing-Safe Compare

Use timing-safe comparison to prevent timing attacks

Code Examples

PHP
PHP Implementation
<?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.

JavaScript
Bearer Token Verification
// 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.

JavaScript
API Key Verification
// 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.

JavaScript
Basic Auth Verification
// 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.