Documentation

Le site repose sur une API publique avec dix-huit endpoints instantanés. Chacun est documenté ci-dessous : ce qu'il fait, ses limites et un exemple requête/réponse. Sans compte ; les résultats instantanés ne sont pas stockés. L'APIs de gestion en fin de page (surveillance de disponibilité, pages de statut, liens courts) sont l'exception avec état : elles persistent sous leurs propres clés de gestion.

Codes de statut

  • 301 — Redirection permanente

    Redirection permanente. Les moteurs de recherche transfèrent les signaux de classement vers la destination.

  • 302 — Redirection temporaire

    Redirection temporaire. L'URL d'origine reste canonique.

  • 307 — Redirection temporaire

    Redirection temporaire qui préserve la méthode et le corps de la requête.

  • 308 — Redirection permanente

    Redirection permanente qui préserve la méthode et le corps de la requête.

  • 200 — Succès

    Destination finale atteinte. La chaîne s'arrête ici.

  • 404 — Erreur client

    La destination n'existe pas — une cible de redirection cassée.

  • 500 — Erreur serveur

    La destination a été résolue mais le serveur n'a pas répondu correctement.

Résultats classifiés, pas des erreurs

Sur les quatre endpoints instantanés, les cibles bloquées, les domaines insolubles et les URL invalides ne sont pas des erreurs : toute requête bien formée renvoie HTTP 200 avec un statut qui classe le résultat. Seules les requêtes malformées, les corps trop volumineux et la limitation de débit produisent des erreurs.

Limites communes

Protocoles
http, https
Méthode
GET
Limites de débit
30 requêtes/minute et 500/jour par IP — partagées par tous les endpoints
Corps de la requête
jusqu'à 10 Kio, JSON strict

Annuaire de services & signalements communautaires (Phase 14)

Un annuaire public et curaté de services en ligne populaires avec signalements communautaires anonymes. Les lectures sont publiques et mises en cache; le POST de signalement est l'exception stateful — il persiste et porte limite de débit, déduplication et modération. Signal communautaire et vérification automatique sont toujours des indicateurs séparés; rien ici n'affirme une panne confirmée.

GET /api/v1/catalog/services?sort=reports_24h&page=1&page_size=20
→ 200
{ "items": [ { "slug": "chatgpt", "name": "ChatGPT",
               "category": { "slug": "ai-assistants", "name": "AI Assistants" },
               "community": { "signal": "reports_increasing", "reports_15m": 3,
                              "reports_1h": 11, "reports_24h": 42,
                              "last_report_at": "2026-10-20T09:58:12Z" },
               "automated_check": { "state": "operational",
                                    "checked_at": "2026-10-20T09:55:00Z" } } ],
  "page": 1, "page_size": 20, "total": 1 }

POST /api/v1/catalog/services/chatgpt/reports
{ "problem_type": "site_unreachable", "comment": "Login loop on mobile" }
→ 201
{ "service_slug": "chatgpt", "problem_type": "site_unreachable",
  "comment": "Login loop on mobile", "created_at": "2026-10-20T10:01:33Z" }

Reads are public and cached (ETag + max-age=30); unknown/draft/hidden
slugs all answer 404 (no existence oracle); merged slugs 301 to the
target. Reports are anonymous: no account, no network fetch on
submission, rate limited per IP, comments plain text (≤500 chars).
Community signal and the automated check are always separate blocks —
no value on this API means "confirmed down".
Pagination
page ≥1, page_size par défaut 20, max 100 ; ordre déterministe (départage par slug)
Limites de signalement
dédup 1 par service toutes les 10 min par IP, puis 10/h et 30/jour par IP ; 429 + Retry-After ; fail-closed
Modération et confidentialité
politique auto-publish ou hold-all ; interrupteurs d'arrêt ; seul un hachage pseudonyme à rotation quotidienne est stocké — jamais l'IP brute ni le user-agent

Redirect Checker — POST /api/v1/check

Suit toute la chaîne de redirections et stocke le résultat.

POST /api/v1/check
Content-Type: application/json

{ "url": "https://bit.ly/example" }

{
  "id": "9pr2kxq4",
  "status": "completed",
  "input_url": "https://bit.ly/example",
  "final_url": "https://example.com/product",
  "final_domain": "example.com",
  "final_status": 200,
  "redirect_count": 3,
  "duration_ms": 482,
  "redirects": [
    { "sequence": 0, "requested_url": "https://bit.ly/example",
      "status_code": 301, "next_url": "https://tracker.example/click",
      "duration_ms": 83 },
    { "sequence": 3, "requested_url": "https://example.com/product",
      "status_code": 200, "duration_ms": 184 }
  ],
  "errors": null
}

GET /api/v1/check/{id} — récupère un résultat enregistré par son ID public.
Redirections max
20 sauts
Délai de requête
15 secondes

GET /api/v1/check/{id} — récupère un résultat enregistré par son ID public.

Uptime Checker — POST /api/v1/uptime-check

Vérifie si un site répond. Le résultat est classé : UP pour une réponse saine, HTTP_ERROR si le site renvoie 4xx/5xx, plus TIMEOUT, DNS_ERROR, TLS_ERROR, CONNECTION_ERROR et BLOCKED_TARGET.

POST /api/v1/uptime-check
Content-Type: application/json

{ "url": "https://example.com" }

{
  "url": "https://example.com",
  "status": "UP",
  "reachable": true,
  "httpStatus": 200,
  "httpStatusText": "OK",
  "responseTimeMs": 184,
  "finalUrl": "https://example.com/",
  "redirectCount": 1,
  "tls": { "enabled": true, "valid": true }
}
Redirections max
10 sauts
Délai de requête
10 secondes

HTTP Status Checker — POST /api/v1/http-status-check

Renvoie le code de statut final d'une URL, les en-têtes de la réponse finale, un résumé compact de la chaîne de redirections et l'état TLS.

POST /api/v1/http-status-check
Content-Type: application/json

{ "url": "https://example.com" }

{
  "url": "https://example.com",
  "status": "UP",
  "reachable": true,
  "httpStatus": 200,
  "httpStatusText": "OK",
  "finalUrl": "https://example.com/",
  "redirectCount": 1,
  "headers": {
    "Content-Type": ["text/html; charset=utf-8"],
    "Server": ["nginx"]
  },
  "headers_analysis": {
    "security_missing": ["Strict-Transport-Security", "Content-Security-Policy"],
    "caching": { "Cache-Control": "max-age=604800" }
  },
  "tls": { "enabled": true, "valid": true }
}
Redirections max
10 sauts
Délai de requête
10 secondes

Response Time Checker — POST /api/v1/response-time-check

Mesure 1 à 5 échantillons et classe un verdict de vitesse à partir de la médiane du time to first byte. Renvoie la durée de chaque échantillon, min/médiane/max/moyenne et la répartition des temps du dernier saut.

POST /api/v1/response-time-check
Content-Type: application/json

{ "url": "https://example.com", "samples": 3 }

{
  "url": "https://example.com",
  "samples": 3,
  "status": "UP",
  "verdict": "FAST",
  "completed": 3,
  "timesMs": [184, 201, 176],
  "stats": { "minMs": 176, "medianMs": 184, "maxMs": 201, "meanMs": 187 },
  "breakdown": { "dnsMs": 12, "connectMs": 23, "tlsMs": 45, "ttfbMs": 184, "totalMs": 190 },
  "httpStatus": 200,
  "httpStatusText": "OK",
  "finalUrl": "https://example.com/",
  "redirectCount": 1,
  "tls": { "enabled": true, "valid": true }
}
Échantillons
1–5 (défaut 3)
Délai par échantillon
10 secondes
Délai global
30 secondes

SSL Checker — POST /api/v1/ssl-check

Inspecte le certificat TLS présenté par un hôte : validité, émetteur, SAN, protocole et chiffrement négociés, chaîne. Sonde TLS uniquement — la page n'est jamais chargée ; un certificat défectueux est signalé avec un motif précis.

POST /api/v1/ssl-check
Content-Type: application/json

{ "url": "https://example.com" }

{
  "url": "https://example.com",
  "host": "example.com",
  "status": "UP",
  "reachable": true,
  "handshakeMs": 45,
  "tls": {
    "version": "TLS 1.3",
    "cipher": "TLS_AES_256_GCM_SHA384",
    "verified": true,
    "status": "valid",
    "daysRemaining": 89,
    "notBefore": "2026-08-01T00:00:00Z",
    "notAfter": "2026-10-30T00:00:00Z",
    "subject": "CN=example.com",
    "issuer": "CN=R3,O=Let's Encrypt,C=US",
    "sans": ["example.com", "www.example.com"],
    "chain": ["CN=example.com", "CN=R3,O=Let's Encrypt,C=US"]
  }
}
Port
celui de l'URL (443 par défaut)
Délai d'expiration de la sonde
10 secondes

DNS Checker — POST /api/v1/dns-check

Recherche les enregistrements A, AAAA, CNAME, MX, TXT et NS d'un hôte. Recherche et rapport uniquement : aucune connexion n'est établie et chaque adresse résolue est étiquetée publique ou non publique.

POST /api/v1/dns-check
Content-Type: application/json

{ "url": "example.com" }

{
  "url": "example.com",
  "host": "example.com",
  "status": "UP",
  "durationMs": 38,
  "records": {
    "a": ["93.184.216.34"],
    "aaaa": ["2606:2800:220:1:248:1893:25c8:1946"],
    "cname": [],
    "mx": [{ "host": "mail.example.com", "pref": 10 }],
    "txt": ["v=spf1 include:_spf.example.com ~all"],
    "ns": ["ns1.example.com", "ns2.example.com"]
  },
  "addressLabels": [
    { "address": "93.184.216.34", "classification": "public", "reason": "public" }
  ]
}
Familles d'enregistrements
A, AAAA, CNAME, MX, TXT, NS
Délai de recherche
10 secondes au total, 3 secondes par famille

Vérificateur d'expiration de domaine — POST /api/v1/domain-expiry-check

Répond à la question « quand ce domaine expire-t-il ? » via le flux RDAP fixe de rdap.org : verdict, jours restants, dates d'enregistrement/expiration et statuts du registre. Premier outil du dépôt à interroger un tiers fixe : HTTPS seul à chaque saut, pas de proxy, redirections et taille bornés ; le domaine interrogé n'est jamais contacté. 404 devient un verdict not_found (non enregistré / registre sans RDAP / rien publié), 429 et 5xx un honnête UNKNOWN_ERROR.

POST /api/v1/domain-expiry-check
Content-Type: application/json

{ "url": "example.com" }

{
  "url": "example.com",
  "host": "example.com",
  "status": "UP",
  "verdict": "ok",
  "daysRemaining": 214,
  "registration": "2019-01-15T05:00:00Z",
  "expiration": "2027-04-15T04:00:00Z",
  "lastChanged": "2026-08-01T11:20:00Z",
  "registryStatuses": ["ok"],
  "source": "rdap",
  "durationMs": 420
}
Verdicts
ok, expiring (≤30 jours), expired, not_found, unknown
Délai de la requête
10 secondes au total (DOMAIN_EXPIRY_TIMEOUT)
Accès sortant
rdap.org uniquement ; HTTPS à chaque saut ; proxy désactivé ; limite de 10 redirections ; plafond de 1 Mio ; le domaine n'est jamais contacté

Requête WHOIS — POST /api/v1/whois-check

Répond à la question « que sait le registre d'un domaine » : registrar, serveurs de noms, dates et statuts. RDAP d'abord via rdap.org, avec repli sur le WHOIS classique (TCP 43 brut) pour les zones sans RDAP — le premier outil du dépôt en TCP brut : chaque renvoi doit être une cible host[:43], résolu via le pipeline SSRF partagé et joint à l'adresse publique épinglée ; limite de 2 renvois, plafond de 64 Kio ; le domaine interrogé n'est jamais contacté. Un 404 du RDAP déclenche le repli ; une réponse définitive d'inexistence apparaît comme notFound.

POST /api/v1/whois-check
Content-Type: application/json

{ "url": "mail.ru" }

{
  "url": "mail.ru",
  "host": "mail.ru",
  "status": "UP",
  "source": "whois",
  "registrar": "VK-TECH",
  "nameservers": ["ns1.mail.ru", "ns2.mail.ru"],
  "registration": "1997-10-01T00:00:00Z",
  "expiration": "2026-10-01T00:00:00Z",
  "statuses": ["REGISTERED, DELEGATED, VERIFIED"],
  "durationMs": 640
}
Sources
RDAP via rdap.org ; WHOIS classique (TCP 43) via la chaîne de renvois de whois.iana.org
Délai de la requête
15 secondes au total (WHOIS_TIMEOUT)
Accès sortant
renvois host[:43] uniquement ; validés SSRF et épinglés avant connexion ; limite de 2 renvois ; plafond de 64 Kio ; le domaine n'est jamais contacté

Disponibilité de domaine — POST /api/v1/domain-availability

Répond, par TLD, à la question « ce nom est-il libre », avec une machine à états honnête : AVAILABLE uniquement si l'interrogation RDAP répond 404 et que les métadonnées TLD intégrées confirment que le registre exploite RDAP ; un introuvable WHOIS-only reste UNKNOWN (probablement disponible) ; RESTRICTED découle des métadonnées. Prise en charge IDN complète (Unicode en entrée, punycode canonique, displayDomain en sortie) et fan-out multi-TLD optionnel (1–10 par requête). Premium reste toujours unknown en MVP — la tarification est un futur sous-système séparé.

POST /api/v1/domain-availability
Content-Type: application/json

{ "domain": "пример.рф", "tlds": ["com", "io"] }

{
  "url": "пример.рф",
  "displayDomain": "пример.рф",
  "host": "xn--e1afmkfd.xn--p1ai",
  "status": "UP",
  "results": [
    {
      "tld": "com",
      "availability": "AVAILABLE",
      "reason": "rdap_not_found",
      "confidence": "high",
      "source": "rdap",
      "premium": "unknown",
      "checkedAt": "2026-09-03T12:00:02Z"
    },
    {
      "tld": "io",
      "availability": "UNKNOWN",
      "reason": "tld_rdap_unknown",
      "confidence": "low",
      "source": "tld-metadata",
      "premium": "unknown",
      "checkedAt": "2026-09-03T12:00:02Z"
    }
  ],
  "tldMetaVersion": "2026-09-02",
  "durationMs": 1240
}
TLDs par requête
1–10 uniques (Unicode ou punycode) ; 4 recherches simultanées au maximum dans un budget de 15 secondes (DOMAIN_AVAILABILITY_TIMEOUT)
Verdicts
AVAILABLE | REGISTERED | RESTRICTED | INVALID | UNKNOWN | ERROR — UNKNOWN n'est jamais maquillé en AVAILABLE ; PREMIUM est inatteignable en MVP
Cache
di:avail 60s, di:reg 6h, fail-open ; les résultats négatifs ne sont jamais mis en cache

Âge de domaine — POST /api/v1/domain-age

Indique l’âge d’un domaine : la date de création exacte du registre et une répartition déterministe années/mois/jours. Projection légère sur les données WHOIS — une seule requête sert disponibilité, âge et registrar dans la fenêtre de cache.

POST /api/v1/domain-age
Content-Type: application/json

{ "domain": "example.com" }

{
  "url": "example.com",
  "host": "example.com",
  "status": "UP",
  "age": {
    "known": true,
    "createdAt": "2019-01-15T05:00:00Z",
    "ageInDays": 2809,
    "years": 7,
    "months": 7,
    "days": 20
  },
  "source": "rdap",
  "durationMs": 390
}
Calcul de l’âge
dates UTC uniquement ; années/mois pleins via une ancre de mois ajustée ; jours totaux arrondis à l’inférieur
Âge inconnu
age.known=false + raison bornée (no_data, inconsistent_data) — jamais une supposition
Cache
di:reg 6h partagé avec disponibilité/registrar (fail-open)

Vérificateur de registrar — POST /api/v1/domain-registrar

Indique auprès de quel registrar un domaine est enregistré, normalisé via une table d’alias curatée (GoDaddy, Namecheap, Cloudflare et plus) — les noms inconnus sont montrés exactement tels que publiés. Organisation du registre et provenance RDAP/WHOIS inclus ; partage le cache d’enregistrement avec âge/disponibilité.

POST /api/v1/domain-registrar
Content-Type: application/json

{ "domain": "example.com" }

{
  "url": "example.com",
  "host": "example.com",
  "status": "UP",
  "registrar": "GoDaddy.com, LLC",
  "registrarCanonical": "GoDaddy",
  "registrarIanaId": 625,
  "registry": "Verisign, Inc.",
  "source": "rdap",
  "checkedAt": "2026-09-04T12:00:00Z",
  "durationMs": 410
}
Registrar
nom affiché + badge d’alias canonique en cas de correspondance ; l’ID IANA affiche « — » (les données du registre ne le portent pas encore)
Non publié
les enregistrements derrière proxy de confidentialité ou censurés répondent UP avec la raison no_data — jamais une coquille vide

Générateur de domaines — POST /api/v1/domain-generate

Génère des domaines candidats à partir de 1–3 mots-clés : formes directes, préfixes, suffixes, composés et formes courtes — dédupliqués, ordonnés et plafonnés. Pur et instantané : aucun appel au registre pendant la génération ; les vérifications par candidat sont des actions séparées et explicites.

POST /api/v1/domain-generate
Content-Type: application/json

{ "keywords": ["coffee", "map"], "tlds": ["com", "io"], "limit": 24 }

{
  "status": "UP",
  "candidates": [
    { "domain": "coffee.com", "strategy": "direct" },
    { "domain": "coffee.io", "strategy": "direct" },
    { "domain": "getcoffee.com", "strategy": "prefix" },
    { "domain": "coffeemap.com", "strategy": "compound" }
  ],
  "total": 24
}
Candidats
plafond strict 50, 24 affichés par défaut ; ≤10 TLDs filtrés par l’instantané de métadonnées TLD
Aucun trafic registre
la génération n’appelle aucun registre — les vérifications par candidat passent par l’endpoint de disponibilité avec ses propres limites

Aperçu de domaine — POST /api/v1/domain-overview

Un profil composé : disponibilité sur le TLD du domaine, date de création avec la répartition d’âge déterministe, registrar (normalisé, avec numéro IANA), projection d’expiration et les constats de configuration du registre. Les sections sont indépendantes — une requête échouée ne vide jamais les autres.

POST /api/v1/domain-overview
Content-Type: application/json

{ "domain": "example.com" }

{
  "url": "example.com",
  "host": "example.com",
  "status": "UP",
  "availability": {
    "tld": "com", "availability": "REGISTERED", "reason": "registered",
    "confidence": "high", "source": "rdap", "premium": "unknown",
    "checkedAt": "2026-09-05T12:00:00Z"
  },
  "registration": {
    "known": true, "createdAt": "2019-01-15T05:00:00Z",
    "ageYears": 7, "ageMonths": 7, "ageDays": 21,
    "registrar": "GoDaddy.com, LLC", "registrarCanonical": "GoDaddy",
    "registrarIanaId": 625, "registry": "Verisign, Inc.", "source": "rdap",
    "checkedAt": "2026-09-05T12:00:01Z"
  },
  "expiry": {
    "host": "example.com", "source": "rdap", "verdict": "ok",
    "daysRemaining": 222, "expiration": "2027-04-15T04:00:00Z",
    "checkedAt": "2026-09-05T12:00:02Z"
  },
  "holds": [],
  "dnssec": [{ "id": "dnssec-absent", "state": "info", "severity": "info", "detail": "unsigned" }],
  "tldMetaVersion": "2026-09-02",
  "durationMs": 1620
}
Sections
disponibilité (avec porte de métadonnées) + enregistrement (di:reg, 6h partagé) + expiration (rdap.org) + constats holds/dnssec
Les constats sont des faits
holds/DNSSEC sont des signaux de configuration du registre — jamais des verdicts de sécurité ; DNS/SSL restent dans leurs outils autonomes

Vérificateur de sécurité — POST /api/v1/security-check

Revise passive et consultative de la configuration d'une réponse obtenue : en-têtes de sécurité (profondeur HSTS, points saillants CSP, COOP/COEP/CORP, X-XSS-Protection), flags de cookies, faits TLS, dégradations de chaîne et divulgation. Aucun sondage, aucune affirmation de vulnérabilité, aucune note ; les valeurs de cookies ne quittent jamais le backend. Le statut HTTP ne conditionne pas la revue (un 404/500 joignable est examiné).

POST /api/v1/security-check
Content-Type: application/json

{ "url": "https://example.com" }

{
  "url": "https://example.com",
  "finalUrl": "https://example.com/",
  "status": "UP",
  "durationMs": 610,
  "summary": { "total": 14, "passed": 9, "warn": 3, "failed": 2 },
  "findings": [
    { "id": "hsts-present", "state": "fail", "severity": "high" },
    { "id": "tls-cert", "state": "pass", "severity": "info", "detail": "valid" }
  ],
  "cookies": [
    { "name": "session", "secure": false, "httpOnly": true, "sameSite": "Lax" }
  ],
  "tls": { "enabled": true, "version": "TLS 1.3", "certStatus": "valid", "daysRemaining": 89 }
}
Périmètre
~23 IDs de vérification sur en-têtes/cookies/TLS/chaîne/divulgation ; requête passive uniquement
Cookies
Noms et flags uniquement (Secure/HttpOnly/SameSite) — les valeurs ne quittent jamais le backend
Délai de la revue
15 secondes au total (SECURITY_CHECK_TIMEOUT) — 10 s par défaut

URL Analyzer — POST /api/v1/url-analyze

Analyse syntaxique pure d'une URL : identité canonique rédigée (schéma, hôte, formes IDN, domaine registrable naïf, port, chemin, requête, fragment), paramètres ordonnés avec doublons et classification de suivi versionnée, comptages de suivi, signaux d'information et notes de normalisation. Aucune requête, aucun DNS, aucune persistance — l'analyse est locale et prend des millisecondes.

POST /api/v1/url-analyze
Content-Type: application/json

{ "url": "https://xn--e1afmkfd.xn--p1ai/search?q=thing&utm_source=news&id=7&id=8" }

{
  "status": "UP",
  "identity": {
    "hasCredentials": false,
    "scheme": "https",
    "host": "xn--e1afmkfd.xn--p1ai",
    "displayHost": "пример.рф",
    "registrableDomain": "пример.рф",
    "isIdn": true,
    "path": "/search",
    "query": "q=thing&utm_source=news&id=7&id=8",
    "normalized": "https://xn--e1afmkfd.xn--p1ai/search?q=thing&utm_source=news&id=7&id=8"
  },
  "parameters": [
    { "name": "q", "value": "thing", "classification": { "category": "unknown", "confidence": "none" } },
    { "name": "utm_source", "value": "news", "classification": { "category": "utm", "label": "utm_source", "confidence": "exact" } },
    { "name": "id", "value": "7", "occurrence": 1 },
    { "name": "id", "value": "8", "occurrence": 2 }
  ],
  "tracking": { "utm": 1, "analytics": 0, "adsClickId": 0, "sessionSensitive": 0, "unknown": 3 },
  "signals": ["idn_host"],
  "normalization": { "schemeAssumed": false, "punycodeApplied": true, "defaultPortOmitted": false, "hostLowercased": false },
  "registryVersion": "2026-09-1"
}
Portée
Composants + paramètres ordonnés + classification du registre + signaux informatifs ; zéro réseau
Rédaction
Les mots de passe ne sont jamais renvoyés ; les valeurs des paramètres sensibles sont masquées (***) partout, les noms restent visibles

URL Expander — POST /api/v1/url-expand

Expansion instantanée et sans persistance d'une chaîne de redirections via le moteur partagé : chaque saut passe la même validation SSRF centrale, les boucles et la limite de 10 redirections sont distinguées par reason, et l'URL finale reçoit une analyse de suivi complète. Les URLs renvoyées sont rédigées — les mots de passe ne quittent jamais le backend. Sans cache : les chaînes sont sensibles au temps, la fraîcheur est la bonne réponse.

POST /api/v1/url-expand
Content-Type: application/json

{ "url": "https://bit.ly/3xyz" }

{
  "url": "https://bit.ly/3xyz",
  "status": "UP",
  "finalUrl": "https://example.com/landing?utm_source=n",
  "hopCount": 2,
  "chain": [
    { "url": "https://bit.ly/3xyz", "status": 301, "location": "https://bit.ly/abc" },
    { "url": "https://bit.ly/abc", "status": 302, "location": "https://example.com/landing?utm_source=n" }
  ],
  "analysis": { "status": "UP", "tracking": { "utm": 1, "analytics": 0, "adsClickId": 0, "sessionSensitive": 0, "unknown": 0 } },
  "source": "engine",
  "durationMs": 820
}
Portée
Chaîne de sauts rédigée + statut classifié (boucle vs limite) + analyse de l'URL finale ; pas de DNS/TLS/chronométrage par saut (domaine du Redirect Checker)
Budget d'expansion
10 secondes au total (URL_EXPAND_TIMEOUT) ; limite de 10 sauts

UTM Builder — POST /api/v1/utm-build

Construction déterministe et sans état d'URLs de campagne : les paramètres non liés sont conservés dans l'ordre, les UTM canoniques sont écrasés ou gardés par champ, les doublons se réduisent au dernier, le fragment est préservé, et les destinations avec identifiants sont refusées in-band. Une même entrée produit toujours une sortie identique à l'octet près.

POST /api/v1/utm-build
Content-Type: application/json

{ "url": "https://example.com/landing?foo=1&utm_source=old#pricing", "utmSource": "newsletter", "utmMedium": "email", "utmCampaign": "spring_sale", "utmContent": "header_cta" }

{
  "status": "UP",
  "generatedUrl": "https://example.com/landing?foo=1&utm_source=newsletter&utm_medium=email&utm_campaign=spring_sale&utm_content=header_cta#pricing",
  "warnings": ["existing_utm_overwritten:utm_source", "empty_field_skipped:utm_term"],
  "parameters": { "utm_source": "newsletter", "utm_medium": "email", "utm_campaign": "spring_sale", "utm_content": "header_cta" }
}
Portée
Ajout d'UTM dans un ordre fixe + politique preserve/overwrite/collapse + avertissements ; pur, sans réseau
Déterminisme
Sortie identique à l'octet près pour une entrée identique ; champs ≤256 caractères ; champs inconnus rejetés

Website Health — POST /api/v1/website-health

Le diagnostic composé : une requête normalise la cible et lance des lanes concurrentes bornées au-dessus des moteurs existants — un seul contrôle HTTP alimente cinq sections (disponibilité, statut, temps de réponse, en-têtes, configuration de sécurité) ; DNS, la sonde de certificat TLS et le profil de domaine s'exécutent comme des lanes distinctes. Le verdict est un état global agrégé avec le détail par contrôle et des liens vers les outils autonomes. Les constats décrivent uniquement les contrôles effectués — jamais une garantie de sécurité.

POST /api/v1/website-health
Content-Type: application/json

{ "url": "https://example.com" }

{
  "target": {
    "canonical": "https://example.com/",
    "displayHost": "example.com",
    "scheme": "https",
    "hasCredentials": false,
    "isDomain": true,
    "isHttp": true,
    "valid": true
  },
  "overall": "WARNING",
  "summaryKey": "health.summary.warning",
  "checks": [
    { "type": "http_availability", "state": "pass",
      "summaryKey": "health.check.httpAvailability.pass",
      "value": "200", "durationMs": 612, "applicable": true,
      "detailUrl": "/uptime-checker" },
    { "type": "http_status", "state": "pass",
      "summaryKey": "health.check.httpStatus.pass",
      "value": "200 OK", "applicable": true,
      "detailUrl": "/http-status-checker" },
    { "type": "response_time", "state": "pass",
      "summaryKey": "health.check.responseTime.pass",
      "value": "612 ms", "applicable": true,
      "detailUrl": "/response-time-checker" },
    { "type": "headers", "state": "warn",
      "summaryKey": "health.check.headers.warn",
      "value": "2 missing", "reason": "findings", "applicable": true,
      "detailUrl": "/security-checker" },
    { "type": "security", "state": "warn",
      "summaryKey": "health.check.security.warn",
      "value": "14 of 21 passed", "reason": "findings", "applicable": true,
      "detailUrl": "/security-checker" },
    { "type": "tls", "state": "pass",
      "summaryKey": "health.check.tls.pass",
      "value": "TLS 1.3 · valid, 89 days", "durationMs": 140,
      "applicable": true, "detailUrl": "/ssl-checker" },
    { "type": "dns", "state": "pass",
      "summaryKey": "health.check.dns.pass",
      "value": "A, AAAA, MX, NS", "durationMs": 95,
      "applicable": true, "detailUrl": "/dns-checker" },
    { "type": "domain", "state": "pass",
      "summaryKey": "health.check.domain.pass",
      "value": "expires in 214 days", "durationMs": 730,
      "applicable": true, "detailUrl": "/domain-overview" }
  ],
  "meta": { "checksTotal": 8, "checksPassed": 6, "checksWarn": 2,
            "checksFailed": 0, "checksUnknown": 0,
            "tldMetaVersion": "2026-09-03", "durationMs": 1480 }
}
Portée
Une exécution HTTP alimente cinq sections ; lanes DNS/TLS/domaine ; agrégat HEALTHY/WARNING/ERROR/PARTIAL/UNKNOWN
Limites
Budget global de 25 s + budgets par lane, ≤3 lanes simultanées ; cibles invalides in-band ; pas de cache — fraîcheur d'abord

Uptime Monitoring — l'API de gestion /api/v1/monitors

L'exception avec état par rapport aux outils instantanés : créez un moniteur pour une URL et le worker le vérifie selon un calendrier fixe — avec historique d'exécutions, statistiques de disponibilité, épisodes d'incidents et notifications webhook. La propriété est une clé de gestion affichée exactement une fois à la création ; chaque lecture et écriture s'authentifie avec elle, et une clé erronée est indiscernable d'un moniteur absent (404 uniforme).

POST /api/v1/monitors
Content-Type: application/json

{ "url": "https://example.com", "name": "Prod site", "interval": 300 }

201 Created
{
  "monitor": { "id": "b1…", "name": "Prod site",
               "normalizedTarget": "https://example.com/",
               "state": "unknown", "interval": 300 },
  "manageUrl": "/monitors/b1…?key=…",
  "key": "…shown exactly once…"
}

GET /api/v1/monitors/{id}?key=…       detail + stats window + incidents
GET /api/v1/monitors/{id}/history     cursor-paginated executions
GET /api/v1/monitors/{id}/history/daily  per-day buckets (30-day cap)
GET /api/v1/monitors/{id}/incidents   incident episodes
POST /api/v1/monitors/{id}/check      enqueue a priority manual check
PATCH /api/v1/monitors/{id}           name / interval / webhook / pause
DELETE /api/v1/monitors/{id}          cascade delete

Credentials in the target are rejected (credentials_not_allowed); the
target is SSRF-pre-validated at creation and re-validated on every run.
Périmètre
Création / liste / détail / pause / suppression / vérification manuelle / historique / incidents ; intervalles fixes 60–3600s ; INVALID_URL et BLOCKED_TARGET in-band à la création
Gestion des clés
Clé 256 bits affichée une seule fois (sha256 stocké) ; les identifiants dans les cibles sont refusés ; SSRF prévalidé à la création et revalidé à chaque exécution

Pages de statut publiques — l'API de gestion /api/v1/status-pages

Composez vos moniteurs en une page de statut partageable : choisissez les composants, ordonnez-les, ajustez les noms affichés et publiez sous un slug stable. Les pages vivent sous la même clé de gestion que les moniteurs ; les brouillons sont privés, la publication est idempotente et réversible, et chaque mutation supprime l'entrée du cache public — les changements d'accès s'appliquent dans la requête même.

POST /api/v1/status-pages
Content-Type: application/json
X-Monitor-Key: <your key>

{ "name": "Prod API" }

201 Created
{
  "page": { "publicId": "p1…", "name": "Prod API",
            "slug": "prod-api", "published": false },
  "manageUrl": "/status-pages/p1…?key=…"
}

GET  /api/v1/status-pages/{id}?key=…   editor payload (members + monitors)
PATCH /api/v1/status-pages/{id}        name/description/slug + membership
POST /api/v1/status-pages/{id}/publish      → live at /status/{slug}
POST /api/v1/status-pages/{id}/unpublish    → removed within the request
DELETE /api/v1/status-pages/{id}       cascade delete

GET /api/v1/public/status-pages/{slug}   the public DTO (no key; cached 60s)

Membership is same-key only (≤25 monitors, replace-list semantics);
publish requires ≥1 visible member; the public DTO is an explicit
projection — target URLs, keys and internal ids never cross.
Périmètre
Créer / lister / modifier / liste de membres de remplacement (≤25, même clé uniquement) / publier / dépublier / supprimer ; slugs de 3 à 63 caractères avec liste de mots réservés ; 5 pages max par clé
Surface publique
GET /api/v1/public/status-pages/{slug} + /status/{slug} ; DTO publique explicite uniquement — URLs cibles, clés, hôtes de notification et ids internes ne traversent jamais la frontière ; cache Redis 60s, fail-open

Transformez une longue URL en lien court /go/{code} que vous pouvez gérer : destination validée, code aléatoire cryptographique, statistiques de clics avec fenêtres aujourd'hui/7 jours/30 jours. Les liens vivent sous la même clé de gestion que vos moniteurs et pages de statut — la création s'attache à une clé existante et n'en génère jamais de nouvelle. Les destinations sont validées à la création et à la modification (syntaxe, identifiants refusés, classification SSRF) ; la redirection publique ne récupère jamais rien et répond 302 sans mise en cache.

POST /api/v1/short-links
Content-Type: application/json
X-Monitor-Key: <your key>

{ "destinationUrl": "https://example.com/launch?utm_source=newsletter" }

201 Created
{
  "link": { "id": "sl1…", "code": "a3f9k2mz",
            "shortUrl": "/go/a3f9k2mz",
            "destination": "https://example.com/launch?utm_source=newsletter" }
}

GET    /api/v1/short-links?key=…              your links (newest first)
GET    /api/v1/short-links/{id}?key=…         full link + stats
PATCH  /api/v1/short-links/{id}?key=…         destination/title/expiry (re-validated)
POST   /api/v1/short-links/{id}/disable       public 404, stats kept (enable restores)
DELETE /api/v1/short-links/{id}?key=…         soft delete (idempotent)
GET    /api/v1/short-links/{id}/stats?key=…   total + today/7d/30d clicks

GET /go/a3f9k2mz   → 302 Found + Location (Cache-Control: no-store);
                     unknown/disabled/expired/deleted all answer 404.

Destinations are validated at create/update (http/https, credentials
rejected, SSRF classification); the redirect itself never fetches.
Portée
Créer / lister / modifier / désactiver / activer / suppression logique / statistiques ; ≤100 liens actifs par clé, 30 créations par clé et par heure ; la destination est revalidée à chaque changement
Redirection publique
GET /go/{code} → 302 + no-store ; inconnu/désactivé/expiré/supprimé répondent le même 404 neutre ; la redirection est une pure recherche — elle n'accepte jamais de paramètre de destination et ne résout ni ne récupère la cible ; aucune IP, aucun user-agent stocké

Si un lien /go/{code} ressemble à du phishing, un malware, du spam ou une arnaque, signalez-le — aucun compte requis, vous ne recevrez pas de réponse. Les signalements arrivent dans la file de révision de l'opérateur ; seul l'opérateur peut retirer un lien. Le point de terminaison répond identiquement à tout signalement bien formé : le signalant n'apprend rien sur l'existence du code.

POST /api/v1/short-link-reports
Content-Type: application/json

{ "code": "a3f9k2mz", "reason": "phishing",
  "detail": "Imitates a login page" }

202 Accepted
{ "status": "received" }

Reasons: phishing | malware | spam | scam | other (detail ≤500 chars,
optional plain text). The SAME 202 comes back for unknown, disabled
and deleted codes — no existence oracle. Limits per IP: one report
per code per 10 min, 10/hour, 30/day (429 + Retry-After; Redis
failure = 503, fail-closed). Reports go to the operator queue; only
the operator can take a link down (the owner key cannot override).
Limites et confidentialité
1 signalement par code et 10 min par IP, puis 10/heure et 30/jour (429 + Retry-After ; en cas d'erreur Redis, 503 fail-closed) ; motifs phishing | malware | spam | scam | other ; sans compte, sans e-mail, IP brute jamais stockée — seulement une empreinte rotative quotidienne
Réponse uniforme
tout signalement bien formé reçoit le même 202 received — pour les codes vivants, désactivés, supprimés ou inconnus (pas d'oracle d'existence) ; un code malformé ou un motif inconnu répond 400 avant toute recherche

Comptes & clés API — /api/v1/auth/register et /api/v1/api-keys

Comptes navigateur (e-mail + mot de passe, inscription ouverte ; l'e-mail n'est pas vérifié dans cette version) avec des sessions serveur dans un cookie durci. Les clés API authentifient les appels serveur à serveur sur une liste explicite des familles de ressources. Les échecs de connexion sont génériques — e-mail inconnu et mot de passe erroné sont indiscernables, y compris par le temps de réponse.

POST /api/v1/auth/register
Content-Type: application/json

{ "email": "you@example.com", "password": "…", "displayName": "…" }

201 Created + Set-Cookie: wg_session (HttpOnly; Secure; SameSite=Lax)
{ "account": { "id": "acc…", "email": "you@example.com", … } }

POST /api/v1/auth/login     200 + Set-Cookie (401 generic on failure)
POST /api/v1/auth/logout    deletes the server-side session
GET  /api/v1/account        the session's own profile
PATCH /api/v1/account       update display name (displayName)
GET  /api/v1/account/summary  account-owned resource counts
POST /api/v1/account/claim  management key → re-own key-held resources

POST /api/v1/api-keys               (SESSION) create; secret shown ONCE
{ "name": "CI", "scopes": ["monitors:read"] }
GET  /api/v1/api-keys               (SESSION) list — no secrets
POST /api/v1/api-keys/{id}/revoke   (SESSION) immediate 401 on next use

Bearer access (Authorization: Bearer wg_live_…) is an EXPLICIT
allowlist only: account GET/PATCH, monitors CRUD + reads, status-pages
GET, short-links CRUD + stats; everything else refuses API keys.
Missing/unknown/revoked → generic 401; wrong scope → 403; per-key and
per-account rate limits → 429 + Retry-After.
Liste Bearer
account GET/PATCH (account:read), moniteurs (lecture/écriture), pages de statut GET (lecture), liens courts (lecture/écriture) ; tout le reste refuse les clés API. Les ressources détenues par une clé restent invisibles tant qu'elles ne sont pas rattachées via claim-by-key.
Traitement du secret
La clé wg_live_ complète n'est affichée qu'une seule fois à la création ; seul un hachage à sens unique est stocké. Clés absentes/inconnues/révoquées : un 401 générique unique ; portée manquante : 403 ; limites : 429 + Retry-After.

Identité e-mail du compte (confirmation et récupération)

Socle e-mail de la Phase J : confirmez votre adresse, récupérez un mot de passe perdu et prouvez une connaissance récente du mot de passe pour les actions sensibles.

POST /api/v1/auth/verification/request     (SESSION) 202 {accepted:true}
POST /api/v1/auth/verification/confirm     { "token": "…" } → 200 {verified:true}
POST /api/v1/auth/password-reset/request   { "email": "you@example.com" } → 202 (always the same answer)
POST /api/v1/auth/password-reset/confirm   { "token": "…", "password": "…" } → 200 {reset:true}
POST /api/v1/auth/reauthenticate           (SESSION) { "password": "…" } → 200 {confirmed:true}

Reset revokes ALL sessions in the same transaction; API keys stay
active. Tokens ride mail links as URL fragments — never query strings —
and every mismatch (wrong account, replay, expiry, unknown) answers the
same generic 404.
Jetons
à usage unique, 24 h (confirmation) / 30 min (réinitialisation), fragment uniquement, 404 générique à tout écart
Réinitialisation
ferme toutes les sessions de manière atomique ; les clés API restent valides
Re-authentification
5 minutes, côté serveur, liée à la session
Espaces de travail
GET /api/v1/workspaces — personnel + organisations, no-store, session uniquement
Organisations
création/liste/lecture/renommage/suppression + transfert de propriété ; sessions vérifiées, versions optimistes, reauth pour les actions destructrices
Invitations
invitations par e-mail avec liens à usage unique de 7 jours, quatre rôles fixes, plafonds avec réservation de places et fenêtres de taux obligatoires
Ressources d'organisation
moniteurs/pages/liens partagés sous /organizations/{orgId} ; RBAC de session, mutations versionnées, clés legacy exclues au niveau SQL
Transfert de ressources
déplacer ses moniteurs/pages/liens personnels vers une organisation de manière atomique (fermeture ≤100, versions vérifiées, idempotent)
Clés d'organisation
clés de service (wg_live_…), affichées une seule fois, à périmètre limité, survivent au départ du créateur, révocation immédiate
Audit d'organisation
historique en ajout seul des changements validés (365 jours), lecture Owner/Admin, divulgation minimale des acteurs