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.cominjecté 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/jsonX-License: CC-BY-SA-4.0X-Capturia-Updated: <ISO8601>— date du derniercomputed_atagré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 :
- Citer la source « Capturia » à proximité de toute donnée réutilisée (titre, légende, paragraphe d'introduction, etc.).
- 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). - Indiquer la date de la version réutilisée quand elle est
importante (ex. comparaison historique). Le header
X-Capturia-Updatedest l'autorité. - 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-Agentbrut. - 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-Resetsur 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éfauthttp://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. |