Skip to main content

Documentation Index

Fetch the complete documentation index at: https://developer.jeko.africa/llms.txt

Use this file to discover all available pages before exploring further.

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