MyTPE is getting a fresh new look! A simpler, faster platform is on the way. Learn more →
MyTPEMyTPE Pay
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

  1. Always verify signatures — Never process unsigned webhooks
  2. Respond quickly (< 10s) — Process asynchronously via a queue
  3. Handle duplicates — Use delivery_id to deduplicate
  4. Use HTTPS — Never use plain HTTP for webhook endpoints
  5. Store the secret securely — Use environment variables, not code
  6. Log everything — Log delivery IDs, events, and processing results
  7. Handle all events — At minimum handle completed and failed

On this page