Webhooks
Selgeo envoie des notifications HTTP POST en temps réel à votre serveur lorsque des événements se produisent dans votre compte. Utilisez les webhooks pour automatiser des workflows comme l'attribution d'accès quand un partenaire est approuvé, la synchronisation des commissions avec votre système comptable, ou l'alerte de votre équipe en cas de fraude.
Toutes les payloads webhook utilisent l'API version v1.
Enregistrement des points de terminaison
Enregistrez les points de terminaison webhook depuis la page Paramètres > Webhooks dans le tableau de bord marchand.
- Cliquez sur Ajouter un point de terminaison.
- Entrez l'URL où vous voulez recevoir les événements. Les points de terminaison en mode live doivent utiliser HTTPS. Les points de terminaison en mode test peuvent utiliser HTTP pour le développement local.
- Sélectionnez les événements auxquels vous souhaitez vous abonner.
- Cliquez sur Créer. Votre secret de signature (
whsec_...) est affiché une fois — copiez-le et stockez-le en sécurité.
Vous pouvez enregistrer jusqu'à 10 points de terminaison par mode (test et live séparément).
Pings de test
Après avoir créé un point de terminaison, utilisez le bouton Envoyer un ping de test pour vérifier la connectivité. Le ping envoie un événement webhook.test à votre URL et signale le code de statut HTTP.
Rotation des secrets
Si votre secret de signature est compromis, utilisez l'action Faire pivoter le secret sur le point de terminaison. Cela génère immédiatement un nouveau secret — l'ancien cesse de fonctionner. Mettez à jour votre code de vérification avant de faire pivoter.
Vérification de signature
Chaque requête webhook inclut un en-tête X-Selgeo-Signature au format :
t=<unix_timestamp>,v1=<hmac_hex>
Le HMAC est calculé comme HMAC-SHA256(signing_secret_bytes, "<timestamp>.<raw_json_body>"). Le secret de signature a un préfixe whsec_ suivi de 64 caractères hexadécimaux. Supprimez le préfixe et décodez en hexadécimal pour obtenir la clé de 32 octets.
Vérifiez toujours la signature avant de traiter la payload. Cela protège contre les requêtes falsifiées.
Exemples de vérification
- Node.js
- Python
- PHP
import crypto from 'node:crypto';
function verifyWebhookSignature(signingSecret, signatureHeader, rawBody) {
if (!signatureHeader) {
return false;
}
// Supprimer le préfixe whsec_ et décoder en hex pour obtenir les octets de clé bruts
const rawSecret = signingSecret.replace(/^whsec_/, '');
const secretBytes = Buffer.from(rawSecret, 'hex');
// Analyser l'en-tête : t=<ts>,v1=<hmac>
const parts = Object.fromEntries(
signatureHeader.split(',').map((part) => {
return part.split('=', 2);
})
);
const timestamp = parts.t;
const receivedHmac = parts.v1;
if (!timestamp || !receivedHmac) {
return false;
}
// Rejeter les horodatages de plus de 5 minutes ou dans le futur (avec tolérance de 30s)
const age = Math.floor(Date.now() / 1000) - parseInt(timestamp, 10);
if (age > 300 || age < -30) {
return false;
}
// Calculer le HMAC attendu
const expectedHmac = crypto
.createHmac('sha256', secretBytes)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
// Comparaison en temps constant (les buffers doivent avoir la même longueur)
const receivedBuf = Buffer.from(receivedHmac, 'hex');
const expectedBuf = Buffer.from(expectedHmac, 'hex');
if (receivedBuf.length !== expectedBuf.length) {
return false;
}
return crypto.timingSafeEqual(receivedBuf, expectedBuf);
}
// Utilisation dans un handler Express
app.post('/webhooks/selgeo', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-selgeo-signature'];
const rawBody = req.body.toString();
if (!verifyWebhookSignature('whsec_YOUR_SIGNING_SECRET', signature, rawBody)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(rawBody);
console.log('Événement reçu :', event.event_name);
// Traiter l'événement...
res.status(200).send('OK');
});
import hashlib
import hmac
import time
def verify_webhook_signature(signing_secret: str, signature_header: str, raw_body: str) -> bool:
if not signature_header:
return False
# Supprimer le préfixe whsec_ et décoder en hex pour obtenir les octets de clé bruts
raw_secret = signing_secret.removeprefix("whsec_")
secret_bytes = bytes.fromhex(raw_secret)
# Analyser l'en-tête : t=<ts>,v1=<hmac>
try:
parts = dict(part.split("=", 1) for part in signature_header.split(","))
except ValueError:
return False
timestamp = parts.get("t")
received_hmac = parts.get("v1")
if not timestamp or not received_hmac:
return False
# Rejeter les horodatages de plus de 5 minutes ou dans le futur (avec tolérance de 30s)
try:
age = int(time.time()) - int(timestamp)
except ValueError:
return False
if age > 300 or age < -30:
return False
# Calculer le HMAC attendu
message = f"{timestamp}.{raw_body}".encode()
expected_hmac = hmac.new(secret_bytes, message, hashlib.sha256).hexdigest()
# Comparaison en temps constant
return hmac.compare_digest(received_hmac, expected_hmac)
# Utilisation dans un handler Flask
@app.route("/webhooks/selgeo", methods=["POST"])
def handle_webhook():
signature = request.headers.get("X-Selgeo-Signature", "")
raw_body = request.get_data(as_text=True)
if not verify_webhook_signature("whsec_YOUR_SIGNING_SECRET", signature, raw_body):
return "Invalid signature", 401
event = request.get_json()
print(f"Événement reçu : {event['event_name']}")
# Traiter l'événement...
return "OK", 200
<?php
function verifyWebhookSignature(
string $signingSecret,
string $signatureHeader,
string $rawBody
): bool {
if ($signatureHeader === '') {
return false;
}
// Supprimer le préfixe whsec_ et décoder en hex pour obtenir les octets de clé bruts
$rawSecret = str_starts_with($signingSecret, 'whsec_')
? substr($signingSecret, 6)
: $signingSecret;
$secretBytes = hex2bin($rawSecret);
if ($secretBytes === false) {
return false;
}
// Analyser l'en-tête : t=<ts>,v1=<hmac>
$parts = [];
foreach (explode(',', $signatureHeader) as $part) {
$segments = explode('=', $part, 2);
if (count($segments) !== 2) {
continue;
}
[$key, $value] = $segments;
$parts[$key] = $value;
}
$timestamp = $parts['t'] ?? null;
$receivedHmac = $parts['v1'] ?? null;
if (!$timestamp || !$receivedHmac) {
return false;
}
// Rejeter les horodatages de plus de 5 minutes ou dans le futur (avec tolérance de 30s)
$age = time() - (int) $timestamp;
if ($age > 300 || $age < -30) {
return false;
}
// Calculer le HMAC attendu
$expectedHmac = hash_hmac('sha256', "{$timestamp}.{$rawBody}", $secretBytes);
// Comparaison en temps constant
return hash_equals($expectedHmac, $receivedHmac);
}
// Utilisation
$signature = $_SERVER['HTTP_X_SELGEO_SIGNATURE'] ?? '';
$rawBody = file_get_contents('php://input');
if (!verifyWebhookSignature('whsec_YOUR_SIGNING_SECRET', $signature, $rawBody)) {
http_response_code(401);
echo 'Invalid signature';
exit;
}
$event = json_decode($rawBody, true);
error_log("Événement reçu : " . $event['event_name']);
// Traiter l'événement...
http_response_code(200);
echo 'OK';
Utilisez toujours la chaîne brute du corps de la requête pour la vérification de signature, pas une version re-sérialisée du JSON analysé. La re-sérialisation peut changer l'ordre des clés ou les espaces, ce qui invalide le HMAC.
Catalogue d'événements
Chaque payload webhook inclut ces champs d'enveloppe :
| Champ | Type | Description |
|---|---|---|
event_id | string | ID unique pour cet événement (nanoid, 21 chars) |
delivery_id | string | ID unique pour cette tentative de livraison |
event_name | string | Type d'événement (voir tableau ci-dessous) |
occurred_at | string | Horodatage ISO 8601 |
merchant_id | string | Votre ID de compte marchand |
mode | "test" | "live" | Si cet événement s'est produit en mode test ou live |
api_version | "v1" | Version de l'API |
Événements partenaires
| Événement | Description |
|---|---|
participant.created | Un nouveau partenaire a postulé ou a été ajouté à un programme |
participant.approved | Un partenaire a été approuvé (manuellement ou via approbation automatique) |
participant.rejected | Une candidature partenaire a été rejetée |
participant.suspended | Un partenaire actif a été suspendu |
participant.reinstated | Un partenaire suspendu a été réintégré |
participant.erased | Les données d'un partenaire ont été effacées (RGPD) |
participant.data_exported | Les données d'un partenaire ont été exportées (RGPD) |
Événements d'attribution
| Événement | Description |
|---|---|
attribution.created | Un clic a été attribué à un partenaire |
attribution.converted | Un clic attribué a abouti à une conversion |
attribution.expired | Une fenêtre d'attribution a expiré sans conversion |
attribution.duplicate_detected | Une attribution en double a été détectée |
Événements de conversion
| Événement | Description |
|---|---|
conversion.created | Une nouvelle conversion a été enregistrée |
conversion.fraud_detected | Une fraude a été détectée (auto-référencement ou conversion en double) |
Événements de commission
| Événement | Description |
|---|---|
commission.created | Une nouvelle commission a été calculée |
commission.approved | Une commission a été approuvée pour paiement |
commission.rejected | Une commission a été rejetée |
commission.paid | Une commission a été incluse dans un paiement (bientôt disponible) |
commission.refunded | Une commission a été annulée suite à un remboursement |
Événements de paiement (bientôt disponible)
Les événements de paiement seront disponibles lors du lancement de la fonctionnalité de traitement des paiements.
| Événement | Description |
|---|---|
payout.created | Un lot de paiements a été créé |
payout.initiated | Un virement de paiement a été initié |
payout.paid | Un paiement a été transféré avec succès |
payout.failed | Un transfert de paiement a échoué |
payout.retried | Un paiement échoué a été réessayé |
payout.cancelled | Un paiement a été annulé |
Événements de suivi
| Événement | Description |
|---|---|
tracking_identifier.created | Un nouveau lien de suivi a été créé pour un partenaire |
Événements de code promo
| Événement | Description |
|---|---|
promo_code.created | Un code promo a été créé |
promo_code.stripe_synced | Un code promo a été synchronisé avec Stripe |
promo_code.deactivated | Un code promo a été désactivé |
Événements d'invitation au programme
| Événement | Description |
|---|---|
program_invite.created | Une invitation a été créée |
program_invite.accepted | Une invitation a été acceptée par un partenaire |
program_invite.revoked | Une invitation a été révoquée |
program_invite.expired | Une invitation a expiré |
Politique de réessai
Si votre point de terminaison retourne un code de statut non-2xx ou si la requête expire (30 secondes), Selgeo réessaie avec un backoff exponentiel :
| Tentative | Délai après échec |
|---|---|
| 1ère tentative | 1 minute |
| 2ème tentative | 2 minutes |
| 3ème tentative | 4 minutes |
| 4ème tentative | 15 minutes |
Après 5 tentatives au total (1 initiale + 4 réessais), la livraison passe à l'état dead-letter. Les livraisons dead-letter sont visibles dans le tableau de bord marchand sous le journal de livraison du point de terminaison mais ne sont pas réessayées automatiquement.
Meilleures pratiques
- Retourner 200 rapidement. Accusez réception avec un code de statut
200dès que vous recevez le webhook. Traitez l'événement de manière asynchrone dans une tâche en arrière-plan. Selgeo expire après 30 secondes. - Gérer les doublons. Utilisez le champ
event_idpour dédupliquer. Le même événement peut être livré plus d'une fois dans de rares cas (ex. expiration réseau juste après que votre serveur l'ait traité). - Vérifier les signatures. Vérifiez toujours l'en-tête
X-Selgeo-Signature. Ne traitez jamais les payloads non vérifiées. - Vérifier l'horodatage. Rejetez les signatures avec des horodatages de plus de 5 minutes ou trop loin dans le futur pour prévenir les attaques par rejeu.
- Utiliser HTTPS en production. Les points de terminaison en mode live nécessitent HTTPS. Les points de terminaison en mode test permettent HTTP pour la commodité du développement local.
- Surveiller les livraisons dead-letter. Vérifiez périodiquement le journal de livraison dans le tableau de bord. Les événements dead-letter peuvent indiquer une panne du point de terminaison ou un bug dans votre handler.