{{ __(setting('site_title').' sends real-time notifications to your specified IPN URL when payment status changes. This ensures you\'re immediately notified of payment completions, failures, and other status updates.') }} {{ __('Webhooks work identically in both sandbox and production environments.') }}
{{ __('Use the same webhook URL for both sandbox and production. '.setting('site_title').' will include environment context in webhook payloads to help you differentiate between test and live transactions.') }}
| {{ __('Header') }} | {{ __('Description') }} | {{ __('Example') }} |
|---|---|---|
Content-Type |
{{ __('Always') }} application/json |
application/json |
X-Signature |
{{ __('Raw HMAC-SHA256 signature for verification') }} | a8b9c2d1e5f3... |
X-Environment |
{{ __('Webhook environment context') }} | sandbox | production |
{{ __('All webhook payloads include environment information to help you differentiate between sandbox and production transactions') }}:
environment {{ __('field will be') }} sandbox {{ __('for test transactions or') }} production {{ __('for live transactions. Transaction IDs are prefixed accordingly') }} (SANDBOX_ {{ __('or') }} PRODUCTION_).
{
"data": {
"ref_trx": "TXNT4AQFESTAG4F",
"description": "Order #1234",
"ipn_url": "https://webhook.site/5711b7d5-917a-4d94-bbb3-c28f4a37bea5",
"cancel_redirect": "https://merchant.com/cancel",
"success_redirect": "https://merchant.com/success",
"customer_name": "John Doe",
"customer_email": "john@example.com",
"merchant_name": "Xanthus Wiggins",
"amount": 200,
"currency_code": "USD",
"environment": "production",
"is_sandbox": false
},
"message": "{{ __('Payment Completed') }}",
"status": "{{ __('completed') }}",
"timestamp": 1705747245
}
{{ __('Always verify webhook signatures to ensure authenticity and prevent unauthorized requests. Use your API secret (environment-specific) to verify signatures') }}:
test_webhook_secret {{ __('for sandbox and') }} webhook_secret {{ __('for production environments.') }}
<?php
// {{ __('Laravel Webhook Handler') }}
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use App\Enums\EnvironmentMode;
class {{ setting('site_title') }}WebhookController extends Controller
{
public function handle(Request $request)
{
// {{ __('Get webhook headers') }}
$environment = $request->header('X-Environment', 'production');
$signature = $request->header('X-Signature');
// {{ __('Get appropriate secret based on environment') }}
$secret = $this->getSecretForEnvironment($environment);
// {{ __('Verify signature') }}
if (!$this->verifySignature($request->getContent(), $signature, $secret)) {
Log::warning('{{ __(setting("site_title")." webhook signature verification failed") }}', [
'environment' => $environment
]);
return response()->json(['error' => '{{ __('Invalid signature') }}'], 401);
}
$payload = $request->json()->all();
// {{ __('Handle based on environment') }}
if ($environment === EnvironmentMode::SANDBOX->value) {
return $this->handleSandboxWebhook($payload);
} else {
return $this->handleProductionWebhook($payload);
}
}
private function getSecretForEnvironment(string $environment): string
{
// {{ __('Return test secret for sandbox, live secret for production') }}
return $environment === 'sandbox'
? config('{{ strtolower(setting("site_title")) }}.test_webhook_secret')
: config('{{ strtolower(setting("site_title")) }}.webhook_secret');
}
private function verifySignature(string $payload, string $signature, string $secret): bool
{
$expectedSignature = hash_hmac('sha256', $payload, $secret);
$providedSignature = str_starts_with($signature, 'sha256=') ? substr($signature, 7) : $signature;
return hash_equals($expectedSignature, $providedSignature);
}
private function handleSandboxWebhook(array $payload): JsonResponse
{
Log::info('{{ __('Processing sandbox webhook') }}', $payload);
// {{ __('Your sandbox-specific logic here') }}
// {{ __('Don\'t fulfill orders, don\'t send emails to real customers, etc.') }}
return response()->json(['status' => '{{ __('sandbox_processed') }}']);
}
private function handleProductionWebhook(array $payload): JsonResponse
{
Log::info('{{ __('Processing production webhook') }}', $payload);
// {{ __('Your production logic here') }}
// {{ __('Fulfill orders, send confirmation emails, etc.') }}
return response()->json(['status' => '{{ __('processed') }}']);
}
}
const crypto = require('crypto');
const express = require('express');
const EnvironmentMode = {
SANDBOX: 'sandbox',
PRODUCTION: 'production'
};
// {{ __('Webhook handler') }}
app.post('/api/webhooks/{{ strtolower(setting("site_title")) }}', async (req, res) => {
const environment = req.headers['x-environment'] || 'production';
const signature = req.headers['x-signature'];
# {{ __('Get appropriate secret based on environment') }}
const secret = getSecretForEnvironment(environment);
# {{ __('Verify signature') }}
if (!verifySignature(JSON.stringify(req.body), signature, secret)) {
console.warn('{{ __(setting("site_title")." webhook signature verification failed") }}', {
environment: environment
});
return res.status(401).json({ error: '{{ __('Invalid signature') }}' });
}
const payload = req.body;
try {
# {{ __('Handle based on environment') }}
if (environment === EnvironmentMode.SANDBOX) {
await handleSandboxWebhook(payload);
} else {
await handleProductionWebhook(payload);
}
res.json({ status: '{{ __('processed') }}' });
} catch (error) {
console.error('{{ __('Webhook processing error') }}:', error);
res.status(500).json({ error: '{{ __('Processing failed') }}' });
}
});
function getSecretForEnvironment(environment) {
# {{ __('Return test secret for sandbox, live secret for production') }}
return environment === 'sandbox'
? process.env.{{ strtoupper(setting("site_title")) }}_TEST_WEBHOOK_SECRET
: process.env.{{ strtoupper(setting("site_title")) }}_WEBHOOK_SECRET;
}
function verifySignature(payload, signature, secret) {
if (!signature) {
return false;
}
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
const providedSignature = signature.startsWith('sha256=') ? signature.slice(7) : signature;
return crypto.timingSafeEqual(
Buffer.from(expectedSignature),
Buffer.from(providedSignature)
);
}
async function handleSandboxWebhook(payload) {
console.log('{{ __('Processing sandbox webhook') }}:', payload);
# {{ __('Your sandbox-specific logic here') }}
# {{ __('Don\'t fulfill orders, don\'t send emails to real customers, etc.') }}
}
async function handleProductionWebhook(payload) {
console.log('{{ __('Processing production webhook') }}:', payload);
# {{ __('Your production logic here') }}
# {{ __('Fulfill orders, send confirmation emails, etc.') }}
}
import hmac
import hashlib
import json
import logging
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_http_methods
logger = logging.getLogger(__name__)
ENVIRONMENT_MODE = {
'SANDBOX': 'sandbox',
'PRODUCTION': 'production'
}
@csrf_exempt
@require_http_methods(["POST"])
def {{ strtolower(setting("site_title")) }}_webhook(request):
environment = request.headers.get('X-Environment', 'production')
signature = request.headers.get('X-Signature', '')
# {{ __('Get appropriate secret based on environment') }}
secret = get_secret_for_environment(environment)
# {{ __('Verify signature') }}
if not verify_signature(request.body, signature, secret):
logger.warning('{{ __(setting("site_title")." webhook signature verification failed") }}', extra={
'environment': environment
})
return JsonResponse({'error': '{{ __('Invalid signature') }}'}, status=401)
try:
payload = json.loads(request.body)
# {{ __('Handle based on environment') }}
if environment == ENVIRONMENT_MODE['SANDBOX']:
handle_sandbox_webhook(payload)
else:
handle_production_webhook(payload)
return JsonResponse({'status': '{{ __('processed') }}'})
except Exception as e:
logger.error(f'{{ __('Webhook processing error') }}:{str(e)}')
return JsonResponse({'error': '{{ __('Processing failed') }}'}, status=500)
def get_secret_for_environment(environment):
from django.conf import settings
# {{ __('Return test secret for sandbox, live secret for production') }}
return (settings.{{ strtoupper(setting("site_title")) }}_TEST_WEBHOOK_SECRET
if environment == 'sandbox'
else settings.{{ strtoupper(setting("site_title")) }}_WEBHOOK_SECRET)
def verify_signature(payload, signature, secret):
if not signature:
return False
expected_signature = hmac.new(
secret.encode('utf-8'),
payload,
hashlib.sha256
).hexdigest()
provided_signature = signature[7:] if signature.startswith('sha256=') else signature
return hmac.compare_digest(expected_signature, provided_signature)
def handle_sandbox_webhook(payload):
logger.info('{{ __('Processing sandbox webhook') }}', extra=payload)
# {{ __('Your sandbox-specific logic here') }}
# {{ __('Don\'t fulfill orders, don\'t send emails to real customers, etc.') }}
def handle_production_webhook(payload):
logger.info('{{ __('Processing production webhook') }}', extra=payload)
# {{ __('Your production logic here') }}
# {{ __('Fulfill orders, send confirmation emails, etc.') }}