leyzly

API pour développeurs

Automatisez vos audits juridiques avec l'API de Leyzly

Une API REST en HTTPS, avec authentification par clé, réponses JSON et webhooks signés. Lancez des audits RGPD, LCEN et cookies, consultez les constats et téléchargez le rapport — le tout depuis votre code.

L'accès à l'API est inclus dans le plan Pro. URL de base : https://api.leyzly.com

Authentification

Chaque requête s'authentifie avec votre clé API dans l'en-tête X-Api-Key. Nous acceptons aussi Authorization: Api-Key <clé>. Générez et révoquez vos clés depuis le tableau de bord — pour des raisons de sécurité, une clé API ne peut ni créer ni révoquer d'autres clés.

curl https://api.leyzly.com/api/v1/account/ \
  -H "X-Api-Key: $LEYZLY_API_KEY"

Réponse · 200 OK

{
  "account": {
    "id": "8f14e45f-ceea-467f-9a36-dedd4bea2543",
    "email": "cliente@empresa.es",
    "full_name": "Cliente Ejemplo"
  },
  "plan": { "code": "pro", "name": "Pro", "api_access": true },
  "is_subscriber": true,
  "ai_audits": { "used_this_month": 12, "limit": 49, "unlimited": false },
  "credits": { "balance": 0, "unit_price_eur": "2.00" }
}
Conservez la clé comme un secret (variable d'environnement). En cas de fuite, révoquez-la depuis le tableau de bord API et créez-en une nouvelle.

Démarrage rapide : lancez un audit

Envoyez un POST avec l'URL du site et la liste des documents juridiques à auditer (l'IA classe chaque document). La réponse arrive immédiatement avec status: "queued" ; l'audit s'exécute en arrière-plan. Utilisez l'en-tête Idempotency-Key pour qu'une nouvelle tentative ne crée (ni ne facture) deux audits.

curl -X POST https://api.leyzly.com/api/v1/scans/ \
  -H "X-Api-Key: $LEYZLY_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://midominio.com",
    "documents": [
      { "url": "https://midominio.com/privacidad" },
      { "url": "https://midominio.com/cookies" }
    ]
  }'

Réponse · 201 Created

{
  "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "slug": "K7M2P9QXZ4",
  "url": "https://midominio.com",
  "host": "midominio.com",
  "status": "queued",
  "score": null,
  "created_at": "2026-06-15T10:32:04.512831Z",
  "started_at": null,
  "finished_at": null,
  "n_findings": 0,
  "public_url": "https://leyzly.com/audit/K7M2P9QXZ4",
  "is_public": true
}

La réponse immédiate arrive avec status: "queued" et sans résultats pour l'instant. Si vous renvoyez la même Idempotency-Key, vous recevez l'audit original avec 200 OK au lieu de 201.

Ensuite, interrogez le détail jusqu'à ce que status soit completed ou failed — ou mieux, abonnez-vous à un webhook et évitez le polling.

curl https://api.leyzly.com/api/v1/scans/SCAN_ID/ \
  -H "X-Api-Key: $LEYZLY_API_KEY"
# status: queued -> running -> completed | failed

Réponse · 200 OK · audit terminé

{
  "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "slug": "K7M2P9QXZ4",
  "url": "https://midominio.com",
  "host": "midominio.com",
  "status": "completed",
  "score": 68,
  "created_at": "2026-06-15T10:32:04.512831Z",
  "started_at": "2026-06-15T10:32:06.001000Z",
  "finished_at": "2026-06-15T10:33:11.882000Z",
  "n_findings": 1,
  "public_url": "https://leyzly.com/audit/K7M2P9QXZ4",
  "is_public": true,
  "engine_version": "0.1.0",
  "error_message": "",
  "source": "user",
  "ai_skip_reason": "",
  "pdf_url": "https://api.leyzly.com/api/v1/reports/scans/f47ac10b…/pdf/?sig=…&exp=…",
  "findings_grouped": {
    "critical": [],
    "high": [],
    "medium": [],
    "low": [],
    "passing": [],
    "ai": {
      "privacy_policy": [
        {
          "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
          "rule": {
            "code": "AI_PRIV_NO_RETENTION",
            "title": "Falta plazo de conservación de los datos",
            "severity": "high",
            "category": "legal_pages",
            "legal_refs": [
              {
                "law": "RGPD",
                "article": "Art. 13.2.a",
                "quote": "el plazo durante el cual se conservarán los datos personales",
                "source_url": "https://eur-lex.europa.eu/legal-content/ES/TXT/?uri=CELEX:32016R0679"
              }
            ],
            "remediation_html": "<p>Añade una cláusula con el plazo de conservación…</p>"
          },
          "severity": "high",
          "is_passing": false,
          "source": "ai",
          "confidence": 0.88,
          "evidence_quote": "Conservaremos sus datos durante el tiempo necesario.",
          "audited_url": "https://midominio.com/privacidad",
          "provenance": { "provider": "google", "model": "gemini-2.5-flash", "task": "audit_privacy" }
        }
      ],
      "cookie_policy": [],
      "legal_notice": [],
      "terms": [],
      "coherence": []
    }
  },
  "behavior_report": {
    "trackers": [
      { "key": "ga4", "label": "Google Analytics 4", "detected_via": "request" }
    ],
    "cookies_pre_consent": [
      { "name": "_ga", "domain": ".midominio.com", "tracker": "ga4", "secure": false, "http_only": false }
    ],
    "security": {
      "https": true,
      "hsts": false,
      "x_content_type_options": true,
      "referrer_policy": false
    },
    "banner": { "present": true, "has_reject": false, "has_granular": false },
    "forms": [
      { "action": "/contacto", "sensitive_fields": ["email"], "has_consent": false, "has_privacy_link": true }
    ],
    "built_at": "2026-06-15T10:33:09.120000Z"
  },
  "documents": [
    {
      "id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
      "url": "https://midominio.com/privacidad",
      "type_label": "Política de Privacidad",
      "status": "audited",
      "detected_type": "privacy_policy",
      "type_matches": true,
      "validation_confidence": 0.96,
      "counts_as_usage": true,
      "error_message": "",
      "created_at": "2026-06-15T10:32:04.600000Z"
    }
  ]
}

Chaque élément de findings_grouped suit la forme d'un constat de la section Consulter les constats. Les constats de l'IA sont regroupés sous ai par type de document et restent privés pour le propriétaire. La pdf_url est une URL signée (valable 24 h) et n'est renvoyée qu'au propriétaire d'un audit terminé — sinon elle vaut null.

Consulter les constats

Récupérez les constats dans un format plat et filtrable. Chaque constat inclut la règle (rule) avec sa référence juridique, la sévérité et — pour les constats de l'IA — la citation textuelle du document (evidence_quote) et le niveau de confiance. Les constats de l'IA restent privés pour le propriétaire de l'audit.

curl "https://api.leyzly.com/api/v1/scans/SCAN_ID/findings/?severity=critical&source=ai" \
  -H "X-Api-Key: $LEYZLY_API_KEY"

Réponse · 200 OK (paginée)

{
  "count": 7,
  "next": "https://api.leyzly.com/api/v1/scans/f47ac10b…/findings/?page=2",
  "previous": null,
  "results": [
    {
      "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
      "rule": {
        "code": "AI_PRIV_NO_RETENTION",
        "title": "Falta plazo de conservación de los datos",
        "positive_title": "Plazo de conservación especificado",
        "description": "La política debe indicar cuánto tiempo se conservan los datos personales.",
        "severity": "high",
        "category": "legal_pages",
        "legal_refs": [
          {
            "law": "RGPD",
            "article": "Art. 13.2.a",
            "quote": "el plazo durante el cual se conservarán los datos personales",
            "source_url": "https://eur-lex.europa.eu/legal-content/ES/TXT/?uri=CELEX:32016R0679"
          }
        ],
        "remediation_html": "<p>Añade una cláusula con el plazo de conservación…</p>"
      },
      "severity": "high",
      "is_passing": false,
      "evidence": { "kind": "privacy_policy" },
      "location": "",
      "created_at": "2026-06-15T10:33:05.400000Z",
      "source": "ai",
      "confidence": 0.88,
      "evidence_quote": "Conservaremos sus datos durante el tiempo necesario.",
      "audited_url": "https://midominio.com/privacidad",
      "provenance": { "provider": "google", "model": "gemini-2.5-flash", "task": "audit_privacy" }
    }
  ]
}

developers.findings.after

Référence des endpoints

Tous les endpoints se trouvent sous /api/v1/ et renvoient du JSON. Ceux marqués clé requièrent votre clé API ; les publics non.

Audits (scans)

POST/api/v1/scans/clé

Lance un audit (site + documents juridiques). Accepte Idempotency-Key.

GET/api/v1/scans/clé

Liste vos audits. Filtres : status, host__contains, ordering. Paginé.

GET/api/v1/scans/{id}/clé

Détail d'un audit : score, behavior_report, documents et pdf_url signée.

DELETE/api/v1/scans/{id}/clé

Supprime un audit (suppression logique). Réservé au propriétaire.

POST/api/v1/scans/{id}/rescan/clé

Relance un audit avec la même URL et les mêmes documents.

GET/api/v1/scans/{id}/findings/clé

Constats au format plat. Filtres : severity, source, is_passing, ordering.

Les constats de l'IA sont privés pour le propriétaire.

GET/api/v1/scans/{id}/documents/clé

Documents envoyés avec leur type détecté par l'IA et leur statut d'audit.

GET/api/v1/scans/{id}/report.pdfclé

Télécharge le rapport PDF (aussi disponible via pdf_url signée 24 h).

Catalogue et compte

GET/api/v1/rules/public

Catalogue public des règles d'audit. Filtres : category, severity.

GET/api/v1/rules/{code}/public

Détail d'une règle, avec référence juridique et remédiation.

GET/api/v1/account/clé

Votre identité, votre plan, l'usage du mois et le solde de crédits en un seul appel.

Webhooks

GET/api/v1/webhooks/deliveries/clé

Journal des livraisons pour le débogage. Filtres : endpoint, event, status.

POST/api/v1/webhooks/endpoints/{id}/test/clé

Envoie un événement de test (ping) signé à votre endpoint. Nécessite une session.

Schéma OpenAPI complet : https://api.leyzly.com/api/v1/schema/ · Swagger UI sur /api/v1/docs/.

Webhooks

Enregistrez un ou plusieurs endpoints HTTPS depuis le tableau de bord et Leyzly vous notifiera les événements du cycle de vie de chaque audit via un POST signé. Chaque livraison est retentée jusqu'à 5 fois avec un backoff exponentiel ; une réponse 2xx la marque comme livrée.

ÉvénementQuand
scan.startedL'audit commence à s'exécuter.
scan.completedL'audit se termine avec succès (inclut pdf_url signée).
scan.failedL'audit échoue (inclut error_message).
pingÉvénement de test que vous déclenchez depuis le tableau de bord ou l'API.

Exemple du corps d'un événement scan.completed :

{
  "event": "scan.completed",
  "scan": {
    "id": "8f3c…",
    "slug": "a1B2c3",
    "url": "https://midominio.com",
    "host": "midominio.com",
    "status": "completed",
    "score": 72,
    "source": "user",
    "started_at": "2026-06-15T10:00:00Z",
    "finished_at": "2026-06-15T10:01:03Z",
    "findings_by_severity": { "critical": 1, "high": 2, "medium": 4, "low": 0 },
    "documents": [
      { "url": "https://midominio.com/privacidad", "status": "audited", "detected_type": "privacy_policy" }
    ],
    "pdf_url": "https://api.leyzly.com/api/v1/reports/signed/…",
    "api_url": "/api/v1/scans/8f3c…/"
  }
}

Vérifier la signature

developers.webhooks.verifyIntro

import hashlib, hmac, time

def verify(raw_body: bytes, header: str, secret: str, tolerance=300) -> bool:
    # header == "t=1700000000,v1=abcd..."
    parts = dict(p.split("=", 1) for p in header.split(","))
    ts, sig = int(parts["t"]), parts["v1"]
    if abs(time.time() - ts) > tolerance:
        return False  # stale: replay protection
    signed = f"{ts}.".encode() + raw_body
    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, sig)

# Flask: verify(request.get_data(), request.headers["X-Leyzly-Signature"], SECRET)
developers.webhooks.debugNote

Conventions, limites et erreurs

Pagination

developers.conventions.paginationBody

Idempotence

Envoyez Idempotency-Key lors de la création d'audits ; une nouvelle tentative avec la même clé renvoie l'audit original.

Limites

Jusqu'à 200 audits par jour et par compte. Au-delà, vous recevez 429.

Format

JSON dans les requêtes et les réponses (Content-Type: application/json). Dates au format ISO 8601 UTC.

CodeSignification
400Requête invalide (URL non acceptée, JSON malformé…).
401Clé API absente, invalide, révoquée ou plan sans accès API.
402Ni crédits ni abonnement pour financer l'audit.
403Vous n'avez pas la permission sur cette ressource.
404Ressource introuvable (ou non vôtre / non publique).
429Vous avez dépassé la limite de requêtes (200 audits/jour).

Format du corps d'erreur

Les réponses d'erreur sont du JSON sous l'une de ces trois formes. Les erreurs de validation renvoient un objet avec les messages par champ :

400 · Validation

{
  "url": ["Solo se admiten URLs http(s)."]
}

L'authentification, les permissions et les 404 ne renvoient que detail :

401 · Authentification

{
  "detail": "Las credenciales de autenticación no se proveyeron."
}

Les erreurs métier ajoutent un champ code stable (pour programmer dessus) et, parfois, des données supplémentaires. Par exemple, en lançant un audit sans solde :

402 · Métier (avec code)

{
  "detail": "Necesitas 2 crédito(s) y tienes 0. Compra auditorías sueltas o suscríbete.",
  "code": "insufficient_credits",
  "credits_balance": 0,
  "credits_needed": 2,
  "upgrade_url": "/precios"
}

Codes métier (code)

codeHTTPQuand
insufficient_credits402Pas d'abonnement et crédits insuffisants pour les documents du scan. Inclut credits_balance, credits_needed et upgrade_url.
trial_exhausted429Essai gratuit anonyme épuisé (limite par IP/domaine). Uniquement dans le flux sans clé.
captcha_failed403Vérification anti-bots (Turnstile) non réussie. Uniquement dans le flux anonyme, pas avec une clé API.

Si un abonné épuise son quota mensuel (ou atteint un plafond de dépense), l'audit n'échoue pas avec une erreur HTTP : il se termine completed avec le rapport de comportement, mais ai_skip_reason indique pourquoi la couche IA a été ignorée :

  • quota_exceededQuota mensuel d'audits IA du plan épuisé.
  • no_creditsNi abonnement ni crédits pour financer la couche IA.
  • overage_cap_reachedVotre plafond mensuel de dépense en dépassement est atteint.
  • budget_exceededLimite globale du budget IA du service.

Prêt à intégrer ?

Créez votre clé API, connectez un webhook et lancez votre premier audit en quelques minutes. Des questions ? Écrivez-nous à hola@leyzly.com.