{{ __('Webhooks (IPN)') }}

{{ __(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.') }}

{{ __('Environment-Aware Webhooks') }}

{{ __('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.') }}

{{ __('Reliable Delivery') }} {{ __(setting('site_title').' retries timeout webhook deliveries up to 3 attempts. Your endpoint should return a 2xx response as soon as the payload is safely queued or processed.') }}

{{ __('Webhook Headers') }}

{{ __('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

{{ __('Webhook Payload') }}

{{ __('All webhook payloads include environment information to help you differentiate between sandbox and production transactions') }}:

{{ __('Environment Context:') }} 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
}

{{ __('Signature Verification') }}

{{ __('Always verify webhook signatures to ensure authenticity and prevent unauthorized requests. Use your API secret (environment-specific) to verify signatures') }}:

{{ __('Environment-Specific Secrets:') }} {{ __('Use') }} 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.') }}

{{ __('Environment-Specific Best Practices') }}

{{ __('Sandbox Webhooks') }}
  • {{ __('Use for testing webhook integration') }}
  • {{ __('No real money transactions') }}
  • {{ __('Don\'t fulfill actual orders') }}
  • {{ __('Don\'t send emails to real customers') }}
  • {{ __('Use test webhook secret for verification') }}
{{ __('Production Webhooks') }}
  • {{ __('Process real customer orders') }}
  • {{ __('Send confirmation emails') }}
  • {{ __('Update inventory systems') }}
  • {{ __('Trigger fulfillment processes') }}
  • {{ __('Use production webhook secret for verification') }}