Skip to main content

Vue d’ensemble

Cette page contient des exemples complets d’implémentation de webhooks JEKO dans différents langages. Tous les exemples incluent :
  • Vérification de la signature HMAC-SHA256
  • Parsing du payload
  • Traitement des événements
  • Gestion d’erreurs

Node.js (Express)

const express = require('express');
const crypto = require('crypto');
const app = express();

// Middleware pour parser le body brut (important pour la vérification de signature)
app.use('/webhook', express.raw({ type: 'application/json' }));

const WEBHOOK_SECRET = process.env.JEKO_WEBHOOK_SECRET;

function verifySignature(rawBody, signature) {
  const expectedSignature = crypto
    .createHmac('sha256', WEBHOOK_SECRET)
    .update(rawBody)
    .digest('hex');
  
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  );
}

app.post('/webhook', async (req, res) => {
  try {
    // Vérifier la signature
    const signature = req.headers['jeko-signature'];
    if (!signature || !verifySignature(req.body, signature)) {
      return res.status(401).json({ error: 'Invalid signature' });
    }
    
    // Parser le payload
    const payload = JSON.parse(req.body.toString());
    
    // Traiter l'événement
    await handleWebhookEvent(payload);
    
    // Répondre rapidement
    res.status(200).json({ received: true });
  } catch (error) {
    console.error('Webhook error:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

async function handleWebhookEvent(payload) {
  const { event, data } = payload;
  
  if (event === 'transaction.completed') {
    await handleTransactionCompleted(data);
  } else {
    console.log('Unknown event type:', event);
  }
}

async function handleTransactionCompleted(data) {
  console.log('Transaction completed:', data.id);
  console.log('Type:', data.transactionType); // "payment" ou "transfer"
  console.log('Status:', data.status); // "success" ou "error"
  console.log('Amount:', data.amount);
  console.log('Fees:', data.fees);
  console.log('Store:', data.storeName);
  console.log('Business:', data.businessName);
  
  // Détails de la transaction (optionnels)
  if (data.transactionDetails) {
    console.log('Transaction ID:', data.transactionDetails.id);
    console.log('Reference:', data.transactionDetails.reference);
    if (data.transactionDetails.paymentLinkId) {
      console.log('Payment Link ID:', data.transactionDetails.paymentLinkId);
    }
  }
  
  if (data.transactionType === 'payment' && data.status === 'success') {
    // Traiter un paiement réussi
    console.log('Payment successful:', data.id);
    console.log('Customer:', data.counterpartLabel);
    console.log('Customer ID:', data.counterpartIdentifier);
    // Mettre à jour votre base de données, envoyer un email de confirmation, etc.
  } else if (data.transactionType === 'transfer' && data.status === 'success') {
    // Traiter un transfert réussi
    console.log('Transfer successful:', data.id);
    console.log('Beneficiary:', data.counterpartLabel);
    console.log('Beneficiary ID:', data.counterpartIdentifier);
    // Mettre à jour votre base de données, notifier le bénéficiaire, etc.
  } else if (data.transactionType === 'transfer' && data.status === 'error') {
    // Traiter un transfert échoué
    console.log('Transfer failed:', data.id);
    // Gérer l'échec, notifier l'utilisateur, etc.
  }
}

app.listen(3000, () => {
  console.log('Webhook server listening on port 3000');
});

PHP

<?php
$webhookSecret = getenv('JEKO_WEBHOOK_SECRET');

// Récupérer le body brut
$rawBody = file_get_contents('php://input');
$signature = $_SERVER['HTTP_JEKO_SIGNATURE'] ?? '';

// Vérifier la signature
$expectedSignature = hash_hmac('sha256', $rawBody, $webhookSecret);
if (!hash_equals($expectedSignature, $signature)) {
    http_response_code(401);
    echo json_encode(['error' => 'Invalid signature']);
    exit;
}

// Parser le payload
$payload = json_decode($rawBody, true);

// Traiter l'événement
handleWebhookEvent($payload);

// Répondre rapidement
http_response_code(200);
echo json_encode(['received' => true]);

function handleWebhookEvent($payload) {
    $event = $payload['event'];
    $data = $payload['data'];
    
    if ($event === 'transaction.completed') {
        handleTransactionCompleted($data);
    } else {
        error_log('Unknown event type: ' . $event);
    }
}

function handleTransactionCompleted($data) {
    error_log('Transaction completed: ' . $data['id']);
    error_log('Type: ' . $data['transactionType']); // "payment" ou "transfer"
    error_log('Status: ' . $data['status']); // "success" ou "error"
    error_log('Amount: ' . $data['amount']['amount']);
    error_log('Fees: ' . $data['fees']['amount']);
    error_log('Store: ' . $data['storeName']);
    error_log('Business: ' . $data['businessName']);
    
    // Détails de la transaction (optionnels)
    if (isset($data['transactionDetails'])) {
        $details = $data['transactionDetails'];
        if (isset($details['id'])) {
            error_log('Transaction ID: ' . $details['id']);
        }
        if (isset($details['reference'])) {
            error_log('Reference: ' . $details['reference']);
        }
        if (isset($details['paymentLinkId'])) {
            error_log('Payment Link ID: ' . $details['paymentLinkId']);
        }
    }
    
    if ($data['transactionType'] === 'payment' && $data['status'] === 'success') {
        // Traiter un paiement réussi
        error_log('Payment successful: ' . $data['id']);
        error_log('Customer: ' . $data['counterpartLabel']);
        error_log('Customer ID: ' . $data['counterpartIdentifier']);
        // Mettre à jour votre base de données, envoyer un email de confirmation, etc.
    } else if ($data['transactionType'] === 'transfer' && $data['status'] === 'success') {
        // Traiter un transfert réussi
        error_log('Transfer successful: ' . $data['id']);
        error_log('Beneficiary: ' . $data['counterpartLabel']);
        error_log('Beneficiary ID: ' . $data['counterpartIdentifier']);
        // Mettre à jour votre base de données, notifier le bénéficiaire, etc.
    } else if ($data['transactionType'] === 'transfer' && $data['status'] === 'error') {
        // Traiter un transfert échoué
        error_log('Transfer failed: ' . $data['id']);
        // Gérer l'échec, notifier l'utilisateur, etc.
    }
}
?>

Python (Flask)

from flask import Flask, request, jsonify
import hmac
import hashlib
import json
import os

app = Flask(__name__)
WEBHOOK_SECRET = os.getenv('JEKO_WEBHOOK_SECRET')

def verify_signature(raw_body, signature):
    expected_signature = hmac.new(
        WEBHOOK_SECRET.encode('utf-8'),
        raw_body,
        hashlib.sha256
    ).hexdigest()
    
    return hmac.compare_digest(expected_signature, signature)

@app.route('/webhook', methods=['POST'])
def webhook():
    try:
        # Récupérer le body brut
        raw_body = request.get_data()
        signature = request.headers.get('Jeko-Signature', '')
        
        # Vérifier la signature
        if not verify_signature(raw_body, signature):
            return jsonify({'error': 'Invalid signature'}), 401
        
        # Parser le payload
        payload = json.loads(raw_body)
        
        # Traiter l'événement (en arrière-plan si nécessaire)
        handle_webhook_event(payload)
        
        # Répondre rapidement
        return jsonify({'received': True}), 200
    except Exception as e:
        print(f'Webhook error: {e}')
        return jsonify({'error': 'Internal server error'}), 500

def handle_webhook_event(payload):
    event = payload.get('event')
    data = payload.get('data')
    
    if event == 'transaction.completed':
        handle_transaction_completed(data)
    else:
        print(f'Unknown event type: {event}')

def handle_transaction_completed(data):
    print(f"Transaction completed: {data['id']}")
    print(f"Type: {data['transactionType']}")  # "payment" ou "transfer"
    print(f"Status: {data['status']}")  # "success" ou "error"
    print(f"Amount: {data['amount']['amount']}")
    print(f"Fees: {data['fees']['amount']}")
    print(f"Store: {data['storeName']}")
    print(f"Business: {data['businessName']}")
    
    # Détails de la transaction (optionnels)
    if 'transactionDetails' in data and data['transactionDetails']:
        details = data['transactionDetails']
        if 'id' in details:
            print(f"Transaction ID: {details['id']}")
        if 'reference' in details:
            print(f"Reference: {details['reference']}")
        if 'paymentLinkId' in details:
            print(f"Payment Link ID: {details['paymentLinkId']}")
    
    if data['transactionType'] == 'payment' and data['status'] == 'success':
        # Traiter un paiement réussi
        print(f"Payment successful: {data['id']}")
        print(f"Customer: {data['counterpartLabel']}")
        print(f"Customer ID: {data['counterpartIdentifier']}")
        # Mettre à jour votre base de données, envoyer un email de confirmation, etc.
    elif data['transactionType'] == 'transfer' and data['status'] == 'success':
        # Traiter un transfert réussi
        print(f"Transfer successful: {data['id']}")
        print(f"Beneficiary: {data['counterpartLabel']}")
        print(f"Beneficiary ID: {data['counterpartIdentifier']}")
        # Mettre à jour votre base de données, notifier le bénéficiaire, etc.
    elif data['transactionType'] == 'transfer' and data['status'] == 'error':
        # Traiter un transfert échoué
        print(f"Transfer failed: {data['id']}")
        # Gérer l'échec, notifier l'utilisateur, etc.

if __name__ == '__main__':
    app.run(port=3000)

Ruby (Sinatra)

require 'sinatra'
require 'json'
require 'openssl'

WEBHOOK_SECRET = ENV['JEKO_WEBHOOK_SECRET']

def verify_signature(raw_body, signature)
  expected_signature = OpenSSL::HMAC.hexdigest(
    OpenSSL::Digest.new('sha256'),
    WEBHOOK_SECRET,
    raw_body
  )
  
  # Comparaison sécurisée pour éviter les attaques par timing
  return false if expected_signature.length != signature.length
  
  result = 0
  expected_signature.bytes.zip(signature.bytes) do |x, y|
    result |= x ^ y
  end
  result == 0
rescue
  false
end

post '/webhook' do
  begin
    # Récupérer le body brut
    raw_body = request.body.read
    signature = request.env['HTTP_JEKO_SIGNATURE'] || ''
    
    # Vérifier la signature
    unless verify_signature(raw_body, signature)
      status 401
      return { error: 'Invalid signature' }.to_json
    end
    
    # Parser le payload
    payload = JSON.parse(raw_body)
    
    # Traiter l'événement
    handle_webhook_event(payload)
    
    # Répondre rapidement
    status 200
    { received: true }.to_json
  rescue => e
    puts "Webhook error: #{e.message}"
    status 500
    { error: 'Internal server error' }.to_json
  end
end

def handle_webhook_event(payload)
  event = payload['event']
  data = payload['data']
  
  if event == 'transaction.completed'
    handle_transaction_completed(data)
  else
    puts "Unknown event type: #{event}"
  end
end

def handle_transaction_completed(data)
  puts "Transaction completed: #{data['id']}"
  puts "Type: #{data['transactionType']}"  # "payment" ou "transfer"
  puts "Status: #{data['status']}"  # "success" ou "error"
  puts "Amount: #{data['amount']['amount']}"
  puts "Fees: #{data['fees']['amount']}"
  puts "Store: #{data['storeName']}"
  puts "Business: #{data['businessName']}"
  
  # Détails de la transaction (optionnels)
  if data['transactionDetails']
    details = data['transactionDetails']
    puts "Transaction ID: #{details['id']}" if details['id']
    puts "Reference: #{details['reference']}" if details['reference']
    puts "Payment Link ID: #{details['paymentLinkId']}" if details['paymentLinkId']
  end
  
  if data['transactionType'] == 'payment' && data['status'] == 'success'
    # Traiter un paiement réussi
    puts "Payment successful: #{data['id']}"
    puts "Customer: #{data['counterpartLabel']}"
    puts "Customer ID: #{data['counterpartIdentifier']}"
    # Mettre à jour votre base de données, envoyer un email de confirmation, etc.
  elsif data['transactionType'] == 'transfer' && data['status'] == 'success'
    # Traiter un transfert réussi
    puts "Transfer successful: #{data['id']}"
    puts "Beneficiary: #{data['counterpartLabel']}"
    puts "Beneficiary ID: #{data['counterpartIdentifier']}"
    # Mettre à jour votre base de données, notifier le bénéficiaire, etc.
  elsif data['transactionType'] == 'transfer' && data['status'] == 'error'
    # Traiter un transfert échoué
    puts "Transfer failed: #{data['id']}"
    # Gérer l'échec, notifier l'utilisateur, etc.
  end
end

Test avec cURL

# Exemple de test local avec ngrok
# 1. Démarrer votre serveur local
# 2. Exposer avec ngrok: ngrok http 3000
# 3. Configurer l'URL ngrok dans le Jeko Cockpit

# Test manuel du webhook
curl -X POST http://localhost:3000/webhook \
  -H "Content-Type: application/json" \
  -H "Jeko-Signature: your_test_signature" \
  -d '{
    "event": "transaction.completed",
    "data": {
      "id": "txn_test123",
      "amount": {
        "amount": 10000,
        "currency": "XOF"
      },
      "fees": {
        "amount": 100,
        "currency": "XOF"
      },
      "status": "success",
      "counterpartLabel": "John Doe",
      "counterpartIdentifier": "+2250701234567",
      "paymentMethod": "wave",
      "transactionType": "payment",
      "businessName": "Ma Boutique",
      "storeName": "Magasin Principal",
      "description": "Test payment",
      "executedAt": "2024-01-15T14:30:25.000Z",
      "transactionDetails": {
        "id": "d22c81f3-ee04-4ec5-8bd2-cd8af5dabcfc",
        "reference": "TEST-001",
        "paymentLinkId": "abc123def456"
      }
    },
    "timestamp": "2024-01-15T14:30:25.000Z"
  }'

Points importants

  1. Body brut : Utilisez toujours le body brut (raw body) pour calculer la signature, pas le JSON parsé
  2. Comparaison sécurisée : Utilisez une comparaison sécurisée (timing-safe) pour éviter les attaques par timing
  3. Réponse rapide : Répondez rapidement (dans les 5 secondes) pour éviter les retries
  4. Traitement asynchrone : Pour les traitements longs, acceptez le webhook immédiatement et traitez-le en arrière-plan
  5. Idempotence : Assurez-vous que le traitement est idempotent pour éviter les doublons