Trackily Docs

API HTTP

Trackily expose deux APIs distinctes : MCP (recommandé pour les agents LLM, cf. /docs/mcp) et HTTP REST classique (recommandé pour les intégrations machine-to-machine, webhooks externes, et postbacks affiliate). Cette section documente la deuxième.

Concept

L'API HTTP de Trackily a quatre familles d'endpoints :

  1. Admin API (/admin/api/*) — pilote toute l'instance. Auth par cookie de session admin (HTTP-only, signé HMAC) + permission check par section. Préfère le MCP pour piloter : moins de boilerplate, scopes plus fins, audit log natif.
  2. Public tracking (/c/:slug, /lp/:slug, /r/:slug) — sert les redirects de tracking et les landings. C'est le hot path de l'app, optimisé pour la perf.
  3. Postback (/postback) — endpoint universel de réception des conversions affiliate. Cf. Postback.
  4. Webhooks ENTRANTS (/webhook/*) — endpoints qui RECEIVENT des events de Shopify, Stripe, PayPal, Mailgun, etc. Cf. Webhooks.

À noter : Trackily n'émet PAS de webhooks sortants (du moins, pas via un système de souscription générique). Toute la propagation d'événement vers l'extérieur passe par d'autres chemins : conversion triggers email, postbacks vers les ad networks, script tags Shopify injectés.

Auth des endpoints /admin/api/*

L'admin API est protégée par authMiddleware qui :

  1. Lit le cookie tly_admin_session (signé HMAC avec SECRETS_MASTER_KEY)
  2. Vérifie la signature + la fraîcheur (TTL configurable, par défaut 7 jours)
  3. Lit users.id depuis le cookie, charge le user row
  4. Pour les endpoints requirePermission(section, action), vérifie que le user a la permission (table user_permissions JSONB)

Pour s'authentifier en POST initial :

POST /admin/api/login HTTP/1.1
Content-Type: application/json

{ "email": "admin@example.com", "password": "..." }

Réponse : Set-Cookie: tly_admin_session=...; HttpOnly; SameSite=Lax (Secure en HTTPS).

Les calls suivants passent le cookie automatiquement. Pour un script externe : extraire le cookie après login, le rejouer dans le header Cookie:.

Pour les agents LLM, préfère MCP qui a un système de bearer + scopes + rate limiting + audit dédié.

Clés API Trackily (tly_…)

Pour un script ou une intégration (Zapier, Make…), crée une clé durable dans Réglages → Clés API et envoie-la sur chaque appel : Authorization: Bearer tly_…. Elle porte les droits de son propriétaire (l'utilisateur à qui elle appartient — celui qui l'a créée, ou celui pour qui un admin l'a créée ; une régénération garde le même propriétaire), pas ceux de la personne qui l'a créée ou régénérée.

Une clé n'est affichée qu'une seule fois : dans la réponse de sa création, puis plus jamais.

Route Ce qu'elle rend
POST /admin/api/api-keys { "name": "Zapier" } { id, name, key_masked, …, "key": "tly_…" } — la seule fois où key (la clé entière) apparaît
GET /admin/api/api-keys la liste, avec key_masked seulement (tly_0123...abcd : le début et les 4 derniers caractères) ; un admin voit toutes les clés de l'instance, avec email
POST /admin/api/api-keys/:id/regenerate une nouvelle clé, rendue une fois dans key, et remplace (l'id révoqué) ; l'ancienne est supprimée dans le même geste — elle cesse de marcher tout de suite. Même nom, mêmes droits. Un non-admin ne régénère que les siennes
PUT /admin/api/api-keys/:id/toggle · DELETE /admin/api/api-keys/:id désactiver / réactiver, supprimer (bornés à ses propres clés pour un non-admin)

Clé perdue ? Elle ne se relit pas : régénère-la (une clé désactivée le reste après régénération). Une fois le nouveau format activé par l'opérateur (SECRETS_CHIFFRER_AU_REPOS=1, voir OPERATIONS.md §3), Trackily n'en garde qu'une empreinte sha256 (colonne api_keys.key = sha256$<hex>) et sa forme abrégée (key_hint, migration v139) ; l'authentification hache le jeton reçu et compare les empreintes. Avant cette activation, la clé est encore stockée en clair (pour que l'ancienne version du serveur, pendant une mise à jour ou un retour arrière, l'accepte), mais elle n'est jamais relue par l'écran ni par l'API : seule la forme abrégée sort.

Clés créées avant la v139 : elles étaient stockées en clair. Elles restent valables ; après l'activation, Trackily les réécrit hachées à leur premier usage, et la tâche de démarrage hache celles qui ne servent plus (sans rien changer pour l'intégration). D'ici là, la liste n'en montre déjà plus que la forme abrégée.

OpenClaw : sa clé (tly_oc_…) est générée par Trackily, chiffrée au repos ; GET /admin/api/openclaw/settings n'en rend que la forme abrégée (openclaw_api_key, openclaw_api_key_set), et POST /admin/api/openclaw/api-key/regenerate en crée une nouvelle, rendue une fois. Le PUT des réglages ne l'écrit plus.

Format des réponses

Convention :

// Success
{ "ok": true, ...payload }
// ou directement le payload (pas d'enveloppe)

// Erreur
{ "error": "description courte" }
// ou
{ "ok": false, "error": "..." }

Status HTTP standard :

  • 200 — succès
  • 400 — payload invalide / validation échouée
  • 401 — pas authentifié
  • 403 — authentifié mais permission manquante
  • 404 — ressource introuvable
  • 429 — rate limited
  • 500 — erreur serveur

Rate limiting

Limites importantes en place côté serveur :

Endpoint Limite
POST /admin/api/login 5 essais / 15 min / IP (loginRateLimit)
GET /postback 600 / minute / IP (postbackRateLimit)
POST /api/account/magic-link ~10 / minute / IP (pixelRateLimit)
POST /api/lead ~10 / minute / IP
Tracking pixels ~30 / min / IP

Le rate limiter est en mémoire process (pas Redis) — si tu tournes plusieurs instances derrière un LB, chaque process a son propre compteur.

CORS

Par défaut, /admin/api/* n'a pas de CORS (même-origin uniquement). Les endpoints publics (/api/account/*, /postback, /api/lead) ont une allowlist configurable dans Settings.

Endpoints publics utiles

GET /postback

Réception des conversions affiliate. Cf. Postback.

POST /api/lead

Soumission de form (landing). Body : email, name, et tout champ custom. Crée une leads row + (si la landing est bound à une email_list) enroll dans la liste.

POST /api/lead HTTP/1.1
Content-Type: application/x-www-form-urlencoded

email=marie@example.com&name=Marie&landing_id=42&city=Paris

Réponse : 200 { ok: true, redirect_url: "/r/..." }.

Consentement WhatsApp. Les clés wa_consent, wa_consent_version et wa_consent_langue ne suffisent pas : le consentement d'un lead n'est retenu que si le corps porte aussi wa_consent_jeton, le jeton signé que Trackily pose avec la case sur les landings qu'il sert (valable 12 heures, pour cette landing seulement). Un formulaire maison qui poste sa propre case vers /api/lead voit son lead enregistré, sans consentement. La réponse ne dit jamais si le consentement a été retenu. Une page hébergée hors de Trackily obtient son jeton (valable 1 heure) par la route ci-dessous, et ne le fait valoir que depuis l'origine déclarée de sa landing. Voir Déclencheurs → La case de consentement.

GET /api/wa/consentement

Le contexte de la case de consentement WhatsApp pour une page hébergée hors de Trackily (landing de type url). public/trackily.js l'appelle tout seul quand sa balise porte data-wa-landing-id ; on n'a normalement pas à l'appeler soi-même.

<script src="https://VOTRE-TRACKER/trackily.js" data-wa-landing-id="42" data-wa-lang="fr"></script>
<div data-trackily-wa-slot></div>

Paramètres : landing_id (obligatoire), lang (fr, en, ar ; l'extrait de l'admin le pose toujours). Sans lang, la langue enregistrée sur la landing s'applique s'il y en a une ; sinon, pas de case : une page dont la langue n’est pas servie n’a pas de case. Réponse 200 { texte, version, langue, jeton, expire_dans_s } — le jeton vaut 1 heure, pour cette landing seulement. La réponse n'est lisible (CORS) que depuis l'origine de l'URL déclarée de la landing ou sa variante www. / sans www. (même schéma, même port ; aucun autre sous-domaine, jamais pour une IP ou localhost), sans cookie. Tout autre cas (landing inconnue ou interne, sans liste WhatsApp active, case éteinte, langue non servie, autre origine) répond 404 { disponible: false }, sans dire pourquoi ; 120 requêtes par minute et par IP au plus (429). Le jeton se renvoie à POST /api/lead dans wa_consent_jeton, depuis la même origine. Voir Déclencheurs → La case de consentement.

POST /api/account/magic-link

Magic-link customer auth. Cf. Magic Link.

GET /pixel.js, GET /trackily.js, GET /cloak.js, GET /i18n.js

Static JS assets servis avec cache headers. Le pixel pixel.js capture les page views côté landing, trackily.js est la lib client pour les events custom, cloak.js est le pilote du cloaking.

GET /mcp et POST /mcp

L'endpoint MCP. Cf. /docs/mcp/concept.

Différences MCP vs Admin REST

Aspect MCP Admin REST
Auth Bearer token tly_ap_<...> Cookie session HTTP-only
Granularité perms 42 scopes 8 sections × {read, write}
Audit log autopilot_audit_log (chaque call) logs/syslog seulement
Rate limiting per-token, 2 buckets (default + ai) per-IP global
Découvrabilité tools/list retourne tout en JSON-Schema OpenAPI spec à parser séparément
Tier-2 (confirm) natif absent — chaque endpoint exécute direct
Idempotency natif sur Tier-2 absent (à toi de gérer côté client)
Compatibilité LLM excellent médiocre (le LLM compose à la main)

Si tu écris un script qui doit appeler 5-10 endpoints, MCP est plus simple. Si tu écris un webhook handler côté ton infra qui doit pinguer Trackily, REST est plus naturel.

Voir aussi