Cette page contient des exemples complets d'implémentation de webhooks Kelyko dans différents langages. Tous les exemples incluent :
- Vérification de la signature HMAC-SHA256
- Brancher sur l'en-tête
Kelyko-Event. Les corps sont dans Événements - Traitement des événements
- Gestion d'erreurs
Serveur webhook
Le même serveur, dans quatre langages. Choisissez votre onglet.
Exemple · Node.js
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.Kelyko_WEBHOOK_SECRET;
function verifySignature(rawBody, signature) {
const expectedSignature = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(rawBody)
.digest('hex');
const received = Buffer.from(signature);
const expected = Buffer.from(expectedSignature);
// timingSafeEqual lève une exception si les longueurs diffèrent, ce qui est
// justement le cas d'une signature falsifiée : comparer les tailles d'abord.
if (received.length !== expected.length) return false;
return crypto.timingSafeEqual(received, expected);
}
app.post('/webhook', async (req, res) => {
try {
// Vérifier la signature
const signature = req.headers['Kelyko-signature'];
if (!signature || !verifySignature(req.body, signature)) {
return res.status(401).json({ error: 'Invalid signature' });
}
// TRANSACTION_COMPLETED est plat. SERVICE_PROVIDER_LINK_REQUEST est aussi
// un corps plat : l'événement est l'en-tête Kelyko-Event. HMAC porte sur le body brut.
const body = JSON.parse(req.body.toString());
const event = req.headers['Kelyko-event'];
// Accuser réception avant de traiter
res.status(200).json({ received: true });
if (event === 'SERVICE_PROVIDER_LINK_REQUEST') {
await handleLinkRequest(body);
} else if (body.event) {
await handleEscrow(body);
} else {
await handleTransaction(body);
}
} catch (error) {
console.error('Webhook error:', error);
// Le 200 est déjà parti avant le traitement : répondre à nouveau lèverait
// ERR_HTTP_HEADERS_SENT. On journalise, et on rejoue de son côté.
if (!res.headersSent) {
res.status(500).json({ error: 'Internal server error' });
}
}
});
async function handleTransaction(transaction) {
console.log('Transaction:', transaction.id);
console.log('Type:', transaction.transactionType);
console.log('Status:', transaction.status); // "pending", "success" ou "error"
console.log('Amount:', transaction.amount);
console.log('Fees:', transaction.fees);
console.log('Store:', transaction.storeName);
console.log('Business:', transaction.businessName);
console.log('Executed at:', transaction.executedAt); // "2024-01-15 14:30:25"
// Détails de la transaction (champs optionnels)
const details = transaction.transactionDetails ?? {};
if (details.id) console.log('Payment request ID:', details.id);
if (details.reference) console.log('Reference:', details.reference);
if (details.paymentLinkId) console.log('Payment Link ID:', details.paymentLinkId);
if (transaction.status === 'success') {
console.log('Transaction réussie:', transaction.id);
console.log('Contrepartie:', transaction.counterpartLabel);
console.log('Identifiant:', transaction.counterpartIdentifier);
// Mettre à jour votre base de données, envoyer une confirmation, etc.
} else if (transaction.status === 'error') {
console.log('Transaction échouée:', transaction.id);
// Gérer l'échec, notifier l'utilisateur, etc.
}
}
async function handleLinkRequest(payload) {
console.log('Link request:', payload.id, payload.status);
}
async function handleEscrow(body) {
console.log('Escrow:', body.event, body.escrowId, body.status);
}
app.listen(3000, () => {
console.log('Webhook server listening on port 3000');
});
Exemple · PHP
<?php
$webhookSecret = getenv('Kelyko_WEBHOOK_SECRET');
// Récupérer le body brut
$rawBody = file_get_contents('php://input');
$signature = $_SERVER['HTTP_Kelyko_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;
}
// TRANSACTION_COMPLETED est plat. SERVICE_PROVIDER_LINK_REQUEST est aussi
// un corps plat : l'événement est l'en-tête Kelyko-Event. HMAC porte sur le body brut.
$body = json_decode($rawBody, true);
$event = $_SERVER['HTTP_Kelyko_EVENT'] ?? '';
// Accuser réception
http_response_code(200);
echo json_encode(['received' => true]);
if ($event === 'SERVICE_PROVIDER_LINK_REQUEST') {
handleLinkRequest($body);
} elseif (!empty($body['event'])) {
handleEscrow($body);
} else {
handleTransaction($body);
}
function handleLinkRequest($payload) {
error_log('Link request: ' . $payload['id'] . ' ' . $payload['status']);
}
function handleEscrow($body) {
error_log('Escrow: ' . $body['event'] . ' ' . $body['escrowId'] . ' ' . $body['status']);
}
function handleTransaction($transaction) {
error_log('Transaction: ' . $transaction['id']);
error_log('Type: ' . $transaction['transactionType']);
error_log('Status: ' . $transaction['status']); // "pending", "success" ou "error"
error_log('Amount: ' . $transaction['amount']['amount']);
error_log('Fees: ' . $transaction['fees']['amount']);
error_log('Store: ' . $transaction['storeName']);
error_log('Business: ' . $transaction['businessName']);
error_log('Executed at: ' . $transaction['executedAt']); // "2024-01-15 14:30:25"
// Détails de la transaction (champs optionnels)
$details = $transaction['transactionDetails'] ?? [];
if (isset($details['id'])) {
error_log('Payment request ID: ' . $details['id']);
}
if (isset($details['reference'])) {
error_log('Reference: ' . $details['reference']);
}
if (isset($details['paymentLinkId'])) {
error_log('Payment Link ID: ' . $details['paymentLinkId']);
}
if ($transaction['status'] === 'success') {
error_log('Transaction réussie: ' . $transaction['id']);
error_log('Contrepartie: ' . $transaction['counterpartLabel']);
error_log('Identifiant: ' . $transaction['counterpartIdentifier']);
// Mettre à jour votre base de données, envoyer une confirmation, etc.
} else if ($transaction['status'] === 'error') {
error_log('Transaction échouée: ' . $transaction['id']);
// Gérer l'échec, notifier l'utilisateur, etc.
}
}
?>
Exemple · Python
from flask import Flask, request, jsonify
import hmac
import hashlib
import json
import os
app = Flask(__name__)
WEBHOOK_SECRET = os.getenv('Kelyko_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('Kelyko-Signature', '')
# Vérifier la signature
if not verify_signature(raw_body, signature):
return jsonify({'error': 'Invalid signature'}), 401
# TRANSACTION_COMPLETED est plat. SERVICE_PROVIDER_LINK_REQUEST est aussi
# un corps plat : l'événement est l'en-tête Kelyko-Event. HMAC porte sur le body brut.
body = json.loads(raw_body)
event = request.headers.get('Kelyko-Event', '')
if event == 'SERVICE_PROVIDER_LINK_REQUEST':
handle_link_request(body)
elif body.get('event'):
handle_escrow(body)
else:
handle_transaction(body)
return jsonify({'received': True}), 200
except Exception as e:
print(f'Webhook error: {e}')
return jsonify({'error': 'Internal server error'}), 500
def handle_transaction(transaction):
print(f"Transaction: {transaction['id']}")
print(f"Type: {transaction['transactionType']}")
print(f"Status: {transaction['status']}") # "pending", "success" ou "error"
print(f"Amount: {transaction['amount']['amount']}")
print(f"Fees: {transaction['fees']['amount']}")
print(f"Store: {transaction['storeName']}")
print(f"Business: {transaction['businessName']}")
print(f"Executed at: {transaction['executedAt']}") # "2024-01-15 14:30:25"
# Détails de la transaction (champs optionnels)
details = transaction.get('transactionDetails') or {}
if 'id' in details:
print(f"Payment request ID: {details['id']}")
if 'reference' in details:
print(f"Reference: {details['reference']}")
if 'paymentLinkId' in details:
print(f"Payment Link ID: {details['paymentLinkId']}")
if transaction['status'] == 'success':
print(f"Transaction réussie: {transaction['id']}")
print(f"Contrepartie: {transaction['counterpartLabel']}")
print(f"Identifiant: {transaction['counterpartIdentifier']}")
# Mettre à jour votre base de données, envoyer une confirmation, etc.
elif transaction['status'] == 'error':
print(f"Transaction échouée: {transaction['id']}")
# Gérer l'échec, notifier l'utilisateur, etc.
def handle_link_request(payload):
print(f"Link request: {payload['id']} {payload['status']}")
def handle_escrow(body):
print(f"Escrow: {body['event']} {body['escrowId']} {body['status']}")
if __name__ == '__main__':
app.run(port=3000)
Exemple · Ruby
require 'sinatra'
require 'json'
require 'openssl'
WEBHOOK_SECRET = ENV['Kelyko_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_Kelyko_SIGNATURE'] || ''
# Vérifier la signature
unless verify_signature(raw_body, signature)
status 401
return { error: 'Invalid signature' }.to_json
end
# TRANSACTION_COMPLETED est plat. SERVICE_PROVIDER_LINK_REQUEST est aussi
# un corps plat : l'événement est l'en-tête Kelyko-Event. HMAC porte sur le body brut.
body = JSON.parse(raw_body)
event = request.env['HTTP_Kelyko_EVENT'] || ''
if event == 'SERVICE_PROVIDER_LINK_REQUEST'
handle_link_request(body)
elsif body['event']
handle_escrow(body)
else
handle_transaction(body)
end
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_transaction(transaction)
puts "Transaction: #{transaction['id']}"
puts "Type: #{transaction['transactionType']}"
puts "Status: #{transaction['status']}" # "pending", "success" ou "error"
puts "Amount: #{transaction['amount']['amount']}"
puts "Fees: #{transaction['fees']['amount']}"
puts "Store: #{transaction['storeName']}"
puts "Business: #{transaction['businessName']}"
puts "Executed at: #{transaction['executedAt']}" # "2024-01-15 14:30:25"
# Détails de la transaction (champs optionnels)
details = transaction['transactionDetails'] || {}
puts "Payment request ID: #{details['id']}" if details['id']
puts "Reference: #{details['reference']}" if details['reference']
puts "Payment Link ID: #{details['paymentLinkId']}" if details['paymentLinkId']
if transaction['status'] == 'success'
puts "Transaction réussie: #{transaction['id']}"
puts "Contrepartie: #{transaction['counterpartLabel']}"
puts "Identifiant: #{transaction['counterpartIdentifier']}"
# Mettre à jour votre base de données, envoyer une confirmation, etc.
elsif transaction['status'] == 'error'
puts "Transaction échouée: #{transaction['id']}"
# Gérer l'échec, notifier l'utilisateur, etc.
end
end
def handle_link_request(payload)
puts "Link request: #{payload['id']} #{payload['status']}"
end
def handle_escrow(body)
puts "Escrow: #{body['event']} #{body['escrowId']} #{body['status']}"
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 portail marchand
# Test manuel du webhook
curl -X POST http://localhost:3000/webhook \
-H "Content-Type: application/json" \
-H "Kelyko-Signature: your_test_signature" \
-d '{
"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-15 14:30:25",
"transactionDetails": {
"id": "d22c81f3-ee04-4ec5-8bd2-cd8af5dabcfc",
"reference": "TEST-001",
"paymentLinkId": "abc123def456"
}
}'
Cinq erreurs à ne pas commettre
- 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 : Accusez réception sans attendre la fin du traitement. Au-delà de 30 secondes, le webhook est réessayé
- 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