Docs développeur

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é.

⚠️ RGPD/CNIL : les traceurs d'affiliation ne sont PAS exemptés de consentement (lignes directrices 2020-091/092). Déclenchez ce snippet UNIQUEMENT après consentement via votre CMP (catégorie marketing/affiliation), durée ≤ 13 mois. Fallback sans cookie : transmettez le ?ref= reçu dans la même session au signup, ou utilisez les codes promo (aucun traceur).
<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 ±300s
  • X-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 7

Ré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'endpoint
  • X-Qaff-Timestamp — epoch seconds, tolérance ±300s
  • X-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 QLOA

Appelé 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'endpoint
  • X-Qaff-Timestamp — epoch seconds, tolérance ±300s
  • X-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 éditeur

Import 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=&lt;epoch&gt;,v1=&lt;hex hmac-sha256(secret, "{t}.{corps brut}")&gt;

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.