Testing Webhooks
Test and debug your webhook integration before going live
Test Mode vs Live Mode
Webhooks in sandbox environment have livemode: false in the payload
The livemode Field
Check the livemode field in the payload to distinguish between test and production webhooks
Send Test Webhook
Send a test webhook directly from your dashboard to verify your endpoint
Open Settings
Go to Settings → Webhooks in your merchant dashboard
Select Webhook
Click on the webhook configuration you want to test
Send Test
Click the "Send Test" button and check the response
Local Development
Test webhooks on your local machine using tunnel services like ngrok
ngrok Setup
Expose your local server to the internet with ngrok
# Install ngrok
brew install ngrok # macOS
# or download from https://ngrok.com/download
# Authenticate (one-time)
ngrok config add-authtoken YOUR_AUTH_TOKEN
# Start tunnel to your local server
ngrok http 8000
# You'll get a URL like: https://abc123.ngrok.io
# Use this URL as your webhook endpointTesting with cURL
Test your webhook endpoint manually by generating signed requests with cURL
Generate Test Signature
# Generate HMAC-SHA256 signature for testing
#!/bin/bash
TIMESTAMP=$(date +%s)
PAYLOAD='{"id":"evt_test_123","type":"test.webhook"}'
SECRET="your_webhook_secret"
# Build signed payload
SIGNED_PAYLOAD="${TIMESTAMP}.${PAYLOAD}"
# Generate signature
SIGNATURE=$(echo -n "$SIGNED_PAYLOAD" | openssl dgst -sha256 -hmac "$SECRET" | cut -d' ' -f2)
echo "X-Webhook-Timestamp: $TIMESTAMP"
echo "X-Webhook-Signature: $SIGNATURE"Send Test Request
# Send a test webhook to your local endpoint
curl -X POST http://localhost:8000/webhooks/epayse \
-H "Content-Type: application/json" \
-H "X-Webhook-Signature: your_computed_signature" \
-H "X-Webhook-Timestamp: $(date +%s)" \
-H "X-Webhook-Event-Id: evt_test_123" \
-H "X-Webhook-Event-Type: test.webhook" \
-d '{
"id": "evt_test_123",
"object": "event",
"type": "test.webhook",
"created": "2025-01-16T08:00:00Z",
"livemode": false,
"api_key_id": "client_test_123",
"data": {
"object": {
"message": "Test webhook successful!"
}
},
"api_version": "2024-01-01"
}'Unit Testing
Write automated tests to verify your webhook handler processes event types correctly
<?php
// tests/Feature/WebhookTest.php
namespace Tests\Feature;
use Tests\TestCase;
class WebhookTest extends TestCase
{
public function test_handles_payment_succeeded_webhook(): void
{
$payload = [
'id' => 'evt_test_123',
'type' => 'payment.succeeded',
'livemode' => false,
'data' => [
'object' => [
'id' => 'txn_test_123',
'amount' => 10000,
'status' => 'SUCCESS',
],
],
];
$timestamp = time();
$signedPayload = $timestamp . '.' . json_encode($payload);
$signature = hash_hmac('sha256', $signedPayload, config('services.epayse.webhook_secret'));
$response = $this->postJson('/webhooks/epayse', $payload, [
'X-Webhook-Signature' => $signature,
'X-Webhook-Timestamp' => $timestamp,
'X-Webhook-Event-Id' => 'evt_test_123',
'X-Webhook-Event-Type' => 'payment.succeeded',
]);
$response->assertStatus(200);
// Add more assertions...
}
public function test_rejects_invalid_signature(): void
{
$response = $this->postJson('/webhooks/epayse', [
'id' => 'evt_test_123',
'type' => 'payment.succeeded',
], [
'X-Webhook-Signature' => 'invalid_signature',
'X-Webhook-Timestamp' => time(),
]);
$response->assertStatus(401);
}
}Debugging & Logging
Log incoming webhooks to debug verification and processing issues
<?php
// Log incoming webhooks for debugging
public function handleWebhook(Request $request)
{
// Log all incoming webhooks (remove in production)
Log::channel('webhooks')->info('Webhook received', [
'headers' => $request->headers->all(),
'payload' => $request->all(),
'ip' => $request->ip(),
]);
// Verify signature...
if (!$this->verifySignature($request)) {
Log::channel('webhooks')->warning('Invalid webhook signature', [
'signature' => $request->header('X-Webhook-Signature'),
'timestamp' => $request->header('X-Webhook-Timestamp'),
]);
return response('Invalid signature', 401);
}
// Process webhook...
return response('OK', 200);
}Retry Mechanism
Failed webhooks are automatically retried with exponential backoff
| Attempt | Delay | Total Time |
|---|---|---|
Initial | Immediate | 0 seconds |
Retry 1 | 30 seconds | 30 seconds |
Retry 2 | 1 minute | 1.5 minutes |
Retry 3 | 5 minutes | 6.5 minutes |
Retry 4 | 30 minutes | 36.5 minutes |
Retry 5 | 1 hour | 1.5 hours |
Response Codes
How EPaySe interprets your webhook endpoint's response
2xx
Webhook delivered and processed successfully
Behavior: No retry, marked as delivered
4xx
Endpoint returned a 4xx error
Behavior: No retry (except 408, 429)
5xx
Endpoint returned a 5xx error
Behavior: Will retry according to schedule
Timeout
Endpoint did not respond within 30 seconds
Behavior: Will retry according to schedule
Testing Best Practices
Separate Environments
Use different webhook endpoints for sandbox and production
Log Everything
In development, log headers, payload, and verification results for easy debugging
Idempotent Processing
Test with duplicate webhooks to ensure your handler correctly handles already-processed events
Respond Quickly
Return 200 within 30 seconds, process asynchronously if more time is needed
