Documentation

Widgets & API publique.

Guide complet d'intégration : 4 widgets, deux modes (iframe et web component), endpoints JSON /api/v1/*, licence Creative Commons CC-BY-SA 4.0, tracking anonyme et rate-limit.

Ce document décrit la totalité de la surface publique du système de widgets embeddables Capturia : les 4 widgets disponibles, les deux modes d'intégration (iframe et web component), l'API JSON sous-jacente, la licence applicable, le tracking anonyme côté serveur et la politique de rate-limit.

Public visé : journalistes, écoles, agences SEO, intégrateurs partenaires souhaitant embarquer les scores Capturia sur leurs pages.

Licence des données : Creative Commons CC-BY-SA 4.0. Réutilisation libre, y compris commerciale, sous réserve de citer la source « Capturia » et de garder visible le lien capturia.ecole-futee.com injecté par le widget.


1. Les 4 widgets disponibles

Widget Description courte URL iframe Web component
badge Carte 320×240 — score d'exposition IA d'un métier + tag + lien fiche. /embed/badge/<slug> <capturia-badge data-metier="<slug>">
sector Carte 600×500 — top 5 métiers d'un secteur les moins exposés à l'IA. /embed/sector/<slug> <capturia-sector data-secteur="<slug>" data-limit="5">
compare Carte 800×400 — comparateur 2 métiers côte à côte (score, niveau, salaire médian). /embed/compare?a=<slugA>&b=<slugB> <capturia-compare data-a="<slugA>" data-b="<slugB>">
score-global Carte 280×120 — indice synthétique d'exposition IA + delta vs trimestre précédent. /embed/score-global <capturia-score-global>

1.1 Mode iframe (zéro JS, indexable, immédiat)

Tous les widgets exposent une route HTML autonome, ~< 50 KB, charte Capturia figée, headers permissifs X-Frame-Options: ALLOWALL et Content-Security-Policy: frame-ancestors *. Aucune dépendance externe, aucun cookie posé.

<iframe
  src="https://capturia.ecole-futee.com/embed/badge/kinesitherapeute"
  width="320" height="240"
  style="border:0;border-radius:14px;overflow:hidden"
  loading="lazy"
  title="Capturia — Badge métier"
  referrerpolicy="no-referrer-when-downgrade"
></iframe>

1.2 Mode web component (responsive, hydraté)

Un seul script vanilla, pas de Shadow DOM (pour rester indexable par Googlebot). Les composants sont rendus en Light DOM et stylés via une feuille préfixée .ssw-. Les liens sortants dofollow sont injectés au runtime, jamais en HTML statique.

<script src="https://capturia.ecole-futee.com/js/capturia-widget.js" async></script>
<capturia-badge data-metier="kinesitherapeute"></capturia-badge>

Le composant détecte les masquages CSS basiques (display:none, visibility:hidden, opacity:0, hauteur nulle) et désactive son rendu en affichant le message « Widget Capturia désactivé : la mention de la source doit rester visible. » C'est volontairement contournable (ce n'est pas une protection blindée — juste un signal honnête).


2. API publique /api/v1/*

Quatre endpoints JSON publics, lecture seule, accessibles sans clé d'API en v1 (cf. § 5 — politique de rate-limit).

2.1 Endpoints

Méthode Chemin Description
GET /api/v1/metier/:slug Fiche métier publique (score, intervalle de confiance, narratif).
GET /api/v1/secteur/:slug Métadonnées secteur + top 10 métiers (asc, moins exposés).
GET /api/v1/score-global Indice synthétique pondéré + décomposition par catégorie.
GET /api/v1/metiers?page=&limit= Liste paginée des métiers scorés (limit max 100).
GET /api/v1/compare?a=&b= Comparateur 2 métiers (utilisé par le widget compare).

2.2 Headers communs

Toutes les réponses 2xx exposent :

  • Content-Type: application/json
  • X-License: CC-BY-SA-4.0
  • X-Capturia-Updated: <ISO8601> — date du dernier computed_at agrégé sur le payload (pour invalider intelligemment les caches CDN).
  • Cache-Control: public, max-age=3600

Aucun cookie n'est posé. CORS ouvert (Access-Control-Allow-Origin: *) en lecture seule, pas de credentials: include.

2.3 Codes d'erreur

Statut Cas Payload
400 Paramètres manquants (/compare sans a ou b) { "error": "params_required", "required": ["a","b"] }
404 Ressource inconnue (metier/inexistant, secteur/inexistant) { "error": "not_found" }
429 Rate-limit dépassé (cf. § 5) { "error": "rate_limit_exceeded", "retry_after": <int> }
500 Erreur interne { "error": "Erreur interne", "code": "INTERNAL_ERROR" }

2.4 Exemples cURL

# Fiche métier
curl https://capturia.ecole-futee.com/api/v1/metier/kinesitherapeute

# Top secteur
curl https://capturia.ecole-futee.com/api/v1/secteur/btp-construction

# Comparateur
curl 'https://capturia.ecole-futee.com/api/v1/compare?a=comptable&b=expert-comptable'

# Indice global
curl https://capturia.ecole-futee.com/api/v1/score-global

# Liste paginée
curl 'https://capturia.ecole-futee.com/api/v1/metiers?page=1&limit=20'

2.5 Schémas de réponse (forme abrégée)

// /api/v1/metier/:slug
{
  "slug": "kinesitherapeute",
  "rome_code": "J1404",
  "title_fr": "Kinésithérapeute",
  "score": 26,
  "score_intervalle": [22, 30],
  "sources_count": 5,
  "sector": "Santé",
  "education_level": "bac+5",
  "tag": "Métier-refuge",
  "narrative": { "short": "...", "tasks": [...] },
  "last_computed": "2026-04-01T12:00:00.000Z",
  "fiche_url": "https://capturia.ecole-futee.com/metier/kinesitherapeute",
  "license": "Creative Commons CC-BY-SA 4.0",
  "_links": {
    "self":        { "href": "https://capturia.ecole-futee.com/api/v1/metier/kinesitherapeute" },
    "embed_badge": { "href": "https://capturia.ecole-futee.com/embed/badge/kinesitherapeute" }
  }
}

// /api/v1/secteur/:slug
{
  "slug": "btp-construction",
  "name": "BTP & Construction",
  "jobs_count": 25,
  "avg_score": 28,
  "top": [
    { "rank": 1, "slug": "couvreur", "title_fr": "Couvreur", "score": 18, "tag": "Métier-refuge", "fiche_url": "..." }
  ],
  "last_computed": "2026-04-01T12:00:00.000Z",
  "license": "Creative Commons CC-BY-SA 4.0"
}

// /api/v1/score-global
{
  "score": 40,
  "tag": "En transformation",
  "sample_size": 235,
  "period": "T2 2026",
  "delta_vs_previous": -1.2,
  "previous_period": "T1 2026",
  "decomposition": {
    "empirical_share": 0.58,
    "theoretical_share": 0.39,
    "institutional_share": 0.03
  },
  "license": "Creative Commons CC-BY-SA 4.0"
}

3. Licence Creative Commons CC-BY-SA 4.0 — obligations

Les données scorées et métadonnées exposées via les widgets et l'API sont publiées sous Creative Commons CC-BY-SA 4.0. Vous pouvez les réutiliser librement, y compris à titre commercial, sous réserve des obligations suivantes :

  1. Citer la source « Capturia » à proximité de toute donnée réutilisée (titre, légende, paragraphe d'introduction, etc.).
  2. Garder visible la mention « Source : Capturia » et le lien vers capturia.ecole-futee.com (ou la fiche concernée) injectés par les widgets. Toute suppression / masquage CSS de cette mention est interdite (cf. anti-masquage § 1.2).
  3. Indiquer la date de la version réutilisée quand elle est importante (ex. comparaison historique). Le header X-Capturia-Updated est l'autorité.
  4. Ne pas laisser entendre que vous êtes affilié à Capturia, ni que vous fournissez vous-même un service Capturia.

4. Tracking anonyme — table widget_views

Chaque chargement d'iframe /embed/... et chaque fetch initial déclenché par un web component incrémente une ligne dans la table PostgreSQL widget_views. Volontairement minimaliste pour rester RGPD-friendly :

Colonne Type Description
id serial PRIMARY KEY Identifiant auto-incrémenté.
widget_type varchar(16) NOT NULL 'badge' | 'sector' | 'compare' | 'global'.
resource_slug text (nullable) Slug de la ressource ciblée. NULL pour widget_type='global'.
viewed_at timestamptz NOT NULL Horodatage UTC.
country_code varchar(2) (nullable) Code ISO 3166-1 alpha-2 extrait des headers de proxy (CF, Vercel) si présents. NULL sinon (documenté).
referrer_domain text (nullable) Domaine seul du Referer (ex. lemonde.fr). Jamais l'URL complète. NULL si absent ou localhost.

Index : (widget_type, viewed_at DESC) et (referrer_domain, viewed_at DESC) pour permettre les agrégations par type / par intégrateur sur fenêtre temporelle.

Ce que nous NE stockons PAS :

  • Aucune adresse IP, en clair ou hashée.
  • Aucun chemin d'URL, paramètre de requête ou identifiant de session en provenance du Referer.
  • Aucun User-Agent brut.
  • Aucun cookie, aucun fingerprint.

L'insertion est strictement fire-and-forget : une panne d'écriture ne bloque jamais le rendu d'un widget ni une réponse API. Une perte ponctuelle de tracking est préférable à une dégradation d'expérience intégrateur.

Note v1 : le double-comptage iframe + appel API web component est volontairement accepté. La déduplication sera évaluée en v2 quand on aura les volumes réels.

Quoi est compté : seuls les rendus réussis (HTTP 200) sont trackés. Les 404 (slug inconnu) et les 400 (paramètres manquants) ne sont pas comptés — ce sont des erreurs d'intégration, pas des vues utilisateur. Si vous remarquez un trou dans vos stats, vérifiez d'abord le slug.

4.1 Stratégie de rétention recommandée

Aucun job de purge / agrégation n'est implémenté en v1, mais la politique cible (à mettre en place quand la table grossit) est la suivante :

  • 0–90 jours : conservation brute (lignes individuelles).
  • 90–365 jours : agrégation mensuelle par couple (widget_type, referrer_domain, country_code, mois) puis suppression des lignes brutes.
  • >365 jours : suppression complète des agrégats détaillés ; on ne garde qu'un total annuel par (widget_type, country_code, année).

Cette stratégie est documentée ici plutôt qu'implémentée pour rester honnête sur l'état v1.


5. Politique de rate-limit

L'API publique /api/v1/* est limitée à 60 requêtes par minute par IP. Au-delà :

  • Statut 429 Too Many Requests.
  • Header Retry-After: <secondes> indiquant le délai avant nouvelle tentative.
  • Headers X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset sur toutes les réponses.
  • Payload : { "error": "rate_limit_exceeded", "message": "...", "retry_after": <int> }.

Le rate-limit s'applique par IP normalisée (IPv4 et IPv6-mapped IPv4 sont fusionnés dans le même bucket).

Bypass interne (server-to-server) : les routes Next.js iframe (/embed/sector, /embed/compare, /embed/score-global) appellent l'API en interne via la boucle locale (API_INTERNAL_URL, par défaut http://localhost:8080). Le rate-limiter exempte les IPs loopback (127.0.0.1, ::1, ::ffff:127.0.0.1) — sinon tout le trafic iframe légitime serait agrégé sur une seule IP backend et déclencherait des 429 dès ~30 req/min (compare = 2 appels). L'IP loopback n'étant jamais routable depuis l'extérieur, l'exemption est intrinsèquement sûre — pas de secret partagé à gérer.

Limite connue v1 — dette technique assumée : l'implémentation est in-memory process-local. Si nous passons à plusieurs instances derrière un load-balancer, chaque instance aura son propre compteur, ce qui multiplie effectivement la limite par le nombre d'instances. À migrer vers un store partagé (Redis, en commençant par un simple INCR + EXPIRE) si la pression réelle le justifie. Pour l'instant (mono-instance Replit deployment), cette implémentation est suffisante.

5.1 Comment éviter le rate-limit

  • Côté CDN : nos réponses sont Cache-Control: public, max-age=3600, un CDN devant votre serveur résout la majorité du trafic sans nous toucher.
  • Côté client : factorisez les appels (pas un fetch par badge si vous affichez 50 badges sur une page — utilisez /api/v1/metiers).
  • Au-delà de 10 000 vues/mois ou en cas de besoin spécifique : voir § 6 — contactez-nous, on peut whitelister ou prévoir un canal dédié.

6. Contact pour usages massifs

Pour toute intégration au-delà de 10 000 vues / mois ou pour qualifier un cas d'usage particulier (presse nationale, école avec intranet, agrégateur orientation…), écrivez-nous à :

contact@capturia.ecole-futee.com

Précisez :

  • Le ou les widgets envisagés.
  • Le volume estimé (vues/mois).
  • Le ou les domaines hôtes.
  • Si vous avez besoin d'une variante (couleurs, langue, branding école — possible à la marge en v2).

Nous répondons sous 5 jours ouvrés.


7. Changelog & versioning

L'API est versionnée par préfixe URL (/api/v1/). Les changements non rétrocompatibles passeront par /api/v2/ ; /api/v1/ restera servi pendant au moins 12 mois après la sortie d'une v2.

Date Version Changement
2026-05-04 v1 Tracking widget_views, rate-limit 60 req/min/IP, doc complète (Task #71).
2026-04 v1 4 web components (badge, sector, compare, score-global) — Task #70.
2026-04 v1 3 widgets iframe additionnels + endpoints API — Task #69.
2026-04 v1 Widget badge iframe + endpoint /api/v1/metier/:slug — Task #68.