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