Webhooks
Integration Guide
Step-by-step guide to integrate MyTPE Pay webhooks in PHP, Node.js, Python, and Laravel.
Integration Guide
This guide shows how to receive and process MyTPE Pay webhooks in different languages and frameworks.
PHP (Vanilla)
<?php
$secret = $_ENV['MYTPEPAY_WEBHOOK_SECRET']; // whsec_...
// Read headers
$signature = $_SERVER['HTTP_X_MYTPEPAY_SIGNATURE'] ?? '';
$timestamp = $_SERVER['HTTP_X_MYTPEPAY_TIMESTAMP'] ?? '';
$deliveryId = $_SERVER['HTTP_X_MYTPEPAY_DELIVERY_ID'] ?? '';
$event = $_SERVER['HTTP_X_MYTPEPAY_EVENT'] ?? '';
// Read body
$body = file_get_contents('php://input');
// 1. Replay protection (reject if older than 5 minutes)
if (abs(time() - (int)$timestamp) > 300) {
http_response_code(403);
exit('Timestamp too old');
}
// 2. Verify signature
$expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $body, $secret);
if (!hash_equals($expected, $signature)) {
http_response_code(403);
exit('Invalid signature');
}
// 3. Parse and process
$payload = json_decode($body, true);
$data = $payload['data'];
switch ($event) {
case 'transaction.completed':
// Payment successful!
$orderId = $data['order_number'];
$amount = $data['amount'];
$customer = $data['customer'];
// Update your database, send confirmation, etc.
break;
case 'transaction.failed':
// Payment failed
// Notify customer, release inventory
break;
case 'transaction.refused':
// Payment refused by bank
break;
}
// Always respond 200 quickly
http_response_code(200);
echo json_encode(['received' => true]);Laravel
<?php
// routes/api.php
Route::post('/webhooks/mytpe', [WebhookController::class, 'handle']);
// app/Http/Controllers/WebhookController.php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class WebhookController extends Controller
{
public function handle(Request $request)
{
$secret = config('services.mytpepay.webhook_secret');
$signature = $request->header('X-MytpePay-Signature');
$timestamp = $request->header('X-MytpePay-Timestamp');
$event = $request->header('X-MytpePay-Event');
// Replay protection
if (abs(time() - (int)$timestamp) > 300) {
return response('Timestamp too old', 403);
}
// Verify signature
$body = $request->getContent();
$expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $body, $secret);
if (!hash_equals($expected, $signature)) {
return response('Invalid signature', 403);
}
// Process event
$data = $request->input('data');
match ($event) {
'transaction.completed' => $this->handleCompleted($data),
'transaction.failed' => $this->handleFailed($data),
'transaction.refused' => $this->handleRefused($data),
default => null,
};
return response()->json(['received' => true]);
}
private function handleCompleted(array $data): void
{
// Mark order as paid
// Order::where('reference', $data['order_number'])->update(['status' => 'paid']);
}
private function handleFailed(array $data): void
{
// Handle failed payment
}
private function handleRefused(array $data): void
{
// Handle refused payment
}
}Disable CSRF for webhook routes
In Laravel, exclude your webhook route from CSRF verification in app/Http/Middleware/VerifyCsrfToken.php:
protected $except = [
'webhooks/mytpe',
];Node.js (Express)
const express = require('express');
const crypto = require('crypto');
const app = express();
app.use('/webhooks/mytpe', express.raw({ type: 'application/json' }));
const WEBHOOK_SECRET = process.env.MYTPEPAY_WEBHOOK_SECRET;
app.post('/webhooks/mytpe', (req, res) => {
const signature = req.headers['x-mytpepay-signature'];
const timestamp = req.headers['x-mytpepay-timestamp'];
const event = req.headers['x-mytpepay-event'];
const deliveryId = req.headers['x-mytpepay-delivery-id'];
const body = req.body.toString();
// Replay protection
if (Math.abs(Date.now() / 1000 - parseInt(timestamp)) > 300) {
return res.status(403).send('Timestamp too old');
}
// Verify signature
const expected = 'sha256=' + crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(timestamp + '.' + body)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))) {
return res.status(403).send('Invalid signature');
}
// Process event
const payload = JSON.parse(body);
const data = payload.data;
switch (event) {
case 'transaction.completed':
console.log(`Payment received: ${data.amount} DA from ${data.customer.name}`);
// Update your database
break;
case 'transaction.failed':
console.log(`Payment failed: ${data.order_number}`);
break;
}
res.json({ received: true });
});
app.listen(3000);Python (Flask)
import hmac
import hashlib
import time
import json
from flask import Flask, request, jsonify
app = Flask(__name__)
WEBHOOK_SECRET = os.environ['MYTPEPAY_WEBHOOK_SECRET']
@app.route('/webhooks/mytpe', methods=['POST'])
def handle_webhook():
signature = request.headers.get('X-MytpePay-Signature', '')
timestamp = request.headers.get('X-MytpePay-Timestamp', '')
event = request.headers.get('X-MytpePay-Event', '')
body = request.get_data(as_text=True)
# Replay protection
if abs(time.time() - int(timestamp)) > 300:
return 'Timestamp too old', 403
# Verify signature
expected = 'sha256=' + hmac.new(
WEBHOOK_SECRET.encode(),
f'{timestamp}.{body}'.encode(),
hashlib.sha256
).hexdigest()
if not hmac.compare_digest(expected, signature):
return 'Invalid signature', 403
# Process
payload = json.loads(body)
data = payload['data']
if event == 'transaction.completed':
print(f"Payment: {data['amount']} DA - {data['customer']['name']}")
# Update your database
return jsonify({'received': True})Best Practices
Production Checklist
- Always verify signatures — Never process unsigned webhooks
- Respond quickly (< 10s) — Process asynchronously via a queue
- Handle duplicates — Use
delivery_idto deduplicate - Use HTTPS — Never use plain HTTP for webhook endpoints
- Store the secret securely — Use environment variables, not code
- Log everything — Log delivery IDs, events, and processing results
- Handle all events — At minimum handle
completedandfailed