Docs développeur — QLOA Affiliation
Intégrer le tracking, envoyer des conversions, poser le snippet cookie côté produit. Toutes les routes ci-dessous vivent sur le domaine de la plateforme.
Lien de tracking — GET /r/{code}
Chaque affilié génère des liens de la forme ci-dessous. Le redirect est immédiat (302) vers l'URL cible avec ?ref={code} ajouté à la query existante ; le clic est enregistré en tâche asynchrone (IP/UA hashés, RGPD).
https://affiliation.antislash.studio/r/abc123 → 302 https://votre-produit.com/pricing?ref=abc123
Snippet cookie first-party (à poser côté produit)
Lit le paramètre ?ref= à l'arrivée sur votre site et pose un cookie first-party (90 jours max recommandé), envoyé au moment du signup pour rattacher le nouveau client à l'affilié.
<script>
(function () {
var params = new URLSearchParams(window.location.search);
var ref = params.get('ref');
if (ref) {
var days = 60;
var expires = new Date(Date.now() + days * 86400000).toUTCString();
document.cookie = 'qaff_ref=' + encodeURIComponent(ref) + '; expires=' + expires + '; path=/; SameSite=Lax';
}
})();
</script>
<!-- Au signup, envoyez la valeur du cookie qaff_ref dans votre payload -->
<!-- (customer_external_id / affiliate_code du POST /api/v1/conversions, voir plus bas) -->
Webhook conversions — POST /api/v1/conversions
Signale une conversion (vente, abonnement…) depuis votre backend. Idempotent sur external_id : rejouer le même événement renvoie {"status": "duplicate"} sans doublon.
Authentification — headers
X-Qaff-Token— le token d'endpoint fourni par votre org (page Settings → Webhooks)X-Qaff-Timestamp— epoch seconds, tolérance ±300sX-Qaff-Signature— HMAC-SHA256 hex de "{timestamp}.{corps brut}" avec votre secret
Corps de la requête (JSON)
{
"external_id": "inv_8842",
"program_id": "0c9e...-uuid-programme",
"amount": 49.90,
"currency": "EUR",
"kind": "one_time",
"occurred_at": "2026-08-20T10:00:00Z",
"customer_email": "client@exemple.com",
// Résolution de l'affilié — un seul suffit, ordre de priorité :
"customer_external_id": "cust_123", // 1. déjà attribué (customer-bound)
"affiliate_code": "abc123", // 2. code du lien /r/{code}
"promo_code": "PARRAIN10" // 3. code promo
}
Exemple curl
BODY='{"external_id":"inv_8842","program_id":"0c9e...","amount":49.90,"affiliate_code":"abc123"}'
TS=$(date +%s)
SIG=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$WEBHOOK_SECRET" -hex | cut -d' ' -f2)
curl -X POST https://affiliation.antislash.studio/api/v1/conversions \
-H "Content-Type: application/json" \
-H "X-Qaff-Token: $WEBHOOK_TOKEN" \
-H "X-Qaff-Timestamp: $TS" \
-H "X-Qaff-Signature: $SIG" \
-d "$BODY"
Exemple Python
import hashlib, hmac, json, time, requests
body = json.dumps({"external_id": "inv_8842", "program_id": "0c9e...", "amount": 49.90, "affiliate_code": "abc123"})
ts = str(int(time.time()))
signature = hmac.new(WEBHOOK_SECRET.encode(), f"{ts}.{body}".encode(), hashlib.sha256).hexdigest()
requests.post(
"https://affiliation.antislash.studio/api/v1/conversions",
data=body,
headers={
"Content-Type": "application/json",
"X-Qaff-Token": WEBHOOK_TOKEN,
"X-Qaff-Timestamp": ts,
"X-Qaff-Signature": signature,
},
)
Exemple Node.js
const crypto = require('crypto');
const body = JSON.stringify({ external_id: 'inv_8842', program_id: '0c9e...', amount: 49.90, affiliate_code: 'abc123' });
const ts = Math.floor(Date.now() / 1000).toString();
const signature = crypto.createHmac('sha256', WEBHOOK_SECRET).update(`${ts}.${body}`).digest('hex');
await fetch('https://affiliation.antislash.studio/api/v1/conversions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Qaff-Token': WEBHOOK_TOKEN,
'X-Qaff-Timestamp': ts,
'X-Qaff-Signature': signature,
},
body,
});
Réponses
- 200 {"status": "created", "conversion_id": "..."}
- 200 {"status": "duplicate"}
- 401 token/signature invalide
- 422 payload invalide
- 404 programme ou affilié introuvable
Webhook usage — POST /api/v1/usage
Espace revendeur — Lot 7Réservé aux instances QLOA (Master/dédiées) : agrégats d'usage par organisation, poussés quotidiennement pour alimenter le dashboard revendeur (clients actifs, MRR, santé). Aucun contenu conversationnel, aucun coût opérateur dans ce payload — uniquement les organisations rattachées à un partner_code ou affiliate_code.
Authentification — headers
Même contrat que le webhook conversions ci-dessus (même token/secret d’endpoint) :
X-Qaff-Token— token de l'endpointX-Qaff-Timestamp— epoch seconds, tolérance ±300sX-Qaff-Signature— HMAC-SHA256 hex de "{timestamp}.{corps brut}"
Corps de la requête (JSON)
{
"instance": "master",
"period": "2026-09-01",
"emitted_at": "2026-09-04T03:00:00Z",
"orgs": [
{
"org_id": "…",
"slug": "ffvelo",
"status": "active",
"plan_type": "starter",
"price_per_month": 49.0,
"billing_period": "monthly",
"sessions_month": 312,
"sessions_limit": 500,
"tokens_month": 1840233,
"widget_installed": true,
"last_activity_at": "2026-09-03T21:14:00Z",
"partner_code": "RSL-AGENCEX",
"affiliate_code": null,
"health": {"quota_alert": false, "unpaid": false, "at_risk": false},
"lifecycle": "active"
}
]
}
Idempotence
Rejouer le même événement (même org, même period, même emitted_at) renvoie {"status": "duplicate"} sans retraiter — inbox rejouable webhook_events(kind='usage').
Cadence
Quotidienne (03:00 UTC), + événementiel sur changement de statut (démo → actif → résilié). Seules les organisations portant un partner_code (revendeur) ou affiliate_code (affilié) sont transmises — jamais toutes les organisations.
Réponses
- 200 {"status": "ok", "clients_updated": 3, "snapshots": 3, "unknown_partner_codes": []}
- 200 {"status": "duplicate"}
- 401 token/signature invalide
- 413 payload > 256 Ko
- 422 payload invalide
Codes promo
Attribution sans cookie : un affilié peut avoir un code promo dédié (programme + affilié). Envoyez-le en promo_code dans le webhook conversions ci-dessus — la conversion sera attribuée même sans clic préalable sur un lien /r/{code}.
Vérification d'un code partenaire — GET /api/v1/partner-codes/{code}
Réservé aux instances QLOAAppelé serveur à serveur par QLOA (Master/dédiées) pour vérifier qu'un code saisi (parrainage, portail self-onboarding, etc.) est un code affilié/revendeur valide de VOTRE org avant d'appliquer une récompense filleul (referee_reward) ou de rattacher un client à un revendeur. Aucune écriture qaff n'est déclenchée par cet appel — lecture seule.
Authentification — headers
Même token/secret d’endpoint que les webhooks ci-dessus, mais la chaîne signée est différente (requête GET sans corps) :
X-Qaff-Token— le token de l'endpointX-Qaff-Timestamp— epoch seconds, tolérance ±300sX-Qaff-Signature— HMAC-SHA256 hex de "{timestamp}.GET:/api/v1/partner-codes/{code}"
La chaîne signée reprend le code EXACTEMENT tel qu'il est envoyé dans l'URL (aucune réécriture avant signature). La comparaison en base, elle, est insensible à la casse : les codes sont stockés en majuscules, « parrain10 » et « PARRAIN10 » désignent donc le même code. La réponse renvoie toujours la forme canonique dans le champ code.
Exemple curl
CODE="PARRAIN10" TS=$(date +%s) SIG=$(printf '%s.GET:/api/v1/partner-codes/%s' "$TS" "$CODE" | openssl dgst -sha256 -hmac "$WEBHOOK_SECRET" -hex | cut -d' ' -f2) curl "https://affiliation.antislash.studio/api/v1/partner-codes/$CODE" \ -H "X-Qaff-Token: $WEBHOOK_TOKEN" \ -H "X-Qaff-Timestamp: $TS" \ -H "X-Qaff-Signature: $SIG"
Réponses
- 200 {"valid": true, "code": "PARRAIN10", "kind": "affiliate", "partner_name": "Jane D.", "affiliate_id": "..."}
- 401 token/signature invalide
- 404 {"valid": false} — code inconnu, désactivé, expiré, quota atteint, ou affilié/candidature non approuvés (réponse identique dans tous les cas — pas d’oracle)
Import CSV
Espace éditeurImport de conversions et d'affiliés depuis un export CSV (matching par code ou email) — utile pour rattraper l'historique ou migrer depuis un autre outil d'affiliation. Accessible depuis /admin/conversions (panneau « Importer un CSV »), avec un aperçu dry-run avant confirmation. Idempotent sur external_id (les doublons sont comptés, jamais recréés).
Colonnes — conversions
external_id,amount,currency,kind,occurred_at,affiliate_code,promo_code,customer_email,customer_external_id,program_id inv_9001,49.90,EUR,one_time,2026-08-20T10:00:00Z,abc123,,client@exemple.com,,
program_id peut être omis si un programme par défaut est choisi lors de l'import. Au moins un moyen de résolution (affiliate_code, promo_code ou customer_external_id déjà attribué) est requis par ligne.
Colonnes — affiliés
email,kind,company,siren,display_name,code alice@exemple.com,pro,Acme SAS,123456789,Alice,PARRAIN10
Crée le compte (invitation par email pour définir le mot de passe), le profil affilié et sa candidature directement approuvée sur le programme cible. code (optionnel) crée un code promo dédié.
Limites
- 2 Mo par fichier, 5000 lignes maximum
- Chaque ligne en erreur est rapportée individuellement — le reste du fichier continue d’être traité
Connecteur Pennylane
Zéro code produit : si votre facturation est réconciliée dans Pennylane (paiements GoCardless notamment), un poll quotidien détecte les factures payées des clients déjà mappés à un affilié (customer_mappings) et crée les conversions automatiquement. Les avoirs émis déclenchent un clawback sur la commission correspondante.
Configuration : votre token API Pennylane est renseigné par votre contact QLOA dans les réglages du connecteur (page Settings de votre org, disponible au Lot 5).
Webhooks sortants — la plateforme vous notifie
À configurer dans /admin/reglages (onglet « Webhooks sortants »). Chaque URL abonnée reçoit un POST signé à chaque événement souscrit — retries exponentiels en cas d'échec (1 min, 5 min, 30 min, 2 h, 6 h puis 24 h), jusqu'à 8 tentatives avant abandon (visible en file d'attente morte dans les réglages).
Événements disponibles
- conversion.created
- conversion.approved
- conversion.rejected
- commission.payable
- payout.paid
- membership.approved
Headers de la requête reçue
X-Qaff-Event— le nom de l'événement (ex. conversion.approved)X-Qaff-Delivery— identifiant de la tentative de livraison (idempotence côté récepteur)X-Qaff-Signature— t=<epoch>,v1=<hex hmac-sha256(secret, "{t}.{corps brut}")>
Vérifier la signature (Node.js)
const crypto = require('crypto');
function verify(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(',').map(p => p.split('=')));
const expected = crypto.createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex');
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}
// verify(rawBody, req.headers['x-qaff-signature'], WEBHOOK_OUT_SECRET)
Exemple de payload
{
"conversion_id": "5b6e...-uuid",
"external_id": "inv_8842",
"amount": 49.90
}
Répondez 2xx pour accuser réception — tout autre code (ou timeout au-delà de 8s) déclenche un nouvel essai selon le palier ci-dessus. Le secret n'est affiché qu'une seule fois à la création/régénération du webhook — le régénérer si vous l'avez perdu.