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 (plat : pas d'enveloppe event/data)
const payload = JSON.parse(req.body.toString());
// Logger le corps brut aide à diagnostiquer une intégration qui démarre
console.log('Webhook reçu:', req.body.toString());
// Traiter la transaction
await handleTransaction(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' });
}
});
function getReference(payload) {
// La référence n'est PAS à la racine : lire les deux emplacements possibles
return (
payload.transactionDetails?.reference ??
payload.apiTransactionableDetails?.reference
);
}
async function handleTransaction(payload) {
// Ignorer les états intermédiaires : ne jamais passer une commande en échec sur un pending
if (payload.status === 'pending') {
console.log('Statut pending ignoré:', payload.id);
return;
}
const reference = getReference(payload);
if (!reference) {
console.warn('Webhook sans référence exploitable:', payload.id);
return;
}
const order = await findOrderByReference(reference);
if (!order) {
console.warn('Aucune commande pour la référence:', reference);
return;
}
// Idempotence : plusieurs webhooks peuvent arriver pour une même transaction
if (order.isFinalized) {
console.log('Commande déjà finalisée, webhook ignoré:', reference);
return;
}
// amount.amount peut être une chaîne ou un nombre : convertir avant comparaison
const amountCents = Number(payload.amount.amount);
if (payload.status === 'success') {
console.log('Transaction réussie:', payload.id, payload.transactionType);
await markOrderPaid(order, amountCents);
} else if (payload.status === 'error') {
console.log('Transaction échouée:', payload.id, payload.errorReason);
await markOrderFailed(order, payload.errorReason);
}
}
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 (plat : pas d'enveloppe event/data)
$payload = json_decode($rawBody, true);
// Logger le corps brut aide à diagnostiquer une intégration qui démarre
error_log('Webhook reçu: ' . $rawBody);
// Traiter la transaction
handleTransaction($payload);
// Répondre rapidement
http_response_code(200);
echo json_encode(['received' => true]);
function getReference($payload) {
// La référence n'est PAS à la racine : lire les deux emplacements possibles
return $payload['transactionDetails']['reference']
?? $payload['apiTransactionableDetails']['reference']
?? null;
}
function handleTransaction($payload) {
// Ignorer les états intermédiaires : ne jamais passer une commande en échec sur un pending
if ($payload['status'] === 'pending') {
error_log('Statut pending ignoré: ' . $payload['id']);
return;
}
$reference = getReference($payload);
if (!$reference) {
error_log('Webhook sans référence exploitable: ' . $payload['id']);
return;
}
$order = findOrderByReference($reference);
if (!$order) {
error_log('Aucune commande pour la référence: ' . $reference);
return;
}
// Idempotence : plusieurs webhooks peuvent arriver pour une même transaction
if ($order['isFinalized']) {
error_log('Commande déjà finalisée, webhook ignoré: ' . $reference);
return;
}
// amount.amount peut être une chaîne ou un nombre : convertir avant comparaison
$amountCents = (int) $payload['amount']['amount'];
if ($payload['status'] === 'success') {
error_log('Transaction réussie: ' . $payload['id'] . ' ' . $payload['transactionType']);
markOrderPaid($order, $amountCents);
} else if ($payload['status'] === 'error') {
error_log('Transaction échouée: ' . $payload['id'] . ' ' . ($payload['errorReason'] ?? ''));
markOrderFailed($order, $payload['errorReason'] ?? null);
}
}
?>
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 (plat : pas d'enveloppe event/data)
payload = json.loads(raw_body)
# Logger le corps brut aide à diagnostiquer une intégration qui démarre
print(f'Webhook reçu: {raw_body}')
# Traiter la transaction (en arrière-plan si nécessaire)
handle_transaction(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 get_reference(payload):
# La référence n'est PAS à la racine : lire les deux emplacements possibles
details = payload.get('transactionDetails') or payload.get('apiTransactionableDetails') or {}
return details.get('reference')
def handle_transaction(payload):
# Ignorer les états intermédiaires : ne jamais passer une commande en échec sur un pending
if payload['status'] == 'pending':
print(f"Statut pending ignoré: {payload['id']}")
return
reference = get_reference(payload)
if not reference:
print(f"Webhook sans référence exploitable: {payload['id']}")
return
order = find_order_by_reference(reference)
if not order:
print(f'Aucune commande pour la référence: {reference}')
return
# Idempotence : plusieurs webhooks peuvent arriver pour une même transaction
if order.is_finalized:
print(f'Commande déjà finalisée, webhook ignoré: {reference}')
return
# amount.amount peut être une chaîne ou un nombre : convertir avant comparaison
amount_cents = int(payload['amount']['amount'])
if payload['status'] == 'success':
print(f"Transaction réussie: {payload['id']} {payload['transactionType']}")
mark_order_paid(order, amount_cents)
elif payload['status'] == 'error':
print(f"Transaction échouée: {payload['id']} {payload.get('errorReason')}")
mark_order_failed(order, payload.get('errorReason'))
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 (plat : pas d'enveloppe event/data)
payload = JSON.parse(raw_body)
# Logger le corps brut aide à diagnostiquer une intégration qui démarre
puts "Webhook reçu: #{raw_body}"
# Traiter la transaction
handle_transaction(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 get_reference(payload)
# La référence n'est PAS à la racine : lire les deux emplacements possibles
details = payload['transactionDetails'] || payload['apiTransactionableDetails'] || {}
details['reference']
end
def handle_transaction(payload)
# Ignorer les états intermédiaires : ne jamais passer une commande en échec sur un pending
if payload['status'] == 'pending'
puts "Statut pending ignoré: #{payload['id']}"
return
end
reference = get_reference(payload)
unless reference
puts "Webhook sans référence exploitable: #{payload['id']}"
return
end
order = find_order_by_reference(reference)
unless order
puts "Aucune commande pour la référence: #{reference}"
return
end
# Idempotence : plusieurs webhooks peuvent arriver pour une même transaction
if order.finalized?
puts "Commande déjà finalisée, webhook ignoré: #{reference}"
return
end
# amount.amount peut être une chaîne ou un nombre : convertir avant comparaison
amount_cents = payload['amount']['amount'].to_i
if payload['status'] == 'success'
puts "Transaction réussie: #{payload['id']} #{payload['transactionType']}"
mark_order_paid(order, amount_cents)
elsif payload['status'] == 'error'
puts "Transaction échouée: #{payload['id']} #{payload['errorReason']}"
mark_order_failed(order, payload['errorReason'])
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 '{
"id": "txn_test123",
"status": "success",
"transactionType": "payment",
"amount": {
"amount": 10000,
"currency": "XOF"
},
"fees": {
"amount": 100,
"currency": "XOF"
},
"paymentMethod": "wave",
"counterpartLabel": "John Doe",
"counterpartIdentifier": "+2250701234567",
"businessName": "Ma Boutique",
"storeId": "59ae202a-f583-4a15-970f-9e99bd1e0baa",
"storeReference": "STORE-001",
"storeName": "Magasin Principal",
"description": "Test payment",
"executedAt": "2024-01-15T14:30:25.000Z",
"errorReason": null,
"transactionDetails": {
"id": "d22c81f3-ee04-4ec5-8bd2-cd8af5dabcfc",
"reference": "TEST-001",
"paymentLinkId": "abc123def456"
}
}'
Points importants
- Payload plat : Les champs de la transaction sont à la racine du corps de la requête, sans enveloppe
event/data - Référence : Lisez
transactionDetails.referenceavec un fallback surapiTransactionableDetails.reference— elle n’est jamais à la racine - Statut
pending: Ignorez-le, ne passez jamais une commande en échec dessus - Idempotence : Plusieurs webhooks arrivent pour une même transaction — ignorez ceux dont la commande est déjà finalisée
amount.amount: Peut être une chaîne ou un nombre, convertissez avant toute comparaison- 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 pour éviter les retries
- Traitement asynchrone : Pour les traitements longs, acceptez le webhook immédiatement et traitez-le en arrière-plan