{
  "openapi": "3.0.3",
  "info": {
    "title": "API ProofSnap",
    "version": "2.0.0",
    "description": "L'API ProofSnap charge une page web dans un navigateur Chromium côté serveur et renvoie un paquet de preuves (ZIP).\n\nChaque paquet de capture contient la capture d'écran, le HTML de la page, le texte extrait de la page, metadata.json, forensic_log.json, chain_of_custody.json, evidence.pdf et un manifest.json qui liste l'empreinte SHA-256 de chaque fichier. manifest.json est signé avec une clé RSA-4096 générée pour cette capture (la clé publique est fournie sous la forme publickey.pem, la clé privée est détruite) et l'empreinte SHA-256 de manifest.json est soumise aux calendriers OpenTimestamps pour un ancrage Bitcoin (manifest.json.ots). Si la requête aux calendriers échoue, le paquet est livré sans la preuve et manifest.json ne la liste pas ; les scripts hors ligne et le vérificateur la signalent donc comme absente (not present) plutôt que manquante (missing). Avec `include_eidas` sur une clé live, la même empreinte reçoit aussi un horodatage qualifié eIDAS (RFC 3161, manifest.json.tsr). Les captures d'écran JPEG sont signées avec des Content Credentials C2PA dans la mesure du possible : lorsque la signature échoue, la capture se termine sans elles. Le dossier verification/ contient des scripts qui vérifient le paquet hors ligne. Chaque manifest.json indique `ots_listed_only_when_present: true` (manifest.json.ots n'est listé que s'il figure dans le paquet) et `eidas_marker_version: 1`. Il n'enregistre `eidas_requested: true` que lorsque le paquet est livré avec son horodatage qualifié. Si le jeton ne peut pas être obtenu, le manifeste est scellé à nouveau sans lui. Un paquet qui enregistre `eidas_requested: true` mais ne contient pas manifest.json.tsr a donc perdu son jeton, et les scripts hors ligne signalent FAIL. chain_of_custody.json enregistre `bitcoin_ots_included` et liste, dans `page_modifications.items[]`, chaque modification apportée à la page avant la capture. manifest.json porte sealed_at (moment de sa construction, après le calcul de chaque empreinte de fichier) et les décalages d'horloge du serveur de capture mesurés par SNTP ; chain_of_custody.json liste les étapes de la capture dans l'ordre où elles ont eu lieu, chacune avec l'heure à laquelle elle s'est terminée, et se clôt par custody_record_written (le hachage, la signature et les horodatages interviennent ensuite et sont attestés par manifest.json et les preuves).\n\nmetadata.json enregistre :\n\n- `browser_identity` : le user agent et les Client Hints effectivement envoyés.\n- `dns` : une vérification DNS indépendante de l'adresse à laquelle le navigateur s'est connecté, avec les réponses de dns.google et cloudflare-dns.com et le résultat : confirmed, not_confirmed, resolvers_unavailable, connected_ip_unknown, skipped_ip_literal (l'URL désigne une adresse IP, il n'y a donc pas de nom d'hôte à résoudre) ou skipped_internal_host (un nom d'hôte interne, qui n'est jamais envoyé aux résolveurs publics).\n- `url_at_load` et `url_at_screenshot` : l'adresse juste après le chargement de la page et au moment de la prise de son image (une page peut modifier sa propre adresse, le plus souvent la partie qui suit #).\n- `redirect_chain` : les redirections du document principal, de la plus ancienne à la plus récente, chacune avec son statut HTTP. Les adresses demandée et finale sont données en entier ; pour chaque étape intermédiaire, les valeurs de la chaîne de requête sont remplacées par [redacted] (`query_values_redacted`).\n- `tls.source` : la provenance de chaque champ TLS, soit `browser_connection`, soit `browser_connection_and_separate_handshake`, ce dernier avec les valeurs `at`, `ip` et `port` de la négociation TLS distincte qui a récupéré certificate_chain.pem après le chargement de la page.\n\nLorsqu'une étape demandée n'a pas été effectuée (un horodatage eIDAS, la signature C2PA, la demande d'archivage, WHOIS, la seconde lecture d'horloge, un WARC tronqué par la limite de temps, ou une attente ou un dépliage de contenu omis en raison de la limite de temps), evidence.pdf la liste avec un motif court et le certificat de provenance la mentionne en une ligne.\n\nLes captures sont asynchrones. POST /captures renvoie 202 avec un capture_id ; interrogez GET /captures/{capture_id} ou passez `options.webhook_url`. Une capture terminée renvoie un package_url présigné valable jusqu'à 24 heures (voir package_url_expires_at) ; récupérez de nouveau la capture pour obtenir un nouveau lien.\n\nLes chemins ci-dessous sont relatifs à l'URL du serveur, qui contient déjà le préfixe /v1. Les valeurs `poll_path` renvoyées par l'API incluent /v1.",
    "contact": {
      "email": "support@getproofsnap.com"
    }
  },
  "servers": [
    {
      "url": "https://api.getproofsnap.com/v1",
      "description": "API ProofSnap (UE)"
    }
  ],
  "security": [
    {
      "ApiKeyAuth": []
    }
  ],
  "tags": [
    {
      "name": "Captures",
      "description": "Créer, lister, lire, supprimer et vérifier des captures.\n\n**Limites de débit.** `POST /captures`, `POST /captures/batch`, `POST /compare`, `POST /captures/{capture_id}/redact` et `POST /certify-hash` sont soumis à une limite de débit par compte sur des fenêtres fixes de 60 secondes : toutes les clés API du compte et sa session du tableau de bord partagent une même fenêtre (ajouter des clés n'augmente pas la limite). Un lot ou une certification de plusieurs empreintes consomme une unité par URL ou par empreinte. Limites par défaut par fenêtre selon le forfait :\n\n| Forfait | Requêtes par 60 s |\n| --- | --- |\n| sandbox | 10 |\n| developer | 60 |\n| startup | 120 |\n| growth | 300 |\n| custom | 1200 |\n| legal_solo | 60 |\n| legal_seats | 120 |\n| website_archive | 60 |\n| archive_only | 30 |\n| tout autre forfait | 30 |\n\nLes requêtes suivantes puisent également dans la même fenêtre, à raison d'une unité par requête sauf indication contraire : `POST /captures/{capture_id}/verify`, `PATCH /captures/{capture_id}` avec l'action legal_hold, `GET /captures/hosts` (uniquement lorsque la liste est recalculée, pas lorsqu'un résultat en cache est renvoyé), `POST /legal-holds` (une unité pour 10 captures), `POST /captures/export-jobs`, ainsi que `POST /imports`, `POST /imports/{import_id}/complete` et `GET /imports/{import_id}?upload=true`.\n\nEn cas de dépassement de la limite, l'API renvoie 429 RATE_LIMITED avec les en-têtes `Retry-After`, `X-RateLimit-Limit`, `X-RateLimit-Remaining` et `X-RateLimit-Reset` et `error.details.retry_after_seconds`. Une requête qui nécessite plus d'unités qu'une fenêtre n'en autorise (une certification de plusieurs empreintes dépassant la limite par fenêtre) ne peut jamais aboutir et renvoie 400 RATE_LIMIT_COST_TOO_HIGH avec `error.details` `{ max_per_window, window_seconds, requested }`. Divisez-la en requêtes plus petites. Une requête refusée ne consomme aucune unité, et les en-têtes indiquent le budget restant réel.\n\n**Limites mensuelles.** Les captures, comparaisons, caviardages et certifications réalisés avec des clés live sont décomptés d'un maximum mensuel par compte (mois civil UTC). La réponse d'acceptation porte les en-têtes `X-Quota-*`. Atteindre le maximum renvoie 402 QUOTA_EXCEEDED avec `error.details` `{ reason, used, limit, max_limit, cap, org_member }`, où `cap` vaut `plan_max`, `customer`, `member_owner` ou `org`. Tant qu'un abonnement live est en retard de paiement ou impayé, les requêtes facturables renvoient 402 PAYMENT_REQUIRED ; pour un tel compte, les captures (unitaires et par lot) sont refusées avec cette erreur quel que soit l'environnement de la clé, clés de test comprises.\n\n**Captures en cours et mode test.** Chaque compte ne peut exécuter qu'un nombre limité de captures simultanément (developer 10, startup 25, growth 100, legal_solo et legal_seats 25, custom 200, website_archive 25, sandbox 5). Au-delà de cette limite, l'API renvoie 429 CONCURRENT_CAPTURE_LIMIT ; un lot lance les URL qui tiennent dans la limite et liste les autres dans `not_admitted` (chacune avec `code` CONCURRENT_CAPTURE_LIMIT, plus `in_flight_limit`) : renvoyez-les plus tard. Le travail en mode test (une clé de test, ou une session sans facturation associée) est limité à 200 unités par compte et par jour UTC (429 TEST_MODE_DAILY_LIMIT ; en-têtes `X-Test-Daily-*`), et une capture en mode test n'est jamais envoyée à l'Internet Archive (`external_archive.skip_reason` \"test_mode\")."
    },
    {
      "name": "Comparer et caviarder",
      "description": "Dérivez un nouveau paquet scellé à partir de captures existantes : une comparaison de deux captures, ou une copie caviardée de l'une d'elles."
    },
    {
      "name": "Certification d'empreintes",
      "description": "Certifiez les empreintes SHA-256 des fichiers que vous conservez. Seules les empreintes sont envoyées ; les fichiers ne parviennent jamais à ProofSnap."
    },
    {
      "name": "Captures automatiques",
      "description": "Captures récurrentes (interval ou cron) et surveillance des modifications (on_change). Chaque exécution qui réalise une capture est une capture normale : elle est comptabilisée et compte dans les limites mensuelles."
    },
    {
      "name": "Affaires",
      "description": "Espaces de travail pour les affaires juridiques, avec un journal de traçabilité chaîné par hachage. Disponibles sur les forfaits Legal (legal_solo, legal_seats) ; les autres forfaits reçoivent 403 MATTERS_NOT_ENABLED, à deux exceptions près. Le propriétaire d'un cabinet dont l'abonnement a pris fin peut encore lister, ouvrir et exporter les affaires de ce cabinet jusqu'à la date de suppression des données du cabinet (data_delete_after) ; cet accès est en lecture seule, plus l'export, et GET /matters ajoute alors read_only: true. Un compte sur le forfait Archive only peut lister, ouvrir et exporter ses affaires. Seul le propriétaire de l'affaire peut la renommer, l'archiver, en gérer les membres, la sceller, la partager ou l'exporter. Le propriétaire de l'affaire est son créateur. Pour une affaire du cabinet, le propriétaire du cabinet ne devient propriétaire de l'affaire qu'après que le créateur de l'affaire a quitté le cabinet. Un propriétaire de cabinet ajouté comme membre ne dispose que des droits de membre. Les membres reçoivent 403 FORBIDDEN pour ces opérations. Toute écriture sur une affaire (création, PATCH, rattachement de captures, partage et sa révocation, scellement, export) nécessite une adresse e-mail confirmée (403 EMAIL_NOT_VERIFIED, pour une session du tableau de bord comme pour une clé API d'un compte dont l'adresse n'est pas confirmée) et, pour un appelant live (clé API live, ou session avec facturation associée), est refusée avec 402 PAYMENT_REQUIRED tant que l'abonnement (ou celui du cabinet) est en retard de paiement ou impayé. Une affaire archivée refuse les renommages, les nouveaux membres, les nouvelles captures, le partage et l'export (409 MATTER_ARCHIVED) jusqu'à sa réouverture ; un appelant qui ne peut pas la rouvrir (le propriétaire d'un cabinet dont l'abonnement a pris fin, un compte Archive only) peut tout de même l'exporter."
    },
    {
      "name": "Webhooks",
      "description": "ProofSnap envoie par POST un événement JSON à la `webhook_url` que vous transmettez avec une capture, une certification, un caviardage ou une planification. L'URL doit utiliser https, ne doit pas contenir d'identifiants (`user:password@`, 400 INVALID_URL) et ne doit pas pointer vers une adresse privée, de bouclage, link-local, NAT de niveau opérateur (CGNAT), multicast, réservée ou de métadonnées cloud, y compris les formes IPv6 qui intègrent une telle adresse IPv4. Cette vérification a lieu lorsque la requête est acceptée (422 SSRF_BLOCKED) et à nouveau à chaque livraison : le nom d'hôte est résolu, chaque adresse est vérifiée, et la livraison ne se connecte qu'à une adresse qui a passé la vérification, de sorte qu'un hôte qui se résout plus tard vers une adresse bloquée n'est pas contacté. Lorsque le nom d'hôte ne peut pas être résolu, la livraison n'est pas envoyée. Sur toutes les routes, une webhook_url vide ou composée uniquement d'espaces est traitée comme une absence de webhook. /compare n'envoie pas de webhooks.\n\n**Abonnements du compte.** Au lieu (ou en plus) d'une webhook_url par requête, abonnez un endpoint aux événements du compte avec `POST /webhook-subscriptions` : chaque événement correspondant du compte lui est envoyé, quelle que soit la clé, la planification ou la session du tableau de bord qui l'a déclenché. Chaque abonnement a son propre secret (renvoyé une seule fois, lors de sa création), liste les types d'événements qu'il souhaite recevoir et peut filtrer par `host` ou `schedule_id` ; un abonnement live ne reçoit que les événements des captures live, et un abonnement de test que ceux des captures de test. Une clé API ne liste et ne modifie que les abonnements de son propre environnement ; la session du tableau de bord les voit tous. Les livraisons d'un abonnement portent `subscription_id` au niveau supérieur et aucun lien présigné (package_url, package_url_expires_at, upgraded_proof_url et compare.package_url sont supprimés ; monitor.change_detected porte compare.compare_id) : récupérez le paquet avec GET /captures/{capture_id}. Les événements de capture portent aussi `capture_id` au niveau supérieur. Le webhook-id d'une livraison d'abonnement est dérivé de l'abonnement, de la capture (ou de la planification) et de l'événement. Les règles de livraison et les nouvelles tentatives décrites ci-dessous s'appliquent. En outre, une réponse 410 désactive immédiatement l'abonnement (disabled_reason gone), des livraisons qui échouent pendant 5 jours sans aucun succès (et au moins un cycle complet de nouvelles tentatives échouées) le désactivent (failing), et la révocation de la clé API qui l'a créé le désactive (key_revoked) ; PATCH status \"active\" le réactive. Chaque événement est également conservé 30 jours dans `GET /events`.\n\n**En-têtes**\n\nChaque livraison est signée conformément à la spécification [Standard Webhooks](https://www.standardwebhooks.com/), de sorte que toute bibliothèque officielle Standard Webhooks peut la vérifier :\n\n| En-tête | Valeur |\n| --- | --- |\n| `Content-Type` | `application/json` |\n| `webhook-id` | `msg_<delivery id>` : identique pour le même événement de la même capture, de sorte qu'une livraison en double porte le même id. Utilisez-le pour dédupliquer : traitez chaque webhook-id une seule fois. `GET /webhook-events/{capture_id}` renvoie le même id sans le préfixe `msg_`, sous la forme `delivery_id` |\n| `webhook-timestamp` | Heure Unix, en secondes, de l'envoi de la livraison. Elle est couverte par la signature |\n| `webhook-signature` | `v1,<base64>` : HMAC-SHA256 sur `<webhook-id>.<webhook-timestamp>.<raw body>`. Pendant une rotation du secret de webhook, il porte deux entrées séparées par une espace, `v1,<new> v1,<previous>` ; acceptez la livraison lorsqu'une des entrées correspond |\n| `User-Agent` | `ProofSnap-Webhook/2.0` |\n\n**Vérifier une livraison.** Effectuez la vérification sur les octets exacts reçus, avant toute analyse. Rejetez une livraison dont le webhook-timestamp s'écarte de votre horloge de plus de 5 minutes (les bibliothèques officielles le font), et dédupliquez par webhook-id. La clé de signature est la partie du secret qui suit `whsec_`, décodée en base64. `webhook_secret` est `whsec_` suivi de base64 standard, la forme qu'attendent les bibliothèques officielles : transmettez-le-leur tel quel.\n\n```js\n// npm install standardwebhooks\nimport { Webhook } from \"standardwebhooks\";\nconst wh = new Webhook(process.env.PROOFSNAP_WEBHOOK_SECRET); // whsec_... (webhook_secret)\n// rawBody: the request body as received (string or Buffer), headers: the request headers\nconst event = wh.verify(rawBody, headers); // throws on a bad signature or a timestamp outside 5 minutes\n```\n\nSans bibliothèque (Node) :\n\n```js\nconst key = Buffer.from(secret.slice(\"whsec_\".length), \"base64\");\nconst signed = `${headers[\"webhook-id\"]}.${headers[\"webhook-timestamp\"]}.${rawBody}`;\nconst expected = crypto.createHmac(\"sha256\", key).update(signed).digest(\"base64\");\nconst ok = headers[\"webhook-signature\"].split(\" \").some((part) => {\n  const [version, sig] = part.split(\",\");\n  return version === \"v1\" && sig && sig.length === expected.length\n    && crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));\n});\n// and: Math.abs(Date.now() / 1000 - Number(headers[\"webhook-timestamp\"])) <= 300\n```\n\n**Secret de webhook.** Chaque clé API a son propre secret (`whsec_...`). Il est affiché une seule fois, lorsque vous créez la clé dans le tableau de bord, sous Développeurs (le champ `webhook_secret` de la réponse qui crée la clé). Il ne peut pas être relu par la suite : la liste des clés ne le renvoie jamais. Si vous le perdez, ou s'il a pu fuiter, générez-en un nouveau dans le tableau de bord, sous Développeurs (Nouveau secret de webhook sur la clé). Le nouveau secret n'est lui aussi affiché qu'une seule fois. **Chevauchement lors d'une rotation :** pendant 24 heures après une rotation (jusqu'à `previous_secret_expires_at` dans la réponse de rotation), chaque livraison est signée à la fois avec le nouveau et avec l'ancien secret (deux entrées `v1,` dans webhook-signature), ce qui vous permet de basculer votre endpoint sur le nouveau secret sans perdre de livraisons. Une seconde rotation pendant ce chevauchement est refusée avec 409 WEBHOOK_SECRET_ROTATION_PENDING et `error.details.previous_secret_expires_at` (moment à partir duquel vous pourrez effectuer une nouvelle rotation) : ainsi, deux secrets au plus sont actifs simultanément, et un secret sur lequel des destinataires peuvent encore s'appuyer n'est jamais retiré prématurément. Pour un secret qui a fuité, révoquez la clé et créez-en une nouvelle. La révocation de la clé supprime les deux secrets. Les livraisons utilisent les secrets de la clé au moment de leur envoi, y compris pour un travail lancé avant une rotation. Une requête provenant d'une session connectée du tableau de bord n'a pas de clé API, et donc pas de secret de webhook ; une `webhook_url` y est donc refusée avec 400 `WEBHOOK_REQUIRES_API_KEY` (captures, lots, caviardages, certifications d'empreinte, création ou modification d'une capture automatique). Les exécutions planifiées utilisent le secret de la clé qui a créé la planification. Lorsque la clé a été révoquée, ou que son secret ne peut pas être lu au moment de la livraison, l'événement n'est pas envoyé et la tentative est journalisée avec le motif (consultez-la avec `GET /webhook-events/{capture_id}`).\n\n**Livraison.** Chaque tentative a un délai d'expiration de 10 secondes. Les redirections ne sont jamais suivies : une réponse 3xx compte comme un échec, au même titre qu'une réponse hors 2xx ou un dépassement de délai. Le corps de la réponse n'est pas lu. **Nouvelles tentatives :** une livraison qui dépasse le délai, ne parvient pas à se connecter ou reçoit une réponse hors 2xx (sauf 410 Gone) est renvoyée environ 5 minutes, 30 minutes, 2 heures, 8 heures et 24 heures plus tard (tentatives 2 à 6, environ 35 heures au total), chaque fois avec le même `webhook-id` et un nouveau `webhook-timestamp`. Ensuite, la livraison est abandonnée. Une livraison renvoyée porte le même payload SANS le lien de téléchargement valable 24 heures (`package_url`, `package_url_expires_at`) : récupérez le paquet avec GET /captures/{capture_id}. Les livraisons concernant une capture effacée entre-temps sont abandonnées. Un échec transitoire de notre côté lors de la lecture du secret de signature donne lieu aux mêmes nouvelles tentatives. Un 410 Gone, une redirection, une adresse bloquée, ou une clé absente ou révoquée ne donne pas lieu à une nouvelle tentative. Le corps d'une nouvelle tentative peut différer de celui de la première (pas de lien de téléchargement) : dédupliquez par webhook-id, et non par une empreinte du corps. Répondez rapidement avec un 2xx. Le journal des tentatives enregistre une classe d'erreur générique (connection_failed, timeout, blocked_address, redirect_not_followed, http_<status>), jamais l'erreur réseau brute. certify.completed et redaction.completed sont envoyés de manière asynchrone, peu après le renvoi de la réponse. Chaque tentative est journalisée ; consultez le journal avec `GET /webhook-events/{capture_id}`.\n\n**Événements**\n\n`capture.completed` : une capture (unique, élément d'un lot, ou exécution d'une planification par intervalle ou cron) est terminée. Les abonnements webhook du compte le reçoivent aussi pour les captures de référence et de modification d'une planification on_change (la webhook_url propre à la planification reçoit monitor.change_detected pour celles-ci). `capture` a les mêmes champs qu'un `GET /captures/{capture_id}` terminé, plus `timestamps.eidas` lorsqu'un horodatage qualifié a été ajouté, `schedule_id` (null hors planification), `test_mode` et `completed_at`.\n\n```json\n{ \"event\": \"capture.completed\",\n  \"capture\": { \"capture_id\": \"cap_...\", \"status\": \"completed\", \"url\": \"https://example.com\", \"schedule_id\": null, \"test_mode\": false, \"captured_at\": \"...\", \"completed_at\": \"...\",\n    \"package_url\": \"https://...\", \"package_url_expires_at\": \"...\", \"verification_url\": \"https://...\",\n    \"manifest_hash\": \"sha256:...\", \"timestamps\": { \"bitcoin_ots\": { \"status\": \"pending_confirmation\", \"block_height\": null } },\n    \"c2pa\": { \"included\": true, \"signed_screenshot_sha256\": \"sha256:...\" }, \"file_count\": 21, \"size_bytes\": 1234567, \"metadata\": {} } }\n```\n\n`capture.failed` : la capture n'a pas pu être menée à terme. `error.message` est un motif public court (pour un échec inattendu, \"The capture could not be completed.\") ; les détails internes de l'erreur ne sont jamais envoyés. Cela inclut une exécution qui a planté ou dépassé le délai : elle est marquée comme échouée par une tâche de nettoyage périodique environ 30 minutes après son démarrage, et capture.failed est alors envoyé (message \"Capture did not finish (timed out).\"). La même tâche envoie certify.failed ou redaction.failed pour une certification d'empreinte ou un caviardage qui ne s'est pas terminé. Une exécution qui se termine après avoir été signalée comme échouée n'est pas facturée et n'envoie pas de capture.completed.\n\n```json\n{ \"event\": \"capture.failed\",\n  \"capture\": { \"capture_id\": \"cap_...\", \"status\": \"failed\", \"url\": \"https://example.com\", \"schedule_id\": null, \"test_mode\": false, \"failed_at\": \"...\",\n    \"error\": { \"code\": \"CAPTURE_FAILED\", \"message\": \"...\" }, \"metadata\": {} } }\n```\n\n`certify.completed` : une certification d'empreinte est terminée. `certificate` a les mêmes champs que la réponse synchrone de `POST /certify-hash`. Livré de manière asynchrone, peu après le renvoi de la réponse.\n\n`certify.failed` : une certification d'empreinte n'a pas pu être menée à terme.\n\n```json\n{ \"event\": \"certify.failed\",\n  \"certificate\": { \"certify_id\": \"cert_...\", \"status\": \"failed\", \"failed_at\": \"...\",\n    \"error\": { \"code\": \"CERTIFY_FAILED\", \"message\": \"The certification could not be completed. Retry the request.\" } } }\n```\n\n`redaction.completed` : un caviardage est terminé. `redaction` a les mêmes champs que la réponse de `POST /captures/{capture_id}/redact`.\n\n`redaction.failed` : un caviardage n'a pas pu être mené à terme. `error.code` est le code que la requête aurait renvoyé (par exemple REDACTION_LEAK_DETECTED), avec `details` s'il y en a ; un caviardage que cette tâche a marqué comme échoué porte REDACTION_FAILED.\n\n```json\n{ \"event\": \"redaction.failed\",\n  \"redaction\": { \"redaction_id\": \"red_...\", \"status\": \"failed\", \"redacted_from\": \"cap_...\", \"failed_at\": \"...\",\n    \"error\": { \"code\": \"REDACTION_LEAK_DETECTED\", \"message\": \"...\", \"details\": { \"files\": [\"page.html\"], \"rule_indexes\": [0], \"metadata_fields\": [], \"in_capture_url\": false } } } }\n```\n\n`monitor.change_detected` : une planification `on_change` a constaté une modification du texte de la page et a réalisé une nouvelle capture.\n\n```json\n{ \"event\": \"monitor.change_detected\", \"schedule_id\": \"sch_...\",\n  \"before_capture_id\": \"cap_...\", \"after_capture_id\": \"cap_...\", \"url\": \"https://example.com\",\n  \"compare\": { \"package_url\": \"https://...\", \"diff_summary\": { } },\n  \"diff_status\": \"ok\", \"verdict\": \"changed\", \"text_verdict\": \"changed\", \"images_verdict\": \"unchanged\", \"metadata_verdict\": \"unchanged\",\n  \"detected_at\": \"...\", \"test_mode\": false }\n```\n\n`verdict`, `text_verdict`, `images_verdict` et `metadata_verdict` (depuis le 2026-10-14) reprennent les résultats par catégorie de la comparaison figurant dans compare.diff_summary (null sans comparaison). Une planification avec `alert_on: \"text\"` n'envoie monitor.change_detected que lorsque text_verdict vaut changed (ou lorsqu'il n'y a pas de résultat de comparaison).\n\n`capture.anchor_confirmed` (abonnements et GET /events uniquement) : l'ancrage Bitcoin de la preuve OpenTimestamps du paquet a été confirmé, généralement quelques heures après la capture.\n\n```json\n{ \"event\": \"capture.anchor_confirmed\", \"capture_id\": \"cap_...\", \"schedule_id\": null, \"url\": \"https://example.com\",\n  \"confirmed_at\": \"...\", \"bitcoin_block_height\": 968549, \"upgraded_proof_available\": true, \"test_mode\": false }\n```\n\n`schedule.paused` (abonnements et GET /events uniquement) : ProofSnap a suspendu une planification. `reason` vaut `no_billing` (pas d'accès à l'API), `plan_downgrade` (le forfait autorise moins de planifications, pas de on_change, ou aucun nouveau travail), `failures` (cinq exécutions échouées d'affilée), `key_revoked` ou `invalid_cron`. Une suspension que vous effectuez vous-même n'est pas un événement.\n\n```json\n{ \"event\": \"schedule.paused\", \"schedule_id\": \"sch_...\", \"url\": \"https://example.com\", \"reason\": \"failures\",\n  \"paused_at\": \"...\", \"test_mode\": false }\n```\n\n`diff_status` vaut `ok` (compare contient la comparaison), `no_baseline` (aucune capture antérieure à comparer) ou `failed` (la comparaison a échoué ; compare vaut null)."
    },
    {
      "name": "Public",
      "description": "Opérations qui ne nécessitent aucune clé API."
    },
    {
      "name": "Importation de paquets",
      "description": "Importez dans votre compte des paquets de preuves ProofSnap existants (fichiers ZIP produits par l'extension ProofSnap : captures web, sessions multi-onglets, certifications de fichiers simples et combinées ; ou par cette API : captures, certifications, comparaisons, caviardages), afin qu'ils soient traités comme vos captures API : conservés 10 ans et disponibles pour la conservation pour litige, les affaires et les exports.\n\nForfaits Legal uniquement (legal_solo, legal_seats), avec une clé API live ou une session du tableau de bord avec facturation associée (une clé de test, une session sans facturation et le Sandbox reçoivent 403 IMPORT_NOT_ALLOWED). Une adresse e-mail non confirmée reçoit 403 EMAIL_NOT_VERIFIED, un abonnement en retard de paiement 402 PAYMENT_REQUIRED. Une importation n'est pas une capture : elle n'est pas décomptée des limites mensuelles et n'entraîne pas de frais par importation ; elle est facturée uniquement en tant que données stockées.\n\n**Déroulement.** 1. `POST /imports` avec le nom du fichier et sa taille exacte en octets (2 Gio au maximum) : la réponse contient une URL présignée par partie de 32 Mio (la dernière partie est plus petite), valable 1 heure. 2. Envoyez par PUT les octets de chaque partie à son URL et conservez l'en-tête de réponse `ETag` de chacune. 3. `POST /imports/{import_id}/complete` avec chaque numéro de partie et son ETag. 4. Interrogez `GET /imports/{import_id}` jusqu'à ce que le statut soit définitif.\n\n**Statuts.** `uploading` (en attente des téléversements des parties et de l'appel complete ; un téléversement non terminé dans les 24 heures passe à `expired`), `verifying` (le paquet est en cours de contrôle), `accepted` (il a été vérifié : `capture_id` est la nouvelle capture, avec le même identifiant que l'importation), `rejected` (il n'a pas été vérifié : `reasons` liste les codes ; seul l'enregistrement de l'importation est conservé), `duplicate` (votre compte détient déjà une capture avec le même manifeste : `duplicate_of`), `expired`.\n\n**Vérification.** Stricte : la signature RSA sur manifest.json, chaque fichier listé présent avec l'empreinte SHA-256 listée (ainsi que SHA-512 et la taille lorsqu'elles sont listées), aucun fichier que le manifeste ne liste pas (à l'exception des fichiers ajoutés par le système d'exploitation, comme .DS_Store, et des preuves dérivées du manifeste), au moins un fichier haché, un identifiant de preuve cohérent, la chaîne de hachage du journal forensique recalculée, la preuve OpenTimestamps confrontée au manifeste (hors ligne) et l'horodatage qualifié eIDAS confronté à la racine Disig épinglée avec les données de validation du paquet. La structure ZIP est contrôlée avant toute lecture (pas de chiffrement, pas de ZIP64, pas de liens, noms sûrs, pas de noms en double, tailles et CRC-32 de chaque entrée). Un paquet de test de l'API (filigrané) est rejeté. Les paquets API scellés avant que evidence.pdf ne soit haché sont acceptés, ce fichier étant listé dans `verification.checks.files.unhashed`.\n\n**Niveaux** (`summary`) : `integrity` (verified ou failed), `time_anchor` (eidas_qualified, eidas_qualified_revocation_unknown, ots_bitcoin_attested_unconfirmed, ots_pending, none) et `provenance` : pour les paquets API, `issued_by_proofsnap` (le manifeste figure dans le registre d'émission des paquets de l'API) ou `unverified` ; pour les paquets de l'extension, `registered_by_extension_account` (un utilisateur connecté de l'extension a enregistré ce manifeste lors de sa création) ou `not_registered` ; `unknown` lorsque la recherche n'a pas pu être effectuée. La provenance ne fait jamais échouer une importation.\n\n**La capture.** Une importation acceptée est une capture avec `source` \"import\" : elle est téléchargeable (le ZIP téléversé, octet pour octet) et vérifiable (POST /captures/{capture_id}/verify exécute à nouveau les mêmes contrôles), peut être placée en conservation pour litige, déplacée dans la corbeille et effacée, apparaît dans les affaires (lorsqu'elle est importée avec `matter_id`, le journal de traçabilité de l'affaire consigne `package.imported`) et dans les exports, et est conservée 10 ans à compter de l'importation. Son `captured_at` est l'heure que le paquet lui-même indique (`import.captured_at_basis` \"package_claim\"), jamais une heure observée par ProofSnap ; `created_at` est le moment où le téléversement a été créé. Les comparaisons et caviardages d'un paquet importé sont refusés (422 IMPORT_NOT_SUPPORTED).\n\n**Limites.** 3 importations en cours (téléversement ou vérification) au maximum par compte (409 IMPORT_LIMIT), 50 importations créées par compte et par jour UTC (429 RATE_LIMITED, `details.action` \"import_create\" ; chaque requête de création compte, y compris une requête rejetée ultérieurement), et la limite de débit par minute du forfait pour create et complete. Volume d'importation : 5 Gio au maximum par jour UTC et 20 Gio par mois UTC de tailles de paquets annoncées par compte, comptés lors de create (et lors de complete pour un téléversement plus ancien) ; au-delà, 429 IMPORT_DAILY_BYTES_LIMIT ou IMPORT_MONTHLY_BYTES_LIMIT avec `Retry-After` ; un téléversement annulé n'est pas compté."
    }
  ],
  "paths": {
    "/captures": {
      "post": {
        "tags": [
          "Captures"
        ],
        "operationId": "createCapture",
        "summary": "Capturer une URL",
        "description": "Accepte la capture et renvoie 202 immédiatement ; la capture s'exécute en arrière-plan. Interrogez `GET /captures/{capture_id}` ou transmettez `options.webhook_url`. Le nom d'hôte de l'URL est résolu et vérifié avant l'acceptation de la capture.\n\nLimite de débit par compte sur des fenêtres de 60 secondes (toutes les clés API du compte et sa session du tableau de bord partagent une même fenêtre). Une fois le limiteur passé, les réponses de cette route incluent les en-têtes `RateLimit-Policy` et `RateLimit` (fondés sur l'Internet-Draft de l'IETF draft-ietf-httpapi-ratelimit-headers) ainsi que les anciens en-têtes `X-RateLimit-Limit`, `X-RateLimit-Remaining` et `X-RateLimit-Reset`. Une réponse 429 ajoute `Retry-After`.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CaptureRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepté. Un rejeu idempotent d'une requête antérieure avec la même Idempotency-Key renvoie également 202, avec `Idempotent-Replay: true` et le corps d'origine.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/XRequestId"
              },
              "Idempotent-Replay": {
                "$ref": "#/components/headers/IdempotentReplay"
              },
              "X-Quota-Limit": {
                "$ref": "#/components/headers/QuotaLimit"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/QuotaRemaining"
              },
              "X-Quota-Used": {
                "$ref": "#/components/headers/QuotaUsed"
              },
              "X-Quota-Cap": {
                "$ref": "#/components/headers/QuotaCap"
              },
              "X-Quota-Org-Limit": {
                "$ref": "#/components/headers/QuotaOrgLimit"
              },
              "X-Quota-Org-Used": {
                "$ref": "#/components/headers/QuotaOrgUsed"
              },
              "X-Sandbox-Limit": {
                "$ref": "#/components/headers/SandboxLimit"
              },
              "X-Sandbox-Remaining": {
                "$ref": "#/components/headers/SandboxRemaining"
              },
              "X-Sandbox-Used": {
                "$ref": "#/components/headers/SandboxUsed"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CaptureAccepted"
                },
                "example": {
                  "capture_id": "cap_a1B2c3D4e5F6g7H8",
                  "status": "processing",
                  "url": "https://example.com",
                  "poll_path": "/v1/captures/cap_a1B2c3D4e5F6g7H8",
                  "message": "Capture accepted. Poll GET /v1/captures/{capture_id} (or use webhook_url) for completion.",
                  "metadata": {}
                }
              }
            }
          },
          "400": {
            "description": "MALFORMED_REQUEST, MISSING_FIELD, INVALID_URL, INVALID_TIMESTAMP_TYPE, INVALID_VIEWPORT, METADATA_TOO_LARGE ou INVALID_EVIDENCE_PACK_OPTIONS. WEBHOOK_REQUIRES_API_KEY : webhook_url envoyé depuis une session du tableau de bord (les webhooks nécessitent une clé API).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "QUOTA_EXCEEDED (maximum mensuel ou quota du sandbox ; porte les en-têtes X-Quota-* ou X-Sandbox-*) ou PAYMENT_REQUIRED.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            }
          },
          "403": {
            "description": "EIDAS_REQUIRES_LIVE_KEY : include_eidas avec une clé de test (une clé ps_live_ d'un compte sans compte de facturation associé s'exécute comme une clé de test). EMAIL_NOT_VERIFIED : une session du tableau de bord dont l'adresse e-mail n'est pas confirmée. PLAN_ARCHIVE_ONLY : le forfait conserve uniquement l'archive et ne crée aucun nouveau travail. PLAN_API_READ_ONLY : une clé API d'un compte Website Archive (sa clé est en lecture seule ; utilisez le tableau de bord). PLAN_API_READ_ONLY avec details.reason \"session_outside_dashboard\" : une session du tableau de bord d'un compte Website Archive dont la requête n'a pas d'en-tête Origin, ou un en-tête qui n'est pas une adresse du tableau de bord ProofSnap (sur ce forfait, les nouveaux travaux se créent uniquement dans le tableau de bord ; il s'agit d'un garde-fou souple, pas d'une frontière de sécurité).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            }
          },
          "409": {
            "description": "IDEMPOTENCY_IN_PROGRESS.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            }
          },
          "422": {
            "description": "SSRF_BLOCKED, TARGET_UNREACHABLE ou IDEMPOTENCY_KEY_REUSED.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            }
          },
          "429": {
            "description": "RATE_LIMITED : trop de requêtes dans la fenêtre de 60 secondes en cours. `error.details.retry_after_seconds` reprend Retry-After. RateLimit indique r=0 et le nombre de secondes avant la réinitialisation de la fenêtre. RATE_LIMITED également, pour une session du tableau de bord Website Archive : son plafond quotidien de 100 unités par jour UTC, partagé entre les captures uniques et par lot (une unité par URL), compare, redact et certify-hash (une unité par requête) ; une requête qui ne tient pas dans le plafond est refusée en entier (details.daily_limit, details.remaining, details.action \"wa_session_captures\" ; Retry-After et details.retry_after_seconds donnent le nombre de secondes jusqu'au jour UTC suivant, et les en-têtes par minute RateLimit et X-RateLimit-* sont omis). Un refus court avec seulement details.retry_after_seconds et Retry-After: 1 signale une mise à jour concurrente du compteur : réessayez. Un refus qui n'est pas imputable à l'appelant restitue les unités ; un travail accepté avec 202 qui échoue ensuite conserve son unité. Il s'agit d'un garde-fou souple, pas d'une frontière de sécurité. Une capture unique dont l'Idempotency-Key est servie depuis le stockage (un rejeu, 409 ou 422) est servie avant le plafond quotidien et ne coûte aucune unité.\n\nCONCURRENT_CAPTURE_LIMIT : trop de captures du compte s'exécutent en même temps (developer 10, startup 25, growth 100, legal_solo et legal_seats 25, custom 200, website_archive 25, sandbox 5) ; `details.limit`, `details.in_flight`, `details.requested` et Retry-After indiquent quand réessayer.\n\nTEST_MODE_DAILY_LIMIT : le travail en mode test (une clé de test, ou une session sans facturation liée) est limité à 200 unités par compte et par jour UTC ; `details.used`, `details.limit`, `details.reset_at`, les en-têtes `X-Test-Daily-*` et Retry-After.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "RATE_LIMITED",
                    "message": "Too many requests. See Retry-After header.",
                    "details": {
                      "retry_after_seconds": 42
                    }
                  },
                  "request_id": "..."
                }
              }
            }
          },
          "500": {
            "description": "CAPTURE_FAILED (la capture n'a pas pu être lancée ; details.capture_id) ou INTERNAL_ERROR.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/EidasOrAuthUnavailable"
          }
        }
      },
      "get": {
        "tags": [
          "Captures"
        ],
        "operationId": "listCaptures",
        "summary": "Lister vos captures",
        "description": "Enregistrements que vous avez créés, du plus récent au plus ancien, dans tous les statuts (y compris supprimés, c'est-à-dire la corbeille ; filtrez avec status). Chaque ligne porte source, redacted_from, label et compared, afin que son type puisse être affiché sans la relire. Une capture réalisée pour un cabinet n'est listée que tant que vous pouvez encore agir sur elle (vous êtes membre de ce cabinet ou vous en êtes propriétaire) ; une page peut donc contenir moins de lignes que limit, et next_cursor reprend après la dernière ligne lue. Les certificats, comparaisons et caviardages sont inclus.\n\nRecherche (votre propre liste uniquement) : url, host ou schedule_id, combinés avec from / to et status, retrouvent l'enregistrement d'une page à une date donnée. Un filtre incorrect, deux filtres parmi url / host / schedule_id, ou un from qui n'est pas antérieur à to renvoient 400 MALFORMED_REQUEST.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Taille de page, de 1 à 200 (50 par défaut). Une valeur hors de cette plage renvoie 400 MALFORMED_REQUEST.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "next_cursor de la page précédente (format created_at|capture_id, ou completed_at|capture_id avec order=completed_at). Transmettez-le sans modification.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Uniquement les lignes ayant ce statut : processing, completed, failed ou deleted (corbeille). Toute autre valeur renvoie 400 MALFORMED_REQUEST.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "url",
            "in": "query",
            "required": false,
            "description": "Uniquement les captures de cette URL exacte, telle qu'elle a été capturée (http ou https). Au plus l'un de url, host et schedule_id.",
            "schema": {
              "type": "string",
              "maxLength": 2048
            }
          },
          {
            "name": "host",
            "in": "query",
            "required": false,
            "description": "Uniquement les captures dont l'URL se trouve sur ce nom d'hôte (insensible à la casse, par exemple www.example.com ; les sous-domaines sont des hôtes distincts). Au plus l'un de url, host et schedule_id. Les lignes créées avant le 2026-10-05 portent l'hôte une fois le rattrapage exécuté.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "schedule_id",
            "in": "query",
            "required": false,
            "description": "Uniquement les captures réalisées par cette planification. Au plus l'un de url, host et schedule_id.",
            "schema": {
              "type": "string",
              "pattern": "^sch_[A-Za-z0-9]{8,32}$"
            }
          },
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "Créé à ce moment ou après : une date (YYYY-MM-DD, minuit UTC) ou un horodatage ISO 8601.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "Créé avant ce moment (exclu) : une date (YYYY-MM-DD, minuit UTC) ou un horodatage ISO 8601. Doit être postérieur à from. Avec schedule_id ou url, la ligne la plus récente avant un moment donné est la version alors en vigueur.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "order",
            "in": "query",
            "required": false,
            "description": "created_at (par défaut : les plus récemment créées en premier) ou completed_at (les plus récemment terminées en premier ; uniquement avec status=completed, sinon 400 MALFORMED_REQUEST). Avec completed_at, from et to bornent completed_at, et next_cursor prend la forme completed_at|capture_id. Fonctionne avec url, host et schedule_id.",
            "schema": {
              "type": "string",
              "enum": [
                "created_at",
                "completed_at"
              ],
              "default": "created_at"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Une page de captures.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "captures": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "capture_id": {
                            "type": "string"
                          },
                          "url": {
                            "type": "string",
                            "nullable": true
                          },
                          "status": {
                            "type": "string",
                            "nullable": true
                          },
                          "created_at": {
                            "type": "string",
                            "nullable": true
                          },
                          "completed_at": {
                            "type": "string",
                            "nullable": true,
                            "description": "Date de fin de la capture (null pendant le traitement ou après un échec)."
                          },
                          "batch_id": {
                            "type": "string",
                            "nullable": true
                          },
                          "schedule_id": {
                            "type": "string",
                            "nullable": true
                          },
                          "test_mode": {
                            "type": "boolean",
                            "nullable": true
                          },
                          "external_archive": {
                            "allOf": [
                              {
                                "$ref": "#/components/schemas/ExternalArchive"
                              }
                            ],
                            "description": "Lignes completed uniquement. Identique à external_archive de GET /captures/{capture_id}."
                          },
                          "label": {
                            "type": "string",
                            "nullable": true,
                            "description": "Le libellé de la ligne (certificats : le libellé indiqué ; caviardages : la capture qu'ils caviardent)."
                          },
                          "compared": {
                            "type": "array",
                            "nullable": true,
                            "items": {
                              "type": "string"
                            },
                            "description": "Comparaisons uniquement : les identifiants des deux captures sources."
                          },
                          "source": {
                            "type": "string",
                            "nullable": true,
                            "description": "Mode de création de la ligne : api (une capture web), schedule, certify-hash (un certificat), compare (une comparaison) ou redact (un caviardage) ; null sur les lignes antérieures à ce champ."
                          },
                          "redacted_from": {
                            "type": "string",
                            "nullable": true,
                            "description": "Caviardages uniquement : la capture à partir de laquelle le caviardage a été réalisé."
                          },
                          "captured_at": {
                            "type": "string",
                            "nullable": true,
                            "format": "date-time",
                            "description": "Date de capture de la page (lignes terminées). Pour un caviardage, l'heure de capture de sa capture source (redacted_at est la date du caviardage)."
                          },
                          "redacted_at": {
                            "type": "string",
                            "format": "date-time",
                            "description": "Caviardages uniquement : date de réalisation du caviardage. Le captured_at d'un caviardage est l'heure de capture de sa capture source."
                          },
                          "compared_at": {
                            "type": "string",
                            "nullable": true,
                            "format": "date-time",
                            "description": "Comparaisons uniquement : le moment où la comparaison a été réalisée. Son captured_at porte la même valeur (conservé pour les anciens clients) ; utilisez compared_at."
                          },
                          "manifest_hash": {
                            "type": "string",
                            "nullable": true,
                            "description": "sha256:<hex> du manifest.json du paquet (lignes terminées)."
                          },
                          "private_note": {
                            "type": "string",
                            "nullable": true,
                            "description": "Votre note privée sur la capture (voir PrivateNote), qui ne fait jamais partie du paquet de preuves."
                          },
                          "legal_hold": {
                            "type": "boolean",
                            "description": "La capture est en conservation pour litige (elle n'est pas supprimée tant que la valeur est true ; conservation levée automatiquement lorsque l'abonnement a pris fin et que ses 30 jours sont écoulés). Voir PATCH /captures/{capture_id}."
                          }
                        }
                      }
                    },
                    "next_cursor": {
                      "type": "string",
                      "description": "null lorsque cette page contient moins de lignes que limit.",
                      "nullable": true
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "MALFORMED_REQUEST : cursor invalide, status invalide (l'une des valeurs processing, completed, failed, deleted), limit invalide (de 1 à 200) ou filtre de recherche invalide (url, host, schedule_id, from, to ; au plus l'un de url / host / schedule_id ; from antérieur à to).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/AuthUnavailable"
          }
        }
      }
    },
    "/captures/batch": {
      "post": {
        "tags": [
          "Captures"
        ],
        "operationId": "createBatch",
        "summary": "Capturer plusieurs URL",
        "description": "Lance une capture par URL avec les mêmes options. Chaque capture est indépendante : interrogez chaque capture_id ou recevez un webhook par capture. Consomme une unité de limite de débit et une unité de limite mensuelle par URL. Un lot comportant plus d'URL que ce qu'autorise une fenêtre de limite de débit ne peut jamais aboutir ; il est donc refusé avec 400 RATE_LIMIT_COST_TOO_HIGH plutôt qu'avec 429. Divisez-le en requêtes plus petites. Un rejeu d'un lot terminé (même Idempotency-Key et même corps) est servi avant l'application de la limite de débit et ne coûte aucune unité de limite de débit.\n\nLimite de débit par compte, par fenêtres de 60 secondes (toutes les clés API du compte et sa session du tableau de bord partagent une même fenêtre). Une fois le limiteur exécuté, les réponses de cette route incluent les en-têtes `RateLimit-Policy` et `RateLimit` (fondés sur l'Internet-Draft de l'IETF draft-ietf-httpapi-ratelimit-headers) ainsi que les en-têtes historiques `X-RateLimit-Limit`, `X-RateLimit-Remaining` et `X-RateLimit-Reset`. Une réponse 429 ajoute `Retry-After`.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BatchRequest"
              },
              "example": {
                "urls": [
                  "https://example.com/a",
                  "https://example.com/b"
                ],
                "options": {
                  "case_reference": "Smith v. Jones"
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepté. Un rejeu idempotent d'un lot terminé avec la même Idempotency-Key et le même corps renvoie également 202, avec `Idempotent-Replay: true` ; il est servi avant l'application de la limite de débit et ne consomme donc aucune unité de limite de débit.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/XRequestId"
              },
              "Idempotent-Replay": {
                "$ref": "#/components/headers/IdempotentReplay"
              },
              "X-Quota-Limit": {
                "$ref": "#/components/headers/QuotaLimit"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/QuotaRemaining"
              },
              "X-Quota-Used": {
                "$ref": "#/components/headers/QuotaUsed"
              },
              "X-Quota-Cap": {
                "$ref": "#/components/headers/QuotaCap"
              },
              "X-Quota-Org-Limit": {
                "$ref": "#/components/headers/QuotaOrgLimit"
              },
              "X-Quota-Org-Used": {
                "$ref": "#/components/headers/QuotaOrgUsed"
              },
              "X-Sandbox-Limit": {
                "$ref": "#/components/headers/SandboxLimit"
              },
              "X-Sandbox-Remaining": {
                "$ref": "#/components/headers/SandboxRemaining"
              },
              "X-Sandbox-Used": {
                "$ref": "#/components/headers/SandboxUsed"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BatchResult"
                }
              }
            }
          },
          "400": {
            "description": "INVALID_BATCH (details : reason et received, ou index, url et un objet reason imbriqué pour l'URL invalide), MALFORMED_REQUEST ou une erreur d'options. WEBHOOK_REQUIRES_API_KEY : webhook_url envoyé depuis une session du tableau de bord (les webhooks nécessitent une clé API). RATE_LIMIT_COST_TOO_HIGH : plus d'URL qu'une fenêtre de limite de débit n'en autorise ; details { reason, max_per_window, window_seconds, requested }.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "QUOTA_EXCEEDED ou PAYMENT_REQUIRED. Un lot est aussi refusé avec QUOTA_EXCEEDED lorsque l'utilisation mensuelle ne peut pas être lue à ce moment-là ; réessayez.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            }
          },
          "403": {
            "description": "EIDAS_REQUIRES_LIVE_KEY : include_eidas avec une clé de test. EMAIL_NOT_VERIFIED : une session du tableau de bord dont l'adresse e-mail n'est pas confirmée. PLAN_ARCHIVE_ONLY : le forfait conserve uniquement l'archive et ne crée aucun nouveau travail. PLAN_API_READ_ONLY : une clé API d'un compte Website Archive (sa clé est en lecture seule ; utilisez le tableau de bord). PLAN_API_READ_ONLY avec details.reason \"session_outside_dashboard\" : une session du tableau de bord d'un compte Website Archive dont la requête n'a pas d'en-tête Origin, ou un en-tête qui n'est pas une adresse du tableau de bord ProofSnap (sur ce forfait, les nouveaux travaux se créent uniquement dans le tableau de bord ; il s'agit d'un garde-fou souple, pas d'une frontière de sécurité).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            }
          },
          "409": {
            "description": "IDEMPOTENCY_IN_PROGRESS.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            }
          },
          "422": {
            "description": "IDEMPOTENCY_KEY_REUSED.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            }
          },
          "429": {
            "description": "RATE_LIMITED : trop de requêtes dans la fenêtre de 60 secondes en cours. `error.details.retry_after_seconds` reprend Retry-After. RateLimit indique r=0 et le nombre de secondes avant la réinitialisation de la fenêtre. RATE_LIMITED également, pour une session du tableau de bord Website Archive : son plafond quotidien de 100 unités par jour UTC, partagé entre les captures uniques et par lot (une unité par URL), compare, redact et certify-hash (une unité par requête) ; une requête qui ne tient pas dans le plafond est refusée en entier (details.daily_limit, details.remaining, details.action \"wa_session_captures\" ; Retry-After et details.retry_after_seconds donnent le nombre de secondes jusqu'au jour UTC suivant, et les en-têtes par minute RateLimit et X-RateLimit-* sont omis). Un refus court avec seulement details.retry_after_seconds et Retry-After: 1 signale une mise à jour concurrente du compteur : réessayez. Un refus qui n'est pas imputable à l'appelant restitue les unités ; un travail accepté avec 202 qui échoue ensuite conserve son unité. Il s'agit d'un garde-fou souple, pas d'une frontière de sécurité.\n\nCONCURRENT_CAPTURE_LIMIT : aucune URL du lot n'a pu démarrer car le compte a déjà autant de captures en cours qu'il en a le droit (developer 10, startup 25, growth 100, legal_solo et legal_seats 25, custom 200, website_archive 25, sandbox 5) ; `details.limit`, `details.in_flight`, `details.requested` et Retry-After. Lorsque seules certaines URL tiennent dans la limite, le lot est accepté avec 202 et liste les autres dans `not_admitted`.\n\nTEST_MODE_DAILY_LIMIT : le travail en mode test (une clé de test, ou une session sans facturation liée) est limité à 200 unités par compte et par jour UTC, une par URL ; `details.used`, `details.limit`, `details.reset_at`, les en-têtes `X-Test-Daily-*` et Retry-After.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "RATE_LIMITED",
                    "message": "Too many requests. See Retry-After header.",
                    "details": {
                      "retry_after_seconds": 42
                    }
                  },
                  "request_id": "..."
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/EidasOrAuthUnavailable"
          }
        }
      }
    },
    "/captures/hosts": {
      "get": {
        "tags": [
          "Captures"
        ],
        "operationId": "listCaptureHosts",
        "summary": "Lister les noms d'hôte que vous avez capturés",
        "description": "Chaque nom d'hôte de votre propre liste de captures, avec le nombre de captures et l'heure de la dernière capture par hôte, pour un filtre par domaine : renvoyez un hôte sous la forme GET /captures?host=. count est le nombre de lignes que GET /captures?host=<host> renvoie sans status (parmi les captures lues) : captures de test et live confondues, tous statuts y compris celles placées dans la corbeille (status deleted), telles que la liste les affiche ; les captures effacées et les lignes sans URL de page (certificats) sont exclues. Une capture est comptée dès qu'elle porte son nom d'hôte stocké (les captures créées avant le 2026-10-05 le portent une fois le rattrapage exécuté). Une capture du cabinet n'est comptée que tant que vous pouvez encore agir sur elle (vous êtes membre de ce cabinet ou en êtes le propriétaire). host est en minuscules. Trié par count (le plus élevé d'abord), puis par host (de A à Z). 1 000 hôtes au maximum. Les 5 000 captures les plus récentes sont lues ; truncated vaut true lorsqu'il y a plus de 1000 hôtes ou des captures plus anciennes que ces 5000, si bien qu'un hôte capturé uniquement avant celles-ci peut manquer. La liste est calculée au plus une fois toutes les 30 secondes par compte et peut dater de 30 secondes au plus (un peu plus pendant qu'une autre requête du compte la calcule). Son calcul coûte une unité de votre limite de débit par minute ; une réponse servie depuis ce cache ne coûte rien. Disponible sur tous les forfaits, pour les clés API et la session du tableau de bord.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Les noms d'hôte.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "hosts",
                    "truncated"
                  ],
                  "properties": {
                    "hosts": {
                      "type": "array",
                      "maxItems": 1000,
                      "items": {
                        "type": "object",
                        "required": [
                          "host",
                          "count",
                          "last_captured_at"
                        ],
                        "properties": {
                          "host": {
                            "type": "string",
                            "example": "www.example.com",
                            "description": "Nom d'hôte en minuscules, la valeur qu'accepte GET /captures?host=."
                          },
                          "count": {
                            "type": "integer",
                            "minimum": 1,
                            "example": 12,
                            "description": "Captures de cet hôte parmi les captures lues : les lignes que GET /captures?host= renvoie sans status, y compris celles placées dans la corbeille."
                          },
                          "last_captured_at": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true,
                            "example": "2026-10-05T07:48:38Z",
                            "description": "Heure de la dernière capture de cet hôte (moment où la page a été capturée, sinon moment où la capture a été demandée), en UTC, à la seconde près."
                          }
                        }
                      }
                    },
                    "truncated": {
                      "type": "boolean",
                      "description": "true lorsque la liste peut être incomplète : plus de 1 000 hôtes, ou plus de captures que les 5 000 plus récentes qui ont été lues."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "MALFORMED_REQUEST : scope non pris en charge (uniquement vos propres captures).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "description": "RATE_LIMITED : la limite de débit par minute est épuisée (le calcul de la liste coûte une unité), ou la liste de votre compte est en cours de calcul par une autre requête et aucun résultat antérieur n'est stocké ; réessayez après Retry-After secondes.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "RATE_LIMITED",
                    "message": "Too many requests. See Retry-After header.",
                    "details": {
                      "retry_after_seconds": 42
                    }
                  },
                  "request_id": "..."
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/AuthUnavailable"
          }
        }
      }
    },
    "/captures/{capture_id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/CaptureId"
        }
      ],
      "get": {
        "tags": [
          "Captures"
        ],
        "operationId": "getCapture",
        "summary": "Obtenir une capture",
        "description": "Statut et, une fois la capture terminée, un package_url récent valable jusqu'à 24 heures (voir package_url_expires_at). Fonctionne pour les identifiants de capture, de certificat, de comparaison et de caviardage. Lisible par le compte qui l'a créé, par les membres de l'affaire à laquelle il appartient et par le propriétaire du cabinet pour lequel il a été réalisé (si l'abonnement du cabinet a pris fin, jusqu'à la fermeture de sa fenêtre de téléchargement).",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "event",
            "in": "query",
            "required": false,
            "description": "Envoyez download lorsque vous téléchargez le paquet : pour une capture rattachée à une affaire, cela enregistre capture.downloaded dans l'historique des accès de l'affaire. Les simples interrogations de statut doivent l'omettre.",
            "schema": {
              "type": "string",
              "enum": [
                "download"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "La capture.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CaptureResult"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "FORBIDDEN : cette capture ne vous appartient pas.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "CAPTURE_NOT_FOUND : identifiant inconnu ou mal formé, capture supprimée, ou fichier du paquet introuvable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/AuthUnavailable"
          }
        }
      },
      "delete": {
        "tags": [
          "Captures"
        ],
        "operationId": "deleteCapture",
        "summary": "Supprimer une capture",
        "description": "Par défaut : déplace la capture dans la corbeille. Elle peut être restaurée avec PATCH jusqu'à restorable_until (30 jours) ; au-delà, elle est purgée. Avec permanent=true, le paquet stocké est effacé immédiatement et l'enregistrement est réduit à un enregistrement minimal de suppression ; cette opération est irréversible. Aucune des deux options ne supprime les paquets que vous avez déjà téléchargés ni la preuve OpenTimestamps. Une capture rattachée à une affaire ouverte ou archivée est d'abord retirée de l'affaire (opération consignée dans son journal de traçabilité) lorsque l'appelant peut ouvrir cette affaire ; un appelant qui ne le peut pas (son auteur après son retrait de l'affaire) est refusé avec 409 MATTER_LINKED, pour la corbeille comme pour permanent=true, jusqu'à ce que le propriétaire de l'affaire l'en retire ; une capture rattachée à une affaire scellée ne peut pas être supprimée. Une capture encore en cours de traitement ne peut pas être supprimée (409 CAPTURE_PROCESSING) ; supprimez-la une fois qu'elle est terminée ou en échec. Supprimer une capture qui ne s'est jamais terminée restitue l'unité mensuelle qu'elle avait réservée.\n\nDélai de grâce après la levée d'une conservation pour litige : pendant 14 jours après la levée d'une conservation pour litige (`deletion_blocked_until` dans GET), la capture n'est pas supprimée définitivement (409 CAPTURE_HOLD_GRACE avec `details.until`), ni par une personne, ni par la purge de la corbeille ou la durée de conservation (la suppression du compte et du cabinet après la fin de l'abonnement ne l'attend pas). Le passage à la corbeille est autorisé ; sa purge attend la fin du délai de grâce. Une suppression définitive lève d'abord toute conservation pour litige S3 Object Lock restant sur les objets stockés ; lorsque le stockage refuse, la requête renvoie 409 CAPTURE_LEGAL_HOLD (\"still locked in storage\", réessayez plus tard) et rien n'est supprimé. Chaque passage à la corbeille, restauration et suppression définitive est consigné dans la chaîne des événements de capture du compte (GET /captures/events). Un caviardage ou une comparaison dont la capture source est sous conservation pour litige renvoie `source_under_legal_hold: true` lorsqu'il est placé dans la corbeille ou supprimé.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "permanent",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "true"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Supprimé. Corbeille : { capture_id, status: \"deleted\", restorable: true, restorable_until }. Définitif : { capture_id, status: \"deleted\", permanent: true }. Déjà dans la corbeille : { capture_id, status }.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "capture_id": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string"
                    },
                    "restorable": {
                      "type": "boolean"
                    },
                    "restorable_until": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "permanent": {
                      "type": "boolean"
                    },
                    "source_under_legal_hold": {
                      "type": "boolean",
                      "description": "Présent (true) uniquement lorsqu'il s'agit d'un caviardage ou d'une comparaison réalisé à partir d'une capture sous conservation pour litige."
                    },
                    "deletion_blocked_until": {
                      "type": "string",
                      "format": "date-time",
                      "description": "Corbeille pendant le délai de grâce après levée : la purge de la corbeille attend jusqu'à ce moment."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "FORBIDDEN : cette capture ne vous appartient pas. FORBIDDEN également lorsque la capture est rattachée à une affaire non scellée et que l'appelant (le propriétaire du cabinet agissant sur la capture d'un membre du cabinet, et non son auteur) ne peut pas ouvrir cette affaire. PLAN_API_READ_ONLY : une clé API d'un compte Website Archive (sa clé est en lecture seule ; utilisez le tableau de bord).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "CAPTURE_NOT_FOUND.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "MATTER_SEALED : la capture est une pièce d'une affaire scellée. MATTER_LINKED : la capture est une pièce d'une affaire dont l'appelant ne fait plus partie ; le propriétaire de l'affaire doit d'abord l'en retirer. CAPTURE_PROCESSING : la capture est encore en cours de traitement (error.details.status). CAPTURE_LEGAL_HOLD : la capture est sous conservation pour litige et personne ne peut la supprimer (ni la placer dans la corbeille, ni l'effacer). CAPTURE_HOLD_GRACE : une suppression définitive pendant le délai de grâce suivant la levée de la conservation pour litige de la capture (LEGAL_HOLD_RELEASE_GRACE_DAYS, 14 jours) ; error.details.until indique quand elle devient possible. Le déplacement dans la corbeille est autorisé pendant le délai de grâce.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "INTERNAL_ERROR : le paquet stocké n'a pas pu être effacé ; rien n'a été supprimé.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/AuthUnavailable"
          }
        }
      },
      "patch": {
        "tags": [
          "Captures"
        ],
        "operationId": "restoreCapture",
        "summary": "Restaurer une capture supprimée, définir sa note privée, ou placer ou lever sa conservation pour litige",
        "description": "Restaure une capture depuis la corbeille. Si elle a été retirée d'une affaire lors de sa suppression, elle y est rajoutée lorsque cette affaire est toujours ouverte et que l'appelant peut encore l'ouvrir ; matter_id et matter_relinked ne figurent dans la réponse que dans ce cas (matter_id vaut null lorsque le rattachement n'a pas eu lieu). Un appelant qui ne peut plus ouvrir cette affaire récupère la capture sans elle, rien n'est écrit dans le journal de traçabilité de l'affaire, et la réponse ne mentionne pas l'affaire. Une capture qui n'est pas dans la corbeille est renvoyée inchangée.\n\nNote privée : un corps contenant `private_note` (une chaîne de 200 caractères au maximum, ou null pour l'effacer) définit à la place la note privée de la capture et ne fait rien d'autre ; il ne peut pas être combiné avec `action`. La note est stockée uniquement dans l'enregistrement de la capture, hors du paquet de preuves scellé, si bien que sa modification n'altère jamais la preuve ni ses empreintes. Seul l'auteur de la capture peut la définir (le propriétaire du cabinet et les membres de l'affaire reçoivent 403), sur une capture dans n'importe quel état sauf supprimée définitivement (404). La réponse est { capture_id, status, private_note }.\n\nConservation pour litige : un corps `{ \"action\": \"legal_hold\", \"enabled\": true|false, \"reason\"?: string }` place (true) ou lève (false) la conservation pour litige de la capture et ne fait rien d'autre. Une capture en conservation pour litige n'est pas supprimée : DELETE (corbeille ou définitif) renvoie 409 CAPTURE_LEGAL_HOLD pour tout le monde, et ni la purge de la corbeille ni la suppression liée à la durée de conservation n'y touchent ; GET affiche `storage.retention.earliest_deletion_at` à null. Une conservation pour litige n'est maintenue que tant que le compte dispose d'un forfait qui conserve les données (tout forfait payant, Archive only compris ; pour une capture de cabinet : le cabinet est actif ou conservé comme archive du cabinet). À la fin de l'abonnement, les captures restent disponibles pendant 30 jours ; si aucun forfait de ce type n'est souscrit d'ici là, toutes les conservations pour litige du compte (ou du cabinet) sont levées automatiquement à cette date (acteur system, motif subscription_ended, enregistrées comme toute levée, sans délai de grâce après levée) et les captures sont supprimées avec les autres. Pour garder les captures en conservation, passez à Archive only (ou achetez l'archive du cabinet) avant cette date, ou téléchargez-les. Pendant ces 30 jours, aucune nouvelle conservation ne peut être placée (le compte n'a pas de forfait payant), mais les conservations existantes peuvent encore être levées. La conservation est stockée uniquement dans l'enregistrement de la capture, hors du paquet de preuves scellé. `reason` est un texte brut facultatif de 200 caractères au maximum (sans caractères de contrôle, invisibles ou de direction du texte ; les espaces sont réduits, une valeur vide signifie aucun motif) ; il est conservé avec la conservation pour litige et effacé lors de sa levée.\n\nQui : pour placer une conservation, toute personne autorisée à agir sur la capture (son auteur, pour une capture de cabinet tant qu'il est encore membre de ce cabinet, et le propriétaire du cabinet, y compris d'un cabinet dont l'abonnement a pris fin, pendant sa durée de conservation ou s'il est conservé comme archive du cabinet ; l'archive uniquement via la session du tableau de bord). Pour la lever : sur une capture personnelle, son auteur ; sur une capture de cabinet, le propriétaire du cabinet uniquement (un membre du cabinet reçoit 403 LEGAL_HOLD_OWNER_ONLY). Placer une conservation exige un forfait payant (tout forfait payant, Archive only compris) : le sandbox gratuit, de même qu'un compte dont l'abonnement a pris fin, reçoit 403 LEGAL_HOLD_NOT_AVAILABLE avec details.reason \"plan\" (une archive du cabinet en cours compte comme payante pour les captures de ce cabinet, via la session du tableau de bord de son propriétaire), et une capture en mode test (avec filigrane, réalisée avec une clé de test ou sans facturation liée) reçoit 403 LEGAL_HOLD_NOT_AVAILABLE avec details.reason \"test_capture\". La levée d'une conservation fonctionne avec tous les forfaits, même après la fin de l'abonnement. Sur Website Archive, la clé API est en lecture seule (403 PLAN_API_READ_ONLY), la session du tableau de bord, elle, y est autorisée. Un abonnement en retard de paiement ou impayé ne le bloque pas. Une session du tableau de bord ou une clé dont l'adresse e-mail n'est pas confirmée reçoit 403 EMAIL_NOT_VERIFIED.\n\nRefusé sur une capture dans la corbeille (409 CAPTURE_IN_TRASH : restaurez-la d'abord) ou encore en cours de traitement (409 CAPTURE_PROCESSING) ; une capture supprimée définitivement, ou dont la suppression définitive est en cours, renvoie 404. Chaque requête de conservation pour litige coûte une unité de la limite de débit par minute du compte (429 RATE_LIMITED avec Retry-After lorsqu'elle est épuisée). Envoyer l'état que la capture a déjà ne change rien et renvoie 200 avec les champs stockés et `changed: false`. Chaque modification est consignée dans le journal d'audit sous capture.legal_hold (celle d'une capture de cabinet dans l'espace du cabinet), enregistrée dans l'historique des accès de la capture (`legal_hold_on` / `legal_hold_off`, conservé lorsque la capture est effacée ultérieurement) et, lorsque la capture appartient à une affaire, écrite dans le journal de traçabilité de l'affaire sous capture.legal_hold_placed ou capture.legal_hold_released avec l'identifiant de la capture pour cible (le motif va dans l'historique des accès de l'affaire, jamais dans la chaîne hachée ; la chaîne d'une affaire scellée ne change pas). La réponse est { capture_id, legal_hold, legal_hold_set_at, legal_hold_reason, changed }.\n\nConservation pour litige v2 : placer ou lever une conservation pose ou retire aussi une conservation pour litige S3 Object Lock sur chaque version du paquet stocké, sa preuve OpenTimestamps mise à niveau et le double stocké séparément (`s3_legal_hold` dans GET ; lorsque le stockage refuse ou n'a pas de configuration Object Lock, la conservation dans l'enregistrement de la capture s'applique toujours et une tentative échouée est relancée par le serveur). Une levée ouvre un délai de grâce de 14 jours pendant lequel la capture n'est pas supprimée définitivement (pas après la fin de l'abonnement : les 30 jours suivant la fin le remplacent). Une conservation placée par la conservation pour litige de son affaire (`legal_hold_source` \"matter:<id>\") se lève sur l'affaire tant que cette conservation est active : la lever ici renvoie 409 LEGAL_HOLD_MATTER (error.details.matter_id pour un lecteur de l'affaire). Chaque modification est enregistrée dans la chaîne d'événements de capture du compte (GET /captures/events).",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "action": {
                    "type": "string",
                    "description": "restore (valeur par défaut pour un corps sans private_note) ou legal_hold (avec enabled et le motif facultatif reason).",
                    "enum": [
                      "restore",
                      "legal_hold"
                    ]
                  },
                  "private_note": {
                    "allOf": [
                      {
                        "$ref": "#/components/schemas/PrivateNote"
                      }
                    ],
                    "description": "Définit (une chaîne) ou efface (null ou une chaîne vide) la note privée. Ne peut pas être combiné avec action."
                  },
                  "enabled": {
                    "type": "boolean",
                    "description": "Avec l'action legal_hold (obligatoire dans ce cas) : true place la conservation pour litige, false la lève."
                  },
                  "reason": {
                    "type": "string",
                    "nullable": true,
                    "maxLength": 200,
                    "description": "Avec l'action legal_hold, facultatif : la raison de la conservation de la capture, en texte brut de 200 caractères au maximum. Stocké avec la conservation, hors du paquet de preuves ; ignoré lors d'une levée (il va alors uniquement dans le journal d'audit)."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Restaurée ({ capture_id, status, restored: true, matter_id?, matter_relinked? }), inchangée ({ capture_id, status }), note privée définie ({ capture_id, status, private_note }) ou conservation pour litige définie ({ capture_id, legal_hold, legal_hold_set_at, legal_hold_reason, changed }).",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "capture_id": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string"
                    },
                    "restored": {
                      "type": "boolean"
                    },
                    "matter_id": {
                      "type": "string",
                      "nullable": true
                    },
                    "matter_relinked": {
                      "type": "boolean"
                    },
                    "private_note": {
                      "type": "string",
                      "nullable": true
                    },
                    "legal_hold": {
                      "type": "boolean",
                      "description": "Action de conservation pour litige : l'état de conservation de la capture après la requête."
                    },
                    "legal_hold_set_at": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true,
                      "description": "Action de conservation pour litige : le moment où la conservation en cours a été placée (null s'il n'y en a pas)."
                    },
                    "legal_hold_reason": {
                      "type": "string",
                      "nullable": true,
                      "description": "Action de conservation pour litige : le motif stocké avec la conservation en cours, ou null."
                    },
                    "changed": {
                      "type": "boolean",
                      "description": "Action de conservation pour litige : false lorsque la capture était déjà dans l'état demandé (rien n'a été modifié ni consigné)."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "MALFORMED_REQUEST : JSON invalide, corps qui n'est pas un objet, action autre que restore ou legal_hold, private_note combiné avec action, ou private_note qui n'est ni une chaîne ni null, dépasse 200 caractères ou contient des caractères de contrôle, invisibles ou de direction du texte. Pour legal_hold : enabled absent ou non booléen, motif qui n'est pas une chaîne, dépasse 200 caractères ou n'est pas en texte brut, ou champ inconnu.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "FORBIDDEN : cette capture ne vous appartient pas. FORBIDDEN également lorsque la capture est rattachée à une affaire non scellée et que l'appelant (le propriétaire du cabinet agissant sur la capture d'un membre du cabinet, et non son auteur) ne peut pas ouvrir cette affaire. PLAN_API_READ_ONLY : une clé API d'un compte Website Archive (sa clé est en lecture seule ; utilisez le tableau de bord). Pour private_note : FORBIDDEN pour toute personne autre que l'auteur de la capture (qui doit encore être autorisé à agir sur celle-ci). Pour legal_hold : FORBIDDEN lorsque l'appelant ne peut pas agir sur la capture ; LEGAL_HOLD_OWNER_ONLY lorsqu'un membre du cabinet (et non le propriétaire du cabinet) lève la conservation pour litige d'une capture du cabinet ; LEGAL_HOLD_NOT_AVAILABLE lors d'un placement en conservation pour litige sans forfait payant (details.reason plan) ou sur une capture en mode test (details.reason test_capture) ; EMAIL_NOT_VERIFIED pour une adresse e-mail non confirmée. La règle relative aux affaires ci-dessus ne s'applique pas à une conservation pour litige.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "CAPTURE_NOT_FOUND, y compris pour une capture supprimée définitivement.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Conservation pour litige uniquement. CAPTURE_IN_TRASH : la capture est dans la corbeille ; restaurez-la d'abord. CAPTURE_PROCESSING : la capture est encore en cours de traitement (error.details.status). LEGAL_HOLD_MATTER : la conservation de la capture a été placée par la conservation pour litige de son affaire, toujours active ; levez-la sur l'affaire. LEGAL_HOLD_SOURCE : la conservation d'un caviardage ou d'une comparaison qui suit une capture dont il est issu (pour une comparaison, l'une ou l'autre des captures comparées), toujours sous conservation pour litige (placée sur cette capture ou via son affaire) : le paquet reste en conservation jusqu'à la levée de celle-ci (`details.source_capture_id`). Une comparaison reste en conservation tant que l'une des captures comparées l'est, et n'est libérée que lorsqu'aucune ne l'est plus (`legal_hold_source` \"derived:<capture id>\" désigne celle qui est en conservation).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Conservation pour litige uniquement. RATE_LIMITED : la limite de débit par minute du compte est épuisée. `error.details.retry_after_seconds` reprend Retry-After.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/AuthUnavailable"
          }
        }
      }
    },
    "/captures/{capture_id}/verify": {
      "parameters": [
        {
          "$ref": "#/components/parameters/CaptureId"
        }
      ],
      "post": {
        "tags": [
          "Captures"
        ],
        "operationId": "verifyCapture",
        "summary": "Vérifier un paquet stocké",
        "description": "Lit le paquet stocké et vérifie la signature RSA de manifest.json ainsi que l'empreinte SHA-256 de chaque fichier qu'il liste. Les paquets de plus de 256 Mio renvoient 413 VERIFY_PACKAGE_TOO_LARGE ; vérifiez-les hors ligne avec les scripts fournis. Chaque requête coûte une unité de la limite de débit par minute du compte (429 RATE_LIMITED). Lorsque le paquet contient manifest.json.tsr, le jeton d'horodatage eIDAS est aussi vérifié par ProofSnap (checks.eidas) : la signature et les attributs signés du jeton, la présence de l'empreinte SHA-256 de manifest.json, l'extendedKeyUsage du certificat de la TSA, sa chaîne de certificats jusqu'à l'AC racine épinglée Disig QCA4 à la date du jeton (genTime), et le statut du certificat de la TSA d'après la réponse OCSP ou la CRL enregistrée lors de l'émission dans eidas_validation.json (vérifiée par signature, non récupérée en direct). Cette vérification ne demande jamais de nouvel horodatage. Elle indique aussi si manifest.json.ots est présent et si l'empreinte du manifeste figure dans le registre d'émission ProofSnap. Le verdict dépend uniquement de la signature du manifeste et des fichiers listés ; un jeton qui échoue à sa vérification est signalé dans checks.eidas et dans warnings (EIDAS_TOKEN_FAILED), sans être intégré au verdict. Un paquet émis avec un horodatage qualifié enregistre eidas_requested: true dans son manifest.json signé ; lorsqu'un tel paquet ne contient plus manifest.json.tsr, checks.eidas et checks.eidas_disig sont signalés comme missing avec l'avertissement EIDAS_TOKEN_MISSING (les scripts de vérification fournis signalent FAIL pour le même paquet). Autorisé pour le propriétaire de la capture, les membres de son affaire et le propriétaire du cabinet pour lequel elle a été réalisée. Pour vérifier les horodatages de manière indépendante, exécutez les scripts du dossier verification/ du paquet.\n\nPaquets importés (source import) : le ZIP stocké est revérifié avec les contrôles d'importation (signature, chaque fichier, traçabilité, chaîne forensique, horodatages ; la provenance est de nouveau recherchée dans le registre d'émission de l'API pour les paquets API et reprise de l'enregistrement d'importation pour les paquets de l'extension). La réponse est { capture_id, source, type: \"import\", package_kind, verifier: \"package-verify\", verdict (match, modified, invalid), rechecked: true, manifest_sha256, zip_sha256, zip_matches_import, kind, summary, levels, checks, reasons, provenance }. Un paquet de plus de 512 Mio, ou une vérification qui ne se termine pas en 20 secondes environ, renvoie rechecked: false avec un motif (too_large_for_synchronous_check, time_budget, storage_error), verdict: null et stored_verification (le rapport enregistré lors de l'importation).",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Résultat de la vérification.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VerifyResult"
                },
                "example": {
                  "verdict": "match",
                  "capture_id": "cap_a1B2c3D4e5F6g7H8",
                  "source": "s3",
                  "checks": {
                    "manifest_signature": {
                      "status": "valid",
                      "algorithm": "RSA-4096"
                    },
                    "declared_files": {
                      "status": "all_match"
                    },
                    "bitcoin_ots": {
                      "status": "present"
                    },
                    "eidas_disig": null,
                    "proofsnap_issuance": {
                      "status": "issued_by_proofsnap",
                      "manifest_sha256": "3f2a...",
                      "issued_at": "2026-09-01T10:00:00.000Z",
                      "type": "capture"
                    },
                    "eidas": null
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "FORBIDDEN.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "CAPTURE_NOT_FOUND : inconnue, supprimée ou sans paquet stocké.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "VERIFY_PACKAGE_TOO_LARGE : le paquet stocké dépasse 256 Mio.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "RATE_LIMITED : la limite de débit par minute du compte est épuisée.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "CONFIGURATION_ERROR : le paquet stocké n'a pas pu être lu.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/AuthUnavailable"
          }
        }
      }
    },
    "/captures/{capture_id}/redact": {
      "parameters": [
        {
          "$ref": "#/components/parameters/CaptureId"
        }
      ],
      "post": {
        "tags": [
          "Comparer et caviarder"
        ],
        "operationId": "redactCapture",
        "summary": "Créer une copie caviardée",
        "description": "Construit un nouveau paquet à partir d'une capture terminée en retirant le contenu sélectionné, le signe à nouveau (nouvelle clé RSA-4096) et le soumet à un ancrage OpenTimestamps. L'original n'est pas modifié. Synchrone : renvoie 201 lorsque le paquet est prêt. Décompté comme une capture.\n\n- Les caviardages par zone noircissent des zones de la capture d'écran. Une capture d'écran JPEG sous la forme dans laquelle la capture l'écrit (séquentielle, en niveaux de gris ou YCbCr) est caviardée sans être décodée en pixels : les blocs compressés (MCU, de 8 à 32 pixels de côté) touchés par une zone sont remplacés par des blocs noirs et tous les autres blocs restent inchangés, de sorte que le reste de l'image n'est pas recompressé (`screenshot_method` `jpeg_dct_blocks`, jusqu'à 150 mégapixels). La surface noircie correspond à chaque zone arrondie vers l'extérieur à des blocs entiers (`painted_rects`) et contient donc toujours la zone. Une capture d'écran PNG, ou un JPEG d'un autre type, est décodée, noircie et réencodée en screenshot.jpeg, puis les blocs 8 x 8 touchés par une zone sont noircis dans le nouveau fichier (`screenshot_method` `decode_repaint_jpeg`, jusqu'à 60 mégapixels). Une capture d'écran qui dépasse sa limite ou dont les données d'image sont illisibles est refusée avec 422 REDACTION_SCREENSHOT_UNREADABLE avant toute facturation. Si l'en-tête est lisible mais pas les données d'image, l'erreur est détectée après la réservation de l'unité mensuelle, et cette unité est libérée. Les caviardages de texte et de métadonnées fonctionnent aussi sans zones. redaction_manifest.json (`screenshot_boxes`) consigne la méthode, `painted_rects`, la grille de blocs et une note explicative destinée aux experts forensiques.\n- Les caviardages de texte suppriment les correspondances de page.html et de domtextcontent.txt, de chaque valeur de texte libre de metadata.json (titre de la page et valeurs meta, enregistrements WHOIS / RDAP, sujet TLS, valeurs des en-têtes de réponse HTTP) et de chaque valeur de texte libre de forensic_log.json et de chain_of_custody.json (case_reference, case_id, case_note, vos valeurs de métadonnées, code injecté et ses erreurs, libellés des pop-ups fermées, legal_context.note). Les identifiants, empreintes, heures et URL de ces enregistrements restent inchangés. Les occurrences encodées sont également supprimées : références de caractères HTML (nommées, décimales, hexadécimales, comme M&uuml;ller ou jan&#64;firma.cz), encodage pourcent (mailto:jan%40firma.cz) et séquences d'échappement JavaScript (\\u0040, \\x40), y compris les encodages imbriqués jusqu'à deux niveaux ; le marqueur remplace les caractères encodés eux-mêmes. Le contrôle post-caviardage des correspondances subsistantes lit les fichiers JSON valeur par valeur et recherche chaque texte tel qu'il est écrit et sous forme décodée ; il échoue donc sur une correspondance sous l'une quelconque de ces formes. Les caviardages de texte noircissent aussi la capture d'écran là où le text_layout.json de la capture source situe une occurrence (`text_rule_rects`) : les lignes estimées du bloc de texte qui contiennent la correspondance sont noircies, et le bloc entier uniquement lorsque l'estimation est incertaine (décompté dans `screenshot_boxes.from_text_rules.rects_whole_block` de redaction_manifest.json) ; les occurrences qu'il ne situe pas sont décomptées dans `text_occurrences_not_located`. Les règles respectent la casse exacte, sauf si `case_insensitive` est défini.\n- Les caviardages de métadonnées suppriment des champs de metadata.json. Deux noms vont plus loin. `ip_dns` (également accepté : `ip`, `dns`, `resolved_ips`) supprime les adresses IP et les réponses DNS du site cible de metadata.json, forensic_log.json et chain_of_custody.json (les adresses propres de la machine de capture sont conservées), et les supprime également, uniquement en tant qu'adresses entières, de page.html, de domtextcontent.txt et de la capture d'écran sous forme de règles de texte automatiques (consignées avec la seule source ip_dns, jamais l'adresse ni son empreinte, et non décomptées de la limite text_redactions). `cookie_names` (également `cookie`, `cookies`, `http_headers.cookie`) supprime les en-têtes Cookie et Set-Cookie et toute liste de noms de cookies dans ces enregistrements ; les décomptes sont conservés. Les valeurs des cookies sont déjà caviardées dans chaque capture.\n\nUne zone ne couvre que des pixels. Le texte situé sous une zone reste dans page.html, domtextcontent.txt et metadata.json, sauf si vous ajoutez aussi un caviardage de texte, et une image située sous une zone peut toujours être référencée ou intégrée dans page.html (img src ou srcset, url() CSS, URI data:) ; un caviardage de texte qui correspond à son adresse supprime cette référence. Le paquet caviardé ne reprend de l'original que screenshot.jpeg (ou screenshot.png lorsqu'aucune zone ne l'a remplacé), page.html, domtextcontent.txt, metadata.json et forensic_log.json, plus les fichiers qu'il écrit à nouveau (evidence.pdf, chain_of_custody.json, redaction_manifest.json, les fichiers de vérification et les fichiers du manifeste). Tous les autres fichiers de l'original, comme images/, image_inventory.json, text_layout.json, page.mhtml, network.har et capture.warc, sont omis et listés dans redaction_manifest.json sous dropped_for_leak_protection. Les caviardages de texte s'appliquent aussi à ces noms de fichiers, car une copie d'image porte le nom de son URL. chain_of_custody.json conserve les événements de l'original qui les concernent, marqués artifact_removed_in_redaction et sans les empreintes des fichiers absents du paquet caviardé. Un rule_902_certification.pdf de l'original est également supprimé, car il reprend les fichiers et les empreintes de l'original ; la suppression est listée dans redaction_manifest.json (removed_as_stale) et dans chain_of_custody.json. L'URL de capture n'est jamais caviardée, car elle fait partie du manifeste signé : un caviardage de texte dont le texte figure aussi dans l'URL de capture ou l'URL finale est appliqué partout ailleurs, le texte reste dans cette URL et la règle est listée dans `rules_in_capture_url` et dans redaction_manifest.json (leakage_check.capture_url). Seules les captures de pages web peuvent être caviardées (une comparaison, une certification ou un caviardage est refusé avec 422 REDACTION_SOURCE_UNSUPPORTED).\n\nLe caviardage d'une capture d'essai (réalisée avec une clé de test, dans le Sandbox ou par un compte sans compte de facturation associé) est un paquet de test, quelle que soit la clé qui le demande : filigrané, `test_mode: true` dans manifest.json, ligne de test (key_env test) non décomptée et ne consommant aucune unité mensuelle. include_eidas sur un tel caviardage est refusé avec 403 EIDAS_REQUIRES_LIVE_KEY (details.reason).\n\nLe rapport de caviardage (evidence.pdf) est rendu dans la langue `language` (par défaut : la langue de la capture source), y compris son corps, sa page de couverture et sa bannière de test.\n\nAvant tout caviardage, chaque fichier que le caviardage lit ou reprend (la capture d'écran, page.html, domtextcontent.txt, metadata.json, forensic_log.json, chain_of_custody.json, text_layout.json) et que le manifeste source liste avec une empreinte SHA-256 est haché à nouveau (redaction_manifest.json derived_from.source_files_rehashed) ; une divergence ou un fichier manquant renvoie 422 REDACTION_SOURCE_INTEGRITY_FAILED et rien n'est émis, de sorte qu'une source corrompue ou altérée n'est jamais rescellée sous une nouvelle signature. Les fichiers omis du paquet caviardé ne sont pas lus. Un caviardage synchrone dont la source a mis trop de temps à être lue s'arrête avant toute réservation, avec 500 CAPTURE_TIME_BUDGET_EXCEEDED.\n\nLe caviardage d'une capture sous conservation pour litige est lui aussi placé en conservation pour litige (legal_hold_source \"derived:<capture id>\"), de même que chaque caviardage d'une capture au moment où celle-ci est placée en conservation pour litige ; lever la conservation pour litige de la capture lève ces conservations et, tant que la capture reste en conservation, elles ne peuvent pas être levées séparément (409 LEGAL_HOLD_SOURCE). Le captured_at d'une ligne de caviardage est l'heure de capture de sa source ; redacted_at est le moment où le caviardage a été réalisé.\n\nLe caviardage est refusé par sécurité : lorsque le contrôle post-caviardage trouve encore un terme caviardé n'importe où dans le paquet en dehors de l'URL de capture et de l'URL finale, aucun paquet n'est émis et la requête renvoie 422 REDACTION_LEAK_DETECTED (`error.details` `{ files, rule_indexes, metadata_fields, in_capture_url }` ; le terme lui-même n'est jamais renvoyé). redaction_manifest.json consigne `leakage_check.fail_closed: true`.\n\nLes valeurs sont tronquées aux limites de caractère (un caractère n'est jamais coupé) : chaînes de correspondance de texte à 2 000 caractères, motifs à 200, reason_summary à 500, champs du dépositaire à 200. Les motifs, reason_summary et les champs du dépositaire sont imprimés sur le certificat de caviardage et doivent être en texte brut : un caractère de contrôle, invisible ou de direction du texte est refusé avec 400.\n\nBudget de temps et nouvelles tentatives : la requête répond dans la limite de 29 secondes d'API Gateway. Le scellement s'exécute dans un budget de temps compté à partir de la requête ; si l'horodatage qualifié (include_eidas) ne tient pas dans ce budget, il n'est pas demandé et le paquet est livré avec le seul ancrage OpenTimestamps (timestamps.eidas statut not_obtained, motif time_budget, non facturé), et une demande OpenTimestamps qui ne tient pas dans le budget est omise (chain_of_custody.json consigne ots_skipped avec le motif time_budget). Envoyez une Idempotency-Key afin qu'une nouvelle tentative (par exemple après un 504 d'une passerelle ou une erreur réseau) renvoie le même caviardage au lieu d'en créer et d'en facturer un second. Une nouvelle tentative alors que la première requête est encore en cours renvoie 409 IDEMPOTENCY_IN_PROGRESS avec l'identifiant du caviardage dans `error.details.capture_id` (interrogez GET /captures/{capture_id} avec celui-ci). Une nouvelle tentative ultérieure renvoie 201 avec le corps d'origine et `Idempotent-Replay: true`. Un caviardage qui échoue libère sa clé, de sorte qu'une nouvelle tentative avec la même clé s'exécute à nouveau.\n\nLimite de débit par compte sur des fenêtres de 60 secondes (toutes les clés API du compte et sa session du tableau de bord partagent une même fenêtre). Une fois le limiteur passé, les réponses de cette route incluent les en-têtes `RateLimit-Policy` et `RateLimit` (fondés sur l'Internet-Draft de l'IETF draft-ietf-httpapi-ratelimit-headers) ainsi que les anciens en-têtes `X-RateLimit-Limit`, `X-RateLimit-Remaining` et `X-RateLimit-Reset`. Une réponse 429 ajoute `Retry-After`.\n\nUn paquet de preuves importé (source import, identifiants préfixés `imp_`) ne peut pas être caviardé : 422 IMPORT_NOT_SUPPORTED.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "boxes": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "x": {
                          "type": "number",
                          "minimum": 0
                        },
                        "y": {
                          "type": "number",
                          "minimum": 0
                        },
                        "w": {
                          "type": "number",
                          "minimum": 0
                        },
                        "h": {
                          "type": "number",
                          "minimum": 0
                        },
                        "reason": {
                          "type": "string",
                          "maxLength": 200
                        }
                      },
                      "required": [
                        "x",
                        "y",
                        "w",
                        "h"
                      ]
                    },
                    "maxItems": 200,
                    "description": "Rectangles en pixels de la capture d'écran de la capture source (x et y depuis son coin supérieur gauche, w et h la largeur et la hauteur). Lorsque les quatre valeurs d'une zone sont comprises entre 0 et 1, elles sont lues comme des fractions de la largeur et de la hauteur de la capture d'écran. Une zone située entièrement hors de la capture d'écran ne masque rien et est listée dans ignored_boxes ; une zone qui dépasse un bord est coupée à ce bord et listée dans clipped_boxes. Lorsque toutes les zones sont hors de l'image et qu'aucun caviardage de texte ou de métadonnées ne modifie quoi que ce soit, la requête est refusée avec 400 REDACTION_NO_OPS avant toute facturation."
                  },
                  "text_redactions": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "match": {
                          "oneOf": [
                            {
                              "type": "string",
                              "description": "Texte littéral.",
                              "minLength": 1
                            },
                            {
                              "type": "object",
                              "properties": {
                                "pattern": {
                                  "type": "string",
                                  "description": "Expression régulière, de 1 à 500 caractères. Les motifs avec des quantificateurs imbriqués, comme (a+)+, sont refusés avec 400.",
                                  "minLength": 1,
                                  "maxLength": 500
                                },
                                "flags": {
                                  "type": "string",
                                  "description": "Parmi g, i, m, s, u, chacun au plus une fois. y (sticky) est refusé avec 400 : il ne trouverait de correspondance qu'à une seule position et laisserait les autres occurrences.",
                                  "pattern": "^[gimsu]*$"
                                }
                              },
                              "required": [
                                "pattern"
                              ]
                            }
                          ]
                        },
                        "case_insensitive": {
                          "type": "boolean",
                          "default": false,
                          "description": "Recherche une chaîne littérale sans tenir compte de la casse (pour une expression régulière, utilisez l'indicateur i)."
                        },
                        "reason": {
                          "type": "string",
                          "maxLength": 200,
                          "description": "Texte brut, imprimé sur le certificat de caviardage."
                        }
                      },
                      "required": [
                        "match"
                      ]
                    },
                    "maxItems": 200
                  },
                  "metadata_fields": {
                    "type": "array",
                    "description": "Chemins de champs dans metadata.json (un segment `*` correspond à chaque élément d'un tableau), ou les noms `ip_dns`, `cookie_names` et `authorization` (voir ci-dessus).",
                    "items": {
                      "oneOf": [
                        {
                          "type": "string",
                          "description": "Chemin du champ dans metadata.json."
                        },
                        {
                          "type": "object",
                          "properties": {
                            "field": {
                              "type": "string"
                            },
                            "reason": {
                              "type": "string"
                            }
                          },
                          "required": [
                            "field"
                          ]
                        }
                      ]
                    },
                    "maxItems": 100
                  },
                  "reason_summary": {
                    "type": "string",
                    "maxLength": 500,
                    "description": "Texte brut, imprimé sur le certificat de caviardage."
                  },
                  "custodian": {
                    "type": "object",
                    "properties": {
                      "name": {
                        "type": "string"
                      },
                      "title": {
                        "type": "string"
                      }
                    },
                    "description": "Imprimé sur le rapport de caviardage. name et title suivent la règle de texte brut (voir PlainText)."
                  },
                  "language": {
                    "type": "string",
                    "description": "Langue des PDF lisibles par un humain. L'une des valeurs suivantes : en, de, it, fr, es, pt_BR, cs, el, hi, hu, id, ja, ko, nl, pl, ro, ru, sv, tr, vi, zh_CN, zh_TW, ar, he, da, fi, nb, sk, uk. La casse, le trait d'union et la région sont tolérés (\"DE\", \"fr-FR\" et \"PT-BR\" sont acceptés) ; \"pt\" et \"zh\" seuls sont rejetés. Une valeur non prise en charge renvoie 400 MALFORMED_REQUEST. Facultatif : sans cette valeur (ou si elle est vide), le rapport est dans la langue de la capture source."
                  },
                  "include_eidas": {
                    "type": "boolean",
                    "description": "Comme dans CaptureOptions.include_eidas.",
                    "default": false
                  },
                  "webhook_url": {
                    "type": "string",
                    "description": "URL https qui reçoit redaction.completed. L'événement est envoyé juste après la réponse 201, pas avant. L'URL doit être en https et ne doit pas pointer vers une adresse privée, de bouclage, link-local, NAT de niveau opérateur (CGNAT) ou de métadonnées cloud (mêmes règles que pour une URL de capture ; 422 SSRF_BLOCKED sinon). Une valeur vide ou composée uniquement d'espaces signifie aucun webhook. Exige une clé API : envoyée depuis une session du tableau de bord, elle est refusée avec 400 WEBHOOK_REQUIRES_API_KEY, car les webhooks sont signés avec le secret de signature du webhook de la clé.",
                    "format": "uri"
                  },
                  "rule_commitment_salt": {
                    "type": "string",
                    "description": "Facultatif. Votre propre sel de 32 octets pour les engagements de règles (redaction_manifest.json rule_commitments), sous forme de 64 chiffres hexadécimaux ou en base64 / base64url ; un sel composé d'un seul octet répété est refusé (400). Envoyez-le pour détenir le sel avant l'appel : sans lui, le serveur en génère un et ne le renvoie que dans la réponse 201, de sorte qu'une réponse perdue à cause d'un dépassement de délai (504 de la passerelle alors que le caviardage s'est tout de même terminé) perd le sel, et un rejeu avec Idempotency-Key ne le contient pas. ProofSnap ne le conserve pas."
                  }
                },
                "description": "Au moins l'un des champs boxes, text_redactions ou metadata_fields."
              },
              "example": {
                "text_redactions": [
                  {
                    "match": "jane.doe@example.com",
                    "reason": "personal data"
                  }
                ],
                "reason_summary": "Personal data of a third party"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Paquet caviardé prêt. Un rejeu idempotent d'un caviardage antérieur avec la même Idempotency-Key renvoie aussi 201, avec `Idempotent-Replay: true` : le corps d'origine, avec le statut actuel et un package_url fraîchement présigné (omis lorsque le paquet ne peut pas être présigné ; récupérez-le avec GET /captures/{capture_id}).",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/XRequestId"
              },
              "Idempotent-Replay": {
                "$ref": "#/components/headers/IdempotentReplay"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "redaction_id": {
                      "type": "string",
                      "example": "red_a1B2c3D4e5F6g7H8"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "completed"
                      ]
                    },
                    "redacted_from": {
                      "type": "string"
                    },
                    "redaction_count": {
                      "type": "integer",
                      "description": "Nombre d'actions de caviardage appliquées."
                    },
                    "original_signature_verified": {
                      "type": "boolean",
                      "description": "Indique si la signature du manifeste d'origine s'est révélée valide lors de la vérification effectuée avant le caviardage."
                    },
                    "leakage_warnings": {
                      "type": "integer",
                      "description": "Toujours 0 : un caviardage dont le contrôle post-caviardage trouve un terme subsistant est refusé avec 422 REDACTION_LEAK_DETECTED au lieu d'être émis. Conservé pour compatibilité."
                    },
                    "package_url": {
                      "type": "string",
                      "format": "uri"
                    },
                    "package_url_expires_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "verify_url": {
                      "type": "string",
                      "description": "Lien vers le vérificateur public ProofSnap.",
                      "format": "uri"
                    },
                    "ignored_boxes": {
                      "type": "array",
                      "items": {
                        "type": "integer",
                        "minimum": 0
                      },
                      "description": "Index (à partir de zéro) dans les boxes de la requête des zones situées entièrement hors de la capture d'écran, qui n'ont rien masqué. Également enregistré dans redaction_manifest.json (screenshot_boxes)."
                    },
                    "clipped_boxes": {
                      "type": "array",
                      "items": {
                        "type": "integer",
                        "minimum": 0
                      },
                      "description": "Index (à partir de zéro) dans les boxes de la requête des zones qui dépassaient le bord de la capture d'écran et y ont été coupées. Également enregistré dans redaction_manifest.json (screenshot_boxes)."
                    },
                    "screenshot_method": {
                      "type": "string",
                      "nullable": true,
                      "enum": [
                        "jpeg_dct_blocks",
                        "decode_repaint_jpeg",
                        null
                      ],
                      "description": "Méthode de noircissement des zones : jpeg_dct_blocks (les blocs compressés du JPEG situés sous chaque zone remplacés par des blocs noirs, tous les autres blocs inchangés) ou decode_repaint_jpeg (la capture d'écran décodée, noircie et réencodée). null lorsqu'aucune zone n'a rien noirci. Également consigné dans redaction_manifest.json (screenshot_boxes.method)."
                    },
                    "painted_rects": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "x": {
                            "type": "integer",
                            "minimum": 0
                          },
                          "y": {
                            "type": "integer",
                            "minimum": 0
                          },
                          "w": {
                            "type": "integer",
                            "minimum": 0
                          },
                          "h": {
                            "type": "integer",
                            "minimum": 0
                          }
                        },
                        "required": [
                          "x",
                          "y",
                          "w",
                          "h"
                        ]
                      },
                      "description": "Les zones effectivement noircies, en pixels de la capture d'écran, une par zone appliquée, dans l'ordre des boxes de la requête sans celles qui ont été ignorées. Avec jpeg_dct_blocks, chacune est la zone arrondie vers l'extérieur à des blocs entiers, de sorte qu'elle contient la zone ; avec decode_repaint_jpeg, il s'agit de la zone limitée à la capture d'écran et arrondie vers l'extérieur à des blocs entiers de 8 x 8 du nouveau fichier, qui sont noircis après le réencodage afin que la compression ne puisse pas faire déborder des couleurs voisines dans la zone. Également enregistré dans redaction_manifest.json (screenshot_boxes.painted_rects)."
                    },
                    "text_rule_rects": {
                      "type": "integer",
                      "minimum": 0,
                      "description": "Zones distinctes de la capture d'écran noircies pour les seuls caviardages de texte, hors zones contenant une zone de caviardage (une zone correspondant à deux règles compte une fois) : dans les blocs de texte du text_layout.json de la source dont le texte correspond à une règle, les lignes estimées qui contiennent la correspondance, ou le bloc entier lorsque l'estimation est incertaine. Listées dans redaction_manifest.json (screenshot_boxes.from_text_rules)."
                    },
                    "text_occurrences_not_located": {
                      "type": "integer",
                      "minimum": 0,
                      "description": "Occurrences des caviardages de texte dans le texte de la page qui n'ont pas pu être situées sur la capture d'écran et n'y ont pas été noircies (texte que le text_layout.json de la source ne liste pas, comme du texte à l'intérieur d'une image ; bloc trop grand pour être noirci pour un seul terme ; zones au-delà de la limite d'un caviardage ; capture d'écran qui n'a pas pu être noircie). Les occurrences situées dans un bloc entièrement en dehors de la capture d'écran (rien à noircir) ne sont pas comptées ici ; redaction_manifest.json les liste comme occurrences_not_on_screenshot. Jamais plus que les occurrences du texte de la page. Vérifiez la capture d'écran et ajoutez des zones. redaction_manifest.json (leakage_check.screenshot_text_advisory) contient les décomptes par règle."
                    },
                    "text_variants_remaining": {
                      "type": "object",
                      "description": "À titre indicatif, jamais un échec : par règle de texte littéral, les occurrences restant dans le domtextcontent.txt et dans le texte du page.html de ce paquet que la règle n'a pas trouvées mais qui correspondent lorsqu'on ignore la casse et les diacritiques (par exemple \"boskovické\" pour la règle sensible à la casse \"Boskovic\"), et l'indication de la présence du terme dans l'URL de capture ou l'URL finale, qui ne sont jamais caviardées. `total` additionne, par règle, le plus grand des deux décomptes (les deux fichiers contiennent le même texte de page). Uniquement des décomptes, jamais le texte. Les règles regex ne sont pas comptées. Figure aussi dans redaction_manifest.json (leakage_check.text_variants_advisory). Présent lorsque la requête comporte des caviardages de texte.",
                      "properties": {
                        "total": {
                          "type": "integer",
                          "minimum": 0
                        },
                        "per_rule": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "rule_index": {
                                "type": "integer",
                                "minimum": 0
                              },
                              "in_domtextcontent": {
                                "type": "integer",
                                "minimum": 0
                              },
                              "in_page_html_text": {
                                "type": "integer",
                                "minimum": 0
                              },
                              "in_capture_url": {
                                "type": "boolean"
                              }
                            }
                          }
                        }
                      }
                    },
                    "rules_in_capture_url": {
                      "type": "array",
                      "items": {
                        "type": "integer",
                        "minimum": 0
                      },
                      "description": "Présent lorsque la requête contient des caviardages de texte. Index (à partir de zéro) des règles de texte dont le texte figure aussi dans l'URL de capture ou l'URL finale. Ces URL ne sont jamais modifiées : le texte a été supprimé partout ailleurs et y reste visible. Vide lorsqu'aucune règle ne leur correspond. Figure aussi dans redaction_manifest.json (leakage_check.capture_url, `remains_in_capture_url: true` par règle) et dans le rapport de caviardage."
                    },
                    "rule_commitment_salt": {
                      "type": "string",
                      "pattern": "^[0-9a-f]{64}$",
                      "description": "Présent lorsque la requête contient des caviardages de texte. Le sel de 32 octets (hex ; le rule_commitment_salt de la requête lorsqu'il a été envoyé) des rule_commitments de redaction_manifest.json : pour chaque règle de texte, le SHA-256 du sel de la règle suivi des octets UTF-8 du JSON canonique de la règle {\"match\": <text, or {\"pattern\", \"flags\"}>, \"case_insensitive\": <bool>}, où le sel de la règle est un HMAC-SHA256 ayant ce sel pour clé, calculé sur le texte UTF-8 \"rule:<rule_index>\" (ainsi, une règle peut être démontrée en divulguant son propre sel, sans les autres). Les règles ne sont pas stockées, et le sel n'est renvoyé que dans cette réponse : ni dans un rejeu Idempotency-Key, ni dans un webhook, ni dans le journal d'événements, ni dans GET /captures/{capture_id}, et ProofSnap ne le conserve pas. Conservez-le avec vos dossiers : il vous permet de démontrer quelles règles le caviardage a appliquées."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "MALFORMED_REQUEST (details.reason ; par exemple un motif qui n'est pas en texte brut ou l'indicateur regex y), REDACTION_NO_OPS (rien de fourni, ou aucune correspondance) ou INVALID_URL (webhook_url pas en https). WEBHOOK_REQUIRES_API_KEY : webhook_url envoyé depuis une session du tableau de bord (les webhooks nécessitent une clé API).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "QUOTA_EXCEEDED ou PAYMENT_REQUIRED.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            }
          },
          "403": {
            "description": "FORBIDDEN : cette capture ne vous appartient pas. EIDAS_REQUIRES_LIVE_KEY : include_eidas avec une clé de test, ou pour un caviardage d'une capture d'essai (alors avec details.reason). EMAIL_NOT_VERIFIED : une session du tableau de bord dont l'adresse e-mail n'est pas confirmée. PLAN_ARCHIVE_ONLY : le forfait conserve uniquement l'archive et ne crée aucun nouveau travail. PLAN_API_READ_ONLY : une clé API d'un compte Website Archive (sa clé est en lecture seule ; utilisez le tableau de bord). PLAN_API_READ_ONLY avec details.reason \"session_outside_dashboard\" : une session du tableau de bord d'un compte Website Archive dont la requête n'a pas d'en-tête Origin, ou un en-tête qui n'est pas une adresse du tableau de bord ProofSnap (sur ce forfait, les nouveaux travaux se créent uniquement dans le tableau de bord ; il s'agit d'un garde-fou souple, pas d'une frontière de sécurité).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            }
          },
          "404": {
            "description": "CAPTURE_NOT_FOUND.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            }
          },
          "409": {
            "description": "REDACTION_NOT_READY : la capture source n'est pas terminée. IDEMPOTENCY_IN_PROGRESS : une requête avec la même Idempotency-Key est encore en cours (details.capture_id est l'identifiant du caviardage à interroger avec GET /captures/{capture_id}). COMPANY_READ_ONLY : la source appartient à un cabinet conservé comme archive du cabinet.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            }
          },
          "422": {
            "description": "REDACTION_SCREENSHOT_UNREADABLE : les données d'image de la capture d'écran sont illisibles, ou la capture d'écran dépasse la limite du caviardage par zones (150 mégapixels pour un JPEG tel que la capture l'écrit, 60 mégapixels pour un PNG ou un autre JPEG) ; les caviardages de texte et de métadonnées fonctionnent toujours sans zones. REDACTION_LEAK_DETECTED : un terme caviardé subsiste en dehors de l'URL de capture (details { files, rule_indexes, metadata_fields, in_capture_url } ; in_capture_url vaut true lorsque le texte de la règle refusée figure aussi dans l'URL de capture ou l'URL finale, où il n'est jamais compté). REDACTION_SOURCE_UNSUPPORTED : la source n'est pas une capture de page web (details { source } ou { manifest_type }). SSRF_BLOCKED : webhook_url cible une adresse bloquée. REDACTION_SOURCE_INTEGRITY_FAILED : un fichier du paquet source ne correspond pas au SHA-256 indiqué dans son manifeste signé, ou un fichier listé est manquant (details { mismatched, missing }, noms de fichiers uniquement) ; rien n'a été caviardé ni facturé. IDEMPOTENCY_KEY_REUSED : l'Idempotency-Key a déjà été utilisée avec un corps de requête différent (ou depuis un autre environnement de clé).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            }
          },
          "429": {
            "description": "RATE_LIMITED : trop de requêtes dans la fenêtre de 60 secondes en cours. `error.details.retry_after_seconds` reprend Retry-After. RateLimit indique r=0 et le nombre de secondes avant la réinitialisation de la fenêtre. RATE_LIMITED également, pour une session du tableau de bord Website Archive : son plafond quotidien de 100 unités par jour UTC, partagé entre les captures uniques et par lot (une unité par URL), compare, redact et certify-hash (une unité par requête) ; une requête qui ne tient pas dans le plafond est refusée en entier (details.daily_limit, details.remaining, details.action \"wa_session_captures\" ; Retry-After et details.retry_after_seconds donnent le nombre de secondes jusqu'au jour UTC suivant, et les en-têtes par minute RateLimit et X-RateLimit-* sont omis). Un refus court avec seulement details.retry_after_seconds et Retry-After: 1 signale une mise à jour concurrente du compteur : réessayez. Un refus qui n'est pas imputable à l'appelant restitue les unités ; un travail accepté avec 202 qui échoue ensuite conserve son unité. Il s'agit d'un garde-fou souple, pas d'une frontière de sécurité.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "RATE_LIMITED",
                    "message": "Too many requests. See Retry-After header.",
                    "details": {
                      "retry_after_seconds": 42
                    }
                  },
                  "request_id": "..."
                }
              }
            }
          },
          "500": {
            "description": "INTERNAL_ERROR : erreur serveur inattendue ; réessayez avec un délai exponentiel. CAPTURE_TIME_BUDGET_EXCEEDED : le caviardage synchrone a épuisé son budget de temps et a été arrêté avant l'émission de tout paquet ; rien n'a été facturé. Avec `details.reason` `source_read`, la lecture du paquet source a pris l'essentiel du budget : le paquet est trop volumineux pour être caviardé pour le moment, réessayez plus tard.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/EidasOrAuthUnavailable"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ]
      }
    },
    "/compare": {
      "post": {
        "tags": [
          "Comparer et caviarder"
        ],
        "operationId": "compareCaptures",
        "summary": "Comparer deux captures",
        "description": "Compare deux de vos captures terminées (texte de la page, images affichées sur la page, métadonnées) et produit un paquet de comparaison signé. Renvoie 202 ; interrogez GET /captures/{compare_id}, qui porte alors type, compared et diff_summary. Les images sont comparées à partir de l'inventaire d'images scellé par chaque capture source (image_inventory.json : l'adresse de chaque image affichée et l'empreinte SHA-256 de ses octets), jamais pixel par pixel ; les résultats figurent dans image_changes.json et dans diff_summary.image_changes. Une source capturée avant que les captures ne consignent un inventaire d'images est tout de même comparée, sans la comparaison des images (diff_summary.image_changes.status not_run). Décompté comme une capture. À conditions égales (2026-10-14) : deux captures rendues avec un profil d'appareil différent ou des paramètres de capture différents sont refusées avec 422 COMPARE_PROFILE_MISMATCH (details.differing, details.before, details.after : uniquement les champs divergents, le CSS et le JavaScript injectés sous forme de leur empreinte SHA-256) sauf si allow_profile_mismatch vaut true, et deux captures de pages différentes avec 422 COMPARE_URL_MISMATCH (details.before et details.after : url et final_url sous forme d'origine et de chemin) sauf si allow_url_mismatch vaut true. Le contrôle porte d'abord sur les deux lignes de capture (avant toute lecture ou réservation), puis sur les metadata.json et forensic_log.json scellés des deux paquets dans le cadre du contrôle des sources décrit ci-dessous ; une divergence détectée uniquement lors de l'exécution de la comparaison fait échouer la comparaison avec ce code et restitue l'unité mensuelle. Le texte est comparé ligne par ligne avec un diff de lignes déterministe (sans délai d'expiration) : diff_summary compte des lignes (text_unit line). Seules les captures web (réalisées via l'API ou par une planification) peuvent être comparées : une certification d'empreintes, une comparaison ou un paquet caviardé est refusé avec 422 COMPARE_SOURCE_UNSUPPORTED (`details.capture_id`, `details.source`) avant toute lecture de paquet et toute réservation. Avant toute comparaison ou réservation, l'empreinte SHA-256 de chaque fichier listé avec une empreinte SHA-256 dans le manifest.json de chaque paquet source est recalculée ; une source comportant un fichier altéré ou manquant, ou un manifeste qui ne liste aucun fichier avec une empreinte SHA-256, est refusée avec 422 COMPARE_SOURCE_INTEGRITY_FAILED (pas de comparaison, pas de ligne, pas d'unité mensuelle). Seuls ces fichiers listés sont comparés : un fichier d'un paquet source que son manifeste ne liste pas avec une empreinte SHA-256 (par exemple un text_layout.json, image_inventory.json, page.html ou domtextcontent.txt ajouté) est traité comme absent. Le contrôle s'exécute sur la requête lorsque les deux paquets sources font 50 Mo ou moins et qu'il se termine en 10 secondes environ ; sinon, la requête est acceptée et l'exécution de la comparaison répète le contrôle, et une source en échec fait alors échouer la comparaison (GET /captures/{compare_id} affiche le statut failed avec error.code COMPARE_SOURCE_INTEGRITY_FAILED) et restitue l'unité mensuelle. Une comparaison terminée consigne ce contrôle dans compare.json `sources[].source_files_reverified` et dans comparison_method.json `source_verification`. /compare n'envoie pas de webhooks : un webhook_url dans le corps est ignoré. Interrogez GET /captures/{compare_id}.\n\nUne comparaison qui implique une capture d'essai (réalisée avec une clé de test, dans le Sandbox ou par un compte sans compte de facturation associé) est un paquet de test, quelle que soit la clé qui la demande : filigranée, `test_mode: true` dans manifest.json, `test_mode: true` pour cette source dans compare.json `sources[]`, et une ligne de test (key_env test) non décomptée et ne consommant aucune unité mensuelle. include_eidas avec une source de test est refusé avec 403 EIDAS_REQUIRES_LIVE_KEY (details.reason).\n\nLimite de débit par compte sur des fenêtres de 60 secondes (toutes les clés API du compte et sa session du tableau de bord partagent une même fenêtre). Une fois le limiteur passé, les réponses de cette route incluent les en-têtes `RateLimit-Policy` et `RateLimit` (fondés sur l'Internet-Draft de l'IETF draft-ietf-httpapi-ratelimit-headers) ainsi que les anciens en-têtes `X-RateLimit-Limit`, `X-RateLimit-Remaining` et `X-RateLimit-Reset`. Une réponse 429 ajoute `Retry-After`.\n\nEnvoyez une Idempotency-Key afin qu'une nouvelle tentative (par exemple après un 504) renvoie la première réponse au lieu d'une seconde exécution facturée (24 heures, mêmes règles que POST /captures). La clé est réservée avant le contrôle des sources, de sorte qu'une nouvelle tentative alors que la première requête est encore en cours renvoie 409 IDEMPOTENCY_IN_PROGRESS ; une requête refusée par le contrôle des sources ou par le quota libère sa clé, de sorte qu'une nouvelle tentative avec la même clé est à nouveau contrôlée.\n\nUn paquet de preuves importé (source import, identifiants préfixés `imp_`) ne peut pas être comparé : 422 IMPORT_NOT_SUPPORTED (details.capture_id).",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "capture_id_1": {
                    "type": "string",
                    "description": "La capture antérieure."
                  },
                  "capture_id_2": {
                    "type": "string",
                    "description": "La capture postérieure. Doit être différente de capture_id_1."
                  },
                  "language": {
                    "type": "string",
                    "description": "Langue du PDF de comparaison. Code exact uniquement ; toute autre valeur se rabat silencieusement sur l'anglais.",
                    "enum": [
                      "en",
                      "de",
                      "it",
                      "fr",
                      "es",
                      "pt_BR",
                      "cs",
                      "el",
                      "hi",
                      "hu",
                      "id",
                      "ja",
                      "ko",
                      "nl",
                      "pl",
                      "ro",
                      "ru",
                      "sv",
                      "tr",
                      "vi",
                      "zh_CN",
                      "zh_TW",
                      "ar",
                      "he",
                      "da",
                      "fi",
                      "nb",
                      "sk",
                      "uk"
                    ]
                  },
                  "include_eidas": {
                    "type": "boolean",
                    "description": "Comme dans CaptureOptions.include_eidas.",
                    "default": false
                  },
                  "include_rule_902": {
                    "type": "boolean",
                    "description": "Ajoute un modèle de certification non signé (rule_902_certification.pdf, voir CaptureOptions.include_rule_902) pour la comparaison. Non disponible pour des captures rendues différemment : avec allow_profile_mismatch à true et une divergence de profil ou de paramètres, 422 RULE902_NOT_AVAILABLE_FOR_MISMATCH. Actuellement désactivé : le modèle de certification est temporairement désactivé ; ce champ est donc accepté et ignoré (aucun rule_902_certification.pdf n'est généré ni listé dans le paquet) et la réponse porte l'avertissement RULE902_DISABLED.",
                    "default": false
                  },
                  "allow_profile_mismatch": {
                    "type": "boolean",
                    "default": false,
                    "description": "Compare deux captures bien qu'elles aient été réalisées avec un profil d'appareil différent (options.viewport mobile ou desktop, viewport_width, viewport_height, device_scale_factor ; niveau scellé : taille de la zone d'affichage, facteur d'échelle de l'appareil, émulation mobile, user agent sans numéros de version) ou avec des paramètres de capture différents qui modifient le rendu d'une page (dark_mode, block_ads, block_resources, inject_css, inject_js par SHA-256, expand_collapsed, dismiss_consent, dismiss_popups, page entière). Sans cette option, une telle paire est refusée avec 422 COMPARE_PROFILE_MISMATCH. Avec elle, diff_summary.text_verdict et images_verdict valent undetermined, changed vaut null sauf si un champ de métadonnées de contenu diffère, et include_rule_902 est refusé."
                  },
                  "allow_url_mismatch": {
                    "type": "boolean",
                    "default": false,
                    "description": "Compare deux captures de pages différentes. Deux captures sont considérées comme portant sur la même page lorsqu'une adresse de l'une (l'url demandée, l'url et la final_url consignées par son metadata.json) est égale à l'une de celles de l'autre après normalisation (hôte en minuscules, http et https équivalents, port par défaut, fragment, barre oblique finale et paramètres de requête volatils ignorés). Sans cette option, des pages différentes sont refusées avec 422 COMPARE_URL_MISMATCH."
                  }
                },
                "required": [
                  "capture_id_1",
                  "capture_id_2"
                ]
              },
              "example": {
                "capture_id_1": "cap_a1B2c3D4e5F6g7H8",
                "capture_id_2": "cap_h8G7f6E5d4C3b2A1"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepté.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/XRequestId"
              },
              "X-Quota-Limit": {
                "$ref": "#/components/headers/QuotaLimit"
              },
              "X-Quota-Remaining": {
                "$ref": "#/components/headers/QuotaRemaining"
              },
              "X-Quota-Used": {
                "$ref": "#/components/headers/QuotaUsed"
              },
              "X-Quota-Cap": {
                "$ref": "#/components/headers/QuotaCap"
              },
              "X-Quota-Org-Limit": {
                "$ref": "#/components/headers/QuotaOrgLimit"
              },
              "X-Quota-Org-Used": {
                "$ref": "#/components/headers/QuotaOrgUsed"
              },
              "X-Sandbox-Limit": {
                "$ref": "#/components/headers/SandboxLimit"
              },
              "X-Sandbox-Remaining": {
                "$ref": "#/components/headers/SandboxRemaining"
              },
              "X-Sandbox-Used": {
                "$ref": "#/components/headers/SandboxUsed"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "compare_id": {
                      "type": "string",
                      "example": "cmp_a1B2c3D4e5F6g7H8"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "processing"
                      ]
                    },
                    "poll_path": {
                      "type": "string"
                    },
                    "message": {
                      "type": "string"
                    },
                    "warnings": {
                      "$ref": "#/components/schemas/Rule902DisabledWarnings"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "MALFORMED_REQUEST : JSON invalide, identifiant manquant, même identifiant en double, ou allow_profile_mismatch / allow_url_mismatch non booléen.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "QUOTA_EXCEEDED ou PAYMENT_REQUIRED.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            }
          },
          "403": {
            "description": "FORBIDDEN : une capture ne vous appartient pas. EIDAS_REQUIRES_LIVE_KEY : include_eidas avec une clé de test, ou avec une capture source d'essai (alors avec details.reason). EMAIL_NOT_VERIFIED : une session du tableau de bord dont l'adresse e-mail n'est pas confirmée. PLAN_ARCHIVE_ONLY : le forfait conserve uniquement l'archive et ne crée aucun nouveau travail. PLAN_API_READ_ONLY : une clé API d'un compte Website Archive (sa clé est en lecture seule ; utilisez le tableau de bord). PLAN_API_READ_ONLY avec details.reason \"session_outside_dashboard\" : une session du tableau de bord d'un compte Website Archive dont la requête n'a pas d'en-tête Origin, ou un en-tête qui n'est pas une adresse du tableau de bord ProofSnap (sur ce forfait, les nouveaux travaux se créent uniquement dans le tableau de bord ; il s'agit d'un garde-fou souple, pas d'une frontière de sécurité).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            }
          },
          "404": {
            "description": "CAPTURE_NOT_FOUND.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            }
          },
          "409": {
            "description": "COMPARE_NOT_READY : les deux captures doivent être terminées. COMPANY_READ_ONLY : une source appartient à un cabinet conservé comme archive du cabinet (rien de nouveau n'est créé à partir de celle-ci).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            }
          },
          "422": {
            "description": "COMPARE_SOURCE_INTEGRITY_FAILED : un fichier d'une capture source ne correspond pas à l'empreinte SHA-256 indiquée par son manifeste signé, ou il est manquant ; rien n'a été comparé ni facturé. `details.capture_id`, `details.mismatched`, `details.missing` désignent la première source en échec ; `details.reason` est présent lorsque le manifeste source lui-même est inutilisable (par exemple \"the source manifest lists no file with a SHA-256\" ou \"the source manifest lists no files\") ; `details.sources[]` liste chaque source en échec avec les mêmes champs. Vérifiez la capture source (POST /captures/{capture_id}/verify). COMPARE_SOURCE_UNSUPPORTED : une source n'est pas une capture web (certification d'empreintes, comparaison ou paquet caviardé) ; rien n'a été lu, comparé ni facturé. `details.capture_id` désigne cette source et `details.source` son type lorsque la ligne en consigne un. Comparez plutôt les captures web d'origine. COMPARE_PROFILE_MISMATCH : les deux captures ont été réalisées avec un profil d'appareil différent ou des paramètres de capture différents ; `details.differing` liste les noms des champs (niveau ligne : viewport, viewport_width, viewport_height, device_scale_factor, dark_mode, block_ads, block_resources, inject_css, inject_js, expand_collapsed, dismiss_consent, dismiss_popups, full_page ; niveau scellé : viewport_width, viewport_height, device_scale_factor, is_mobile, user_agent et les mêmes paramètres), `details.before` et `details.after` leurs valeurs. Envoyez allow_profile_mismatch: true pour comparer malgré tout. COMPARE_URL_MISMATCH : les deux captures portent sur des pages différentes ; `details.before` et `details.after` donnent url et final_url sous forme d'origine et de chemin. Envoyez allow_url_mismatch: true pour comparer malgré tout. RULE902_NOT_AVAILABLE_FOR_MISMATCH : include_rule_902 avec allow_profile_mismatch pour des captures rendues différemment ; `details.differing`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            }
          },
          "429": {
            "description": "RATE_LIMITED : trop de requêtes dans la fenêtre de 60 secondes en cours. `error.details.retry_after_seconds` reprend Retry-After. RateLimit indique r=0 et le nombre de secondes avant la réinitialisation de la fenêtre. RATE_LIMITED également, pour une session du tableau de bord Website Archive : son plafond quotidien de 100 unités par jour UTC, partagé entre les captures uniques et par lot (une unité par URL), compare, redact et certify-hash (une unité par requête) ; une requête qui ne tient pas dans le plafond est refusée en entier (details.daily_limit, details.remaining, details.action \"wa_session_captures\" ; Retry-After et details.retry_after_seconds donnent le nombre de secondes jusqu'au jour UTC suivant, et les en-têtes par minute RateLimit et X-RateLimit-* sont omis). Un refus court avec seulement details.retry_after_seconds et Retry-After: 1 signale une mise à jour concurrente du compteur : réessayez. Un refus qui n'est pas imputable à l'appelant restitue les unités ; un travail accepté avec 202 qui échoue ensuite conserve son unité. Il s'agit d'un garde-fou souple, pas d'une frontière de sécurité.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "RATE_LIMITED",
                    "message": "Too many requests. See Retry-After header.",
                    "details": {
                      "retry_after_seconds": 42
                    }
                  },
                  "request_id": "..."
                }
              }
            }
          },
          "500": {
            "description": "INTERNAL_ERROR.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/EidasOrAuthUnavailable"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ]
      }
    },
    "/certify-hash": {
      "post": {
        "tags": [
          "Certification d'empreintes"
        ],
        "operationId": "certifyHash",
        "summary": "Certifier des empreintes de fichiers",
        "description": "Envoyez une empreinte SHA-256 (hash) ou jusqu'à 100 (hashes). Le paquet contient manifest.json (avec vos empreintes), manifest.sig, publickey.pem, evidence.pdf, les scripts de verification/ et, lorsqu'il est disponible, manifest.json.ots.\n\n- Une empreinte : synchrone, renvoie 200 avec le certificat terminé.\n- Plusieurs empreintes avec combined: true : un seul certificat listant toutes les empreintes, synchrone, renvoie 200.\n- Plusieurs empreintes sans combined : un certificat indépendant par empreinte, lancé en arrière-plan ; renvoie 202 avec les certify_ids. Interrogez GET /captures/{certify_id} ; avec webhook_url, chacun envoie certify.completed.\n\nChaque certificat compte comme une capture pour la comptabilisation, la limite mensuelle et la limite de débit (N empreintes sans combined comptent pour N ; un N supérieur à la limite par fenêtre renvoie 400 RATE_LIMIT_COST_TOO_HIGH). Un `label` est imprimé sur le certificat : il est tronqué à 200 caractères et doit être du texte brut (sans caractères de contrôle, invisibles ou de direction du texte, 400 sinon). Dans un lot, une erreur portant sur un élément le désigne dans `error.details.index`.\n\nLimite de débit par compte, par fenêtres de 60 secondes (toutes les clés API du compte et sa session du tableau de bord partagent une même fenêtre). Une fois le limiteur exécuté, les réponses de cette route incluent les en-têtes `RateLimit-Policy` et `RateLimit` (fondés sur l'Internet-Draft de l'IETF draft-ietf-httpapi-ratelimit-headers) ainsi que les en-têtes historiques `X-RateLimit-Limit`, `X-RateLimit-Remaining` et `X-RateLimit-Reset`. Une réponse 429 ajoute `Retry-After`.\n\nEnvoyez une Idempotency-Key afin qu'une nouvelle tentative (par exemple après un 504) renvoie la première réponse au lieu d'une seconde exécution facturée (24 heures, mêmes règles que POST /captures).",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "hash": {
                    "type": "string",
                    "description": "SHA-256 sous forme de 64 caractères hexadécimaux (insensible à la casse). Utilisez hash ou hashes."
                  },
                  "label": {
                    "type": "string",
                    "description": "Nom affiché pour le fichier. Les valeurs plus longues sont raccourcies à 200 caractères."
                  },
                  "hashes": {
                    "type": "array",
                    "items": {
                      "oneOf": [
                        {
                          "type": "string",
                          "description": "SHA-256 en hexadécimal."
                        },
                        {
                          "type": "object",
                          "properties": {
                            "hash": {
                              "type": "string"
                            },
                            "label": {
                              "type": "string"
                            }
                          },
                          "required": [
                            "hash"
                          ]
                        }
                      ]
                    },
                    "minItems": 1,
                    "maxItems": 100
                  },
                  "combined": {
                    "type": "boolean",
                    "description": "Certifier toutes les empreintes dans un seul certificat.",
                    "default": false
                  },
                  "algorithm": {
                    "type": "string",
                    "enum": [
                      "sha256"
                    ],
                    "default": "sha256"
                  },
                  "language": {
                    "type": "string",
                    "description": "Langue des PDF lisibles par un humain. L'une des valeurs suivantes : en, de, it, fr, es, pt_BR, cs, el, hi, hu, id, ja, ko, nl, pl, ro, ru, sv, tr, vi, zh_CN, zh_TW, ar, he, da, fi, nb, sk, uk. La casse, le trait d'union et la région sont tolérés (\"DE\", \"fr-FR\" et \"PT-BR\" sont acceptés) ; \"pt\" et \"zh\" seuls sont rejetés. Une valeur non prise en charge renvoie 400 MALFORMED_REQUEST."
                  },
                  "case_reference": {
                    "type": "string",
                    "description": "Imprimé sur le certificat. Suit la règle de texte brut (voir PlainText) ; 200 caractères au maximum.",
                    "maxLength": 200
                  },
                  "include_eidas": {
                    "type": "boolean",
                    "description": "Comme dans CaptureOptions.include_eidas.",
                    "default": false
                  },
                  "include_rule_902": {
                    "type": "boolean",
                    "description": "Ajoute un modèle de certification non signé (rule_902_certification.pdf, voir CaptureOptions.include_rule_902). Actuellement désactivé : le modèle de certification est temporairement désactivé ; ce champ est donc accepté et ignoré (aucun rule_902_certification.pdf n'est généré ni listé dans le paquet) et la réponse porte l'avertissement RULE902_DISABLED.",
                    "default": false
                  },
                  "webhook_url": {
                    "type": "string",
                    "description": "URL https qui reçoit certify.completed. L'URL doit être en https et ne doit pas pointer vers une adresse privée, de bouclage, link-local, NAT de niveau opérateur (CGNAT) ou de métadonnées cloud (mêmes règles que pour une URL de capture ; 422 SSRF_BLOCKED sinon). Une valeur vide ou composée uniquement d'espaces signifie aucun webhook. Exige une clé API : envoyée depuis une session du tableau de bord, elle est refusée avec 400 WEBHOOK_REQUIRES_API_KEY, car les webhooks sont signés avec le secret de signature du webhook de la clé.",
                    "format": "uri"
                  }
                }
              },
              "example": {
                "hash": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
                "label": "contract.pdf"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Certificat prêt (une empreinte, ou combiné).",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/XRequestId"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CertifyResult"
                }
              }
            }
          },
          "202": {
            "description": "Plusieurs certificats indépendants lancés. Un certificat refusé à son exécution par la limite mensuelle ou par le quota du sandbox échoue : sa ligne (GET /captures/{certify_id}) a le statut failed et error.code QUOTA_EXCEEDED.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/XRequestId"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "batch_id": {
                      "type": "string"
                    },
                    "count": {
                      "type": "integer"
                    },
                    "certify_ids": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Identifiants des certificats créés (y compris un certificat dont le démarrage a échoué)."
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "processing"
                      ]
                    },
                    "certificates": {
                      "type": "array",
                      "description": "Une entrée par empreinte soumise, dans l'ordre. status failed : ce certificat n'a pas pu être lancé (sa ligne, si elle a été créée, est marquée failed) ; les autres continuent de s'exécuter.",
                      "items": {
                        "type": "object",
                        "properties": {
                          "certify_id": {
                            "type": "string",
                            "nullable": true
                          },
                          "hash_sha256": {
                            "type": "string"
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "processing",
                              "failed"
                            ]
                          }
                        }
                      }
                    },
                    "failed_count": {
                      "type": "integer",
                      "description": "Nombre d'entrées de certificats dont le démarrage a échoué. Lorsque toutes ont échoué, la réponse est 500 CONFIGURATION_ERROR."
                    },
                    "warnings": {
                      "$ref": "#/components/schemas/Rule902DisabledWarnings"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "MALFORMED_REQUEST (details.reason désigne le champ, par exemple \"label must be a string\" ; details.index désigne l'élément d'un lot), INVALID_HASH (details.index dans un lot) ou INVALID_URL (webhook_url pas en https, ou contenant des identifiants de connexion). WEBHOOK_REQUIRES_API_KEY : webhook_url envoyé depuis une session du tableau de bord (les webhooks nécessitent une clé API). RATE_LIMIT_COST_TOO_HIGH : plus d'empreintes (sans combined) qu'une fenêtre de limite de débit n'en autorise ; details { max_per_window, window_seconds, requested }.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "PAYMENT_REQUIRED, ou QUOTA_EXCEEDED sur les chemins synchrones.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            }
          },
          "403": {
            "description": "EIDAS_REQUIRES_LIVE_KEY (details.reason \"no_billing_account\" lorsqu'une clé live ou une session s'exécute en mode test faute de compte de facturation associé). EMAIL_NOT_VERIFIED : une session du tableau de bord dont l'adresse e-mail n'est pas confirmée, ou une clé API d'un tel compte. PLAN_ARCHIVE_ONLY : le forfait conserve uniquement l'archive et ne crée aucun nouveau travail. PLAN_API_READ_ONLY : une clé API d'un compte Website Archive (sa clé est en lecture seule ; utilisez le tableau de bord). PLAN_API_READ_ONLY avec details.reason \"session_outside_dashboard\" : une session du tableau de bord d'un compte Website Archive dont la requête n'a pas d'en-tête Origin, ou un en-tête qui n'est pas une adresse du tableau de bord ProofSnap (sur ce forfait, les nouveaux travaux se créent uniquement dans le tableau de bord ; il s'agit d'un garde-fou souple, pas d'une frontière de sécurité).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            }
          },
          "422": {
            "description": "SSRF_BLOCKED : webhook_url cible une adresse privée, de bouclage, link-local ou bloquée pour une autre raison.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "RATE_LIMITED : trop de requêtes dans la fenêtre de 60 secondes en cours. `error.details.retry_after_seconds` reprend Retry-After. RateLimit indique r=0 et le nombre de secondes avant la réinitialisation de la fenêtre. RATE_LIMITED également, pour une session du tableau de bord Website Archive : son plafond quotidien de 100 unités par jour UTC, partagé entre les captures uniques et par lot (une unité par URL), compare, redact et certify-hash (une unité par requête) ; une requête qui ne tient pas dans le plafond est refusée en entier (details.daily_limit, details.remaining, details.action \"wa_session_captures\" ; Retry-After et details.retry_after_seconds donnent le nombre de secondes jusqu'au jour UTC suivant, et les en-têtes par minute RateLimit et X-RateLimit-* sont omis). Un refus court avec seulement details.retry_after_seconds et Retry-After: 1 signale une mise à jour concurrente du compteur : réessayez. Un refus qui n'est pas imputable à l'appelant restitue les unités ; un travail accepté avec 202 qui échoue ensuite conserve son unité. Il s'agit d'un garde-fou souple, pas d'une frontière de sécurité.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "RATE_LIMITED",
                    "message": "Too many requests. See Retry-After header.",
                    "details": {
                      "retry_after_seconds": 42
                    }
                  },
                  "request_id": "..."
                }
              }
            }
          },
          "500": {
            "description": "CONFIGURATION_ERROR : la certification a échoué (details.reason).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/AuthUnavailable"
          },
          "504": {
            "description": "UPSTREAM_TIMEOUT : la certification synchrone (une empreinte, ou combined) ne s'est pas terminée en 25 s environ. Elle peut encore aboutir : details.certify_id et details.poll_path désignent la ligne à interroger avec GET /captures/{capture_id}.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            }
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ]
      }
    },
    "/webhook-events/{capture_id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/CaptureId"
        }
      ],
      "get": {
        "tags": [
          "Webhooks"
        ],
        "operationId": "listWebhookEvents",
        "summary": "Journal de livraison des webhooks",
        "description": "Toutes les tentatives de livraison pour une capture, un certificat ou un caviardage, de la plus ancienne à la plus récente.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Journal des livraisons.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "capture_id": {
                      "type": "string"
                    },
                    "webhook_url": {
                      "type": "string",
                      "description": "L'URL de webhook de la ligne (options.webhook_url d'une capture, ou l'URL indiquée avec un certificat ou un caviardage ; null s'il n'y en a pas). Pour le propriétaire du cabinet pour lequel une capture a été réalisée, lorsqu'il lit le journal d'un membre du cabinet, uniquement le schéma, l'hôte, le port et le chemin (chaîne de requête et fragment supprimés, car ils peuvent contenir un jeton) ; cette lecture est conservée dans l'historique des accès de la capture (action webhook_events).",
                      "nullable": true
                    },
                    "attempts": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "attempt_number": {
                            "type": "integer"
                          },
                          "attempted_at": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "status_code": {
                            "type": "integer",
                            "nullable": true
                          },
                          "latency_ms": {
                            "type": "integer",
                            "nullable": true
                          },
                          "success": {
                            "type": "boolean"
                          },
                          "error": {
                            "type": "string",
                            "nullable": true,
                            "description": "null lorsque la livraison a abouti. Sinon, une classe générique, jamais l'erreur réseau brute : connection_failed, timeout, blocked_address (l'hôte s'est résolu vers une adresse privée ou bloquée pour une autre raison au moment de la livraison), redirect_not_followed (l'endpoint a répondu avec un 3xx ; les redirections ne sont jamais suivies), http_<status> (une réponse hors 2xx), ou une phrase expliquant pourquoi l'événement n'a pas été envoyé (pas de clé API, clé révoquée ou introuvable, pas de secret de webhook, échec de la lecture du secret)."
                          },
                          "delivery_id": {
                            "type": "string",
                            "description": "L'en-tête webhook-id de cette livraison, sans le préfixe `msg_`."
                          }
                        }
                      }
                    },
                    "attempt_count": {
                      "type": "integer"
                    },
                    "last_success": {
                      "type": "string",
                      "description": "attempted_at de la dernière tentative réussie.",
                      "nullable": true
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "FORBIDDEN : cette capture ne vous appartient pas.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "CAPTURE_NOT_FOUND.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/AuthUnavailable"
          }
        }
      }
    },
    "/scheduled-captures": {
      "post": {
        "tags": [
          "Captures automatiques"
        ],
        "operationId": "createSchedule",
        "summary": "Créer une planification",
        "description": "interval et cron : chaque exécution réalise une capture et, avec webhook_url, envoie capture.completed ou capture.failed.\n\non_change : chaque exécution récupère d'abord la page (HTTP simple, ou rendu dans un navigateur avec js_render) et calcule l'empreinte de son texte normalisé. Cette vérification n'est pas une capture et n'est pas facturée. La première exécution réalise une capture de référence. Lorsque l'empreinte change, une nouvelle capture est réalisée, comparée à la précédente, et monitor.change_detected est envoyé à webhook_url. Une page qui renvoie une erreur ou presque aucun texte est considérée comme sans modification.\n\nAprès 5 exécutions échouées consécutives, une planification se suspend d'elle-même. Lorsque la limite mensuelle est atteinte ou que l'abonnement est en retard de paiement, les exécutions sont ignorées et la planification reste active.\n\nLimites par forfait :\n\n| Forfait | Planifications actives | Cadence minimale | on_change |\n| --- | --- | --- | --- |\n| sandbox | 0 | non disponible | non |\n| developer | 3 | 300 s | oui |\n| startup | 25 | 300 s | oui |\n| growth | 200 | 300 s | oui |\n| custom | illimité | 300 s | oui |\n| legal_solo | 3 | 300 s | oui |\n| legal_seats | 3 | 300 s | oui |\n| website_archive | 30 | 86 400 s | oui |\n| archive_only | 0 | non disponible | non |\n\nIdempotency-Key : une nouvelle tentative avec la même clé et le même corps dans les 24 heures récupère la première réponse 201 (en-tête Idempotent-Replay: true) au lieu de créer une seconde planification ; la même clé avec un autre corps renvoie 422 IDEMPOTENCY_KEY_REUSED.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ScheduleRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Créé.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Schedule"
                }
              }
            }
          },
          "400": {
            "description": "MALFORMED_REQUEST, INVALID_URL, INVALID_CRON, INTERVAL_TOO_SHORT (details.min_interval_seconds) ou une erreur d'options de capture. WEBHOOK_REQUIRES_API_KEY : webhook_url envoyé depuis une session du tableau de bord (les webhooks nécessitent une clé API).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "SCHEDULE_LIMIT_REACHED (details.max_active, details.active) ou ON_CHANGE_REQUIRES_PLAN. EMAIL_NOT_VERIFIED : une session du tableau de bord dont l'adresse e-mail n'est pas confirmée. PLAN_ARCHIVE_ONLY : le forfait conserve uniquement l'archive et ne crée aucun nouveau travail. PLAN_API_READ_ONLY : clé API d'un compte Website Archive (sa clé est en lecture seule ; utilisez le tableau de bord). PLAN_API_READ_ONLY avec details.reason \"session_outside_dashboard\" : une session du tableau de bord d'un compte Website Archive dont la requête n'a pas d'en-tête Origin, ou un en-tête qui n'est pas une adresse du tableau de bord ProofSnap (sur ce forfait, le nouveau travail se crée uniquement dans le tableau de bord ; il s'agit d'un garde-fou souple, pas d'une frontière de sécurité).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "SSRF_BLOCKED : url ou webhook_url cible une adresse privée, de bouclage, link-local ou bloquée pour une autre raison.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/AuthUnavailable"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ]
      },
      "get": {
        "tags": [
          "Captures automatiques"
        ],
        "operationId": "listSchedules",
        "summary": "Lister les planifications",
        "description": "Jusqu'à 100 de vos planifications.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Planifications.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "schedules": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Schedule"
                      }
                    },
                    "total": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/AuthUnavailable"
          }
        }
      }
    },
    "/scheduled-captures/{schedule_id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ScheduleId"
        }
      ],
      "get": {
        "tags": [
          "Captures automatiques"
        ],
        "operationId": "getSchedule",
        "summary": "Obtenir une planification",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "La planification.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Schedule"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "SCHEDULE_NOT_FOUND.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/AuthUnavailable"
          }
        }
      },
      "patch": {
        "tags": [
          "Captures automatiques"
        ],
        "operationId": "updateSchedule",
        "summary": "Suspendre, reprendre ou modifier une planification",
        "description": "Envoyez status pour suspendre ou reprendre, et/ou n'importe quel champ de ScheduleRequest pour modifier. Un champ de premier niveau que vous envoyez remplace celui qui est stocké, y compris un null explicite (webhook_url, timezone et js_render s'effacent ainsi) ; un champ que vous omettez conserve sa valeur stockée ; le résultat est de nouveau validé dans son ensemble. `options` fait exception à la modification champ par champ : il est remplacé comme une table entière, sans fusion clé par clé, envoyez donc l'ensemble complet des options souhaitées (lisez-les d'abord avec GET). Une option que vous omettez de la table est supprimée. La reprise vérifie la limite de planifications actives, remet à zéro le compteur d'échecs et calcule la prochaine exécution à partir de maintenant.\n\nDepuis une session du tableau de bord, une webhook_url ne peut pas être définie (400 WEBHOOK_REQUIRES_API_KEY) ; envoyer null ou une chaîne vide l'efface. Une planification créée depuis une session du tableau de bord n'a jamais eu de clé API pour signer ; la modifier supprime donc aussi toute webhook_url qui y était stockée auparavant, et une clé API ne peut pas non plus lui en ajouter une (400 WEBHOOK_REQUIRES_API_KEY) : le planificateur signe les webhooks d'une planification avec le secret de signature du webhook de la clé qui a servi à la créer. Pour recevoir des webhooks, créez la planification avec une clé API.\n\nModifier url, mode ou js_render d'une planification on_change (ou faire passer une planification vers ou depuis on_change) efface sa référence (l'empreinte de sondage stockée et la dernière capture), de sorte que l'exécution suivante réalise une nouvelle capture de référence au lieu de comparer la nouvelle cible à l'ancienne. Le corps de la requête doit être un objet JSON (400 MALFORMED_REQUEST sinon).",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "status": {
                    "type": "string",
                    "enum": [
                      "active",
                      "paused"
                    ]
                  },
                  "url": {
                    "type": "string",
                    "format": "uri"
                  },
                  "mode": {
                    "type": "string",
                    "enum": [
                      "interval",
                      "cron",
                      "on_change"
                    ]
                  },
                  "interval_seconds": {
                    "type": "integer"
                  },
                  "cron_expr": {
                    "type": "string"
                  },
                  "timezone": {
                    "type": "string",
                    "nullable": true,
                    "description": "Fuseau horaire IANA pour cron. null ou une chaîne vide rétablit UTC."
                  },
                  "webhook_url": {
                    "type": "string",
                    "description": "Envoyez null ou une chaîne vide pour la supprimer. L'URL doit être en https et ne doit pas pointer vers une adresse privée, de bouclage, link-local, NAT de niveau opérateur (CGNAT) ou de métadonnées cloud (mêmes règles que pour une URL de capture ; 422 SSRF_BLOCKED sinon). Exige une clé API des deux côtés : envoyée depuis une session du tableau de bord, ou envoyée avec une clé API pour une planification créée dans le tableau de bord, elle est refusée avec 400 WEBHOOK_REQUIRES_API_KEY, car les webhooks sont signés avec le secret de signature du webhook de la clé qui a servi à créer la planification.",
                    "nullable": true
                  },
                  "js_render": {
                    "type": "boolean",
                    "nullable": true,
                    "description": "null signifie false."
                  },
                  "options": {
                    "allOf": [
                      {
                        "$ref": "#/components/schemas/CaptureOptions"
                      }
                    ],
                    "description": "Remplace les options stockées dans leur ensemble (sans fusion clé par clé). Envoyez la table complète ; une option omise est supprimée. null rétablit les options par défaut. options.webhook_url n'est jamais stocké (voir webhook_url)."
                  },
                  "max_capture_age_seconds": {
                    "type": "integer",
                    "nullable": true,
                    "minimum": 86400,
                    "maximum": 34560000,
                    "description": "on_change uniquement : une capture complète au moins toutes les N secondes, même sans modification (de 1 à 400 jours, plus long que interval_seconds). null la supprime. Passer le mode à interval ou cron la supprime."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "La planification mise à jour.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Schedule"
                }
              }
            }
          },
          "400": {
            "description": "MALFORMED_REQUEST (y compris rien à mettre à jour), INVALID_URL, INVALID_CRON ou INTERVAL_TOO_SHORT. WEBHOOK_REQUIRES_API_KEY : webhook_url envoyé depuis une session du tableau de bord, ou envoyé avec une clé API pour une planification créée dans le tableau de bord (elle n'a aucune clé dont le secret de signature du webhook pourrait signer les webhooks).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "SCHEDULE_LIMIT_REACHED ou ON_CHANGE_REQUIRES_PLAN. EMAIL_NOT_VERIFIED : une session du tableau de bord dont l'adresse e-mail n'est pas confirmée (reprise d'une planification suspendue ; la suspension et la modification restent possibles). PLAN_API_READ_ONLY : clé API d'un compte Website Archive, pour tout PATCH (suspension et modification comprises). PLAN_API_READ_ONLY avec details.reason \"session_outside_dashboard\" : une session du tableau de bord d'un compte Website Archive dont la requête n'a pas d'en-tête Origin, ou un en-tête qui n'est pas une adresse du tableau de bord ProofSnap (sur ce forfait, le nouveau travail se crée uniquement dans le tableau de bord ; il s'agit d'un garde-fou souple, pas d'une frontière de sécurité). PLAN_ARCHIVE_ONLY : reprise d'une planification sur le forfait Archive only.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "SCHEDULE_NOT_FOUND.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "SSRF_BLOCKED : url ou webhook_url cible une adresse privée, de bouclage, link-local ou bloquée pour une autre raison.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/AuthUnavailable"
          }
        }
      },
      "delete": {
        "tags": [
          "Captures automatiques"
        ],
        "operationId": "deleteSchedule",
        "summary": "Supprimer une planification",
        "description": "Arrête les exécutions futures. Les captures déjà réalisées sont conservées.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Supprimé.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "schedule_id": {
                      "type": "string"
                    },
                    "deleted": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "PLAN_API_READ_ONLY : clé API d'un compte Website Archive (sa clé est en lecture seule ; utilisez le tableau de bord).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "SCHEDULE_NOT_FOUND.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/AuthUnavailable"
          }
        }
      }
    },
    "/scheduled-captures/{schedule_id}/runs": {
      "parameters": [
        {
          "$ref": "#/components/parameters/ScheduleId"
        }
      ],
      "get": {
        "tags": [
          "Captures automatiques"
        ],
        "operationId": "listScheduleRuns",
        "summary": "Lister les exécutions d'une planification",
        "description": "Les 50 captures les plus récentes réalisées par cette planification (les vérifications on_change n'ayant détecté aucune modification ne sont pas listées).",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Exécutions.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "schedule_id": {
                      "type": "string"
                    },
                    "runs": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "capture_id": {
                            "type": "string"
                          },
                          "status": {
                            "type": "string"
                          },
                          "created_at": {
                            "type": "string"
                          },
                          "captured_at": {
                            "type": "string",
                            "nullable": true
                          },
                          "file_count": {
                            "type": "integer",
                            "nullable": true
                          }
                        }
                      }
                    },
                    "total": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "SCHEDULE_NOT_FOUND.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/AuthUnavailable"
          }
        }
      }
    },
    "/matters": {
      "get": {
        "tags": [
          "Affaires"
        ],
        "operationId": "listMatters",
        "summary": "Lister les affaires",
        "description": "Jusqu'à 200 affaires dont vous êtes membre. Les affaires d'un cabinet que vous avez quitté ne sont pas listées.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Affaires.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "matters": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Matter"
                      }
                    },
                    "total": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "MATTERS_NOT_ENABLED : le forfait n'est pas un forfait Legal.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/AuthUnavailable"
          }
        }
      },
      "post": {
        "tags": [
          "Affaires"
        ],
        "operationId": "createMatter",
        "summary": "Créer une affaire",
        "description": "Vous en devenez le propriétaire. Le journal de traçabilité commence par une entrée matter.created. Les champs de texte sont rognés et raccourcis à 200 caractères.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 200,
                    "description": "Texte brut : les espaces sont réduits et rognés, 200 caractères au maximum, sans caractères de contrôle, de direction du texte (bidi) ni invisibles (les caractères de liaison U+200C et U+200D, nécessaires à l'orthographe hindi et persane, sont autorisés) (400 MALFORMED_REQUEST sinon). Obligatoire. Affiché sur la page publique de vérification de l'affaire."
                  },
                  "client": {
                    "type": "string",
                    "maxLength": 200,
                    "description": "Texte brut : les espaces sont réduits et rognés, 200 caractères au maximum, sans caractères de contrôle, de direction du texte (bidi) ni invisibles (les caractères de liaison U+200C et U+200D, nécessaires à l'orthographe hindi et persane, sont autorisés) (400 MALFORMED_REQUEST sinon). Une valeur vide ou null signifie non défini (stocké comme null). Une valeur qui n'est pas une chaîne est refusée.",
                    "nullable": true
                  },
                  "case_number": {
                    "type": "string",
                    "maxLength": 200,
                    "description": "Texte brut : les espaces sont réduits et rognés, 200 caractères au maximum, sans caractères de contrôle, de direction du texte (bidi) ni invisibles (les caractères de liaison U+200C et U+200D, nécessaires à l'orthographe hindi et persane, sont autorisés) (400 MALFORMED_REQUEST sinon). Une valeur vide ou null signifie non défini (stocké comme null). Une valeur qui n'est pas une chaîne est refusée. Affiché sur la page publique de vérification de l'affaire.",
                    "nullable": true
                  },
                  "opposing_party": {
                    "type": "string",
                    "maxLength": 200,
                    "description": "Texte brut : les espaces sont réduits et rognés, 200 caractères au maximum, sans caractères de contrôle, de direction du texte (bidi) ni invisibles (les caractères de liaison U+200C et U+200D, nécessaires à l'orthographe hindi et persane, sont autorisés) (400 MALFORMED_REQUEST sinon). Une valeur vide ou null signifie non défini (stocké comme null). Une valeur qui n'est pas une chaîne est refusée.",
                    "nullable": true
                  },
                  "jurisdiction": {
                    "type": "string",
                    "description": "Texte libre. Texte brut : les espaces sont réduits et rognés, 200 caractères au maximum, sans caractères de contrôle, de direction du texte (bidi) ni invisibles (les liants U+200C et U+200D, nécessaires à l'orthographe du hindi et du persan, sont autorisés) (sinon 400 MALFORMED_REQUEST). Une valeur vide ou null signifie non défini (stocké comme null). Une valeur qui n'est pas une chaîne est refusée. Affiché sur la page publique de vérification de l'affaire.",
                    "maxLength": 200,
                    "nullable": true
                  }
                },
                "required": [
                  "name"
                ]
              },
              "example": {
                "name": "Smith v. Jones",
                "case_number": "2026-CV-0142"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Créé.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Matter"
                }
              }
            }
          },
          "400": {
            "description": "MISSING_FIELD : name est absent ou vide. MALFORMED_REQUEST : JSON invalide. MALFORMED_REQUEST : un champ texte qui n'est pas une chaîne, dépasse 200 caractères ou contient des caractères de contrôle ou de direction du texte.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "PAYMENT_REQUIRED : appelant live dont l'abonnement (ou celui du cabinet) est en retard de paiement ou impayé."
          },
          "403": {
            "description": "MATTERS_NOT_ENABLED : le forfait n'est pas un forfait Legal. EMAIL_NOT_VERIFIED : une session du tableau de bord dont l'adresse e-mail n'est pas confirmée, ou une clé API d'un tel compte.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/AuthUnavailable"
          }
        }
      }
    },
    "/matters/{matter_id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/MatterId"
        }
      ],
      "get": {
        "tags": [
          "Affaires"
        ],
        "operationId": "getMatter",
        "summary": "Obtenir une affaire",
        "description": "L'affaire avec ses captures (jusqu'à 500), son journal de traçabilité et jusqu'à 100 événements d'accès. `members` donne l'e-mail de chaque entrée de `member_uids`, et `actor_emails` associe à leur e-mail les uid cités dans le journal de traçabilité et dans audit_stream de cette affaire (acteurs, et cible de `member.added` / `member.removed`, donc y compris les membres retirés) ; un e-mail vaut null, ou l'uid est absent de la table, lorsqu'il ne peut pas être lu. Seuls les appelants qui peuvent ouvrir l'affaire reçoivent ces champs ; `/matter-verify` ne contient jamais d'e-mails. Un appelant autorisé à révoquer le lien de partage public (le propriétaire de l'affaire, le propriétaire d'un cabinet dont l'abonnement a pris fin ou qui est archivé) reçoit aussi `share` : `{ share_url, created_at, expires_at }` du lien actif (`expires_at` vaut toujours null : les liens fonctionnent jusqu'à leur révocation), ou null s'il n'y en a pas ; les autres membres ne reçoivent jamais ce champ. Les entrées de audit_stream ne portent source_ip et user_agent que pour le propriétaire de l'affaire ; les membres reçoivent les entrées sans ces deux champs.\n\nUne affaire d'un cabinet qui a pris fin porte `read_only: true`.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "L'affaire.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Matter"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "members": {
                          "type": "array",
                          "description": "Une entrée par entrée de `member_uids`, dans le même ordre.",
                          "items": {
                            "type": "object",
                            "properties": {
                              "uid": {
                                "type": "string"
                              },
                              "email": {
                                "type": "string",
                                "format": "email",
                                "nullable": true,
                                "description": "Null lorsque le compte ne peut pas être lu."
                              }
                            }
                          }
                        },
                        "actor_emails": {
                          "type": "object",
                          "description": "`{ uid: email }` pour les personnes citées dans custody_log et audit_stream de cette affaire (acteur system exclu). Un uid dont l'e-mail ne peut pas être lu est absent.",
                          "additionalProperties": {
                            "type": "string",
                            "format": "email"
                          }
                        },
                        "captures": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/MatterCapture"
                          }
                        },
                        "custody_log": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/CustodyEntry"
                          }
                        },
                        "audit_stream": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/AuditEntry"
                          }
                        },
                        "share": {
                          "type": "object",
                          "nullable": true,
                          "description": "Uniquement pour un appelant autorisé à révoquer le lien. Null lorsqu'aucun lien actif n'existe.",
                          "properties": {
                            "share_url": {
                              "type": "string",
                              "format": "uri"
                            },
                            "created_at": {
                              "type": "string",
                              "format": "date-time",
                              "nullable": true
                            },
                            "expires_at": {
                              "type": "string",
                              "format": "date-time",
                              "nullable": true,
                              "description": "Toujours null : les liens de partage n'expirent pas."
                            }
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "MATTERS_NOT_ENABLED : le forfait n'est pas un forfait Legal.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "MATTER_NOT_FOUND.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/AuthUnavailable"
          }
        }
      },
      "patch": {
        "tags": [
          "Affaires"
        ],
        "operationId": "updateMatter",
        "summary": "Mettre à jour une affaire",
        "description": "Propriétaire uniquement. Les champs texte sont rognés et raccourcis à 200 caractères. name est obligatoire : lorsqu'il est envoyé, il doit s'agir d'une chaîne non vide, et un nom vide ou composé uniquement d'espaces, null ou une valeur qui n'est pas une chaîne est refusé avec 400 et rien n'est modifié. Membres : add_member_email ajoute une personne qui possède déjà un compte ProofSnap et appartient au cabinet de l'affaire ; l'absence de compte, une adresse non confirmée et un compte extérieur au cabinet reçoivent une seule et même réponse 404 NOT_FOUND ; remove_member_uid retire un membre (pas le propriétaire). Lorsque les deux sont envoyés, seul add_member_email est appliqué. Une affaire compte 50 membres au maximum (une affaire complète refuse avant la recherche de l'adresse). Les changements de membres sont consignés dans le journal de traçabilité. Un changement de name, client, case_number, opposing_party, jurisdiction ou status est consigné dans le journal de traçabilité comme matter.updated ; son target est une chaîne JSON contenant fields (les noms des champs modifiés) et, lorsque le statut a changé, le nouveau statut. Les valeurs en texte libre ne sont pas écrites dans le journal de traçabilité haché et public : les anciennes et nouvelles valeurs vont dans l'historique des accès (audit_stream, entrée matter.updated). Sur une affaire archivée, un renommage ou add_member_email est refusé avec 409 MATTER_ARCHIVED, sauf si la même requête la rouvre (status \"open\") ; remove_member_uid et la réouverture fonctionnent toujours. Chaque invitation est décomptée d'une limite quotidienne de 50 par propriétaire invitant (invitations au cabinet depuis le tableau de bord et add_member_email des affaires confondus, refus compris, puisque chacune recherche une adresse) ; à la limite, la requête renvoie 429 RATE_LIMITED. Un échec côté serveur n'est pas compté.\n\nConservation pour litige : `{ \"legal_hold\": true|false }` seul définit la conservation pour litige de l'affaire (propriétaire de l'affaire uniquement) et l'applique à ses captures dans la même requête, avec les effets d'une conservation pour litige de capture (conservation pour litige S3 Object Lock, entrées de traçabilité capture.legal_hold_placed / capture.legal_hold_released, chaîne des événements de capture). La réponse est l'affaire plus `changed` (l'indicateur a changé), `captures_held` ou `captures_released`, `skipped` ([{ capture_id, code }], par exemple une capture dans la corbeille) et `truncated` (plus de 1000 captures : renvoyez la même valeur pour continuer). Les captures rattachées ultérieurement à l'affaire pendant que la conservation est active sont placées en conservation pour litige lors de leur rattachement. Une unité de limite de débit par requête plus une pour 10 captures modifiées.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "description": "Doit contenir au moins un caractère autre qu'un espace. Omettez-le pour conserver le nom actuel. Même règle du texte brut : espaces réduits, 200 caractères au maximum, sans caractères de contrôle, de direction du texte ni invisibles (U+200C et U+200D sont autorisés).",
                    "maxLength": 200
                  },
                  "client": {
                    "type": "string",
                    "nullable": true,
                    "description": "Texte brut : les espaces sont réduits et rognés, 200 caractères au maximum, sans caractères de contrôle, de direction du texte (bidi) ni invisibles (les caractères de liaison U+200C et U+200D, nécessaires à l'orthographe hindi et persane, sont autorisés) (400 MALFORMED_REQUEST sinon). null ou une chaîne composée uniquement d'espaces efface le champ (stocké comme null). Une valeur qui n'est pas une chaîne est refusée avec 400.",
                    "maxLength": 200
                  },
                  "case_number": {
                    "type": "string",
                    "nullable": true,
                    "description": "Texte brut : les espaces sont réduits et rognés, 200 caractères au maximum, sans caractères de contrôle, de direction du texte (bidi) ni invisibles (les caractères de liaison U+200C et U+200D, nécessaires à l'orthographe hindi et persane, sont autorisés) (400 MALFORMED_REQUEST sinon). null ou une chaîne composée uniquement d'espaces efface le champ (stocké comme null). Une valeur qui n'est pas une chaîne est refusée avec 400.",
                    "maxLength": 200
                  },
                  "opposing_party": {
                    "type": "string",
                    "nullable": true,
                    "description": "Texte brut : les espaces sont réduits et rognés, 200 caractères au maximum, sans caractères de contrôle, de direction du texte (bidi) ni invisibles (les caractères de liaison U+200C et U+200D, nécessaires à l'orthographe hindi et persane, sont autorisés) (400 MALFORMED_REQUEST sinon). null ou une chaîne composée uniquement d'espaces efface le champ (stocké comme null). Une valeur qui n'est pas une chaîne est refusée avec 400.",
                    "maxLength": 200
                  },
                  "jurisdiction": {
                    "type": "string",
                    "nullable": true,
                    "description": "Texte brut : les espaces sont réduits et rognés, 200 caractères au maximum, sans caractères de contrôle, de direction du texte (bidi) ni invisibles (les caractères de liaison U+200C et U+200D, nécessaires à l'orthographe hindi et persane, sont autorisés) (400 MALFORMED_REQUEST sinon). null ou une chaîne composée uniquement d'espaces efface le champ (stocké comme null). Une valeur qui n'est pas une chaîne est refusée avec 400.",
                    "maxLength": 200
                  },
                  "status": {
                    "type": "string",
                    "description": "Utilisez POST /matters/{matter_id}/seal pour sceller.",
                    "enum": [
                      "open",
                      "archived"
                    ]
                  },
                  "add_member_email": {
                    "type": "string",
                    "format": "email"
                  },
                  "remove_member_uid": {
                    "type": "string"
                  },
                  "legal_hold": {
                    "type": "boolean",
                    "description": "Conservation pour litige de l'affaire, envoyée seule (combinée avec un autre champ : 400). true place en conservation pour litige chaque capture actuelle de l'affaire et chaque capture qui lui est rattachée tant que la conservation est active (legal_hold_source \"matter:<id>\" ; une capture déjà en conservation garde sa propre conservation). false lève uniquement les conservations placées par l'affaire ; une conservation manuelle ou groupée subsiste. Propriétaire de l'affaire uniquement. Non bloqué par un abonnement en retard de paiement. Le placement nécessite un forfait payant (403 LEGAL_HOLD_NOT_AVAILABLE, details.reason plan) et les captures en mode test sont ignorées (code LEGAL_HOLD_NOT_AVAILABLE dans `skipped`) ; false seul est accepté même après la fin du forfait Legal."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "L'affaire mise à jour.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Matter"
                }
              }
            }
          },
          "400": {
            "description": "MALFORMED_REQUEST : name vide, composé uniquement d'espaces ou qui n'est pas une chaîne ; champ texte qui n'est pas une chaîne, dépasse 200 caractères ou contient des caractères de contrôle ou de direction du texte ; status invalide ; rien à mettre à jour ; déjà membre ; ou tentative de retrait du propriétaire.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "PAYMENT_REQUIRED : appelant live dont l'abonnement (ou celui du cabinet) est en retard de paiement ou impayé."
          },
          "403": {
            "description": "MATTERS_NOT_ENABLED, FORBIDDEN (pas le propriétaire), LEGAL_HOLD_NOT_AVAILABLE (legal_hold à true sans forfait payant), ou EMAIL_NOT_VERIFIED : une session du tableau de bord dont l'adresse e-mail n'est pas confirmée, ou une clé API d'un tel compte.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "MATTER_NOT_FOUND, ou NOT_FOUND (aucun compte éligible pour cette adresse e-mail : inexistant, non confirmé ou extérieur au cabinet, avec une seule réponse pour tous les cas ; ou remove_member_uid n'est pas membre).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "MATTER_SEALED, ou MATTER_ARCHIVED (renommage ou nouveau membre sur une affaire archivée). COMPANY_READ_ONLY : l'affaire appartient à un cabinet dont l'abonnement a pris fin ou qui est conservé comme archive du cabinet (appelant sur un forfait Legal ; l'ouverture, l'export et la révocation des partages restent possibles).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "RATE_LIMITED : la limite quotidienne de 50 invitations de membres par propriétaire invitant a été atteinte (les invitations au cabinet depuis le tableau de bord et add_member_email d'une affaire sont comptées ensemble ; details.daily_limit, details.action \"member_invite\"). Retry-After indique quand le jour UTC se termine.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/AuthUnavailable"
          }
        }
      }
    },
    "/matters/{matter_id}/captures": {
      "parameters": [
        {
          "$ref": "#/components/parameters/MatterId"
        }
      ],
      "post": {
        "tags": [
          "Affaires"
        ],
        "operationId": "assignMatterCaptures",
        "summary": "Ajouter ou retirer des captures",
        "description": "Tout membre peut appeler cette route, mais seules les captures créées par l'appelant sont déplacées ; les autres sont renvoyées dans skipped, de même que les captures déjà rattachées à une autre affaire et celles déjà présentes dans cette affaire (ou, avec unassign, absentes de celle-ci). Les entrées qui ne sont pas des identifiants de capture sont ignorées et 100 au maximum sont traitées. Chaque modification est consignée dans le journal de traçabilité. Une affaire archivée n'accepte plus de nouvelles captures (409 MATTER_ARCHIVED) ; unassign fonctionne toujours.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "capture_ids": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "minItems": 1,
                    "maxItems": 100
                  },
                  "unassign": {
                    "type": "boolean",
                    "description": "true retire les captures de l'affaire.",
                    "default": false
                  }
                },
                "required": [
                  "capture_ids"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Résultat.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "matter_id": {
                      "type": "string"
                    },
                    "assigned": {
                      "type": "integer",
                      "description": "Nombre de captures modifiées."
                    },
                    "skipped": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "unassign": {
                      "type": "boolean"
                    },
                    "legal_holds_placed": {
                      "type": "integer",
                      "description": "Présent lorsque la conservation pour litige de l'affaire est active : nombre de captures rattachées sur lesquelles elle a placé une conservation pour litige."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "MALFORMED_REQUEST : aucun identifiant de capture valide.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "PAYMENT_REQUIRED : appelant live dont l'abonnement (ou celui du cabinet) est en retard de paiement ou impayé."
          },
          "403": {
            "description": "MATTERS_NOT_ENABLED, ou EMAIL_NOT_VERIFIED : une session du tableau de bord dont l'adresse e-mail n'est pas confirmée, ou une clé API d'un tel compte.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "MATTER_NOT_FOUND.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "MATTER_SEALED, ou MATTER_ARCHIVED (ajout à une affaire archivée). COMPANY_READ_ONLY : l'affaire appartient à un cabinet dont l'abonnement a pris fin ou qui est conservé comme archive du cabinet (appelant sur un forfait Legal ; l'ouverture, l'export et la révocation des partages restent possibles).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/AuthUnavailable"
          }
        }
      }
    },
    "/matters/{matter_id}/seal": {
      "parameters": [
        {
          "$ref": "#/components/parameters/MatterId"
        }
      ],
      "post": {
        "tags": [
          "Affaires"
        ],
        "operationId": "sealMatter",
        "summary": "Sceller une affaire",
        "description": "Propriétaire uniquement. Ancre l'empreinte de la dernière entrée actuelle du journal de traçabilité avec OpenTimestamps et, lorsque eIDAS est configuré sur le déploiement et que l'appelant est live (une clé API live, ou une session du tableau de bord avec facturation associée), avec un horodatage eIDAS (l'horodatage eIDAS du sceau n'est pas facturé ; en cas d'échec, le scellement se poursuit sans lui). Un appelant de test (clé API de test ou session Sandbox) est scellé avec OpenTimestamps uniquement et le sceau consigne eidas_status not_obtained avec eidas_reason test_key ; la requête n'accepte aucune option eIDAS, il ne s'agit donc pas d'une erreur. L'affaire passe ensuite en lecture seule : aucune modification, aucun changement de captures, et ses captures ne peuvent pas être supprimées. Une affaire qui contient encore une capture dans la corbeille n'est pas scellée (409 MATTER_HAS_TRASHED_CAPTURES) : restaurez la capture ou retirez-la d'abord de l'affaire.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Scellée.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "matter_id": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "sealed"
                      ]
                    },
                    "seal": {
                      "$ref": "#/components/schemas/MatterSeal"
                    },
                    "custody_head_hash": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "MALFORMED_REQUEST : le journal de traçabilité de l'affaire n'a encore aucune entrée.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "PAYMENT_REQUIRED : appelant live dont l'abonnement (ou celui du cabinet) est en retard de paiement ou impayé."
          },
          "403": {
            "description": "MATTERS_NOT_ENABLED : le forfait n'est pas un forfait Legal. FORBIDDEN : pas le propriétaire. EMAIL_NOT_VERIFIED : une session du tableau de bord dont l'adresse e-mail n'est pas confirmée, ou une clé API d'un tel compte.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "MATTER_NOT_FOUND.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "MATTER_SEALED (déjà scellée), MATTER_CHANGED (le journal de traçabilité a changé pendant le scellement ; réessayez) ou MATTER_HAS_TRASHED_CAPTURES (une capture de l'affaire est dans la corbeille ; restaurez-la ou retirez-la d'abord de l'affaire). COMPANY_READ_ONLY : l'affaire appartient à un cabinet dont l'abonnement a pris fin ou qui est conservé comme archive du cabinet (appelant sur un forfait Legal ; l'ouverture, l'export et la révocation des partages restent possibles).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "TIMESTAMP_SERVICE_UNAVAILABLE (l'ancrage a échoué, rien n'a changé ; réessayez) ou CONFIGURATION_ERROR.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "EIDAS_SEAL_RETRY : l'horodatage eIDAS du sceau n'est pas arrivé à temps (ou est encore en cours d'émission). Rien n'a été scellé. Réessayez après Retry-After secondes : la nouvelle tentative reçoit l'horodatage déjà émis pour cette dernière entrée du journal de traçabilité, sans second horodatage. SERVICE_UNAVAILABLE : la passerelle n'a pas pu vérifier les informations d'authentification ; réessayez avec un délai progressif.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "X-Request-ID": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "code": "EIDAS_SEAL_RETRY",
                    "message": "The eIDAS timestamp of the seal did not arrive in time. Nothing was sealed. Retry after the Retry-After interval; the retry reuses the timestamp already issued for this custody head.",
                    "details": {
                      "reason": "The eIDAS timestamp did not arrive in time. Nothing was sealed. Retry the seal.",
                      "retry_after_seconds": 15
                    }
                  },
                  "request_id": "..."
                }
              }
            }
          }
        }
      }
    },
    "/matters/{matter_id}/export": {
      "parameters": [
        {
          "$ref": "#/components/parameters/MatterId"
        }
      ],
      "post": {
        "tags": [
          "Affaires"
        ],
        "operationId": "exportMatter",
        "summary": "Exporter une affaire",
        "description": "Propriétaire uniquement. Construit un seul ZIP contenant custody_report.pdf, custody_log.json, matter.json, README.txt, access_log.json (lorsqu'il existe des événements d'accès) et le paquet de preuves de chaque capture. Avec la numérotation Bates activée (par défaut), il ajoute des copies numérotées Bates de chaque evidence.pdf sous exhibits/ (pièces numérotées à partir de la preuve la plus ancienne) et, avec formats.loadfile, un fichier de chargement Concordance/Opticon. Avec formats.pdfa, les copies de dépôt sont également converties en PDF/A lorsque le convertisseur est disponible (pour un export synchrone, uniquement dans un budget de temps) ; seules les copies comptées dans pdfa_count sont au format PDF/A, les autres sont des PDF qui ne revendiquent pas le format PDF/A, et README.txt ne mentionne PDF/A que lorsque pdfa_count est supérieur à 0.\n\nUne affaire comptant jusqu'à 20 captures est exportée de manière synchrone (200). Une affaire plus volumineuse est exportée en arrière-plan : la réponse est 202 avec un job_id ; interrogez GET /matters/{matter_id}/export?job_id=. Les captures qui dépassent la limite de taille (120 Mo en synchrone ; 800 Mo et 300 captures en arrière-plan) sont listées dans les rapports mais non incluses.\n\nFonctionne sur les affaires ouvertes et scellées. Une affaire archivée est refusée avec 409 MATTER_ARCHIVED (rouvrez-la d'abord), sauf pour un appelant qui ne peut pas la rouvrir : le propriétaire d'un cabinet dont l'abonnement a pris fin, pendant sa durée de conservation, et un compte Archive only. Chaque export est consigné comme export.generated dans l'historique des accès. Chaque export est décompté d'une limite quotidienne de 50 par utilisateur ; un export en arrière-plan qui échoue restitue son unité.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "bates": {
                    "type": "object",
                    "properties": {
                      "enabled": {
                        "type": "boolean",
                        "default": true
                      },
                      "prefix": {
                        "type": "string",
                        "description": "Lettres et chiffres uniquement, convertis en majuscules, 12 au maximum. Par défaut : à partir du numéro de dossier ou du nom de l'affaire."
                      },
                      "start": {
                        "type": "integer",
                        "minimum": 1,
                        "default": 1
                      },
                      "exhibit_start": {
                        "type": "integer",
                        "minimum": 1,
                        "default": 1
                      },
                      "pad": {
                        "type": "integer",
                        "minimum": 4,
                        "maximum": 8,
                        "default": 6
                      }
                    }
                  },
                  "formats": {
                    "type": "object",
                    "properties": {
                      "loadfile": {
                        "type": "boolean",
                        "description": "Inclut le fichier de chargement e-discovery (nécessite la numérotation Bates).",
                        "default": true
                      },
                      "pdfa": {
                        "type": "boolean",
                        "description": "Convertit les copies de dépôt numérotées Bates en PDF/A (nécessite la numérotation Bates). Dans la mesure du possible : une copie non convertie reste un PDF sans mention PDF/A ; pdfa_count dans la réponse indique combien de copies ont été converties.",
                        "default": true
                      }
                    }
                  },
                  "export_locale": {
                    "type": "string",
                    "description": "Langue de custody_report.pdf, de l'index des pièces et de README.txt : en, de, it, fr, es, pt_BR, cs, el, hi, hu, id, ja, ko, nl, pl, ro, ru, sv, tr, vi, zh_CN, zh_TW, ar, he, da, fi, nb, sk, uk (un trait d'union est accepté à la place du trait de soulignement). Les autres valeurs se rabattent sur l'anglais."
                  },
                  "declaration": {
                    "type": "object",
                    "description": "Accepté pour compatibilité. Ses champs texte (name, title, party, case_caption, executed_location) suivent la règle du texte brut (voir PlainText) : une valeur contenant un caractère interdit est refusée avec 400 MALFORMED_REQUEST. Les valeurs trop longues sont raccourcies (name, title, executed_location 120, party 160, case_caption 200 caractères)."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Export prêt.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "matter_id": {
                      "type": "string"
                    },
                    "url": {
                      "type": "string",
                      "description": "Lien de téléchargement présigné, valide 24 heures.",
                      "format": "uri"
                    },
                    "expires_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "capture_count": {
                      "type": "integer"
                    },
                    "captures_included": {
                      "type": "integer"
                    },
                    "captures_skipped": {
                      "type": "integer",
                      "description": "Captures listées dans les rapports mais non incluses parce qu'une limite de taille a été atteinte."
                    },
                    "custody_events": {
                      "type": "integer"
                    },
                    "exhibits_count": {
                      "type": "integer",
                      "description": "Copies des pièces avec numérotation Bates."
                    },
                    "bates_prefix": {
                      "type": "string",
                      "nullable": true
                    },
                    "searchable": {
                      "type": "boolean",
                      "description": "Toujours false : aucune couche texte n'est ajoutée aux copies de dépôt. Conservé pour garder une forme de réponse stable."
                    },
                    "pdfa_count": {
                      "type": "integer",
                      "description": "Copies de dépôt sous exhibits/ qui sont au format PDF/A. 0 signifie qu'aucune copie n'est au format PDF/A ni ne prétend l'être."
                    },
                    "loadfile": {
                      "type": "boolean"
                    },
                    "size_bytes": {
                      "type": "integer"
                    },
                    "generated_at": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          },
          "202": {
            "description": "Export lancé en arrière-plan.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "matter_id": {
                      "type": "string"
                    },
                    "async": {
                      "type": "boolean"
                    },
                    "job_id": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "processing"
                      ]
                    },
                    "poll_path": {
                      "type": "string",
                      "description": "Inclut le préfixe /v1."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "MALFORMED_REQUEST : une option d'export est invalide (details.reason).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "PAYMENT_REQUIRED : appelant live dont l'abonnement (ou celui du cabinet) est en retard de paiement ou impayé."
          },
          "403": {
            "description": "MATTERS_NOT_ENABLED : le forfait n'est pas un forfait Legal. FORBIDDEN : pas le propriétaire. EMAIL_NOT_VERIFIED : une session du tableau de bord dont l'adresse e-mail n'est pas confirmée, ou une clé API d'un tel compte.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "MATTER_NOT_FOUND.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "MATTER_ARCHIVED : rouvrez d'abord l'affaire (PATCH status \"open\").",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "CONFIGURATION_ERROR : l'export n'a pas pu être construit ni lancé.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/AuthUnavailable"
          },
          "504": {
            "description": "UPSTREAM_TIMEOUT : l'exportation synchrone ne s'est pas terminée en 25 s environ (details.reason). Réessayez.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "Affaires"
        ],
        "operationId": "getMatterExport",
        "summary": "Interroger une exportation en arrière-plan",
        "description": "Propriétaire uniquement. Une tâche encore en cours 30 minutes après son démarrage est signalée comme failed avec l'erreur \"export timed out\". Une tâche ne se termine qu'une fois : une tâche signalée failed ne devient jamais ready par la suite, et une tâche ready n'est jamais signalée failed.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "job_id",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Statut de la tâche. Lorsqu'elle est prête, url et les décomptes sont renseignés.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "job_id": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "processing",
                        "ready",
                        "failed"
                      ]
                    },
                    "url": {
                      "type": "string",
                      "nullable": true
                    },
                    "captures_included": {
                      "type": "integer",
                      "nullable": true
                    },
                    "captures_skipped": {
                      "type": "integer",
                      "nullable": true
                    },
                    "custody_events": {
                      "type": "integer",
                      "nullable": true
                    },
                    "size_bytes": {
                      "type": "integer",
                      "nullable": true
                    },
                    "error": {
                      "type": "string",
                      "nullable": true
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "MALFORMED_REQUEST : job_id manquant.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "MATTERS_NOT_ENABLED : le forfait n'est pas un forfait Legal. FORBIDDEN : pas le propriétaire.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "MATTER_NOT_FOUND, ou NOT_FOUND pour une tâche inconnue.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/AuthUnavailable"
          }
        }
      }
    },
    "/matters/{matter_id}/share": {
      "parameters": [
        {
          "$ref": "#/components/parameters/MatterId"
        }
      ],
      "post": {
        "tags": [
          "Affaires"
        ],
        "operationId": "shareMatter",
        "summary": "Créer un lien de vérification public",
        "description": "Propriétaire uniquement. Renvoie un lien que toute personne peut ouvrir pour voir le nom de l'affaire, le numéro de dossier, la juridiction, le statut, le journal de traçabilité et le sceau (voir GET /matter-verify). Aucun contenu de capture n'est partagé. Un nouvel appel renvoie le même lien. Le lien fonctionne jusqu'à ce que vous le révoquiez.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Lien de partage.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "matter_id": {
                      "type": "string"
                    },
                    "share_url": {
                      "type": "string",
                      "format": "uri"
                    },
                    "token": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "PAYMENT_REQUIRED : appelant live dont l'abonnement (ou celui du cabinet) est en retard de paiement ou impayé."
          },
          "403": {
            "description": "MATTERS_NOT_ENABLED : le forfait n'est pas un forfait Legal. FORBIDDEN : pas le propriétaire. EMAIL_NOT_VERIFIED : une session du tableau de bord dont l'adresse e-mail n'est pas confirmée, ou une clé API d'un tel compte.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "MATTER_NOT_FOUND.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "MATTER_ARCHIVED : une affaire archivée ne peut pas être partagée. COMPANY_READ_ONLY : l'affaire appartient à un cabinet dont l'abonnement a pris fin ou qui est conservé comme archive du cabinet (appelant sur un forfait Legal ; l'ouverture, l'export et la révocation des partages restent possibles).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/AuthUnavailable"
          }
        }
      },
      "delete": {
        "tags": [
          "Affaires"
        ],
        "operationId": "revokeMatterShare",
        "summary": "Révoquer le lien public",
        "description": "Propriétaire uniquement. Le lien cesse de fonctionner. Un POST ultérieur en crée un nouveau. Le propriétaire d'un cabinet dont l'abonnement a pris fin (ou conservé comme archive du cabinet) et le propriétaire d'une affaire sur le forfait Archive only peuvent révoquer le lien sans forfait Legal (la création d'un lien reste une fonctionnalité des forfaits Legal).",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Révoqué.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "matter_id": {
                      "type": "string"
                    },
                    "revoked": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "PAYMENT_REQUIRED : appelant live dont l'abonnement (ou celui du cabinet) est en retard de paiement ou impayé."
          },
          "403": {
            "description": "MATTERS_NOT_ENABLED, FORBIDDEN (pas le propriétaire), ou EMAIL_NOT_VERIFIED : une session du tableau de bord dont l'adresse e-mail n'est pas confirmée, ou une clé API d'un tel compte.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "MATTER_NOT_FOUND.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/AuthUnavailable"
          }
        }
      }
    },
    "/health": {
      "get": {
        "tags": [
          "Public"
        ],
        "operationId": "health",
        "summary": "Vérifier l'état du service",
        "description": "Toujours 200 tant que l'API répond. Avec deep=1 (ou true), la réponse ajoute dependencies : la configuration ou non de chaque service en aval et, pour eIDAS, une sonde en direct du service d'horodatage. Seul eIDAS est sondé ; les autres ne signalent que leur configuration. status devient degraded lorsque eIDAS est configuré et que sa sonde échoue. Une sonde qui ne répond pas à temps est signalée dans dependencies (probe_timeout, ou eidas.error timeout), jamais comme un échec de /health. deep=1 est soumis à une limite de débit par IP client (10 par minute) : au-delà, la requête renvoie 429 RATE_LIMITED avec Retry-After.",
        "security": [],
        "parameters": [
          {
            "name": "deep",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "1",
                "true"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "État du service.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": [
                        "ok",
                        "degraded"
                      ]
                    },
                    "stage": {
                      "type": "string"
                    },
                    "timestamp": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "uptime_ms": {
                      "type": "integer",
                      "description": "Millisecondes écoulées depuis le démarrage de l'instance qui répond."
                    },
                    "version": {
                      "type": "string"
                    },
                    "dependencies": {
                      "type": "object",
                      "properties": {
                        "eidas": {
                          "type": "object",
                          "properties": {
                            "configured": {
                              "type": "boolean"
                            },
                            "healthy": {
                              "type": "boolean",
                              "description": "null lorsque non configuré.",
                              "nullable": true
                            },
                            "error": {
                              "type": "string",
                              "description": "Code fixe lorsque la sonde eIDAS n'a pas signalé un état sain : timeout (pas de réponse en 15 s), unavailable (le service d'horodatage a répondu qu'il n'était pas sain), error (la sonde n'a pas pu être exécutée). Les détails sont journalisés côté serveur, jamais renvoyés.",
                              "enum": [
                                "timeout",
                                "unavailable",
                                "error"
                              ]
                            }
                          }
                        },
                        "ots": {
                          "type": "object",
                          "properties": {
                            "configured": {
                              "type": "boolean"
                            }
                          }
                        },
                        "c2pa_sign": {
                          "type": "object",
                          "properties": {
                            "configured": {
                              "type": "boolean"
                            }
                          }
                        },
                        "ntp": {
                          "type": "object",
                          "properties": {
                            "configured": {
                              "type": "boolean"
                            }
                          }
                        },
                        "whois": {
                          "type": "object",
                          "properties": {
                            "configured": {
                              "type": "boolean"
                            }
                          }
                        },
                        "error": {
                          "type": "string",
                          "description": "Uniquement lorsque le contrôle des dépendances lui-même a échoué : probe_timeout lorsqu'il n'a pas répondu en 20 s, probe_failed sinon. La réponse reste 200."
                        }
                      },
                      "description": "Uniquement avec deep=1."
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/ledger/{hash}": {
      "get": {
        "tags": [
          "Public"
        ],
        "operationId": "ledgerLookup",
        "summary": "Rechercher une empreinte de manifeste",
        "description": "Vérifie si ProofSnap a consigné l'émission d'un paquet dont le manifest.json a cette empreinte SHA-256. Les captures, certificats, comparaisons et caviardages sont consignés lors de leur émission, avec l'empreinte du manifeste livré (après l'étape d'horodatage qualifié) ; les paquets émis avant l'existence du registre d'émission n'y figurent pas. issued: false signifie seulement qu'il n'existe aucun enregistrement.",
        "security": [],
        "parameters": [
          {
            "name": "hash",
            "in": "path",
            "required": true,
            "description": "SHA-256 de manifest.json, 64 caractères hexadécimaux, casse indifférente, éventuellement précédé de sha256:.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Résultat de la recherche.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "manifest_sha256": {
                      "type": "string"
                    },
                    "issued": {
                      "type": "boolean"
                    },
                    "issued_at": {
                      "type": "string",
                      "nullable": true,
                      "format": "date-time",
                      "description": "Absent lorsque issued vaut false."
                    },
                    "type": {
                      "type": "string",
                      "enum": [
                        "capture",
                        "certify",
                        "compare",
                        "redaction"
                      ],
                      "nullable": true,
                      "description": "Absent lorsque issued vaut false."
                    },
                    "eidas": {
                      "type": "boolean",
                      "nullable": true,
                      "description": "Indique si le paquet émis porte un horodatage qualifié eIDAS (manifest.json.tsr). Absent lorsque issued vaut false ; null pour les enregistrements écrits avant le 2026-10-01."
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "NOT_FOUND : l'empreinte ne fait pas 64 caractères hexadécimaux.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/matter-verify": {
      "get": {
        "tags": [
          "Public"
        ],
        "operationId": "matterVerify",
        "summary": "Vue publique d'une affaire partagée",
        "description": "Données en lecture seule derrière un lien de partage d'affaire : détails de l'affaire, journal de traçabilité (pour que la chaîne d'empreintes puisse être recalculée) et le sceau avec sa preuve OpenTimestamps et son jeton eIDAS. Aucun contenu de capture, aucun lien de téléchargement ni historique des accès. attested_by.organization est le nom, défini par le propriétaire, du cabinet auquel appartient l'affaire (affaire de cabinet), ou null ; une affaire personnelle ne nomme aucune organisation. custody_log[].actor_org est le cabinet auquel appartenait l'acteur lorsque cette entrée a été écrite (enregistré avec l'entrée, hors du payload haché), uniquement pour une affaire de cabinet ; les entrées écrites avant que cette information soit enregistrée affichent null. Limite de débit par adresse IP du client (30 requêtes par 60 secondes par défaut).",
        "security": [],
        "parameters": [
          {
            "name": "token",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_-]{16,64}$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Vue de l'affaire.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "matter": {
                      "type": "object",
                      "properties": {
                        "name": {
                          "type": "string"
                        },
                        "case_number": {
                          "type": "string",
                          "nullable": true
                        },
                        "jurisdiction": {
                          "type": "string",
                          "nullable": true
                        },
                        "status": {
                          "type": "string"
                        },
                        "sealed": {
                          "type": "boolean"
                        },
                        "attested_by": {
                          "type": "object",
                          "properties": {
                            "organization": {
                              "type": "string",
                              "nullable": true
                            }
                          }
                        },
                        "capture_count": {
                          "type": "integer"
                        },
                        "custody_count": {
                          "type": "integer"
                        },
                        "custody_head_hash": {
                          "type": "string",
                          "nullable": true
                        },
                        "seal": {
                          "type": "object",
                          "properties": {
                            "root_hash": {
                              "type": "string"
                            },
                            "ots_present": {
                              "type": "boolean"
                            },
                            "eidas_present": {
                              "type": "boolean"
                            },
                            "ots_hex": {
                              "type": "string",
                              "nullable": true
                            },
                            "ots_pending": {
                              "type": "boolean"
                            },
                            "tsr_base64": {
                              "type": "string",
                              "nullable": true
                            },
                            "eidas_serial": {
                              "type": "string",
                              "nullable": true
                            },
                            "eidas_issued_at": {
                              "type": "string",
                              "nullable": true
                            },
                            "eidas_ltv": {
                              "type": "boolean"
                            },
                            "sealed_at": {
                              "type": "string"
                            },
                            "eidas_status": {
                              "type": "string",
                              "enum": [
                                "confirmed",
                                "not_obtained",
                                "not_configured"
                              ],
                              "nullable": true,
                              "description": "null pour les sceaux créés avant le 2026-10-01 sans jeton eIDAS."
                            },
                            "eidas_reason": {
                              "type": "string",
                              "enum": [
                                "token_refused",
                                "unavailable",
                                "test_key",
                                "account_limit"
                              ],
                              "description": "Uniquement lorsque eidas_status vaut not_obtained. test_key : le sceau a été réalisé par un appelant de test, aucun horodatage eIDAS n'a donc été demandé. account_limit : le compte n'a encore aucune facture payée et a atteint sa limite eIDAS (20 par jour UTC, 50 par mois UTC), aucun horodatage eIDAS n'a donc été demandé."
                            },
                            "eidas_verification_status": {
                              "type": "string",
                              "nullable": true,
                              "description": "verified lorsque le jeton a passé la vérification ProofSnap lors du scellement."
                            },
                            "eidas_revocation_status": {
                              "type": "string",
                              "nullable": true,
                              "enum": [
                                "good",
                                "revoked_after_gen_time",
                                "unknown",
                                "not_checked",
                                null
                              ],
                              "description": "Statut du certificat de la TSA établi lors du scellement (non récupéré en direct)."
                            },
                            "eidas_validation": {
                              "type": "object",
                              "nullable": true,
                              "additionalProperties": true,
                              "description": "Le contenu de eidas_validation.json enregistré lors du scellement (chaîne de la TSA, racine épinglée, réponse OCSP, CRL, enregistrement de vérification) afin que le jeton de tsr_base64 puisse être vérifié hors ligne. null pour les sceaux plus anciens."
                            }
                          },
                          "nullable": true
                        },
                        "created_at": {
                          "type": "string"
                        }
                      }
                    },
                    "custody_log": {
                      "type": "array",
                      "items": {
                        "allOf": [
                          {
                            "$ref": "#/components/schemas/CustodyEntry"
                          },
                          {
                            "type": "object",
                            "properties": {
                              "actor_org": {
                                "type": "string",
                                "nullable": true
                              }
                            }
                          }
                        ]
                      }
                    },
                    "issuer": {
                      "type": "string",
                      "enum": [
                        "ProofSnap"
                      ]
                    },
                    "verified_at": {
                      "type": "string",
                      "nullable": true,
                      "description": "Toujours null."
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "NOT_FOUND : jeton invalide, inconnu ou révoqué.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/captures/export-jobs": {
      "post": {
        "tags": [
          "Captures"
        ],
        "operationId": "startCaptureExportJob",
        "summary": "Exporter toutes les captures en un seul téléchargement (tâche asynchrone)",
        "description": "Lance une tâche qui regroupe toutes vos captures en un seul téléchargement, découpé en parties d'environ 2 Go au maximum. Chaque partie est un simple ZIP : README.txt (en anglais) et README.<language>.txt (le même texte traduit, lorsque language n'est pas en ; tous deux en UTF-8 avec indicateur d'ordre des octets et fins de ligne CRLF ; la ligne 1 de chacun renvoie à l'autre), captures.csv (UTF-8 avec indicateur d'ordre des octets, CRLF ; chaque capture de l'exportation avec les colonnes member (l'e-mail de l'auteur dans une exportation de cabinet, vide dans votre propre exportation), capture_id, created_at (réception de la requête), captured_at (moment de la capture ; vide pour une capture échouée), url, status (completed, failed ou unavailable), export_part, file, ots_file (captures/<capture_id>.upgraded.ots lorsque cette partie contient la preuve, sinon vide), test_mode (true pour une capture réalisée avec une clé de test : avec filigrane, sans valeur de preuve) et note (raison pour laquelle un paquet n'est pas inclus, également pour chaque ligne unavailable) ; la même liste figure dans chaque partie, et en cas de divergence entre parties, la liste de la partie la plus élevée fait foi) et captures/<capture_id>.zip, le paquet de preuves scellé d'origine octet pour octet, plus captures/<capture_id>.upgraded.ots lorsqu'une preuve OpenTimestamps mise à niveau est stockée. Chaque paquet interne se vérifie séparément avec son verification/verify.sh ou verify.ps1. Un paquet de plus de 2 Go n'est pas inclus (listé unavailable avec une note ; téléchargez-le séparément avec GET /captures/{capture_id}). Les captures échouées sont listées uniquement dans captures.csv ; les captures placées dans la corbeille et effacées ne sont pas exportées. Les entrées de chaque partie sont datées de l'heure de l'exportation (UTC, également dans le champ d'horodatage étendu). Interrogez GET /captures/export-jobs/{job_id} jusqu'à ce que status vaille ready. **Réutilisation :** lorsque votre dernière exportation est prête, a le même périmètre (et org) et la même langue, qu'il reste au moins un jour dans sa fenêtre de téléchargement, que vos droits sont inchangés et qu'aucune capture n'a été ajoutée, terminée, placée dans la corbeille, restaurée ou effacée depuis sa préparation, la requête renvoie 200 avec cette tâche (reused: true) : rien n'est reconstruit et aucune unité quotidienne n'est consommée. Une exportation prête plus récente du même périmètre remplace la précédente (la tâche précédente passe à expired, expired_reason superseded). Qui peut la lancer : une clé live ou la session du tableau de bord, sur tous les forfaits, y compris sandbox, Archive only et les clés en lecture seule (Website Archive). Une clé de test reçoit 403 FORBIDDEN (l'exportation contient des captures live), et GET /captures/export-jobs/{job_id} renvoie 404 EXPORT_JOB_NOT_FOUND à une clé de test pour toute tâche. Votre propre exportation (scope own) contient vos captures personnelles et celles que vous avez réalisées pour votre cabinet actuel ou pour un cabinet dont vous êtes propriétaire, y compris via une clé tant que ce cabinet est une archive du cabinet en cours (les captures des autres membres d'une archive ne sont accessibles que via la session du tableau de bord). scope company (propriétaire du cabinet uniquement, y compris pour un cabinet ayant pris fin pendant sa fenêtre de téléchargement ; une archive du cabinet en cours uniquement via la session du tableau de bord) exporte les captures de chaque membre du cabinet réalisées pour le cabinet ainsi que les vôtres, avec l'e-mail de l'auteur dans la colonne member. Une seule tâche en cours par compte (409 EXPORT_JOB_RUNNING la désigne), 5 tâches lancées par compte et par jour UTC (une tâche échouée restitue son unité, une tâche réutilisée n'en consomme aucune), et la limite de débit par minute de votre forfait. Au plus les 20 000 captures les plus récentes par tâche (truncated).",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "scope": {
                    "type": "string",
                    "enum": [
                      "own",
                      "company"
                    ],
                    "default": "own"
                  },
                  "org": {
                    "type": "string",
                    "description": "Avec scope company : l'un des cabinets dont vous êtes propriétaire (actuel ou dernier), lorsque vous possédez les deux."
                  },
                  "language": {
                    "type": "string",
                    "default": "en",
                    "description": "Langue de la traduction du README : README.<language>.txt (la traduction complète) à côté du README.txt anglais dans chaque partie ; en ne fournit que README.txt. L'une des valeurs suivantes : en, de, it, fr, es, pt_BR, cs, el, hi, hu, id, ja, ko, nl, pl, ro, ru, sv, tr, vi, zh_CN, zh_TW, ar, he, da, fi, nb, sk, uk. La casse, le trait d'union et la région sont tolérés (\"PT-BR\" correspond à pt_BR) ; \"pt\" et \"zh\" seuls, ainsi que toute autre valeur, renvoient 400 MALFORMED_REQUEST. Un export prêt n'est réutilisé que pour la même langue. Le tableau de bord envoie la langue de son interface."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Votre exportation prête est toujours à jour et est réutilisée : aucune nouvelle tâche, aucune unité quotidienne. Récupérez ses liens avec GET poll_path.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "job_id",
                    "status",
                    "scope",
                    "poll_path",
                    "reused"
                  ],
                  "properties": {
                    "job_id": {
                      "type": "string",
                      "example": "cexp_3Fh8sKq0aZr2LmN1"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "ready"
                      ]
                    },
                    "scope": {
                      "type": "string",
                      "enum": [
                        "own",
                        "company"
                      ]
                    },
                    "poll_path": {
                      "type": "string",
                      "example": "/v1/captures/export-jobs/cexp_3Fh8sKq0aZr2LmN1"
                    },
                    "reused": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    }
                  }
                }
              }
            }
          },
          "202": {
            "description": "La tâche a été lancée.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "job_id",
                    "status",
                    "scope",
                    "poll_path"
                  ],
                  "properties": {
                    "job_id": {
                      "type": "string",
                      "example": "cexp_3Fh8sKq0aZr2LmN1"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "processing"
                      ]
                    },
                    "scope": {
                      "type": "string",
                      "enum": [
                        "own",
                        "company"
                      ]
                    },
                    "poll_path": {
                      "type": "string",
                      "example": "/v1/captures/export-jobs/cexp_3Fh8sKq0aZr2LmN1"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "MALFORMED_REQUEST : le corps n'est pas un objet JSON, ou scope / org est invalide.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "FORBIDDEN : la requête a été effectuée avec une clé de test, ou avec scope company par une personne qui n'est pas propriétaire d'un cabinet se trouvant dans sa fenêtre de téléchargement.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "EXPORT_JOB_RUNNING : une tâche du compte est encore en cours de traitement ; details.job_id et details.poll_path la désignent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/AuthUnavailable"
          }
        }
      }
    },
    "/captures/export-jobs/{job_id}": {
      "get": {
        "tags": [
          "Captures"
        ],
        "operationId": "getCaptureExportJob",
        "summary": "Statut et liens de téléchargement d'une tâche d'exportation de captures",
        "description": "Le statut et la progression de la tâche ; une fois prête, un lien présigné par partie. Un lien est valide jusqu'à url_expires_at : au plus 24 heures, jamais au-delà de download_expires_at, ni au-delà de la validité des identifiants qui l'ont signé (cela peut être moins d'une heure) ; interrogez de nouveau après url_expires_at pour obtenir des liens récents. download_expires_at correspond à 7 jours après le minuit UTC précédant le lancement de la première partie de la tâche (6 à 7 jours après que la tâche est devenue prête) ; les parties sont supprimées ensuite. La tâche perd son téléchargement de manière anticipée (status expired, parties supprimées, expired_reason) lorsqu'une exportation plus récente du même périmètre l'a remplacée, qu'une capture qu'elle contient a été effacée, que les données de son cabinet ont été supprimées, ou que vos droits ne la couvrent plus (vérifié à chaque lecture d'une tâche prête). Une tâche qui ne signale aucune progression pendant 30 minutes est clôturée comme échouée par cet appel, et ce qu'elle a écrit est supprimé. La première réponse contenant des liens au cours d'une heure est consignée dans le journal d'audit (capture.export_job.links_issued). Seul le compte qui a lancé la tâche la voit ; une exportation de cabinet portant sur une archive du cabinet uniquement via la session du tableau de bord.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "job_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^cexp_[A-Za-z0-9]{16}$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "La tâche.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "job_id",
                    "status",
                    "scope",
                    "parts"
                  ],
                  "properties": {
                    "job_id": {
                      "type": "string",
                      "example": "cexp_3Fh8sKq0aZr2LmN1",
                      "pattern": "^cexp_[A-Za-z0-9]{16}$"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "processing",
                        "ready",
                        "failed",
                        "expired"
                      ],
                      "description": "processing pendant l'écriture du plan et des parties ; ready avec les liens de téléchargement ; failed (lancez une nouvelle tâche, l'unité du jour a été restituée) ; expired lorsque la fenêtre de téléchargement est terminée ou que le téléchargement a été retiré (expired_reason ; les parties sont supprimées)."
                    },
                    "scope": {
                      "type": "string",
                      "enum": [
                        "own",
                        "company"
                      ]
                    },
                    "language": {
                      "type": "string",
                      "enum": [
                        "en",
                        "de",
                        "it",
                        "fr",
                        "es",
                        "pt_BR",
                        "cs",
                        "el",
                        "hi",
                        "hu",
                        "id",
                        "ja",
                        "ko",
                        "nl",
                        "pl",
                        "ro",
                        "ru",
                        "sv",
                        "tr",
                        "vi",
                        "zh_CN",
                        "zh_TW",
                        "ar",
                        "he",
                        "da",
                        "fi",
                        "nb",
                        "sk",
                        "uk"
                      ],
                      "description": "La langue du README de la tâche (README.<language>.txt en plus de README.txt ; en pour README.txt seul)."
                    },
                    "created_at": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "ended_at": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "progress": {
                      "type": "object",
                      "nullable": true,
                      "properties": {
                        "captures_total": {
                          "type": "integer",
                          "nullable": true
                        },
                        "captures_done": {
                          "type": "integer"
                        },
                        "parts_total": {
                          "type": "integer",
                          "nullable": true
                        },
                        "parts_done": {
                          "type": "integer"
                        }
                      }
                    },
                    "truncated": {
                      "type": "boolean",
                      "description": "true lorsque le compte compte plus de 20 000 captures exportables : la tâche contient les 20 000 plus récentes."
                    },
                    "counts": {
                      "type": "object",
                      "nullable": true,
                      "description": "Défini lorsque la tâche est prête.",
                      "properties": {
                        "captures": {
                          "type": "integer",
                          "description": "Lignes de captures.csv."
                        },
                        "packaged": {
                          "type": "integer",
                          "description": "Paquets contenus dans les parties."
                        },
                        "failed": {
                          "type": "integer",
                          "description": "Captures en échec (sans paquet, listées uniquement dans captures.csv)."
                        },
                        "unavailable": {
                          "type": "integer",
                          "description": "Paquets non inclus : ils n'ont pas pu être lus pendant l'exportation, ou dépassent 2 Go (captures.csv indique lesquels)."
                        }
                      }
                    },
                    "parts": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "part",
                          "bytes",
                          "count",
                          "download_url"
                        ],
                        "properties": {
                          "part": {
                            "type": "integer",
                            "minimum": 1,
                            "example": 1,
                            "description": "Numéro de partie, de 1 au nombre de parties."
                          },
                          "bytes": {
                            "type": "integer",
                            "example": 1834201112,
                            "description": "Taille du ZIP de la partie, en octets."
                          },
                          "count": {
                            "type": "integer",
                            "example": 412,
                            "description": "Paquets de capture de cette partie."
                          },
                          "filename": {
                            "type": "string",
                            "example": "proofsnap-captures-2026-10-07T1430Z-part-1-of-3.zip",
                            "description": "Nom de fichier sous lequel le téléchargement est enregistré (Content-Disposition) : proofsnap-captures-<date et heure UTC du démarrage de la tâche>, avec -part-N-of-M lorsqu'il y a plusieurs parties."
                          },
                          "download_url": {
                            "type": "string",
                            "format": "uri",
                            "nullable": true,
                            "description": "GET présigné du ZIP de la partie, valide jusqu'à url_expires_at. null lorsque la tâche a expiré."
                          }
                        }
                      }
                    },
                    "download_expires_at": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true,
                      "description": "Fin de la fenêtre de téléchargement : 7 jours après le minuit UTC précédant le démarrage de la première partie (le cycle de vie du stockage supprime les parties au-delà)."
                    },
                    "url_expires_at": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true,
                      "description": "Moment où les valeurs download_url de cette réponse cessent de fonctionner : au plus 24 heures, jamais au-delà de download_expires_at, ni au-delà de la validité des identifiants de signature. Interrogez de nouveau ensuite pour obtenir des liens récents."
                    },
                    "error": {
                      "type": "string",
                      "nullable": true,
                      "example": "export timed out"
                    },
                    "expired_reason": {
                      "type": "string",
                      "nullable": true,
                      "enum": [
                        "window_ended",
                        "superseded",
                        "rights_changed",
                        "capture_erased",
                        "company_deleted",
                        null
                      ],
                      "description": "Défini lorsque status vaut expired : window_ended (download_expires_at est dépassé), superseded (une exportation plus récente du même périmètre), rights_changed (vos droits ne couvrent plus la tâche), capture_erased (une capture qu'elle contient a été effacée), company_deleted (les données de son cabinet ont été supprimées)."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "EXPORT_JOB_NOT_FOUND : tâche inexistante, tâche d'un autre compte, enregistrement expiré (8 jours après sa fin), ou requête effectuée avec une clé de test.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/AuthUnavailable"
          }
        }
      }
    },
    "/webhook-subscriptions": {
      "get": {
        "tags": [
          "Webhooks"
        ],
        "operationId": "listWebhookSubscriptions",
        "summary": "Lister les abonnements webhook",
        "description": "Les abonnements webhook du compte, du plus récent au plus ancien, actifs et désactivés, sans les secrets. Clé API ou session du tableau de bord ; une clé en lecture seule (Website Archive) peut l'utiliser.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Abonnements.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "subscriptions": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/WebhookSubscription"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "503": {
            "$ref": "#/components/responses/AuthUnavailable"
          }
        }
      },
      "post": {
        "tags": [
          "Webhooks"
        ],
        "operationId": "createWebhookSubscription",
        "summary": "S'abonner aux événements du compte",
        "description": "Abonne un endpoint https aux événements du compte : chaque événement correspondant du compte lui est envoyé par POST, signé avec le secret propre à l'abonnement (renvoyé une seule fois dans cette réponse). Exige une adresse e-mail confirmée (403 EMAIL_NOT_VERIFIED). Autorisé lorsqu'un abonnement est en retard de paiement (rien n'est facturé). Limites d'abonnements ACTIFS par compte : 25 sur developer, startup, growth, custom, legal_solo et legal_seats, 10 sur website_archive, 0 sur archive_only et sandbox (409 WEBHOOK_SUBSCRIPTION_LIMIT). L'URL suit les règles de webhook_url (https, sans identifiants, sans adresse privée ou réservée ; vérifiée à nouveau à chaque livraison). Idempotency-Key est pris en charge : une nouvelle tentative avec la même clé et le même corps dans les 24 heures récupère la première réponse, secret compris, au lieu de créer un second abonnement. Clé API ou session du tableau de bord ; une clé en lecture seule (Website Archive) peut l'utiliser.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "url",
                  "events"
                ],
                "additionalProperties": false,
                "properties": {
                  "url": {
                    "type": "string",
                    "example": "https://hooks.example.com/proofsnap"
                  },
                  "events": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 9,
                    "uniqueItems": true,
                    "items": {
                      "$ref": "#/components/schemas/WebhookEventType"
                    }
                  },
                  "filters": {
                    "$ref": "#/components/schemas/WebhookSubscriptionFilters"
                  },
                  "description": {
                    "type": "string",
                    "maxLength": 200,
                    "description": "Texte brut, 200 caractères au maximum."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Créé. `secret` n'est affiché qu'ici.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookSubscriptionCreated"
                }
              }
            }
          },
          "400": {
            "description": "MALFORMED_REQUEST (events, filters, description, champ inconnu), MISSING_FIELD (url) ou INVALID_URL.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "EMAIL_NOT_VERIFIED.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "WEBHOOK_SUBSCRIPTION_LIMIT, IDEMPOTENCY_IN_PROGRESS.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "SSRF_BLOCKED (l'URL pointe vers une adresse bloquée) ou IDEMPOTENCY_KEY_REUSED.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/AuthUnavailable"
          }
        }
      }
    },
    "/webhook-subscriptions/{subscription_id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/SubscriptionId"
        }
      ],
      "get": {
        "tags": [
          "Webhooks"
        ],
        "operationId": "getWebhookSubscription",
        "summary": "Obtenir un abonnement webhook",
        "description": "Un abonnement avec son état de livraison, sans le secret. Clé API ou session du tableau de bord ; une clé en lecture seule (Website Archive) peut l'utiliser.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "L'abonnement.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookSubscription"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "WEBHOOK_SUBSCRIPTION_NOT_FOUND : inconnu, supprimé ou appartenant à un autre compte.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/AuthUnavailable"
          }
        }
      },
      "patch": {
        "tags": [
          "Webhooks"
        ],
        "operationId": "updateWebhookSubscription",
        "summary": "Modifier ou réactiver un abonnement webhook",
        "description": "Modifie les événements, les filtres (null ou {} les efface) ou la description, ou réactive un abonnement désactivé avec status \"active\" (réinitialise les compteurs d'échecs ; soumis à la limite d'abonnements actifs). L'URL ne peut pas être modifiée : créez un nouvel abonnement. Nécessite une adresse e-mail confirmée. Clé API ou session du tableau de bord ; une clé en lecture seule (Website Archive) peut l'utiliser.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "events": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 9,
                    "uniqueItems": true,
                    "items": {
                      "$ref": "#/components/schemas/WebhookEventType"
                    }
                  },
                  "filters": {
                    "allOf": [
                      {
                        "$ref": "#/components/schemas/WebhookSubscriptionFilters"
                      }
                    ],
                    "nullable": true
                  },
                  "description": {
                    "type": "string",
                    "maxLength": 200,
                    "nullable": true
                  },
                  "status": {
                    "type": "string",
                    "enum": [
                      "active"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "L'abonnement modifié.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookSubscription"
                }
              }
            }
          },
          "400": {
            "description": "MALFORMED_REQUEST.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "EMAIL_NOT_VERIFIED.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "WEBHOOK_SUBSCRIPTION_NOT_FOUND : inconnu, supprimé ou appartenant à un autre compte.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "WEBHOOK_SUBSCRIPTION_LIMIT (réactivation au-delà de la limite).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/AuthUnavailable"
          }
        }
      },
      "delete": {
        "tags": [
          "Webhooks"
        ],
        "operationId": "deleteWebhookSubscription",
        "summary": "Supprimer un abonnement webhook",
        "description": "Supprime l'abonnement, son secret et ses livraisons en file d'attente. 404 pour un abonnement inconnu ou déjà supprimé (considérez-le comme disparu). Clé API ou session du tableau de bord ; une clé en lecture seule (Website Archive) peut l'utiliser.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "responses": {
          "204": {
            "description": "Supprimé.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "WEBHOOK_SUBSCRIPTION_NOT_FOUND : inconnu, supprimé ou appartenant à un autre compte.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/AuthUnavailable"
          }
        }
      }
    },
    "/events": {
      "get": {
        "tags": [
          "Webhooks"
        ],
        "operationId": "listEvents",
        "summary": "Journal des événements",
        "description": "Tous les événements émis pour le compte au cours des 30 derniers jours, qu'un abonnement les ait reçus ou non, du plus récent au plus ancien. Une clé API voit les événements de l'environnement dans lequel elle s'exécute (une clé live les événements live, une clé de test les événements de test) ; une session du tableau de bord voit les événements live. Utilisez cette route pour rattraper des événements ou pour interroger au lieu de vous abonner. Clé API ou session du tableau de bord ; une clé en lecture seule (Website Archive) peut l'utiliser.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "type",
            "in": "query",
            "required": false,
            "description": "Uniquement ce type d'événement.",
            "schema": {
              "$ref": "#/components/schemas/WebhookEventType"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "De 1 à 100, 25 par défaut.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          },
          {
            "name": "starting_after",
            "in": "query",
            "required": false,
            "description": "Un identifiant d'événement : la page commence après celui-ci (transmettez next_cursor). Un identifiant d'un autre compte ou environnement, ou datant de plus de 30 jours, renvoie 400.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Une page d'événements.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "events": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Event"
                      }
                    },
                    "has_more": {
                      "type": "boolean"
                    },
                    "next_cursor": {
                      "type": "string",
                      "nullable": true,
                      "description": "À passer comme starting_after pour la page suivante ; null sur la dernière page."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "MALFORMED_REQUEST (type, limit ou starting_after).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "503": {
            "$ref": "#/components/responses/AuthUnavailable"
          }
        }
      }
    },
    "/whoami": {
      "get": {
        "tags": [
          "Webhooks"
        ],
        "operationId": "whoami",
        "summary": "Qui suis-je",
        "description": "Nature des informations d'authentification : forfait, environnement, indication de lecture seule de la clé API, ainsi que l'id, le nom et le préfixe de la clé (null pour une session du tableau de bord). Les intégrations l'utilisent pour tester une connexion et la libeller. Aucune adresse e-mail ni aucun identifiant de compte. Clé API ou session du tableau de bord ; une clé en lecture seule (Website Archive) peut l'utiliser.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Les informations d'authentification.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "plan": {
                      "type": "string",
                      "nullable": true,
                      "example": "startup",
                      "enum": [
                        "developer",
                        "startup",
                        "growth",
                        "custom",
                        "legal_solo",
                        "legal_seats",
                        "website_archive",
                        "archive_only",
                        "sandbox",
                        null
                      ]
                    },
                    "api_read_only": {
                      "type": "boolean",
                      "description": "true pour une clé API d'un compte Website Archive (elle peut lire et s'abonner, mais pas créer de captures)."
                    },
                    "env": {
                      "type": "string",
                      "enum": [
                        "live",
                        "test"
                      ],
                      "description": "L'environnement dans lequel s'exécutent les informations d'authentification."
                    },
                    "key": {
                      "type": "object",
                      "nullable": true,
                      "properties": {
                        "key_id": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string",
                          "nullable": true
                        },
                        "prefix": {
                          "type": "string",
                          "enum": [
                            "ps_live_",
                            "ps_test_"
                          ]
                        }
                      }
                    },
                    "email_verified": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "503": {
            "$ref": "#/components/responses/AuthUnavailable"
          }
        }
      }
    },
    "/captures/events": {
      "get": {
        "tags": [
          "Captures"
        ],
        "operationId": "listCaptureEvents",
        "summary": "Chaîne des événements de capture (suppressions et conservations pour litige)",
        "description": "L'enregistrement, à l'échelle du compte et dont toute altération est détectable, de ce qui est arrivé à ses captures : mise à la corbeille, restauration, suppression définitive (erase), purge de la corbeille, suppression liée à la durée de conservation, suppression avec le compte ou le cabinet, conservation pour litige placée et levée (une levée par le système après la fin de l'abonnement a pour acteur `system` et pour détail `subscription_ended`), résultat S3 Object Lock, et importation d'un paquet de preuves (type `import`, détail = le type de paquet). Du plus ancien au plus récent. La chaîne survit aux captures et aux données du compte : elle ne contient que des identifiants et des empreintes de manifeste, aucun contenu de capture ni texte libre.\n\nVérification : (1) pour chaque événement, calculez le SHA-256 du JSON canonique des champs hachés (verification.hashed_fields ; clés triées, sans espaces, valeurs absentes comme null) et comparez-le à `hash` ; (2) vérifiez que chaque `prev_hash` est égal au `hash` de l'événement précédent (64 zéros pour seq 1) et que seq ne présente pas de trous ; (3) pour un ancrage, vérifiez que `head_hash` est égal au `hash` de l'événement `to_seq`, écrivez `ots_proof_base64` dans un fichier (décodé du base64) et exécutez `ots verify -d <head_hash> anchor.ots` (client OpenTimestamps) : cela prouve que la chaîne jusqu'à cet événement existait au plus tard au moment du bloc Bitcoin. Les ancrages sont réalisés une fois par jour pour chaque compte ayant des événements d'un jour antérieur.\n\nClé API ou session du tableau de bord, pour votre propre compte ; `scope=company` donne au propriétaire d'un cabinet la chaîne des captures du cabinet (`org` en choisit un lorsque l'appelant en possède plusieurs ; un cabinet conservé comme archive du cabinet uniquement via la session du tableau de bord). Une clé de test reçoit 403 FORBIDDEN : la chaîne couvre les captures live et ne peut pas être filtrée sans être rompue.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "De 1 à 500, 100 par défaut.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "default": 100
            }
          },
          {
            "name": "starting_after",
            "in": "query",
            "required": false,
            "description": "Un seq d'événement : la page commence après celui-ci (transmettez next_starting_after).",
            "schema": {
              "type": "integer",
              "minimum": 0
            }
          },
          {
            "name": "scope",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "own",
                "company"
              ],
              "default": "own"
            }
          },
          {
            "name": "org",
            "in": "query",
            "required": false,
            "description": "scope=company : l'identifiant du cabinet.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Une page de la chaîne.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "chain": {
                      "type": "string"
                    },
                    "scope": {
                      "type": "string",
                      "enum": [
                        "own",
                        "company"
                      ]
                    },
                    "head": {
                      "type": "object",
                      "properties": {
                        "chain": {
                          "type": "string"
                        },
                        "seq": {
                          "type": "integer"
                        },
                        "head_hash": {
                          "type": "string",
                          "nullable": true
                        },
                        "anchored_seq": {
                          "type": "integer"
                        }
                      }
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/CaptureEvent"
                      }
                    },
                    "has_more": {
                      "type": "boolean"
                    },
                    "next_starting_after": {
                      "type": "integer",
                      "nullable": true
                    },
                    "anchors": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/CaptureEventAnchor"
                      }
                    },
                    "verification": {
                      "type": "object",
                      "properties": {
                        "hash": {
                          "type": "string"
                        },
                        "hashed_fields": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        },
                        "genesis_prev_hash": {
                          "type": "string"
                        },
                        "anchor": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "MALFORMED_REQUEST : limit, starting_after ou scope invalide.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "FORBIDDEN : la requête a été effectuée avec une clé de test, ou avec scope=company par une personne qui n'est pas propriétaire du cabinet.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/AuthUnavailable"
          }
        }
      }
    },
    "/legal-holds": {
      "post": {
        "tags": [
          "Captures"
        ],
        "operationId": "bulkLegalHold",
        "summary": "Placer ou lever des conservations pour litige en masse",
        "description": "Place (enabled true) ou lève (false) la conservation pour litige de nombreuses captures à la fois. selector contient exactement un des éléments suivants : `capture_ids` (de 1 à 500 identifiants), `matter_id` (les captures d'une affaire que l'appelant peut ouvrir), `host` (les propres captures de l'appelant pour ce nom d'hôte) ou `schedule_id` (les propres captures de l'appelant réalisées par l'une de ses planifications). Chaque capture passe par les règles de PATCH /captures/{capture_id} avec l'action legal_hold et obtient son propre résultat : applied, unchanged (déjà dans cet état) ou refusée avec le code d'erreur (par exemple FORBIDDEN, CAPTURE_IN_TRASH, CAPTURE_PROCESSING, LEGAL_HOLD_OWNER_ONLY, LEGAL_HOLD_MATTER, LEGAL_HOLD_SOURCE pour un caviardage dont la capture source est encore en conservation pour litige, CAPTURE_NOT_FOUND, LEGAL_HOLD_NOT_AVAILABLE pour une capture en mode test). Les caviardages réalisés à partir de chaque capture suivent sa conservation pour litige, y compris pour une capture signalée comme unchanged ; si cela ne se termine pas dans le délai de la requête, l'opération est achevée en quelques minutes. Placer des conservations pour litige exige un forfait payant : le sandbox gratuit reçoit 403 LEGAL_HOLD_NOT_AVAILABLE (details.reason \"plan\") pour toute la requête ; la levée fonctionne avec tous les forfaits. Une conservation placée ici a legal_hold_source \"bulk\" ; la lever en masse ou une par une fonctionne de la même façon. 500 captures au maximum par requête : une affaire, un hôte ou une planification qui en compte davantage est tronqué (`truncated: true`, renvoyez la requête). La conservation pour litige S3 Object Lock des 25 premières captures modifiées est appliquée pendant la requête, le reste en quelques minutes (`s3_legal_hold.status` pending). Coût : une unité de limite de débit par tranche de 10 captures (au moins une) ; une requête qui exige plus que ce qu'autorise une fenêtre renvoie 400 RATE_LIMIT_COST_TOO_HIGH. Clé API ou session du tableau de bord ; sur Website Archive, la clé API est en lecture seule (403 PLAN_API_READ_ONLY). Une session du tableau de bord ou une clé dont l'adresse e-mail n'est pas confirmée reçoit 403 EMAIL_NOT_VERIFIED. Non bloqué par un abonnement en retard de paiement.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "enabled",
                  "selector"
                ],
                "additionalProperties": false,
                "properties": {
                  "enabled": {
                    "type": "boolean"
                  },
                  "reason": {
                    "type": "string",
                    "maxLength": 200,
                    "description": "Texte brut conservé avec chaque conservation placée (pas avec une levée) ; jamais dans les chaînes hachées."
                  },
                  "selector": {
                    "type": "object",
                    "minProperties": 1,
                    "maxProperties": 1,
                    "properties": {
                      "capture_ids": {
                        "type": "array",
                        "minItems": 1,
                        "maxItems": 500,
                        "items": {
                          "type": "string"
                        }
                      },
                      "matter_id": {
                        "type": "string"
                      },
                      "host": {
                        "type": "string"
                      },
                      "schedule_id": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Résultats par capture.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "enabled": {
                      "type": "boolean"
                    },
                    "selector": {
                      "type": "string",
                      "enum": [
                        "capture_ids",
                        "matter_id",
                        "host",
                        "schedule_id"
                      ]
                    },
                    "requested": {
                      "type": "integer"
                    },
                    "truncated": {
                      "type": "boolean"
                    },
                    "applied": {
                      "type": "integer"
                    },
                    "unchanged": {
                      "type": "integer"
                    },
                    "refused": {
                      "type": "integer"
                    },
                    "results": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "capture_id": {
                            "type": "string"
                          },
                          "result": {
                            "type": "string",
                            "enum": [
                              "applied",
                              "unchanged",
                              "refused"
                            ]
                          },
                          "code": {
                            "type": "string",
                            "description": "Le code d'erreur d'un refus."
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "MALFORMED_REQUEST (corps, selector, plus de 500 capture_ids), RATE_LIMIT_COST_TOO_HIGH.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "EMAIL_NOT_VERIFIED, PLAN_API_READ_ONLY, LEGAL_HOLD_NOT_AVAILABLE (placement sans forfait payant, details.reason plan).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "MATTER_NOT_FOUND (matter_id que l'appelant ne peut pas ouvrir), SCHEDULE_NOT_FOUND.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "RATE_LIMITED.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/AuthUnavailable"
          }
        }
      }
    },
    "/imports": {
      "post": {
        "tags": [
          "Importation de paquets"
        ],
        "operationId": "createImport",
        "summary": "Lancer une importation de paquet",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "description": "Crée une importation et lance son téléversement. Envoyez la taille exacte du ZIP en octets ; la réponse contient une URL PUT présignée par partie de 32 Mio (la dernière étant plus petite), valable 1 heure (`upload.expires_at` ; jamais plus longtemps que les identifiants de signature du serveur). Envoyez par PUT les octets de chaque partie à son URL, conservez l'en-tête `ETag` de chaque réponse, puis appelez POST /imports/{import_id}/complete dans les 24 heures. Avec `matter_id`, la capture acceptée est ajoutée à cette affaire : vous devez pouvoir y ajouter des captures dès maintenant (une affaire ouverte que vous pouvez ouvrir ; l'importation d'un membre du cabinet uniquement vers une affaire de ce cabinet). Chaque création est décomptée de la limite quotidienne de 50.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "filename",
                  "size_bytes"
                ],
                "additionalProperties": false,
                "properties": {
                  "filename": {
                    "type": "string",
                    "maxLength": 200,
                    "description": "Le nom de fichier à afficher (texte brut : sans caractères de contrôle, invisibles ou de direction du texte)."
                  },
                  "size_bytes": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 2147483648,
                    "description": "Taille exacte du ZIP en octets, 2 Gio au maximum."
                  },
                  "matter_id": {
                    "type": "string",
                    "description": "Facultatif : ajoute la capture acceptée à cette affaire."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Importation créée ; téléversez les parties.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "import_id": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "uploading"
                      ]
                    },
                    "upload": {
                      "type": "object",
                      "properties": {
                        "upload_id": {
                          "type": "string"
                        },
                        "part_size": {
                          "type": "integer",
                          "example": 33554432
                        },
                        "parts": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "part_number": {
                                "type": "integer"
                              },
                              "url": {
                                "type": "string",
                                "format": "uri",
                                "description": "URL S3 UploadPart présignée : envoyez par PUT exactement size_bytes octets (l'URL signe le Content-Length), puis lisez l'en-tête de réponse ETag. Aucun autre en-tête n'est nécessaire."
                              },
                              "size_bytes": {
                                "type": "integer",
                                "description": "La longueur exacte en octets de cette partie : part_size pour chaque partie sauf la dernière, le reste pour la dernière (la partie n commence à (n - 1) x part_size)."
                              }
                            }
                          }
                        },
                        "expires_at": {
                          "type": "string",
                          "format": "date-time"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "MALFORMED_REQUEST (corps, filename, size_bytes, matter_id, champ inconnu).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "PAYMENT_REQUIRED : l'abonnement est en retard de paiement ou impayé.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "IMPORT_NOT_ALLOWED (details.reason plan : forfaits Legal uniquement ; live_credentials_required : une clé de test, une session sans facturation associée ou le Sandbox), EMAIL_NOT_VERIFIED, FORBIDDEN (importation d'un membre du cabinet vers une affaire d'un autre cabinet).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "MATTER_NOT_FOUND.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "IMPORT_LIMIT (3 importations en cours ; details.limit, details.in_progress), MATTER_SEALED, MATTER_ARCHIVED, COMPANY_READ_ONLY.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "IMPORT_TOO_LARGE : size_bytes supérieur à 2 Gio (details.max_bytes).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "RATE_LIMITED : la limite par minute, ou la limite quotidienne de 50 importations (details.action import_create, details.daily_limit).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/AuthUnavailable"
          }
        }
      },
      "get": {
        "tags": [
          "Importation de paquets"
        ],
        "operationId": "listImports",
        "summary": "Lister vos importations",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "description": "Vos importations, de la plus récente à la plus ancienne. Les importations définitives sont conservées 30 jours (la capture d'une importation acceptée reste). Un téléversement ayant dépassé ses 24 heures est affiché comme expiré.",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "next_cursor de la page précédente."
          }
        ],
        "responses": {
          "200": {
            "description": "Une page d'importations.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "imports": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Import"
                      }
                    },
                    "next_cursor": {
                      "type": "string",
                      "nullable": true
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "MALFORMED_REQUEST (limit, cursor).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "503": {
            "$ref": "#/components/responses/AuthUnavailable"
          }
        }
      }
    },
    "/imports/{import_id}": {
      "parameters": [
        {
          "name": "import_id",
          "in": "path",
          "required": true,
          "description": "Identifiant d'importation (imp_ suivi de 16 à 24 lettres et chiffres). Un identifiant mal formé renvoie 404 IMPORT_NOT_FOUND.",
          "schema": {
            "type": "string",
            "pattern": "^imp_[A-Za-z0-9]{16,24}$"
          }
        }
      ],
      "get": {
        "tags": [
          "Importation de paquets"
        ],
        "operationId": "getImport",
        "summary": "Obtenir une importation",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "description": "L'importation et, une fois définitif, son résultat. Interrogez-la après POST /imports/{import_id}/complete ; la vérification prend généralement quelques secondes, et jusqu'à quelques minutes pour un paquet de 2 Gio. Avec ?upload=true sur un téléversement encore en cours, la réponse ajoute upload : des URL présignées récentes (parts) pour les parties que le stockage ne détient pas encore et les parties déjà reçues (received : part_number, etag, size_bytes ; null lorsqu'elles n'ont pas pu être listées, auquel cas chaque partie est de nouveau présignée), ou completed: true lorsque le téléversement était déjà terminé (envoyez de nouveau complete). Utilisez-le pour reprendre après une coupure réseau, une URL de partie expirée (elles durent au plus 1 heure, la fenêtre de téléversement 24 heures) ou un rechargement de page. ?upload=true coûte une unité de la limite de débit par minute.",
        "responses": {
          "200": {
            "description": "L'importation.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Import"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "IMPORT_NOT_FOUND (y compris l'importation d'un autre compte).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "RATE_LIMITED (uniquement avec ?upload=true).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/AuthUnavailable"
          }
        },
        "parameters": [
          {
            "name": "upload",
            "in": "query",
            "required": false,
            "description": "true (ou 1) : ajoute des URL de parties récentes et les parties reçues d'un téléversement en cours.",
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "1"
              ]
            }
          }
        ]
      },
      "delete": {
        "tags": [
          "Importation de paquets"
        ],
        "operationId": "cancelImport",
        "summary": "Annuler un téléversement",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "description": "Annule une importation dont le téléversement est encore en cours : le téléversement est interrompu, les parties sont supprimées et l'importation est retirée (GET renvoie ensuite 404).",
        "responses": {
          "200": {
            "description": "Annulé.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "import_id": {
                      "type": "string"
                    },
                    "deleted": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "IMPORT_NOT_FOUND.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "IMPORT_STATE : le téléversement de l'importation n'est plus en cours (details.status).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/AuthUnavailable"
          }
        }
      }
    },
    "/imports/{import_id}/complete": {
      "parameters": [
        {
          "name": "import_id",
          "in": "path",
          "required": true,
          "description": "Identifiant d'importation (imp_ suivi de 16 à 24 lettres et chiffres). Un identifiant mal formé renvoie 404 IMPORT_NOT_FOUND.",
          "schema": {
            "type": "string",
            "pattern": "^imp_[A-Za-z0-9]{16,24}$"
          }
        }
      ],
      "post": {
        "tags": [
          "Importation de paquets"
        ],
        "operationId": "completeImport",
        "summary": "Terminer le téléversement et vérifier",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "description": "Termine le téléversement et lance la vérification. parts est facultatif : envoyez chaque numéro de partie (de 1 à N, dans l'ordre) avec l'ETag renvoyé par le PUT de la partie, ou omettez-le et le serveur lit les parties téléversées depuis le stockage (utile dans un navigateur qui ne peut pas lire l'en-tête ETag). Vérifie que la taille stockée est égale à size_bytes. Interrogez GET /imports/{import_id}. Une liste de parties refusée par le stockage, ou une partie pas encore téléversée (details.missing_parts), renvoie 400 et le téléversement reste ouvert : téléversez ce qui manque (GET /imports/{import_id}?upload=true fournit de nouvelles URL) et renvoyez la requête. Renvoyer complete après la perte d'une réponse ne pose pas de problème : un complete encore en cours renvoie 409, un complete interrompu est repris après 2 minutes, et un téléversement déjà terminé passe à la vérification. Une taille différente de size_bytes entraîne l'abandon du téléversement (422 IMPORT_SIZE_MISMATCH ; l'importation est rejetée, créez-en une nouvelle).",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "parts": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "required": [
                        "part_number",
                        "etag"
                      ],
                      "properties": {
                        "part_number": {
                          "type": "integer",
                          "minimum": 1
                        },
                        "etag": {
                          "type": "string",
                          "description": "L'en-tête de réponse ETag du PUT de la partie, avec ou sans guillemets."
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Vérification lancée.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "import_id": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "verifying"
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "MALFORMED_REQUEST (parties dans le désordre ou ne correspondant pas aux parties téléversées ; ou, sans parts, une partie pas encore téléversée : details.missing_parts).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "PAYMENT_REQUIRED.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "IMPORT_NOT_ALLOWED, EMAIL_NOT_VERIFIED.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "IMPORT_NOT_FOUND.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "IMPORT_STATE : le téléversement de l'importation n'est plus en cours (details.status, par exemple verifying ou expired).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "IMPORT_SIZE_MISMATCH (details.expected, details.actual).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "RATE_LIMITED.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/AuthUnavailable"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiKeyAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Envoyez votre clé API sous la forme `Authorization: Bearer ps_live_...` (le nom du schéma est insensible à la casse). Un en-tête absent ou mal formé renvoie 401 MISSING_API_KEY. Une clé inconnue, révoquée ou expirée, ou un compte sans accès à l'API, renvoie 401 INVALID_API_KEY avec `WWW-Authenticate: Bearer realm=\"ProofSnap API\", error=\"invalid_token\"`. Le code 403 est réservé à un identifiant valide qui n'est pas autorisé à effectuer une action (FORBIDDEN, restrictions de forfait). Une clé `ps_live_` d'un compte sans compte de facturation lié (client Stripe) fonctionne comme une clé de test : les captures portent un filigrane et ne sont jamais facturées, et include_eidas est refusé avec 403 EIDAS_REQUIRES_LIVE_KEY. La passerelle met en cache la décision concernant une clé pendant 60 secondes au maximum ; une clé révoquée peut donc continuer à fonctionner pendant 60 secondes au plus. Si l'identifiant ne peut pas être vérifié en raison d'une défaillance temporaire du backend, la passerelle renvoie 503 SERVICE_UNAVAILABLE à la place ; cette réponse n'est pas mise en cache, réessayez donc avec un délai progressif. La demande d'authentification `WWW-Authenticate` est renvoyé tel quel pour un 401 émis par la passerelle (vérification de l'identifiant) ; pour un 401 émis par un gestionnaire de route, la passerelle transmet la même demande d'authentification renommée en `x-amzn-Remapped-WWW-Authenticate`."
      }
    },
    "headers": {
      "XRequestId": {
        "description": "Identifiant de requête, également utile dans les demandes d'assistance. Envoyé avec les réponses de succès ; les corps d'erreur le portent sous la forme request_id.",
        "schema": {
          "type": "string"
        }
      },
      "RateLimitLimit": {
        "description": "Requêtes autorisées dans la fenêtre de 60 secondes en cours. En-tête historique, conservé à côté de RateLimit / RateLimit-Policy.",
        "schema": {
          "type": "integer"
        }
      },
      "RateLimitRemaining": {
        "description": "Requêtes restantes dans la fenêtre en cours. En-tête historique, conservé à côté de RateLimit / RateLimit-Policy.",
        "schema": {
          "type": "integer"
        }
      },
      "RateLimitReset": {
        "description": "Heure ISO 8601 à laquelle la fenêtre en cours se termine. En-tête historique, conservé à côté de RateLimit / RateLimit-Policy.",
        "schema": {
          "type": "string",
          "format": "date-time"
        }
      },
      "RetryAfter": {
        "description": "Nombre de secondes à attendre avant de réessayer.",
        "schema": {
          "type": "integer"
        }
      },
      "QuotaLimit": {
        "description": "Captures incluses dans le forfait par mois (clés live sur les forfaits avec quota mensuel).",
        "schema": {
          "type": "integer"
        }
      },
      "QuotaRemaining": {
        "description": "Captures incluses restantes ce mois-ci.",
        "schema": {
          "type": "integer"
        }
      },
      "QuotaUsed": {
        "description": "Captures terminées ce mois-ci.",
        "schema": {
          "type": "integer"
        }
      },
      "QuotaCap": {
        "description": "Le maximum mensuel applicable à l'appelant (valeur par défaut du forfait, plafond propre ou limite du cabinet).",
        "schema": {
          "type": "integer"
        }
      },
      "QuotaOrgLimit": {
        "description": "Membres d'un cabinet uniquement : la limite mensuelle à l'échelle du cabinet, lorsque le propriétaire en a fixé une.",
        "schema": {
          "type": "integer"
        }
      },
      "QuotaOrgUsed": {
        "description": "Membres d'un cabinet uniquement : captures admises pour l'ensemble du cabinet ce mois-ci.",
        "schema": {
          "type": "integer"
        }
      },
      "SandboxLimit": {
        "description": "Sandbox (session du tableau de bord sans forfait) : quota à vie de captures d'essai avec filigrane (50).",
        "schema": {
          "type": "integer"
        }
      },
      "SandboxRemaining": {
        "description": "Sandbox : quota restant.",
        "schema": {
          "type": "integer"
        }
      },
      "SandboxUsed": {
        "description": "Sandbox : quota utilisé.",
        "schema": {
          "type": "integer"
        }
      },
      "IdempotentReplay": {
        "description": "\"true\" lorsque cette réponse rejoue une requête antérieure portant la même Idempotency-Key.",
        "schema": {
          "type": "string",
          "enum": [
            "true"
          ]
        }
      },
      "WWWAuthenticate": {
        "description": "Demande d'authentification RFC 6750 envoyée avec chaque 401. Pour un 401 renvoyé par un gestionnaire de route plutôt que par la passerelle, API Gateway le transmet renommé en `x-amzn-Remapped-WWW-Authenticate`. Une clé API invalide, révoquée ou expirée, ou un jeton de session incorrect ou révoqué (INVALID_API_KEY) : `Bearer realm=\"ProofSnap API\", error=\"invalid_token\"`. Un en-tête Authorization absent ou mal formé (MISSING_API_KEY) : `Bearer realm=\"ProofSnap API\"`.",
        "schema": {
          "type": "string"
        },
        "example": "Bearer realm=\"ProofSnap API\", error=\"invalid_token\""
      },
      "RateLimitPolicy": {
        "description": "Politique de limite de débit, conforme à l'Internet-Draft de l'IETF draft-ietf-httpapi-ratelimit-headers (forme structured-field ; fondée sur un Internet-Draft, elle peut évoluer avec celui-ci) : `\"default\";q=<requests per window>;w=60`.",
        "schema": {
          "type": "string"
        },
        "example": "\"default\";q=120;w=60"
      },
      "RateLimit": {
        "description": "État actuel de la limite de débit, conformément à l'Internet-Draft de l'IETF draft-ietf-httpapi-ratelimit-headers (fondé sur l'Internet-Draft) : `\"default\";r=<requests left>;t=<seconds until the window resets>`. Envoyé sur les réponses des routes soumises à une limite de débit (captures unitaires et par lot, compare, redact, certify-hash) une fois le limiteur passé, pas seulement sur 429 (une requête refusée avant le limiteur, par exemple pour l'authentification, n'en a pas).",
        "schema": {
          "type": "string"
        },
        "example": "\"default\";r=117;t=42"
      }
    },
    "parameters": {
      "CaptureId": {
        "name": "capture_id",
        "in": "path",
        "required": true,
        "description": "Identifiant de capture, de certificat, de comparaison, de caviardage ou de paquet importé. Les identifiants commencent par cap_, cert_, cmp_, red_ ou imp_. Un identifiant mal formé renvoie 404 CAPTURE_NOT_FOUND.",
        "schema": {
          "type": "string",
          "example": "cap_a1B2c3D4e5F6g7H8"
        }
      },
      "ScheduleId": {
        "name": "schedule_id",
        "in": "path",
        "required": true,
        "description": "Identifiant de planification. Une planification d'un autre compte renvoie 404 SCHEDULE_NOT_FOUND.",
        "schema": {
          "type": "string",
          "example": "sch_a1B2c3D4e5F6g7H8"
        }
      },
      "MatterId": {
        "name": "matter_id",
        "in": "path",
        "required": true,
        "description": "Identifiant de l'affaire. Une affaire dont vous n'êtes pas membre renvoie 404 MATTER_NOT_FOUND.",
        "schema": {
          "type": "string",
          "example": "mat_a1B2c3D4e5F6g7H8"
        }
      },
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": false,
        "description": "Clé client facultative qui sécurise les nouvelles tentatives. Seuls les 255 premiers caractères sont utilisés, et les clés sont rattachées à votre compte pendant 24 heures. Une nouvelle tentative avec la même clé et le même corps renvoie le corps de la réponse d'origine avec `Idempotent-Replay: true` (202 pour une capture unitaire, un lot ou une comparaison, 201 pour un caviardage ; `status` est actualisé avec le statut actuel de la capture, de la comparaison ou du caviardage, et de chaque capture d'un lot) ; aucune nouvelle capture n'est créée ni facturée. La même clé avec un corps différent renvoie 422 IDEMPOTENCY_KEY_REUSED. Une nouvelle tentative alors que la première requête est encore en cours d'acceptation renvoie 409 IDEMPOTENCY_IN_PROGRESS avec l'identifiant d'origine dans `error.details.capture_id` (le batch_id pour un lot).",
        "schema": {
          "type": "string",
          "maxLength": 255
        }
      },
      "SubscriptionId": {
        "name": "subscription_id",
        "in": "path",
        "required": true,
        "description": "Identifiant d'abonnement webhook. Un abonnement d'un autre compte renvoie 404 WEBHOOK_SUBSCRIPTION_NOT_FOUND.",
        "schema": {
          "type": "string",
          "example": "whs_a1B2c3D4e5F6g7H8"
        }
      }
    },
    "responses": {
      "RateLimited": {
        "description": "RATE_LIMITED : trop de requêtes dans la fenêtre de 60 secondes en cours. `error.details.retry_after_seconds` reprend Retry-After. RateLimit indique r=0 et le nombre de secondes avant la réinitialisation de la fenêtre.",
        "headers": {
          "Retry-After": {
            "$ref": "#/components/headers/RetryAfter"
          },
          "RateLimit-Policy": {
            "$ref": "#/components/headers/RateLimitPolicy"
          },
          "RateLimit": {
            "$ref": "#/components/headers/RateLimit"
          },
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimitLimit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimitRemaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimitReset"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "RATE_LIMITED",
                "message": "Too many requests. See Retry-After header.",
                "details": {
                  "retry_after_seconds": 42
                }
              },
              "request_id": "..."
            }
          }
        }
      },
      "InternalError": {
        "description": "INTERNAL_ERROR : erreur serveur inattendue. Réessayez avec un délai exponentiel.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "AuthUnavailable": {
        "description": "SERVICE_UNAVAILABLE : la passerelle n'a pas pu vérifier les informations d'authentification (défaillance temporaire du backend). Non mis en cache : réessayez avec un délai progressif.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "SERVICE_UNAVAILABLE",
                "message": "Authentication is temporarily unavailable. Please retry."
              },
              "request_id": "..."
            }
          }
        }
      },
      "EidasOrAuthUnavailable": {
        "description": "EIDAS_UNAVAILABLE : include_eidas a été demandé, mais l'horodatage qualifié n'est pas configuré sur ce déploiement ; réessayez sans include_eidas. SERVICE_UNAVAILABLE : la passerelle n'a pas pu vérifier les informations d'authentification (panne temporaire du backend, non mise en cache) ; réessayez avec un délai progressif.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Unauthorized": {
        "description": "401, deux codes. MISSING_API_KEY : l'en-tête Authorization est absent ou mal formé (`WWW-Authenticate: Bearer realm=\"ProofSnap API\"`). INVALID_API_KEY : les informations d'authentification ont été envoyées mais ne sont pas valides : clé API inconnue, révoquée ou expirée, compte sans accès à l'API, ou jeton de session invalide ou révoqué (`WWW-Authenticate: Bearer realm=\"ProofSnap API\", error=\"invalid_token\"`). Ne réessayez pas avec le même identifiant. 403 signifie que l'identifiant est valide mais n'autorise pas cette opération (FORBIDDEN, restrictions du forfait).",
        "headers": {
          "WWW-Authenticate": {
            "$ref": "#/components/headers/WWWAuthenticate"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "missing": {
                "value": {
                  "error": {
                    "code": "MISSING_API_KEY",
                    "message": "Missing or malformed Authorization header. Send Authorization: Bearer <your API key>."
                  },
                  "request_id": "..."
                }
              },
              "invalid": {
                "value": {
                  "error": {
                    "code": "INVALID_API_KEY",
                    "message": "The API key is invalid, revoked or expired, or the account has no API access."
                  },
                  "request_id": "..."
                }
              }
            }
          }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "description": "Toutes les erreurs ont cette forme. Les réponses d'erreur portent l'en-tête `X-Request-ID` avec la même valeur que `request_id`.\n\nTaille de la requête : un corps de requête peut faire au plus 6 Mo (la limite de payload Lambda ; API Gateway lui-même s'arrête à 10 Mo). Un corps plus volumineux est refusé avant d'atteindre le code de l'API, de sorte que cette réponse n'a pas cette forme.\n\n| HTTP | code | Signification |\n| --- | --- | --- |\n| 400 | `MALFORMED_REQUEST` | Le corps n'est pas du JSON valide (message \"Request body is not valid JSON\"), ou la requête est invalide : lorsque `details.reason` indique quel champ et pourquoi, `message` reprend ce motif, sinon il vaut \"The request is invalid.\" |\n| 400 | `MISSING_FIELD` | Un champ obligatoire est manquant. |\n| 400 | `INVALID_URL` | L'URL n'est pas une URL http(s) valide, dépasse 2 048 caractères, ou un webhook_url n'est pas en https. |\n| 400 | `INVALID_HASH` | certify-hash : une empreinte ne fait pas 64 caractères hexadécimaux. |\n| 400 | `INVALID_TIMESTAMP_TYPE` | options.timestamp_type ne vaut pas \"bitcoin\". |\n| 400 | `INVALID_VIEWPORT` | options.viewport ne vaut ni \"desktop\" ni \"mobile\". |\n| 400 | `METADATA_TOO_LARGE` | options.metadata comporte plus de 10 clés, une valeur qui n'est pas une chaîne ou une valeur de plus de 500 caractères. |\n| 400 | `INVALID_EVIDENCE_PACK_OPTIONS` | options.evidence_pack est invalide. |\n| 400 | `INVALID_BATCH` | Le champ urls du lot n'est pas un tableau de 2 à 50 URL valides. |\n| 400 | `REDACTION_NO_OPS` | Le caviardage ne comporte aucune opération, ou aucune n'a trouvé de correspondance. |\n| 400 | `INVALID_CRON` | cron_expr n'a pas pu être analysé. |\n| 400 | `INTERVAL_TOO_SHORT` | La cadence de la planification est inférieure au minimum du forfait. |\n| 400 | `RATE_LIMIT_COST_TOO_HIGH` | La requête nécessite plus d'unités de limite de débit qu'une fenêtre n'en autorise ; `details.max_per_window`, `details.window_seconds`, `details.requested`. Divisez-la. |\n| 400 | `WEBHOOK_REQUIRES_API_KEY` | Un `webhook_url` a été envoyé depuis une session du tableau de bord. Les webhooks sont signés avec le secret de signature du webhook d'une clé API ; ils nécessitent donc une clé API. |\n| 401 | `MISSING_API_KEY` | En-tête Authorization absent ou mal formé. `WWW-Authenticate: Bearer realm=\"ProofSnap API\"`. |\n| 401 | `INVALID_API_KEY` | Clé inconnue, révoquée ou expirée, pas d'accès à l'API, ou jeton de session invalide ou révoqué. `WWW-Authenticate: Bearer realm=\"ProofSnap API\", error=\"invalid_token\"`. |\n| 402 | `QUOTA_EXCEEDED` | Maximum mensuel ou quota du Sandbox atteint (figure aussi dans le `error` d'une certification en arrière-plan refusée pour ce motif). |\n| 402 | `PAYMENT_REQUIRED` | L'abonnement live est en retard de paiement ou impayé (figure aussi dans le `error` d'une capture planifiée ignorée pour ce motif). |\n| 403 | `FORBIDDEN` | La ressource appartient à quelqu'un d'autre, ou l'action est réservée au propriétaire. Message : \"You do not have access to this resource.\" (details.reason donne des précisions lorsque c'est utile). |\n| 403 | `EMAIL_NOT_VERIFIED` | Une session du tableau de bord dont l'adresse e-mail n'est pas confirmée a tenté de créer quelque chose de persistant, de payant ou qui invite des personnes (captures, lots, comparaisons, caviardages, clés API, secrets de signature du webhook, paiement, changements de forfait, changements de sièges, certify-hash, planifications, invitations au cabinet, affaires et toute modification de celles-ci, membres d'affaires, captures rattachées à des affaires, partages et leur révocation, sceaux, exports, conservations pour litige). Les lectures restent possibles. Message : \"Confirm your email address first. Use the link we sent you, or send a new one from the dashboard.\" Une clé API est refusée de la même façon lorsque l'adresse e-mail de son compte n'est pas confirmée. |\n| 403 | `EIDAS_REQUIRES_LIVE_KEY` | include_eidas envoyé avec une clé de test (une clé ps_live_ d'un compte sans compte de facturation associé compte comme telle), ou pour une comparaison ou un caviardage d'une capture d'essai (`details.reason`). Une clé live ou une session qui s'exécute en mode test faute de compte de facturation associé reçoit `details.reason` \"no_billing_account\". |\n| 403 | `MATTERS_NOT_ENABLED` | Les affaires nécessitent un forfait Legal. |\n| 403 | `SCHEDULE_LIMIT_REACHED` | Limite de planifications actives du forfait atteinte (ou le forfait ne comprend pas de planifications). |\n| 403 | `ON_CHANGE_REQUIRES_PLAN` | Le forfait ne permet pas la surveillance des modifications (on_change). |\n| 403 | `PLAN_ARCHIVE_ONLY` | Le forfait Archive only garde vos preuves lisibles, téléchargeables et vérifiables ; il ne crée ni captures, ni planifications, ni certifications, ni comparaisons, ni caviardages. |\n| 403 | `PLAN_NOT_AVAILABLE` | Ce compte ne peut pas choisir ce forfait : Archive only s'achète après la fin d'un abonnement API payant (tant que les preuves sont encore conservées) ou s'obtient en changeant de forfait depuis developer, startup, growth, website_archive ou legal_solo ; l'archive du cabinet uniquement par le propriétaire d'un cabinet dont l'abonnement a pris fin, pendant sa fenêtre de téléchargement. `details.reason` indique pourquoi. |\n| 403 | `PLAN_API_READ_ONLY` | Forfait Website Archive : la clé API est en lecture seule (liste, téléchargement, vérification, webhooks). Créez les captures, planifications et certifications dans le tableau de bord, ou utilisez un forfait API. |\n| 403 | `LEGAL_HOLD_OWNER_ONLY` | Seul le propriétaire du cabinet lève la conservation pour litige d'une capture du cabinet (un membre du cabinet peut la placer). |\n| 403 | `LEGAL_HOLD_NOT_AVAILABLE` | Placer en conservation pour litige nécessite un forfait payant (`details.reason` \"plan\" : le Sandbox gratuit, ou un compte dont l'abonnement a pris fin) et une capture live (`details.reason` \"test_capture\" : une capture en mode test filigranée). La levée d'une conservation pour litige n'est jamais refusée pour ce motif. |\n| 403 | `IMPORT_NOT_ALLOWED` | L'importation de paquets nécessite un forfait Legal (`details.reason` \"plan\") et une clé API live ou une session du tableau de bord avec facturation associée (`details.reason` \"live_credentials_required\"). |\n| 404 | `CAPTURE_NOT_FOUND` | La capture n'existe pas, a été supprimée, ou l'identifiant est mal formé. |\n| 404 | `MATTER_NOT_FOUND` | L'affaire n'existe pas ou vous n'en êtes pas membre. |\n| 404 | `IMPORT_NOT_FOUND` | L'importation n'existe pas, appartient à un autre compte, ou l'identifiant est mal formé. |\n| 404 | `WEBHOOK_SUBSCRIPTION_NOT_FOUND` | L'abonnement webhook n'existe pas, a été supprimé, ou appartient à un autre compte. |\n| 404 | `SCHEDULE_NOT_FOUND` | La planification n'existe pas ou appartient à un autre compte. |\n| 404 | `NOT_FOUND` | Autre ressource introuvable (format d'empreinte du registre d'émission, jeton de partage, tâche d'export, membre). |\n| 404 | `EXPORT_JOB_NOT_FOUND` | Une tâche d'export de captures n'existe pas, appartient à un autre compte, a expiré, ou a été demandée avec une clé de test. |\n| 405 | `METHOD_NOT_ALLOWED` | La méthode HTTP n'est pas autorisée pour cette route. |\n| 409 | `CAPTURE_PROCESSING` | La capture est encore en cours de traitement et ne peut pas encore être supprimée (ni voir sa conservation pour litige modifiée). |\n| 409 | `CAPTURE_LEGAL_HOLD` | La capture est sous conservation pour litige et ne peut pas être supprimée. |\n| 409 | `CAPTURE_IN_TRASH` | Conservation pour litige : la capture est dans la corbeille ; restaurez-la d'abord. |\n| 409 | `CAPTURE_HOLD_GRACE` | Suppression définitive pendant le délai de grâce suivant la levée de la conservation pour litige de la capture ; `details.until`. Le passage à la corbeille est autorisé. |\n| 409 | `LEGAL_HOLD_MATTER` | La conservation de la capture a été placée par la conservation pour litige de son affaire, toujours active : levez-la sur l'affaire (`details.matter_id` pour un lecteur de l'affaire). |\n| 409 | `LEGAL_HOLD_SOURCE` | La conservation d'un caviardage ou d'une comparaison suit une capture dont il est issu (pour une comparaison, l'une ou l'autre des captures comparées), qui est toujours sous conservation pour litige (placée sur cette capture ou via son affaire) : le paquet reste en conservation jusqu'à la levée de celle-ci (`details.source_capture_id`). |\n| 409 | `COMPANY_READ_ONLY` | L'abonnement du cabinet a pris fin (ou il est conservé comme archive du cabinet) : ses affaires peuvent être ouvertes et exportées et leurs liens de partage révoqués, rien d'autre, et rien de nouveau n'est dérivé de ses captures. |\n| 409 | `WEBHOOK_SUBSCRIPTION_LIMIT` | Limite d'abonnements webhook actifs du forfait atteinte (25 sur les forfaits API et Legal, 10 sur Website Archive, 0 sur Archive only et le Sandbox) ; `details.limit`, `details.active`. |\n| 409 | `WEBHOOK_SECRET_ROTATION_PENDING` | Le précédent secret de signature du webhook est encore dans sa période de chevauchement de 24 heures ; `details.previous_secret_expires_at`. |\n| 409 | `IDEMPOTENCY_IN_PROGRESS` | La première requête avec cette Idempotency-Key est encore en cours d'acceptation. |\n| 409 | `COMPARE_NOT_READY` | Les deux captures doivent être terminées avant la comparaison. |\n| 409 | `REDACTION_NOT_READY` | La capture source doit être terminée avant le caviardage. |\n| 409 | `MATTER_SEALED` | L'affaire (ou l'affaire de la capture) est scellée et ne peut plus être modifiée. |\n| 409 | `MATTER_SEALING` | Un scellement de l'affaire est en cours (60 s au maximum) : entre-temps, ni rattachement de capture, ni renommage, ni changement de membre, ni passage à la corbeille d'une capture rattachée, ni second scellement. `Retry-After`. |\n| 409 | `MATTER_ARCHIVED` | L'affaire est archivée : ni nouvelles captures, ni renommages, ni nouveaux membres, ni partage, ni export ; rouvrez-la d'abord (PATCH status \"open\"). Retirer une capture ou un membre reste possible. |\n| 409 | `MATTER_CHANGED` | Le journal de traçabilité a changé pendant le scellement ; rien n'a été scellé, réessayez. |\n| 409 | `MATTER_LINKED` | Suppression de capture : la capture est une pièce d'une affaire dont l'appelant ne fait plus partie. Le propriétaire de l'affaire doit d'abord l'en retirer. |\n| 409 | `MATTER_HAS_TRASHED_CAPTURES` | Scellement d'affaire : une capture de l'affaire est dans la corbeille. Restaurez-la ou retirez-la de l'affaire, puis scellez à nouveau. |\n| 409 | `MEMBER_HAS_INDIVIDUAL_SUBSCRIPTION` | Invitation au cabinet : l'abonnement API personnel de la personne invitée est toujours reconduit. Elle doit le résilier, puis le propriétaire l'invite à nouveau. |\n| 409 | `IMPORT_LIMIT` | 3 importations sont déjà en cours (téléversement ou vérification) ; `details.limit`, `details.in_progress`. |\n| 409 | `IMPORT_STATE` | L'importation n'est pas dans un état qui le permet (complete ou cancel d'une importation dont le téléversement n'est plus en cours) ; `details.status`. |\n| 409 | `EXPORT_JOB_RUNNING` | Un export de vos captures est encore en préparation ; `details.job_id` et `details.poll_path` le désignent. Lancez-en un nouveau une fois celui-ci terminé. |\n| 413 | `IMPORT_TOO_LARGE` | Le paquet dépasse 2 Gio ; `details.max_bytes`. |\n| 413 | `VERIFY_PACKAGE_TOO_LARGE` | POST /captures/{capture_id}/verify : le paquet stocké dépasse 256 Mio ; vérifiez-le hors ligne avec les scripts fournis. `details.size_bytes`, `details.limit_bytes`. |\n| 422 | `IDEMPOTENCY_KEY_REUSED` | Idempotency-Key réutilisée avec un corps différent ou un environnement de clé différent (live ou test). |\n| 422 | `SSRF_BLOCKED` | L'URL cible une adresse privée, de bouclage, link-local ou autrement bloquée. |\n| 422 | `TARGET_UNREACHABLE` | Le nom d'hôte de l'URL n'a pas d'enregistrements DNS, ou n'a pas pu être résolu. |\n| 422 | `REDACTION_SCREENSHOT_UNREADABLE` | La capture d'écran n'a pas pu être décodée pour le caviardage par zone. |\n| 422 | `REDACTION_LEAK_DETECTED` | Un terme caviardé a subsisté après le caviardage en dehors de l'URL de capture ; rien n'a été émis. `details.files`, `details.rule_indexes`, `details.metadata_fields`, `details.in_capture_url`. |\n| 422 | `REDACTION_SOURCE_UNSUPPORTED` | Seules les captures de pages web peuvent être caviardées. `details.source` ou `details.manifest_type`. |\n| 422 | `REDACTION_SOURCE_INTEGRITY_FAILED` | Un fichier de la capture source ne correspond pas à l'empreinte SHA-256 indiquée par son manifeste signé, ou il est manquant ; rien n'a été émis. `details.mismatched`, `details.missing`. |\n| 422 | `COMPARE_PROFILE_MISMATCH` | POST /compare : les deux captures ont été réalisées avec un profil d'appareil différent ou des paramètres de capture différents ; `details.differing`, `details.before`, `details.after`. Envoyez allow_profile_mismatch: true pour comparer malgré tout (le résultat pour le texte et les images est alors indéterminé). |\n| 422 | `COMPARE_URL_MISMATCH` | POST /compare : les deux captures portent sur des pages différentes ; `details.before`, `details.after` (url et final_url sous forme d'origine et de chemin). Envoyez allow_url_mismatch: true pour comparer malgré tout. |\n| 422 | `RULE902_NOT_AVAILABLE_FOR_MISMATCH` | POST /compare : include_rule_902 pour des captures rendues différemment (allow_profile_mismatch) ; `details.differing`. |\n| 422 | `COMPARE_SOURCE_INTEGRITY_FAILED` | Un fichier d'une capture source de POST /compare ne correspond pas à l'empreinte SHA-256 indiquée par son manifeste signé, ou il est manquant ; rien n'a été comparé ni facturé. `details.capture_id`, `details.mismatched`, `details.missing`, `details.sources[]`. |\n| 422 | `REDACTION_PATTERN_TOO_SLOW` | Un motif regex de text_redactions s'est exécuté trop longtemps sur cette page (retour sur trace catastrophique) ; rien n'a été émis ni facturé. `details.rule_index`. Simplifiez le motif. |\n| 422 | `IMPORT_SIZE_MISMATCH` | Le paquet téléversé n'a pas la taille size_bytes annoncée ; le téléversement a été abandonné. `details.expected`, `details.actual`. |\n| 422 | `IMPORT_NOT_SUPPORTED` | Comparaison ou caviardage d'un paquet de preuves importé. |\n| 429 | `RATE_LIMITED` | Trop de requêtes dans la fenêtre en cours, ou un plafond quotidien a été atteint pour la création d'affaires (200), le scellement (20), l'export (50), les invitations de membres (50) ou les importations de paquets (50, `import_create`) (`details.daily_limit`, `details.action`). `Retry-After` indique quand réessayer. |\n| 429 | `CONCURRENT_CAPTURE_LIMIT` | Trop de captures du compte s'exécutent simultanément (developer 10, startup 25, growth 100, legal_solo et legal_seats 25, custom 200, website_archive 25, sandbox 5). Un lot lance autant d'URL que la limite le permet et liste les autres dans `not_admitted` ; sans emplacement libre, toute la requête est refusée. `details.limit`, `details.in_flight`, `details.requested`, `Retry-After`. |\n| 429 | `TEST_MODE_DAILY_LIMIT` | Le travail en mode test (une clé de test ou une session sans facturation associée ; filigrané, non facturé) est limité à 200 unités par compte et par jour UTC (un lot compte ses URL). `details.used`, `details.limit`, `details.reset_at`, `Retry-After`. |\n| 429 | `IMPORT_DAILY_BYTES_LIMIT` | Importation de paquets : le volume d'importation du compte pour le jour UTC (5 Gio de tailles annoncées) est atteint. `details.period`, `details.limit_bytes`, `details.used_bytes`, `details.requested_bytes`, `details.reset_at`. |\n| 429 | `IMPORT_MONTHLY_BYTES_LIMIT` | Importation de paquets : le volume d'importation du compte pour le mois UTC (20 Gio) est atteint. Mêmes détails. |\n| 500 | `INTERNAL_ERROR` | Erreur inattendue. Réessayez avec un délai progressif. |\n| 500 | `CAPTURE_FAILED` | La capture n'a pas pu être lancée ou (dans le `error` d'une capture en échec) n'a pas pu être menée à terme. |\n| 500 | `CAPTURE_TIME_BUDGET_EXCEEDED` | Dans le `error` d'une capture en échec, ou pour un caviardage synchrone : l'exécution a épuisé son budget de temps et aucun paquet n'a été émis ni facturé. |\n| 500 | `CAPTURE_SCREENSHOT_TIMEOUT` | Dans le `error` d'une capture en échec : le navigateur n'a pas terminé le rendu de la capture d'écran : toutes les méthodes de prise (en une fois, par bandes plus petites, capture d'écran propre au navigateur) ont dépassé le délai, de même que l'unique nouvelle tentative dans un navigateur relancé lorsque le budget de temps le permettait. Rien n'a été facturé ; réessayez. |\n| 500 | `CERTIFY_FAILED` | Dans le `error` d'une certification en échec : la certification n'a pas pu être menée à terme. |\n| 500 | `REDACTION_FAILED` | Dans le `error` d'un caviardage en échec : le caviardage n'a pas pu être mené à terme. Un caviardage refusé pour un motif connu porte plutôt le code de ce motif (par exemple REDACTION_LEAK_DETECTED). |\n| 500 | `TIMESTAMP_SERVICE_UNAVAILABLE` | Scellement d'affaire : l'ancrage OpenTimestamps a échoué ; rien n'a changé, réessayez. |\n| 500 | `CONFIGURATION_ERROR` | Une étape côté serveur a échoué ou n'est pas configurée (`details.reason`). |\n| 503 | `SERVICE_UNAVAILABLE` | L'authentification est temporairement indisponible (l'identifiant n'a pas pu être vérifié). Non mis en cache ; réessayez. |\n| 503 | `EIDAS_UNAVAILABLE` | include_eidas demandé, mais l'horodatage eIDAS n'est pas configuré. |\n| 503 | `EIDAS_SEAL_RETRY` | Scellement d'affaire : l'horodatage eIDAS n'est pas arrivé à temps ; rien n'a été scellé. Réessayez après `Retry-After` ; la nouvelle tentative réutilise l'horodatage déjà émis. |\n| 503 | `LIMIT_CHECK_UNAVAILABLE` | Scellement d'affaire : sa limite quotidienne n'a pas pu être vérifiée ; rien n'a été fait. Réessayez. |\n| 504 | `UPSTREAM_TIMEOUT` | Une étape de traitement synchrone (certify-hash, export d'affaire) n'a pas répondu à temps. Elle peut encore aboutir : interrogez ce que désigne `details` (`certify_id`, `poll_path`), ou réessayez. |\n| 404 | `ORG_INVITE_NOT_FOUND` | Une invitation au cabinet inconnue, adressée à quelqu'un d'autre, qui n'est plus en attente, ou dont le cabinet n'est plus actif (details.reason `company_inactive`). |\n| 409 | `ORG_INVITE_NO_SEATS` | Acceptation d'une invitation au cabinet : tous les sièges du cabinet sont occupés ; l'invitation reste ouverte. |\n| 409 | `ORG_INVITE_IN_OTHER_ORG` | Acceptation d'une invitation au cabinet : l'appelant est membre d'un autre cabinet ; l'invitation reste ouverte. |\n| 409 | `ORG_INVITE_HAS_SUBSCRIPTION` | Acceptation d'une invitation au cabinet : l'abonnement API personnel de l'appelant est toujours reconduit ; l'invitation reste ouverte. |\n| 409 | `ORG_OWNER_CANNOT_LEAVE` | Le propriétaire du cabinet a tenté de quitter son propre cabinet. |\n| 410 | `ORG_INVITE_EXPIRED` | Acceptation d'une invitation au cabinet datant de plus de 7 jours. |",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "description": "Code d'erreur lisible par une machine (UPPER_SNAKE_CASE)."
              },
              "message": {
                "type": "string",
                "description": "Message lisible par un humain."
              },
              "details": {
                "type": "object",
                "additionalProperties": true,
                "description": "Contexte supplémentaire facultatif, par exemple `reason`, `field` ou `retry_after_seconds`."
              }
            },
            "required": [
              "code",
              "message"
            ]
          },
          "request_id": {
            "type": "string",
            "description": "Identifiant de requête."
          }
        },
        "example": {
          "error": {
            "code": "INVALID_URL",
            "message": "URL is not a valid http(s) URL",
            "details": {
              "reason": "Only http and https schemes are allowed"
            }
          },
          "request_id": "..."
        }
      },
      "CaptureOptions": {
        "type": "object",
        "description": "Options de capture. Tous les champs sont facultatifs.",
        "properties": {
          "scroll_full_page": {
            "type": "boolean",
            "description": "Capture toute la page défilante. false ne capture que la zone d'affichage.",
            "default": true
          },
          "expand_collapsed": {
            "type": "boolean",
            "description": "Ouvre les sections repliées avant la capture d'écran : éléments <details> fermés, boutons d'accordéon et de divulgation (aria-expanded avec aria-controls) et contrôles textuels \"see more\" / \"show replies\". Avec garde-fous : rien à l'intérieur des navigations, en-têtes, menus ou boîtes de dialogue, aucun lien vers une autre URL, aucune soumission de formulaire, aucun libellé modifiant un état (like, follow, buy, delete, ...), chaque contrôle au plus une fois, et un contrôle qui tente de quitter la page arrête le dépliage et l'URL d'origine est rechargée. Le résultat est consigné par contrôle dans forensic_log.json (page_modifications.expand_collapsed : trouvés, ouverts, laissés fermés, motifs d'omission, requêtes lancées, hauteur de la page avant et après) avec un résumé dans metadata.json (expand_collapsed : requested, opened_total, left_closed_total) ; evidence.pdf et les certificats n'indiquent que ce qui a réellement été ouvert.",
            "default": true
          },
          "viewport": {
            "type": "string",
            "description": "desktop correspond à 1920x1080 avec un ratio de pixels de 1 ; mobile correspond à 390x844 avec un ratio de pixels de 3 et un user agent de Safari mobile. Les autres valeurs renvoient 400 INVALID_VIEWPORT.",
            "enum": [
              "desktop",
              "mobile"
            ],
            "default": "desktop"
          },
          "viewport_width": {
            "type": "number",
            "minimum": 320,
            "maximum": 3840,
            "description": "Largeur personnalisée de la zone d'affichage en pixels CSS. Doit être envoyée avec viewport_height, sinon 400 MALFORMED_REQUEST."
          },
          "viewport_height": {
            "type": "number",
            "minimum": 240,
            "maximum": 4320,
            "description": "Hauteur personnalisée de la zone d'affichage en pixels CSS. Doit être envoyée avec viewport_width."
          },
          "device_scale_factor": {
            "type": "number",
            "minimum": 1,
            "maximum": 3,
            "description": "Rapport de pixels de l'appareil pour la capture d'écran."
          },
          "wait_for_selector": {
            "type": "string",
            "description": "Attend jusqu'à 10 secondes l'apparition de ce sélecteur CSS avant la capture. S'il n'apparaît pas, la capture se poursuit sans erreur. Enregistré dans forensic_log.json page_modifications.",
            "maxLength": 200
          },
          "wait_ms": {
            "type": "number",
            "minimum": 0,
            "maximum": 15000,
            "description": "Attente supplémentaire en millisecondes après le chargement de la page."
          },
          "screenshot_format": {
            "type": "string",
            "description": "jpeg fournit screenshot.jpeg ; png fournit un screenshot.png sans perte. Les Content Credentials C2PA ne sont ajoutés qu'aux captures d'écran JPEG.",
            "enum": [
              "jpeg",
              "png"
            ],
            "default": "jpeg"
          },
          "url_stamp": {
            "type": "boolean",
            "description": "Ajoute au-dessus de l'image de la page un bandeau d'en-tête indiquant l'URL capturée, l'heure de capture UTC (le moment où le navigateur a produit la capture d'écran ; pour une page longue prise par bandes, le moment où la dernière bande a été produite) et l'identifiant de preuve, étiqueté comme annotation logicielle. Le bandeau est ajouté en haut de l'image, sans recouvrir aucun pixel de la page. chain_of_custody.json consigne screenshot_url_header.raw_screenshot_sha256 avec raw_screenshot_source : pour une page prise d'un seul tenant (browser_image, browser_png, browser_jpeg_joined, browser_jpeg_reencoded), l'empreinte SHA-256 de l'image de la page produite par le navigateur, avant le bandeau ; pour une page longue prise par bandes puis assemblée (joined_bands, joined_bands_lossless, joined_bands_reencoded), l'empreinte SHA-256 de l'image assemblée telle que ProofSnap l'a écrite avec le bandeau, avant tout Content Credentials C2PA. Lorsque le bandeau ne peut pas être ajouté (avec lui, l'image dépasserait les 65 535 px qu'un JPEG peut contenir, son tracé a échoué ou, uniquement sur le chemin de capture d'écran de secours, l'image de la page dépasse 32 000 000 pixels), la capture d'écran est livrée sans lui : metadata.json le consigne dans screenshot.url_stamp_skipped et chain_of_custody.json dans screenshot_url_header (applied false, avec le motif).",
            "default": false
          },
          "network_log": {
            "type": "boolean",
            "description": "Inclut network.har (HAR avec les en-têtes de requête et de réponse et les temps, sans les corps) et network.original.har.",
            "default": true
          },
          "warc": {
            "type": "boolean",
            "description": "Inclut capture.warc (ISO 28500) avec les transactions HTTP du chargement de la page. Les corps de réponse sont lus dans un délai limité (30 s au maximum, et seulement tant que la partie facultative du budget de temps de la capture le permet, après le temps réservé à eIDAS, à C2PA et à la lecture finale de l'horloge) ; un corps interrompu par cette limite est stocké vide avec WARC-Truncated: time, une réponse non atteinte est omise, et warc_captured dans chain_of_custody.json (ainsi que warc dans forensic_log.json) consigne truncated, truncation_reason, response_pairs, records_total et les décomptes.",
            "default": false
          },
          "external_archive": {
            "type": "boolean",
            "description": "Demande au service Save Page Now de l'Internet Archive d'archiver l'URL (l'Internet Archive conserve cette copie publiquement). Le résultat est classé selon la requête à l'origine de l'instantané et selon son heure par rapport à la requête : created (créé pour cette requête), recent_existing (l'Internet Archive a répondu avec un instantané antérieur), created_after_request (un instantané plus récent existe mais la requête n'a pas été confirmée), existing (seul un instantané plus ancien existe), pending, rate_limited, failed (l'Internet Archive a refusé la requête), unconfirmed (aucune réponse ne l'a confirmée ni refusée et aucun instantané n'est connu), unavailable, skipped, skipped_sensitive ou skipped_query. Les URL qui semblent contenir des identifiants de connexion, ou qui portent des paramètres, noms de paramètres ou segments de chemin ressemblant à des jetons, ne sont jamais envoyées. Une URL avec une chaîne de requête n'est envoyée que si cette option est explicitement définie à true ; avec la valeur par défaut, elle est ignorée (skipped_query). Le résultat est consigné dans forensic_log.json (external_archive, avec snapshot_offset_seconds par rapport à la capture) et dans chain_of_custody.json, et renvoyé comme external_archive par GET /captures/{capture_id}. Définissez false pour les URL privées. La requête est envoyée dès que la page est chargée, en parallèle du reste de la capture. Un échec ne fait pas échouer la capture.",
            "default": true
          },
          "dismiss_consent": {
            "type": "boolean",
            "description": "Définit des cookies de consentement « tout accepter » avant le chargement de la page et tente de fermer les bannières de consentement aux cookies. Cela modifie la page, l'option est donc désactivée par défaut ; ce qui a été fait est consigné dans forensic_log.json consent_dismissal.",
            "default": false
          },
          "dismiss_popups": {
            "type": "boolean",
            "description": "Ferme les fenêtres surgissantes de newsletter, de promotion et similaires au moyen de leur propre bouton de fermeture. Le nombre de fenêtres fermées et les boutons utilisés sont consignés dans forensic_log.json page_modifications. Chaque fenêtre fermée est consignée dans forensic_log.json page_modifications (popups_dismissed, popups_dismissed_controls) ; evidence.pdf indique combien ont été fermées.",
            "default": false
          },
          "dark_mode": {
            "type": "boolean",
            "description": "Rendu avec le jeu de couleurs sombre. Modifie l'apparence de la page ; enregistré dans forensic_log.json page_modifications. Signalé dans evidence.pdf et dans forensic_log.json page_modifications.",
            "default": false
          },
          "block_ads": {
            "type": "boolean",
            "description": "Bloque les requêtes vers les hôtes publicitaires et de traçage connus. Omet du contenu ; consigné dans forensic_log.json page_modifications.",
            "default": false
          },
          "block_resources": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "image",
                "font",
                "media",
                "stylesheet"
              ]
            },
            "description": "Types de ressources à bloquer. Omet du contenu ; enregistré dans forensic_log.json page_modifications."
          },
          "inject_css": {
            "type": "string",
            "description": "CSS ajouté à la page avant la capture. Modifie la page ; stocké tel quel dans forensic_log.json page_modifications. Toujours stocké tel quel dans forensic_log.json page_modifications (injected_css) avec injected_css_status `applied` ou `failed` et injected_css_error. evidence.pdf indique qu'un CSS personnalisé a été injecté ; il n'imprime pas le code.",
            "maxLength": 10000
          },
          "inject_js": {
            "type": "string",
            "description": "JavaScript exécuté dans la page avant la capture. Modifie la page. Toujours stocké tel quel dans forensic_log.json page_modifications (injected_js) avec injected_js_status et injected_js_error : `applied` (il s'est exécuté jusqu'au bout), `invalid` (il n'a pas été compilé, par exemple en raison d'une erreur de syntaxe, et ne s'est donc jamais exécuté ni n'a modifié la page), `failed` (il a levé une exception pendant son exécution et a donc pu modifier la page avant de s'arrêter) ou `timed_out` (la capture a cessé de l'attendre au bout de 5 secondes ; le script a pu continuer à s'exécuter et à modifier la page). evidence.pdf indique le résultat et renvoie à forensic_log.json, et le certificat de provenance liste le code injecté parmi les modifications de la page, sauf s'il était `invalid` ; aucun des deux n'imprime le code.",
            "maxLength": 10000
          },
          "locale": {
            "type": "string",
            "description": "Paramètres régionaux du navigateur à émuler, par exemple de ou de-DE (définit la langue du navigateur et Accept-Language). Émulation uniquement : aucun proxy n'est utilisé, de sorte que le contenu qui dépend de l'IP du visiteur reflète toujours la région du serveur de capture. Consigné dans metadata.json geo.",
            "pattern": "^[a-z]{2}(-[A-Z]{2})?$"
          },
          "timezone": {
            "type": "string",
            "description": "Fuseau horaire du navigateur à émuler (nom IANA, par exemple Europe/Berlin). Émulation uniquement."
          },
          "language": {
            "type": "string",
            "description": "Langue des PDF lisibles par un humain. L'une des valeurs suivantes : en, de, it, fr, es, pt_BR, cs, el, hi, hu, id, ja, ko, nl, pl, ro, ru, sv, tr, vi, zh_CN, zh_TW, ar, he, da, fi, nb, sk, uk. La casse, le trait d'union et la région sont tolérés (\"DE\", \"fr-FR\" et \"PT-BR\" sont acceptés) ; \"pt\" et \"zh\" seuls sont rejetés. Une valeur non prise en charge renvoie 400 MALFORMED_REQUEST.",
            "default": "en",
            "example": "de"
          },
          "timestamp_type": {
            "type": "string",
            "description": "Seule la valeur \"bitcoin\" est acceptée (l'ancrage OpenTimestamps est toujours tenté). Toute autre valeur renvoie 400 INVALID_TIMESTAMP_TYPE. Utilisez include_eidas pour un horodatage qualifié.",
            "enum": [
              "bitcoin"
            ],
            "default": "bitcoin"
          },
          "include_eidas": {
            "type": "boolean",
            "description": "Demande également un horodatage qualifié eIDAS (RFC 3161) sur l'empreinte SHA-256 de manifest.json, livré sous la forme manifest.json.tsr (plus eidas_validation.json lorsque des données de validation sont renvoyées). Clés live uniquement : une clé de test renvoie 403 EIDAS_REQUIRES_LIVE_KEY. Si aucun service eIDAS n'est configuré sur le déploiement, la requête est rejetée avec 503 EIDAS_UNAVAILABLE avant toute facturation. Avant d'utiliser un jeton, ProofSnap le contrôle (signature, empreinte qu'il porte, chaîne de certificats jusqu'à la racine CA Disig QCA4, statut du certificat de la TSA) et consigne ce contrôle dans eidas_validation.json (verification). Si la demande d'horodatage échoue pendant la capture, ou si le jeton ne passe pas ce contrôle, le paquet est livré sans lui, avec le seul ancrage OpenTimestamps, l'horodatage n'est pas facturé et timestamps.eidas (ligne de capture, GET /captures/{capture_id}, webhook completed) indique le statut not_obtained avec le motif (token_refused, unavailable ou time_budget). À partir de la capture d'écran, les étapes facultatives de la capture (corps WARC, réponses de l'archive et WHOIS, signature C2PA, seconde lecture de l'horloge) laissent du temps à l'horodatage et sont abandonnées en premier. Facturé par horodatage.",
            "default": false
          },
          "include_rule_902": {
            "type": "boolean",
            "description": "Ajoute rule_902_certification.pdf : un modèle de certification [FRE 902(13)/(14)](https://www.govinfo.gov/content/pkg/USCODE-2023-title28/html/USCODE-2023-title28-app-federalru-dup2-rule902.htm) et [28 U.S.C. 1746](https://www.govinfo.gov/content/pkg/USCODE-2023-title28/html/USCODE-2023-title28-partV-chap115-sec1746.htm) non signé, qu'une personne doit compléter et signer. Son empreinte SHA-256 figure dans manifest.json. Il est également ajouté lorsque evidence_pack.custodian a une valeur non vide. Si sa génération échoue, la capture se termine sans lui. Actuellement désactivé : le modèle de certification est temporairement désactivé ; ce champ est donc accepté et ignoré (aucun rule_902_certification.pdf n'est généré ni listé dans le paquet) et la réponse porte l'avertissement RULE902_DISABLED.",
            "default": false
          },
          "case_reference": {
            "allOf": [
              {
                "$ref": "#/components/schemas/PlainText"
              }
            ],
            "description": "Votre libellé d'affaire ou de dossier. Les espaces sont réduits. Suit la règle de texte brut (voir PlainText). Enregistré dans metadata.json, forensic_log.json et chain_of_custody.json, et imprimé sur evidence.pdf.",
            "maxLength": 200
          },
          "case_id": {
            "allOf": [
              {
                "$ref": "#/components/schemas/PlainText"
              }
            ],
            "description": "L'identifiant de dossier de votre système. Enregistré comme case_reference ; suit la règle de texte brut (voir PlainText).",
            "maxLength": 200
          },
          "case_note": {
            "allOf": [
              {
                "$ref": "#/components/schemas/PlainText"
              }
            ],
            "description": "Note en texte libre. Suit la règle du texte brut (voir PlainText). Consignée dans metadata.json, forensic_log.json et chain_of_custody.json (legal_context.note).",
            "maxLength": 500
          },
          "webhook_url": {
            "type": "string",
            "description": "URL https qui reçoit capture.completed ou capture.failed. Voir Webhooks. L'URL doit être en https et ne doit pas pointer vers une adresse privée, de bouclage, link-local, NAT de niveau opérateur (CGNAT) ou de métadonnées cloud (mêmes règles que pour une URL de capture ; 422 SSRF_BLOCKED sinon). Une valeur vide ou composée uniquement d'espaces signifie aucun webhook. Exige une clé API : envoyée depuis une session du tableau de bord, elle est refusée avec 400 WEBHOOK_REQUIRES_API_KEY, car les webhooks sont signés avec le secret de signature du webhook de la clé.",
            "format": "uri"
          },
          "metadata": {
            "type": "object",
            "additionalProperties": {
              "type": "string",
              "maxLength": 500
            },
            "maxProperties": 10,
            "description": "Jusqu'à 10 étiquettes de type chaîne (valeurs de 500 caractères au maximum), renvoyées avec la capture. Les dépassements renvoient 400 METADATA_TOO_LARGE."
          },
          "evidence_pack": {
            "type": "object",
            "properties": {
              "enabled": {
                "type": "boolean",
                "description": "false exclut la page de certification de evidence.pdf.",
                "default": true
              },
              "jurisdiction": {
                "type": "string",
                "description": "Enregistré dans les options de forensic_log.json. N'a aucun autre effet.",
                "enum": [
                  "US-Federal",
                  "EU-eIDAS"
                ],
                "default": "US-Federal"
              },
              "custodian": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Accepté ; non imprimé dans le modèle, dont les champs du déclarant restent vierges pour la personne qui le signe.",
                    "maxLength": 200
                  },
                  "law_firm": {
                    "type": "string",
                    "description": "Accepté ; non imprimé dans le modèle, dont les champs du déclarant restent vierges pour la personne qui le signe.",
                    "maxLength": 200
                  },
                  "role": {
                    "type": "string",
                    "description": "Accepté ; non utilisé dans le modèle.",
                    "maxLength": 200
                  },
                  "case_caption": {
                    "type": "string",
                    "description": "Accepté ; non utilisé dans le modèle.",
                    "maxLength": 200
                  }
                },
                "additionalProperties": false,
                "description": "Chaque valeur suit la règle du texte brut (voir PlainText) ; sinon 400 INVALID_EVIDENCE_PACK_OPTIONS."
              }
            },
            "description": "Les valeurs invalides renvoient 400 INVALID_EVIDENCE_PACK_OPTIONS."
          }
        }
      },
      "CaptureRequest": {
        "type": "object",
        "required": [
          "url"
        ],
        "properties": {
          "url": {
            "type": "string",
            "description": "Page à capturer. http ou https uniquement, 2 048 caractères au maximum. Les adresses privées, de bouclage (loopback), link-local et de métadonnées sont refusées avec 422 SSRF_BLOCKED, et un nom d'hôte sans enregistrement DNS renvoie 422 TARGET_UNREACHABLE.",
            "format": "uri",
            "maxLength": 2048
          },
          "options": {
            "$ref": "#/components/schemas/CaptureOptions"
          },
          "private_note": {
            "$ref": "#/components/schemas/PrivateNote"
          }
        },
        "example": {
          "url": "https://example.com",
          "options": {
            "case_reference": "Smith v. Jones",
            "webhook_url": "https://your-app.example/webhooks/proofsnap"
          }
        }
      },
      "CaptureAccepted": {
        "type": "object",
        "properties": {
          "capture_id": {
            "type": "string",
            "example": "cap_a1B2c3D4e5F6g7H8"
          },
          "status": {
            "type": "string",
            "enum": [
              "processing"
            ]
          },
          "url": {
            "type": "string"
          },
          "poll_path": {
            "type": "string",
            "description": "Chemin de statut incluant le préfixe /v1, par exemple /v1/captures/cap_..."
          },
          "message": {
            "type": "string"
          },
          "metadata": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          },
          "private_note": {
            "type": "string",
            "description": "La note privée telle que stockée (normalisée), présente uniquement si une note a été envoyée."
          },
          "warnings": {
            "$ref": "#/components/schemas/Rule902DisabledWarnings"
          }
        }
      },
      "CaptureResult": {
        "type": "object",
        "description": "Une capture, un certificat, une comparaison ou un caviardage. Les champs dépendent du statut.",
        "properties": {
          "capture_id": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "processing",
              "completed",
              "failed"
            ]
          },
          "url": {
            "type": "string",
            "description": "URL capturée (null pour les certificats et les comparaisons).",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "batch_id": {
            "type": "string",
            "nullable": true
          },
          "metadata": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          },
          "type": {
            "type": "string",
            "description": "comparison pour les comparaisons, import pour un paquet de preuves importé.",
            "enum": [
              "comparison",
              "import"
            ]
          },
          "compared": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Uniquement sur les comparaisons : les identifiants des deux captures sources."
          },
          "diff_summary": {
            "$ref": "#/components/schemas/DiffSummary"
          },
          "estimated_completion_seconds": {
            "type": "integer",
            "description": "processing uniquement : estimation approximative, au moins 5."
          },
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "CAPTURE_FAILED",
                  "CAPTURE_TIME_BUDGET_EXCEEDED",
                  "CAPTURE_SCREENSHOT_TIMEOUT",
                  "SSRF_BLOCKED",
                  "TARGET_UNREACHABLE",
                  "PAYMENT_REQUIRED",
                  "COMPARE_PROFILE_MISMATCH",
                  "COMPARE_URL_MISMATCH",
                  "RULE902_NOT_AVAILABLE_FOR_MISMATCH",
                  "COMPARE_SOURCE_INTEGRITY_FAILED",
                  "QUOTA_EXCEEDED",
                  "CERTIFY_FAILED",
                  "REDACTION_FAILED"
                ],
                "description": "Le motif public de l'échec (voir le tableau des erreurs). CAPTURE_SCREENSHOT_TIMEOUT : le navigateur n'a pas terminé la capture d'écran ; CAPTURE_TIME_BUDGET_EXCEEDED : l'exécution a épuisé la totalité de son budget de temps ; aucun des deux n'est facturé. Une ligne de comparaison (cmp_) peut aussi échouer avec COMPARE_SOURCE_INTEGRITY_FAILED (un paquet source modifié après l'acceptation de la requête), COMPARE_PROFILE_MISMATCH ou COMPARE_URL_MISMATCH (constatés uniquement dans les enregistrements scellés que l'exécution compare) ou RULE902_NOT_AVAILABLE_FOR_MISMATCH ; l'unité est restituée. Une certification d'empreinte exécutée en arrière-plan échoue avec QUOTA_EXCEEDED lorsque le maximum mensuel est atteint au moment de son exécution. CERTIFY_FAILED et REDACTION_FAILED : une certification d'empreinte ou un caviardage n'a pas pu être mené à terme ; réessayez la requête."
              },
              "message": {
                "type": "string"
              }
            },
            "description": "failed uniquement."
          },
          "failed_at": {
            "type": "string",
            "description": "failed uniquement.",
            "format": "date-time"
          },
          "captured_at": {
            "type": "string",
            "description": "Date de capture de la page (lignes terminées uniquement). Pour un caviardage, l'heure de capture de sa capture source (redacted_at est la date du caviardage).",
            "format": "date-time"
          },
          "redacted_at": {
            "type": "string",
            "format": "date-time",
            "description": "Caviardages uniquement : date de réalisation du caviardage. Le captured_at d'un caviardage est l'heure de capture de sa capture source."
          },
          "compared_at": {
            "type": "string",
            "nullable": true,
            "format": "date-time",
            "description": "Comparaisons uniquement : le moment où la comparaison a été réalisée. Son captured_at porte la même valeur (conservé pour les anciens clients) ; utilisez compared_at."
          },
          "package_url": {
            "type": "string",
            "description": "completed uniquement : lien de téléchargement présigné du ZIP du paquet, valable jusqu'à 24 heures (voir package_url_expires_at) ; récupérez de nouveau la capture pour obtenir un nouveau lien.",
            "format": "uri"
          },
          "package_url_expires_at": {
            "type": "string",
            "description": "completed uniquement : moment où package_url cesse de fonctionner, au plus 24 heures après cette réponse (un lien signé avec des identifiants temporaires du serveur peut cesser de fonctionner plus tôt) ; récupérez de nouveau la capture pour obtenir un nouveau lien.",
            "format": "date-time"
          },
          "verification_url": {
            "type": "string",
            "description": "completed uniquement : lien vers le vérificateur public ProofSnap.",
            "format": "uri"
          },
          "manifest_hash": {
            "type": "string",
            "description": "completed uniquement : sha256:<hex> de manifest.json.",
            "example": "sha256:3f2a..."
          },
          "timestamps": {
            "$ref": "#/components/schemas/Timestamps"
          },
          "c2pa": {
            "type": "object",
            "properties": {
              "included": {
                "type": "boolean"
              },
              "signed_screenshot_sha256": {
                "type": "string",
                "description": "sha256:<hex> de la capture d'écran signée.",
                "nullable": true
              }
            },
            "description": "completed uniquement."
          },
          "file_count": {
            "type": "integer",
            "description": "completed uniquement."
          },
          "size_bytes": {
            "type": "integer",
            "description": "completed uniquement."
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "code": {
                  "type": "string",
                  "enum": [
                    "consent_gated"
                  ]
                },
                "reason": {
                  "type": "string",
                  "nullable": true
                },
                "final_host": {
                  "type": "string",
                  "nullable": true
                },
                "final_url": {
                  "type": "string",
                  "nullable": true
                },
                "message": {
                  "type": "string"
                },
                "remedy": {
                  "type": "object",
                  "properties": {
                    "option": {
                      "type": "string"
                    },
                    "value": {
                      "type": "boolean"
                    }
                  }
                }
              }
            },
            "description": "completed uniquement, lorsque la capture est tombée sur un mur de consentement aux cookies ou une page interstitielle au lieu de la page. Relancez avec dismiss_consent: true."
          },
          "external_archive": {
            "allOf": [
              {
                "$ref": "#/components/schemas/ExternalArchive"
              }
            ],
            "description": "completed uniquement. La demande Internet Archive effectuée avec l'option external_archive : le résultat actuel (status), l'état scellé dans le paquet (status_at_sealing) et le lien de l'instantané. { requested: false } lorsqu'aucune demande n'a été effectuée."
          },
          "storage": {
            "$ref": "#/components/schemas/CaptureStorage"
          },
          "private_note": {
            "type": "string",
            "nullable": true,
            "description": "Votre note privée (voir PrivateNote). Renvoyée uniquement à l'auteur de la capture ; ne fait jamais partie du paquet de preuves."
          },
          "legal_hold": {
            "type": "boolean",
            "description": "La capture est en conservation pour litige : elle n'est pas supprimée tant que la valeur est true (PATCH /captures/{capture_id} avec l'action legal_hold). Conservation levée automatiquement, et capture supprimée, lorsque l'abonnement a pris fin et que ses 30 jours se sont écoulés sans forfait qui conserve les données."
          },
          "legal_hold_set_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "Date à laquelle la conservation pour litige actuelle a été placée ; null sans conservation."
          },
          "legal_hold_reason": {
            "type": "string",
            "nullable": true,
            "description": "Le motif stocké avec la conservation pour litige actuelle ; null sans conservation ou sans motif."
          },
          "legal_hold_set_by_email": {
            "type": "string",
            "nullable": true,
            "description": "Propriétaire du cabinet uniquement, sur une capture du cabinet en conservation pour litige : l'adresse e-mail de la personne qui l'a placée en conservation pour litige."
          },
          "legal_hold_source": {
            "type": "string",
            "nullable": true,
            "description": "Origine de la conservation actuelle : \"manual\" (PATCH /captures/{capture_id}), \"bulk\" (POST /legal-holds), \"matter:<matter_id>\" (la conservation pour litige de l'affaire) ou \"derived:<capture_id>\" (un caviardage ou une comparaison en conservation parce qu'une capture dont il est issu est en conservation). null sans conservation."
          },
          "legal_hold_released_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "Date de la dernière levée de conservation (début du délai de grâce après levée) ; null tant que la conservation est active ou si elle n'a jamais été levée, ainsi qu'après la levée automatique à la fin d'un abonnement (sans délai de grâce)."
          },
          "deletion_blocked_until": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "Fin du délai de grâce après une levée : jusque-là, la capture n'est pas supprimée définitivement (DELETE définitif 409 CAPTURE_HOLD_GRACE, pas de purge de la corbeille, pas de suppression au titre de la durée de conservation) ; son déplacement dans la corbeille est possible. La suppression du compte et du cabinet après la fin de l'abonnement ne l'attend pas. null lorsqu'aucun délai de grâce n'est en cours (également pendant une conservation pour litige : celle-ci bloque sans échéance)."
          },
          "s3_legal_hold": {
            "type": "object",
            "nullable": true,
            "description": "La conservation pour litige S3 Object Lock des objets du paquet stocké (ZIP, preuve OpenTimestamps mise à niveau et double stocké séparément, toutes versions). status : on, off, pending (en cours d'application), failed (relancée par le serveur), unsupported (le stockage n'a pas de configuration Object Lock ; la conservation dans l'enregistrement de la capture s'applique toujours), none (aucun paquet n'est stocké, par exemple pour une capture échouée).",
            "properties": {
              "status": {
                "type": "string",
                "enum": [
                  "on",
                  "off",
                  "pending",
                  "failed",
                  "unsupported",
                  "none"
                ]
              },
              "at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              }
            }
          },
          "source_held": {
            "type": "boolean",
            "nullable": true,
            "description": "Caviardages et comparaisons : true lorsqu'une capture dont ils sont issus est en conservation pour litige. Un paquet dérivé suit la conservation de sa source : tant qu'une source est en conservation, le caviardage ou la comparaison l'est aussi (legal_hold_source derived:<capture_id>) et sa conservation ne peut pas être levée isolément (409 LEGAL_HOLD_SOURCE). null pour les autres captures."
          },
          "import": {
            "$ref": "#/components/schemas/CaptureImport"
          },
          "source": {
            "type": "string",
            "description": "Mode de réalisation de la capture : api, schedule, certify-hash, compare, redact, import. GET /captures/{capture_id} ne le renvoie que pour un paquet importé (\"import\") ; les lignes de GET /captures le portent toujours."
          }
        }
      },
      "Timestamps": {
        "type": "object",
        "properties": {
          "bitcoin_ots": {
            "type": "object",
            "properties": {
              "status": {
                "type": "string",
                "description": "État de la preuve Bitcoin OpenTimestamps dans manifest.json.ots. pending_confirmation : la preuve attend un bloc Bitcoin (généralement quelques heures ; les vérifications commencent environ 2 heures après la fin de la capture et se répètent à intervalles croissants). verified : un bloc Bitcoin atteste l'empreinte du manifeste (block_height, block_time). not_present : le paquet ne contient pas de manifest.json.ots (les calendriers étaient injoignables lors du scellement du paquet). unconfirmed : aucune confirmation n'a été trouvée au bout de 7 jours de vérification ; la preuve du paquet peut encore être mise à niveau et vérifiée avec le client OpenTimestamps (ots upgrade, ots verify). package_missing : le paquet de preuves ne se trouve plus dans le stockage, la preuve ne peut donc plus être vérifiée. manifest_mismatch : le manifest.json du paquet stocké ne correspond pas à l'empreinte du manifeste enregistrée lors du scellement ; l'horodatage n'est pas confirmé pour celui-ci. Ces deux états sont définitifs. Le paquet lui-même n'est jamais modifié. not_checked : la capture s'est terminée avant l'existence des vérifications automatiques de confirmation et n'a jamais été vérifiée ; son paquet peut encore contenir un manifest.json.ots que le client OpenTimestamps peut mettre à niveau et vérifier. Les webhooks et les réponses de création sont envoyés à la fin de la capture ; ils indiquent donc pending_confirmation (ou not_present lorsque les calendriers ont échoué) ; consultez GET /captures/{capture_id} pour connaître l'état actuel. Un paquet est scellé avec la preuve en attente : à ce moment, l'empreinte du manifeste a été soumise aux calendriers OpenTimestamps. Une fois la confirmation obtenue dans le bloc Bitcoin {block_height} ({block_time} UTC), la preuve montre que manifest.json existait au plus tard à ce moment ; téléchargez la preuve mise à niveau (upgraded_proof_url) et placez-la à côté de manifest.json sous le nom manifest.json.upgraded.ots : verify.sh et verify.ps1 la vérifient automatiquement.",
                "enum": [
                  "pending_confirmation",
                  "verified",
                  "not_present",
                  "unconfirmed",
                  "package_missing",
                  "manifest_mismatch",
                  "not_checked"
                ]
              },
              "block_height": {
                "type": "integer",
                "nullable": true,
                "description": "Hauteur du bloc Bitcoin qui atteste la preuve. Renseignée lorsque status vaut verified."
              },
              "block_time": {
                "type": "string",
                "format": "date-time",
                "nullable": true,
                "description": "Heure de ce bloc Bitcoin. GET /captures/{capture_id} uniquement, défini lorsque status vaut verified."
              },
              "verified_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true,
                "description": "Date à laquelle ProofSnap a confirmé l'ancrage. GET /captures/{capture_id} uniquement, défini lorsque status vaut verified."
              },
              "upgraded_proof_url": {
                "type": "string",
                "format": "uri",
                "nullable": true,
                "description": "GET /captures/{capture_id} uniquement. Une fois l'ancrage vérifié, ProofSnap stocke la preuve OpenTimestamps mise à niveau à partir des calendriers (elle porte l'attestation du bloc Bitcoin et se vérifie donc hors ligne sans contacter aucun calendrier). Il s'agit d'un téléchargement présigné de celle-ci, valable jusqu'à 24 heures comme package_url ; récupérez de nouveau la capture pour obtenir un nouveau lien. null lorsqu'il n'y en a pas (pas encore vérifiée, ou stockée plus tard par un rattrapage en arrière-plan). Le paquet scellé n'est jamais modifié : enregistrez le fichier à côté du manifest.json du paquet sous le nom manifest.json.upgraded.ots. Les paquets scellés à partir du 2026-09-29 la vérifient avec leurs scripts fournis (verification/verify.sh et verify.ps1 l'utilisent à la place de manifest.json.ots) ; pour un paquet plus ancien, exécutez `ots verify manifest.json.upgraded.ots -f manifest.json`. Les exports d'affaires la contiennent sous captures/<capture_id>.upgraded.ots à côté de captures/<capture_id>.zip. Supprimée en même temps que le paquet."
              }
            }
          },
          "eidas": {
            "$ref": "#/components/schemas/EidasTimestamp"
          }
        }
      },
      "DiffSummary": {
        "type": "object",
        "properties": {
          "text_insertions": {
            "type": "integer",
            "description": "Lignes ajoutées (même valeur que text_lines_added ; depuis le 2026-10-14, le texte est compté en lignes, voir text_unit). Les comparaisons antérieures comptent des caractères. Obsolète (2026-10-14) : conservé pour les clients existants, non supprimé ; utilisez text_lines_added.",
            "deprecated": true
          },
          "text_deletions": {
            "type": "integer",
            "description": "Lignes supprimées (même valeur que text_lines_removed ; des lignes depuis le 2026-10-14, des caractères auparavant). Obsolète (2026-10-14) : conservé pour les clients existants, non supprimé ; utilisez text_lines_removed.",
            "deprecated": true
          },
          "text_unit": {
            "type": "string",
            "enum": [
              "line"
            ],
            "description": "L'unité des décomptes de texte : line (chaque ligne de domtextcontent.txt avec son saut de ligne). Absent des comparaisons réalisées avant le 2026-10-14, qui comptaient des caractères."
          },
          "text_lines_added": {
            "type": "integer",
            "description": "Lignes ajoutées (lignes du texte After dans les séquences ajoutées du diff de lignes)."
          },
          "text_lines_removed": {
            "type": "integer",
            "description": "Lignes supprimées (lignes du texte Before dans les séquences supprimées du diff de lignes)."
          },
          "text_lines_before": {
            "type": "integer",
            "description": "Lignes du texte Before."
          },
          "text_lines_after": {
            "type": "integer",
            "description": "Lignes du texte After."
          },
          "text_changed_percent": {
            "type": "number",
            "description": "100 x (lignes supprimées + lignes ajoutées) / (lignes avant + lignes après), avec une décimale."
          },
          "text_diff_capped": {
            "type": "boolean",
            "description": "true lorsque le diff par lignes a atteint son budget de travail et qu'une partie des textes a été comptée comme entièrement remplacée (surestime, ne masque jamais une modification)."
          },
          "metadata_changes": {
            "type": "array",
            "items": {
              "type": "object"
            },
            "description": "{ key, before, after, class } pour chaque champ de metadata.json qui diffère, exactement ceux-ci : url, final_url, page_title, http_status, http_headers.content-type, http_headers.server, dns.connected_ip, resolved_ips (comparés sous forme de liste triée), tls.issuer, tls.valid_to, ainsi que les champs de profil d'appareil viewport_width, viewport_height, device_scale_factor, is_mobile et user_agent. class vaut content (url, final_url, page_title, http_status, http_headers.content-type : compté comme une modification), infrastructure (http_headers.server, dns.connected_ip, resolved_ips, tls.issuer, tls.valid_to : le serveur qui a livré la page, signalé uniquement) ou device_profile (la manière dont la capture a rendu la page, signalé uniquement). Une comparaison réalisée avant le 2026-10-10 n'a pas de class."
          },
          "meta_content": {
            "type": "integer",
            "description": "Champs de métadonnées de classe content qui diffèrent ; ils sont comptés dans changed."
          },
          "meta_infra": {
            "type": "integer",
            "description": "Champs de métadonnées de classe infrastructure qui diffèrent (logiciel serveur, IP connectée, IP résolues, émetteur et expiration TLS). Un réseau de diffusion de contenu répond depuis des adresses tournantes, de sorte que deux chargements d'une page inchangée peuvent différer ici ; signalés à titre d'information, jamais comptés dans changed."
          },
          "meta_device_profile": {
            "type": "integer",
            "description": "Champs du profil d'appareil qui diffèrent (zone d'affichage, échelle, émulation mobile, user agent) ; signalés à titre d'information, jamais comptés dans changed."
          },
          "image_changes": {
            "type": "object",
            "description": "La comparaison des images, à partir de l'inventaire d'images que chaque capture source a scellé (image_inventory.json). status vaut ran, ou not_run avec le motif source_without_inventory (une source capturée avant que les captures n'en enregistrent un), source_inventory_failed ou source_inventory_unreadable ; rien d'autre n'est utilisé à sa place. Les résultats complets par image figurent dans le fichier image_changes.json du paquet.",
            "properties": {
              "status": {
                "type": "string",
                "enum": [
                  "ran",
                  "not_run"
                ]
              },
              "reason": {
                "type": "string",
                "nullable": true,
                "enum": [
                  "source_without_inventory",
                  "source_inventory_failed",
                  "source_inventory_unreadable",
                  null
                ],
                "description": "Raison pour laquelle les images n'ont pas été comparées : source_without_inventory (une source réalisée avant que les captures n'enregistrent un inventaire d'images ; les nouvelles captures en enregistrent un), source_inventory_failed (l'inventaire n'a pas pu être enregistré lors de la réalisation de cette capture) ou source_inventory_unreadable ; null lorsque la comparaison a eu lieu."
              },
              "images_changed": {
                "type": "integer",
                "nullable": true,
                "description": "Images propres au site (servies depuis le domaine enregistrable de la page elle-même ou, depuis le 2026-10-14, depuis un domaine que la page déclare comme son propre og:image ou icône, ou qui sert la plupart des images de la page dans les deux captures : image_changes.json site_image_hosts) remplacées, ajoutées ou supprimées, la partie images de changed ; les images déplacées ne sont pas incluses. null lorsque la comparaison n'a pas été exécutée. Obsolète (2026-10-14) : conservé pour les clients existants, non supprimé ; utilisez site_images_changed.",
                "deprecated": true
              },
              "first_party": {
                "$ref": "#/components/schemas/ImageChangeCounts"
              },
              "third_party": {
                "$ref": "#/components/schemas/ImageChangeCounts"
              },
              "tracking_pixel": {
                "$ref": "#/components/schemas/ImageChangeCounts"
              },
              "inventories_truncated": {
                "type": "boolean",
                "description": "true lorsqu'un inventaire source a atteint sa limite d'entrées ; voir inventory_cutoff."
              },
              "site_images_changed": {
                "type": "integer",
                "description": "Identique à images_changed (dénombré)."
              },
              "other_images_changed": {
                "type": "integer",
                "description": "Images d'autres domaines et pixels de suivi remplacés, supprimés ou ajoutés (jamais comptés)."
              },
              "site_image_hosts": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "Domaines enregistrables considérés comme appartenant au site, en plus de celui de la page (site_image_hosts de image_changes.json)."
              },
              "inventory_cutoff": {
                "type": "object",
                "nullable": true,
                "properties": {
                  "before_only_index_limit": {
                    "type": "integer",
                    "nullable": true,
                    "description": "Une image présente uniquement dans Before est déterminable (removed) lorsque son index dans l'inventaire Before est inférieur à cette valeur ; null : aucune limite (l'inventaire After est complet)."
                  },
                  "after_only_index_limit": {
                    "type": "integer",
                    "nullable": true,
                    "description": "Une image présente uniquement dans After est déterminable (added) lorsque son index dans l'inventaire After est inférieur à cette valeur ; null : aucune limite (l'inventaire Before est complet)."
                  }
                },
                "description": "Depuis le second audit du 2026-10-14 (algorithme 3 de image_changes.json), un objet, déterminé par l'index dans le document : lorsque l'AUTRE inventaire a atteint sa limite d'entrées, une image trouvée d'un seul côté n'est déterminable que si son index est inférieur au nombre d'entrées de l'autre inventaire moins une marge (la plus grande valeur entre 10 entrées et 5 pour cent de ce nombre) ; sinon, elle est indéterminée (undetermined_reason beyond_inventory_limit). null lorsqu'aucun inventaire n'est tronqué. Les comparaisons réalisées auparavant portent un nombre (la position relative dans le document selon l'ancienne règle)."
              }
            }
          },
          "changed": {
            "type": "boolean",
            "nullable": true,
            "description": "true lorsque le texte visible diffère, qu'un champ de métadonnées de contenu diffère (meta_content), ou qu'une image propre au site a été remplacée, supprimée ou ajoutée (image_changes.images_changed). Les champs d'infrastructure et de profil d'appareil, les images déplacées (mêmes octets à une autre adresse), les images tierces et les pixels de suivi sont signalés mais ne le déclenchent pas. null (depuis le 2026-10-14) lorsque rien de ce qui est pris en compte n'a changé de manière fiable mais que le résultat du texte ou des images est indéterminé (captures rendues différemment, allow_profile_mismatch). Le paquet indique la règle dans comparison_method.json counted_as_change. Obsolète (2026-10-14) : conservé pour les lecteurs existants, non supprimé ; utilisez verdict (changed | unchanged | undetermined) et, par type, text_verdict, images_verdict, metadata_verdict.",
            "deprecated": true
          },
          "text_verdict": {
            "type": "string",
            "enum": [
              "changed",
              "unchanged",
              "undetermined"
            ],
            "description": "undetermined lorsque les deux captures ont été rendues avec un profil d'appareil ou des paramètres de capture différents (allow_profile_mismatch)."
          },
          "images_verdict": {
            "type": "string",
            "enum": [
              "changed",
              "unchanged",
              "undetermined",
              "not_run"
            ]
          },
          "metadata_verdict": {
            "type": "string",
            "enum": [
              "changed",
              "unchanged"
            ],
            "description": "Issu des champs de métadonnées de classe content (adresse, adresse finale, titre, statut HTTP, type de contenu)."
          },
          "verdict": {
            "type": "string",
            "enum": [
              "changed",
              "unchanged",
              "undetermined"
            ],
            "description": "Le résultat global en toutes lettres : changed lorsque changed vaut true, undetermined lorsque changed vaut null."
          },
          "undetermined_reason": {
            "type": "string",
            "nullable": true,
            "enum": [
              "render_profile_mismatch",
              "capture_settings_mismatch",
              null
            ]
          },
          "text_changes": {
            "type": "array",
            "items": {
              "type": "object"
            },
            "description": "GET uniquement : jusqu'à 60 fragments de texte modifiés."
          },
          "change_regions": {
            "type": "array",
            "items": {
              "type": "object"
            },
            "description": "GET uniquement : jusqu'à 60 modifications de texte situées sur les captures d'écran (d'après le text_layout.json des sources), en pixels de capture d'écran. Chaque modification porte counterpart_y_px (sa position verticale sur l'autre capture d'écran, reprise du bloc de texte inchangé le plus proche) et report_region (le numéro de l'emplacement de modification sous lequel evidence.pdf l'affiche, 10 emplacements au maximum, ou null), comme dans change_regions.json."
          },
          "change_regions_info": {
            "type": "object",
            "nullable": true
          },
          "order_reversed": {
            "type": "boolean",
            "description": "GET uniquement : true lorsque la capture indiquée comme capture_id_1 (Before) a été réalisée après celle indiquée comme capture_id_2 (After), comme order_reversed dans compare.json. La comparaison conserve l'ordre indiqué ; le rapport et le tableau de bord le signalent."
          },
          "render_profile_mismatch": {
            "type": "boolean",
            "description": "GET uniquement : les deux captures ont été rendues avec un profil d'appareil différent (compare.json)."
          },
          "capture_settings_mismatch": {
            "type": "boolean",
            "description": "GET uniquement : les deux captures ont été réalisées avec des paramètres de capture différents (compare.json)."
          },
          "capture_settings_differing": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "setting": {
                  "type": "string"
                },
                "before": {},
                "after": {}
              }
            },
            "description": "GET uniquement : les paramètres qui diffèrent, { setting, before, after }."
          },
          "profile_mismatch_allowed": {
            "type": "boolean",
            "description": "GET uniquement : la comparaison a été réalisée avec allow_profile_mismatch pour des captures rendues différemment. Obsolète (2026-10-14) : conservé pour les clients existants, non supprimé ; utilisez render_profile_mismatch et capture_settings_mismatch.",
            "deprecated": true
          },
          "url_mismatch": {
            "type": "boolean",
            "description": "GET uniquement : les deux captures n'ont aucune adresse en commun (réalisée avec allow_url_mismatch)."
          }
        },
        "description": "Résumé de la comparaison."
      },
      "ImageChangeCounts": {
        "type": "object",
        "properties": {
          "unchanged": {
            "type": "integer"
          },
          "replaced": {
            "type": "integer"
          },
          "moved": {
            "type": "integer"
          },
          "removed": {
            "type": "integer"
          },
          "added": {
            "type": "integer"
          },
          "undetermined": {
            "type": "integer"
          },
          "alt_changed": {
            "type": "integer"
          },
          "position_changed": {
            "type": "integer"
          },
          "undetermined_same_error": {
            "type": "integer",
            "description": "Paires indéterminées dont les deux côtés ont renvoyé le même statut d'erreur HTTP (par exemple 404 dans les deux captures) ; incluses dans undetermined, comptées dans le rapport et non listées."
          },
          "occurrences": {
            "type": "integer",
            "description": "Tous les résultats de cette catégorie de provenance (site ou tiers) : occurrences sur la page."
          },
          "distinct": {
            "type": "object",
            "description": "Le nombre d'images différentes parmi les résultats, au total et par catégorie (une image affichée plusieurs fois compte une seule fois) : chaque résultat représente le SHA-256 de son côté After, ou de son côté Before s'il n'a pas de côté After, ou l'adresse de ce côté lorsqu'il ne porte pas de SHA-256.",
            "properties": {
              "total": {
                "type": "integer"
              },
              "unchanged": {
                "type": "integer"
              },
              "replaced": {
                "type": "integer"
              },
              "moved": {
                "type": "integer"
              },
              "removed": {
                "type": "integer"
              },
              "added": {
                "type": "integer"
              },
              "undetermined": {
                "type": "integer"
              }
            }
          }
        },
        "description": "Décomptes par catégorie, en occurrences sur la page (une image affichée trois fois compte trois fois) : unchanged, replaced (même adresse, octets différents), moved (adresse différente, mêmes octets ; signalé, non compté comme une modification), removed, added, undetermined (une image sans empreinte SHA-256 d'un côté ou de l'autre, jamais comptée comme removed ou added) ; plus les images appariées dont le texte alternatif ou la place parmi les images de la page a changé, les paires indéterminées présentant la même erreur HTTP des deux côtés, et les images distinctes."
      },
      "BatchRequest": {
        "type": "object",
        "required": [
          "urls"
        ],
        "properties": {
          "urls": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uri",
              "maxLength": 2048
            },
            "minItems": 2,
            "maxItems": 50,
            "description": "De 2 à 50 URL, chacune validée comme CaptureRequest.url. Les noms d'hôte ne font l'objet d'aucune vérification DNS lors de l'acceptation du lot ; une URL qui échoue à la vérification DNS ou d'adresse échoue plus tard avec le statut failed (et capture.failed)."
          },
          "options": {
            "$ref": "#/components/schemas/CaptureOptions"
          },
          "private_note": {
            "allOf": [
              {
                "$ref": "#/components/schemas/PrivateNote"
              }
            ],
            "description": "Une note privée stockée sur chaque capture du lot. Votre note privée sur la capture (par exemple \"mobile capture\"), 200 caractères au maximum, en texte brut (la règle PlainText). Elle est stockée uniquement dans l'enregistrement de la capture : elle ne fait PAS partie du paquet de preuves scellé (ni dans le ZIP, ni dans le PDF, ni dans le manifeste, ni dans aucune empreinte) et n'a aucune valeur probante ; elle peut donc être modifiée ou effacée ultérieurement avec PATCH /v1/captures/{capture_id} sans incidence sur la preuve. Seul l'auteur de la capture la lit. Omettez-la, ou envoyez null ou une chaîne vide, pour ne pas avoir de note."
          }
        }
      },
      "BatchResult": {
        "type": "object",
        "properties": {
          "batch_id": {
            "type": "string",
            "example": "batch_a1B2c3D4e5F6g7H8"
          },
          "count": {
            "type": "integer"
          },
          "captures": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "capture_id": {
                  "type": "string"
                },
                "url": {
                  "type": "string"
                },
                "status": {
                  "type": "string",
                  "description": "failed lorsque la capture n'a pas pu être lancée.",
                  "enum": [
                    "processing",
                    "failed"
                  ]
                }
              }
            }
          },
          "not_admitted": {
            "type": "array",
            "description": "Uniquement lorsque la limite de captures simultanées du compte a été atteinte : les URL qui n'ont pas été lancées, dans l'ordre d'envoi. Elles ne coûtent rien (aucune unité de limite de débit ni unité mensuelle) ; renvoyez-les plus tard. Absent lorsque toutes les URL ont été lancées.",
            "items": {
              "type": "object",
              "properties": {
                "url": {
                  "type": "string"
                },
                "code": {
                  "type": "string",
                  "description": "Raison pour laquelle la capture de cette URL n'a pas été lancée.",
                  "enum": [
                    "CONCURRENT_CAPTURE_LIMIT"
                  ]
                }
              }
            }
          },
          "in_flight_limit": {
            "type": "integer",
            "description": "Uniquement avec not_admitted : le nombre de captures que le compte peut exécuter simultanément (developer 10, startup 25, growth 100, legal_solo et legal_seats 25, custom 200, website_archive 25, sandbox 5)."
          },
          "warnings": {
            "$ref": "#/components/schemas/Rule902DisabledWarnings"
          }
        }
      },
      "VerifyResult": {
        "type": "object",
        "properties": {
          "verdict": {
            "type": "string",
            "nullable": true,
            "description": "match : signature valide et chaque fichier listé présent avec son empreinte SHA-256 enregistrée. modified : signature invalide, ou fichier manquant ou modifié. invalid : le paquet n'a pas pu être vérifié. null uniquement pour un paquet importé qui n'a pas été revérifié cette fois-ci (`rechecked: false`, avec `reason` et la vérification enregistrée lors de l'importation dans `stored_verification`).",
            "enum": [
              "match",
              "modified",
              "invalid",
              null
            ]
          },
          "reason": {
            "type": "string",
            "description": "invalid uniquement ; pour un paquet importé avec `rechecked: false`, raison pour laquelle il n'a pas été revérifié (too_large_for_synchronous_check, time_budget, storage_error)."
          },
          "capture_id": {
            "type": "string"
          },
          "source": {
            "type": "string",
            "enum": [
              "s3"
            ]
          },
          "checks": {
            "type": "object",
            "properties": {
              "manifest_signature": {
                "type": "object",
                "properties": {
                  "status": {
                    "type": "string",
                    "enum": [
                      "valid",
                      "invalid"
                    ]
                  },
                  "algorithm": {
                    "type": "string",
                    "enum": [
                      "RSA-4096"
                    ]
                  }
                }
              },
              "declared_files": {
                "type": "object",
                "properties": {
                  "status": {
                    "type": "string",
                    "enum": [
                      "all_match",
                      "mismatch"
                    ]
                  },
                  "mismatched": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "missing": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  }
                }
              },
              "bitcoin_ots": {
                "type": "object",
                "properties": {
                  "status": {
                    "type": "string",
                    "description": "Présence de manifest.json.ots uniquement ; la preuve n'est pas vérifiée ici.",
                    "enum": [
                      "present",
                      "not_present"
                    ]
                  }
                }
              },
              "eidas_disig": {
                "type": "object",
                "properties": {
                  "status": {
                    "type": "string",
                    "description": "Présence de manifest.json.tsr. Le jeton lui-même est vérifié dans checks.eidas. missing : manifest.json enregistre eidas_requested (le paquet a été émis avec le jeton), mais manifest.json.tsr ne figure pas dans le paquet.",
                    "enum": [
                      "present",
                      "missing"
                    ]
                  }
                },
                "nullable": true,
                "description": "null lorsqu'il n'y a pas de manifest.json.tsr et que manifest.json n'enregistre pas eidas_requested."
              },
              "proofsnap_issuance": {
                "type": "object",
                "properties": {
                  "status": {
                    "type": "string",
                    "description": "issued_by_proofsnap : l'empreinte du manifeste figure dans le registre d'émission ProofSnap. unverified : elle n'y figure pas (les paquets émis avant l'existence du registre d'émission n'y figurent pas). unknown : la recherche a échoué.",
                    "enum": [
                      "issued_by_proofsnap",
                      "unverified",
                      "unknown"
                    ]
                  },
                  "manifest_sha256": {
                    "type": "string"
                  },
                  "issued_at": {
                    "type": "string",
                    "nullable": true,
                    "format": "date-time"
                  },
                  "type": {
                    "type": "string"
                  }
                }
              },
              "eidas": {
                "type": "object",
                "nullable": true,
                "description": "null lorsqu'il n'y a pas de manifest.json.tsr et que manifest.json n'enregistre pas eidas_requested. Le jeton d'horodatage eIDAS tel que vérifié par ProofSnap, sur demande, à partir du paquet stocké. Il s'agit d'une vérification effectuée par ProofSnap ; le champ note indique comment vérifier le jeton de manière indépendante.",
                "properties": {
                  "status": {
                    "type": "string",
                    "enum": [
                      "verified",
                      "verified_revocation_unknown",
                      "failed",
                      "unavailable",
                      "missing"
                    ],
                    "description": "missing : manifest.json enregistre eidas_requested: true, le paquet a donc été émis avec un horodatage qualifié, mais manifest.json.tsr n'y figure pas (la réponse porte alors l'avertissement EIDAS_TOKEN_MISSING). verified : tous les contrôles ont réussi et le statut du certificat de la TSA enregistré lors de l'émission est connu (good, ou révoqué après l'heure du jeton pour une raison autre qu'une compromission de clé). verified_revocation_unknown : tous les contrôles exécutés ont réussi, mais le statut du certificat de la TSA n'a pas pu être établi à partir des données enregistrées (revocation.status unknown ou not_checked). failed : au moins un contrôle a échoué, voir failures (la réponse porte alors l'avertissement EIDAS_TOKEN_FAILED). unavailable : aucun verdict sur le jeton (reason : non configuré, timeout, input_rejected lorsque le vérificateur a refusé l'entrée, par exemple des données de validation trop volumineuses, inconclusive) ; rien n'est présumé quant au jeton."
                  },
                  "reason": {
                    "type": "string",
                    "description": "unavailable uniquement : timeout, input_rejected, inconclusive, ou une courte explication."
                  },
                  "checked_by": {
                    "type": "string",
                    "example": "ProofSnap API on request"
                  },
                  "checked_at": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "gen_time": {
                    "type": "string",
                    "format": "date-time",
                    "nullable": true,
                    "description": "L'heure figurant dans le jeton (TSTInfo genTime)."
                  },
                  "tsa_name": {
                    "type": "string",
                    "nullable": true,
                    "description": "Issu du certificat du signataire (nom commun et organisation), jamais une valeur par défaut."
                  },
                  "serial_number": {
                    "type": "string",
                    "nullable": true
                  },
                  "policy_oid": {
                    "type": "string",
                    "nullable": true
                  },
                  "hash_algorithm": {
                    "type": "string",
                    "nullable": true
                  },
                  "signature_algorithm": {
                    "type": "string"
                  },
                  "signing_certificate_form": {
                    "type": "string",
                    "enum": [
                      "ESSCertIDv2",
                      "ESSCertID"
                    ]
                  },
                  "signer_certificate": {
                    "type": "object",
                    "nullable": true,
                    "properties": {
                      "subject": {
                        "type": "string"
                      },
                      "issuer": {
                        "type": "string"
                      },
                      "serial_number": {
                        "type": "string"
                      },
                      "sha256": {
                        "type": "string"
                      },
                      "not_before": {
                        "type": "string",
                        "format": "date-time"
                      },
                      "not_after": {
                        "type": "string",
                        "format": "date-time"
                      }
                    }
                  },
                  "root": {
                    "type": "object",
                    "properties": {
                      "label": {
                        "type": "string",
                        "example": "CA Disig QCA4"
                      },
                      "sha256": {
                        "type": "string"
                      }
                    }
                  },
                  "chain_checked_at": {
                    "type": "string",
                    "format": "date-time",
                    "nullable": true,
                    "description": "Le moment par rapport auquel la chaîne a été vérifiée : le genTime du jeton, de sorte qu'un certificat ayant expiré après l'émission du jeton ne le fait pas échouer."
                  },
                  "chain": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "subject": {
                          "type": "string"
                        },
                        "sha256": {
                          "type": "string"
                        }
                      }
                    }
                  },
                  "revocation": {
                    "type": "object",
                    "description": "Statut du certificat de la TSA d'après les données enregistrées lors de l'émission (base recorded_at_issuance), vérifié par signature et devant avoir été valide à genTime. Non récupéré en direct.",
                    "properties": {
                      "status": {
                        "type": "string",
                        "enum": [
                          "good",
                          "revoked",
                          "revoked_after_gen_time",
                          "unknown",
                          "not_checked"
                        ],
                        "description": "revoked_after_gen_time : révoqué après l'heure du jeton pour une raison autre qu'une compromission de clé (le jeton reste vérifié). not_checked : aucune donnée de révocation n'a été enregistrée ni vérifiée."
                      },
                      "source": {
                        "type": "string",
                        "enum": [
                          "ocsp",
                          "crl"
                        ],
                        "nullable": true
                      },
                      "basis": {
                        "type": "string",
                        "enum": [
                          "recorded_at_issuance"
                        ]
                      },
                      "produced_at": {
                        "type": "string",
                        "format": "date-time"
                      },
                      "this_update": {
                        "type": "string",
                        "format": "date-time"
                      },
                      "next_update": {
                        "type": "string",
                        "format": "date-time",
                        "nullable": true
                      },
                      "reason": {
                        "type": "string",
                        "description": "Raison pour laquelle le statut est inconnu."
                      }
                    }
                  },
                  "checks": {
                    "type": "object",
                    "description": "Chaque contrôle : passed, failed, not_applicable (nonce : aucun consigné pour un jeton stocké), unknown (révocation sans données exploitables) ou not_checked (un échec antérieur a interrompu le contrôle).",
                    "properties": {
                      "response_status": {
                        "type": "string",
                        "enum": [
                          "passed",
                          "failed",
                          "not_applicable",
                          "unknown",
                          "not_checked"
                        ]
                      },
                      "message_imprint": {
                        "type": "string",
                        "enum": [
                          "passed",
                          "failed",
                          "not_applicable",
                          "unknown",
                          "not_checked"
                        ]
                      },
                      "nonce": {
                        "type": "string",
                        "enum": [
                          "passed",
                          "failed",
                          "not_applicable",
                          "unknown",
                          "not_checked"
                        ]
                      },
                      "content_type": {
                        "type": "string",
                        "enum": [
                          "passed",
                          "failed",
                          "not_applicable",
                          "unknown",
                          "not_checked"
                        ]
                      },
                      "message_digest": {
                        "type": "string",
                        "enum": [
                          "passed",
                          "failed",
                          "not_applicable",
                          "unknown",
                          "not_checked"
                        ]
                      },
                      "signature": {
                        "type": "string",
                        "enum": [
                          "passed",
                          "failed",
                          "not_applicable",
                          "unknown",
                          "not_checked"
                        ]
                      },
                      "signing_certificate": {
                        "type": "string",
                        "enum": [
                          "passed",
                          "failed",
                          "not_applicable",
                          "unknown",
                          "not_checked"
                        ]
                      },
                      "timestamping_eku": {
                        "type": "string",
                        "enum": [
                          "passed",
                          "failed",
                          "not_applicable",
                          "unknown",
                          "not_checked"
                        ]
                      },
                      "chain": {
                        "type": "string",
                        "enum": [
                          "passed",
                          "failed",
                          "not_applicable",
                          "unknown",
                          "not_checked"
                        ]
                      },
                      "revocation": {
                        "type": "string",
                        "enum": [
                          "passed",
                          "failed",
                          "not_applicable",
                          "unknown",
                          "not_checked"
                        ]
                      }
                    }
                  },
                  "failures": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "check": {
                          "type": "string"
                        },
                        "reason": {
                          "type": "string"
                        }
                      }
                    }
                  },
                  "note": {
                    "type": "string"
                  },
                  "detail": {
                    "type": "string",
                    "nullable": true,
                    "description": "input_rejected uniquement : ce que le vérificateur a refusé."
                  }
                }
              }
            },
            "description": "Absent lorsque verdict vaut invalid. Le verdict dépend uniquement de manifest_signature et de declared_files ; eidas est indiqué à côté et, en cas d'échec, dans warnings."
          },
          "warnings": {
            "type": "array",
            "description": "Présent uniquement lorsqu'un élément extérieur au verdict demande votre attention. EIDAS_TOKEN_FAILED : manifest.json.tsr n'a pas passé la vérification de checks.eidas. EIDAS_TOKEN_MISSING : manifest.json enregistre eidas_requested, mais manifest.json.tsr ne figure pas dans le paquet.",
            "items": {
              "type": "object",
              "properties": {
                "code": {
                  "type": "string",
                  "enum": [
                    "EIDAS_TOKEN_FAILED",
                    "EIDAS_TOKEN_MISSING"
                  ]
                },
                "message": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "ScheduleRequest": {
        "type": "object",
        "required": [
          "url",
          "mode"
        ],
        "properties": {
          "url": {
            "type": "string",
            "description": "Page à capturer, validée comme CaptureRequest.url.",
            "format": "uri",
            "maxLength": 2048
          },
          "mode": {
            "type": "string",
            "description": "interval : toutes les interval_seconds. cron : selon cron_expr. on_change : vérifie la page toutes les interval_seconds et ne capture que lorsque son texte a changé.",
            "enum": [
              "interval",
              "cron",
              "on_change"
            ]
          },
          "interval_seconds": {
            "type": "integer",
            "description": "Obligatoire pour interval et on_change. Au moins le minimum du forfait (300 sur developer, startup, growth, custom, legal_solo et legal_seats ; 86 400 sur website_archive), sinon 400 INTERVAL_TOO_SHORT.",
            "minimum": 300
          },
          "cron_expr": {
            "type": "string",
            "description": "Obligatoire pour cron (200 caractères au maximum). Une expression impossible à analyser renvoie 400 INVALID_CRON ; deux exécutions consécutives plus rapprochées que le minimum du forfait renvoient 400 INTERVAL_TOO_SHORT.",
            "maxLength": 200,
            "example": "0 9 * * 1-5"
          },
          "timezone": {
            "type": "string",
            "description": "Fuseau horaire IANA utilisé pour évaluer cron_expr. UTC par défaut.",
            "example": "Europe/Berlin"
          },
          "webhook_url": {
            "type": "string",
            "description": "URL https. Les exécutions interval et cron envoient capture.completed ou capture.failed ; on_change envoie monitor.change_detected. L'URL doit être en https et ne doit pas pointer vers une adresse privée, de bouclage, link-local, NAT de niveau opérateur (CGNAT) ou de métadonnées cloud (mêmes règles que pour une URL de capture ; 422 SSRF_BLOCKED sinon). Une valeur vide ou composée uniquement d'espaces signifie aucun webhook. Une planification a une seule webhook_url, au niveau supérieur : une options.webhook_url n'est pas stockée (lorsqu'aucune webhook_url de niveau supérieur n'est envoyée, elle est utilisée comme telle), si bien qu'une planification on_change n'envoie jamais aussi capture.completed. Exige une clé API : envoyée depuis une session du tableau de bord, elle est refusée avec 400 WEBHOOK_REQUIRES_API_KEY, car les webhooks sont signés avec le secret de signature du webhook de la clé.",
            "format": "uri"
          },
          "js_render": {
            "type": "boolean",
            "description": "on_change uniquement : vérifie la page avec un rendu complet dans un navigateur au lieu d'une simple récupération HTTP (pour les pages qui nécessitent JavaScript).",
            "default": false
          },
          "options": {
            "$ref": "#/components/schemas/CaptureOptions"
          },
          "max_capture_age_seconds": {
            "type": "integer",
            "nullable": true,
            "minimum": 86400,
            "maximum": 34560000,
            "description": "on_change uniquement : réalise aussi une capture complète (qui envoie capture.completed) lorsque la capture la plus récente de la planification est plus ancienne que cette durée, même si la page n'a pas changé. Pour les règles qui exigent une copie au moins tous les N jours, par exemple 7776000 pour 90 jours. De 1 jour à 400 jours et plus long que interval_seconds ; null (par défaut) ne capture qu'en cas de modification. PATCH avec null la supprime."
          },
          "alert_on": {
            "type": "string",
            "enum": [
              "any",
              "text"
            ],
            "nullable": true,
            "description": "on_change uniquement (2026-10-14) : quelle modification détectée envoie monitor.change_detected. any (par défaut, comportement antérieur) : chaque capture de modification. text : uniquement lorsque la comparaison avec la capture précédente constate une modification du texte visible (diff_summary.text_verdict changed) ; une capture de modification dont la comparaison indique que le texte n'a pas changé ou n'a pas pu être déterminé n'envoie rien (la capture est tout de même réalisée, facturée, et envoie capture.completed aux abonnements). Sans résultat de comparaison (pas de référence, comparaison échouée), une alerte est envoyée. 400 MALFORMED_REQUEST avec un autre mode ou une autre valeur. null pour les planifications interval et cron."
          }
        },
        "example": {
          "url": "https://example.com/terms",
          "mode": "on_change",
          "interval_seconds": 3600,
          "webhook_url": "https://your-app.example/webhooks/proofsnap"
        }
      },
      "Schedule": {
        "type": "object",
        "properties": {
          "schedule_id": {
            "type": "string"
          },
          "url": {
            "type": "string"
          },
          "mode": {
            "type": "string",
            "enum": [
              "interval",
              "cron",
              "on_change"
            ]
          },
          "interval_seconds": {
            "type": "integer",
            "nullable": true
          },
          "cron_expr": {
            "type": "string",
            "nullable": true
          },
          "timezone": {
            "type": "string",
            "nullable": true
          },
          "webhook_url": {
            "type": "string",
            "nullable": true,
            "description": "Le webhook de la planification (pour une planification plus ancienne qui le conservait dans options.webhook_url, cette valeur est affichée ici et options ne la contient jamais). Exige une clé API : envoyé depuis une session du tableau de bord, il est refusé avec 400 WEBHOOK_REQUIRES_API_KEY, car les webhooks sont signés avec le secret de signature du webhook de la clé."
          },
          "options": {
            "type": "object",
            "description": "Options de capture avec les valeurs par défaut appliquées."
          },
          "js_render": {
            "type": "boolean"
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "paused"
            ]
          },
          "next_run_at": {
            "type": "string",
            "format": "date-time"
          },
          "last_run_at": {
            "type": "string",
            "nullable": true,
            "format": "date-time"
          },
          "last_capture_id": {
            "type": "string",
            "nullable": true
          },
          "run_count": {
            "type": "integer"
          },
          "consecutive_failures": {
            "type": "integer"
          },
          "pause_reason": {
            "type": "string",
            "description": "Raison pour laquelle la planification a été suspendue automatiquement : max_failures (5 exécutions échouées consécutives), no_billing, plan_downgrade (limites du forfait, ou forfait Archive only), invalid_cron ou key_revoked. null lorsqu'elle est active ou suspendue par vous.",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "max_capture_age_seconds": {
            "type": "integer",
            "nullable": true
          },
          "current_capture_id": {
            "type": "string",
            "nullable": true,
            "description": "La capture TERMINÉE la plus récente de la planification (sa version actuelle ; last_capture_id est la plus récente lancée)."
          },
          "current_captured_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "alert_on": {
            "type": "string",
            "enum": [
              "any",
              "text"
            ],
            "nullable": true,
            "description": "on_change : any ou text (any pour une planification créée avant l'option) ; null pour interval et cron."
          },
          "warnings": {
            "$ref": "#/components/schemas/Rule902DisabledWarnings"
          }
        }
      },
      "Matter": {
        "type": "object",
        "properties": {
          "matter_id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "client": {
            "type": "string",
            "nullable": true
          },
          "case_number": {
            "type": "string",
            "nullable": true
          },
          "opposing_party": {
            "type": "string",
            "nullable": true
          },
          "jurisdiction": {
            "type": "string",
            "nullable": true
          },
          "status": {
            "type": "string",
            "enum": [
              "open",
              "archived",
              "sealed"
            ]
          },
          "sealed": {
            "type": "boolean"
          },
          "capture_count": {
            "type": "integer"
          },
          "custody_count": {
            "type": "integer",
            "description": "Entrées du journal de traçabilité."
          },
          "custody_head_hash": {
            "type": "string",
            "description": "sha256:<hex> de l'entrée la plus récente du journal de traçabilité.",
            "nullable": true
          },
          "seal": {
            "$ref": "#/components/schemas/MatterSeal",
            "description": "null jusqu'au scellement."
          },
          "member_uids": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "created_by": {
            "type": "string",
            "description": "Le propriétaire.",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "read_only": {
            "type": "boolean",
            "description": "Présent (true) sur une affaire d'un cabinet dont l'abonnement a pris fin ou qui est conservé comme archive du cabinet : ouverture, exportation et révocation du partage uniquement."
          },
          "legal_hold": {
            "type": "boolean",
            "description": "La conservation pour litige de l'affaire est active (PATCH { legal_hold })."
          },
          "legal_hold_set_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        }
      },
      "MatterSeal": {
        "type": "object",
        "properties": {
          "root_hash": {
            "type": "string",
            "description": "sha256:<hex> : la dernière entrée du journal de traçabilité qui a été scellée et soumise à OpenTimestamps."
          },
          "ots": {
            "type": "string",
            "description": "Preuve OpenTimestamps, en hexadécimal."
          },
          "ots_pending": {
            "type": "boolean"
          },
          "eidas": {
            "type": "object",
            "properties": {
              "tsr_base64": {
                "type": "string",
                "nullable": true
              },
              "serial": {
                "type": "string",
                "nullable": true
              },
              "issued_at": {
                "type": "string",
                "nullable": true
              },
              "ltv": {
                "type": "boolean"
              },
              "verification_status": {
                "type": "string",
                "enum": [
                  "verified"
                ],
                "description": "Le jeton a passé la vérification ProofSnap lors du scellement (vérificateur partagé, mêmes contrôles que pour le jeton d'une capture)."
              },
              "revocation_status": {
                "type": "string",
                "enum": [
                  "good",
                  "revoked_after_gen_time",
                  "unknown",
                  "not_checked"
                ],
                "description": "Statut du certificat de la TSA établi lors du scellement à partir de la réponse OCSP ou de la CRL collectée à ce moment-là."
              },
              "validation": {
                "type": "object",
                "nullable": true,
                "description": "Le contenu de eidas_validation.json enregistré lors du scellement, pour une vérification hors ligne du jeton : tsaCertChain, rootCaPem, ocspResponse, crlResponse (retiré et mentionné dans omitted lorsqu'il est trop volumineux), ocspUrl, ocspFetchedAt, ocspStatus, crlUrl, verification. Les sceaux créés avant le 2026-10-01 n'en ont pas.",
                "additionalProperties": true
              }
            },
            "nullable": true,
            "description": "Horodatage eIDAS portant sur la même empreinte lorsque le déploiement a eIDAS configuré, que l'appelant est en mode live et qu'un jeton a été obtenu et a passé la vérification ; sinon null (voir eidas_status)."
          },
          "sealed_by": {
            "type": "string",
            "nullable": true
          },
          "sealed_at": {
            "type": "string",
            "format": "date-time"
          },
          "eidas_status": {
            "type": "string",
            "enum": [
              "confirmed",
              "not_obtained",
              "not_configured"
            ],
            "description": "Absent sur les sceaux créés avant le 2026-10-01. not_obtained : le sceau ne porte que l'ancrage OpenTimestamps (eidas_reason token_refused, unavailable ou test_key). Un dépassement de délai de l'horodatage eIDAS ne produit jamais de sceau : la requête renvoie alors 503 EIDAS_SEAL_RETRY."
          },
          "eidas_reason": {
            "type": "string",
            "enum": [
              "token_refused",
              "unavailable",
              "test_key",
              "account_limit"
            ],
            "description": "not_obtained uniquement. test_key : scellé par un appelant de test (clé API de test ou session sandbox), aucun horodatage eIDAS n'a donc été demandé. account_limit : le compte n'a pas encore de facture payée et a atteint sa limite eIDAS (20 par jour UTC, 50 par mois UTC)."
          }
        },
        "nullable": true
      },
      "CustodyEntry": {
        "type": "object",
        "properties": {
          "seq": {
            "type": "integer"
          },
          "ts": {
            "type": "string",
            "format": "date-time"
          },
          "actor_uid": {
            "type": "string",
            "nullable": true
          },
          "actor_name": {
            "type": "string",
            "nullable": true
          },
          "actor_role": {
            "type": "string",
            "enum": [
              "api",
              "user"
            ],
            "nullable": true
          },
          "action": {
            "type": "string",
            "description": "Par exemple matter.created, matter.updated, capture.added, capture.removed, capture.legal_hold_placed, capture.legal_hold_released, member.added, member.removed, package.imported (un paquet de preuves importé avec matter_id ; target est l'identifiant de la capture)."
          },
          "target": {
            "type": "string",
            "nullable": true
          },
          "prev_hash": {
            "type": "string"
          },
          "entry_hash": {
            "type": "string"
          },
          "details": {
            "type": "object",
            "description": "package.imported uniquement : manifest_sha256, zip_sha256, package_kind, verdict, levels. Stocké avec l'entrée mais NON couvert par entry_hash.",
            "additionalProperties": true
          }
        },
        "description": "Un maillon de la chaîne de hachage. entry_hash couvre seq, ts, actor, action, target et prev_hash."
      },
      "MatterCapture": {
        "type": "object",
        "properties": {
          "capture_id": {
            "type": "string"
          },
          "status": {
            "type": "string"
          },
          "url": {
            "type": "string",
            "nullable": true
          },
          "source": {
            "type": "string"
          },
          "label": {
            "type": "string",
            "nullable": true
          },
          "created_at": {
            "type": "string"
          },
          "captured_at": {
            "type": "string",
            "nullable": true
          },
          "file_count": {
            "type": "integer",
            "nullable": true
          },
          "manifest_hash": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "AuditEntry": {
        "type": "object",
        "properties": {
          "ts": {
            "type": "string"
          },
          "actor_uid": {
            "type": "string",
            "nullable": true
          },
          "actor_name": {
            "type": "string",
            "nullable": true
          },
          "actor_role": {
            "type": "string",
            "nullable": true
          },
          "action": {
            "type": "string",
            "description": "Par exemple capture.downloaded, export.generated, share.created, share.revoked."
          },
          "target": {
            "type": "string",
            "nullable": true
          },
          "source_ip": {
            "type": "string",
            "nullable": true,
            "description": "Propriétaire de l'affaire uniquement (absent pour les membres)."
          },
          "user_agent": {
            "type": "string",
            "nullable": true,
            "description": "Propriétaire de l'affaire uniquement (absent pour les membres)."
          }
        },
        "description": "Événement d'accès. Ne fait pas partie de la chaîne de hachage."
      },
      "CertifyResult": {
        "type": "object",
        "properties": {
          "certify_id": {
            "type": "string",
            "example": "cert_a1B2c3D4e5F6g7H8"
          },
          "status": {
            "type": "string",
            "enum": [
              "completed"
            ]
          },
          "hash_sha256": {
            "type": "string",
            "description": "Certificat individuel seulement."
          },
          "combined": {
            "type": "boolean",
            "description": "Certificat combiné uniquement."
          },
          "file_count": {
            "type": "integer",
            "description": "Certificat combiné uniquement."
          },
          "files": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "name": {
                  "type": "string",
                  "description": "Le libellé que vous avez envoyé.",
                  "nullable": true
                },
                "sha256": {
                  "type": "string"
                }
              }
            },
            "description": "Certificat combiné uniquement."
          },
          "label": {
            "type": "string",
            "description": "Votre libellé, ou \"N files\" pour un certificat combiné.",
            "nullable": true
          },
          "certified_at": {
            "type": "string",
            "format": "date-time"
          },
          "timestamps": {
            "type": "object",
            "properties": {
              "bitcoin_ots": {
                "type": "object",
                "properties": {
                  "status": {
                    "type": "string",
                    "enum": [
                      "pending_confirmation",
                      "not_present"
                    ],
                    "description": "pending_confirmation : manifest.json.ots figure dans le paquet et attend un bloc Bitcoin. not_present : les calendriers OpenTimestamps étaient injoignables, le paquet ne contient pas de manifest.json.ots et manifest.json ne le liste pas."
                  },
                  "block_height": {
                    "type": "integer",
                    "nullable": true
                  }
                }
              },
              "eidas": {
                "$ref": "#/components/schemas/EidasTimestamp"
              }
            }
          },
          "package_url": {
            "type": "string",
            "format": "uri"
          },
          "package_url_expires_at": {
            "type": "string",
            "format": "date-time"
          },
          "verify_url": {
            "type": "string",
            "description": "Lien vers le vérificateur public ProofSnap.",
            "format": "uri"
          },
          "warnings": {
            "$ref": "#/components/schemas/Rule902DisabledWarnings"
          }
        }
      },
      "PlainText": {
        "type": "string",
        "description": "Règle de texte brut pour les champs courts en texte libre imprimés sur des documents destinés au tribunal ou sur des pages publiques (case_reference, case_id, case_note, valeurs de evidence_pack.custodian, case_reference de certify-hash, dépositaire du caviardage, déclaration d'exportation, noms de cabinet et d'affaire). La valeur est normalisée en NFC, les espaces sont réduits et rognés, et la longueur est comptée en caractères (points de code Unicode). Refusés : caractères de contrôle (C0, C1), plongements, forçages et isolats bidi (U+202A à U+202E, U+2066 à U+2069), marques de direction (U+200E, U+200F, U+061C), caractères de largeur nulle et invisibles (U+200B, U+2060 à U+2064, U+FEFF, U+00AD, U+034F, U+180E, U+206A à U+206F, U+FFF9 à U+FFFB), caractères de remplissage hangeul (U+115F, U+1160, U+3164, U+FFA0), caractères d'étiquette (U+E0000 à U+E007F), demi-codets isolés et caractères à usage privé. Les lettres de toutes les écritures sont acceptées, y compris les syllabes hangeul, l'arabe, l'hébreu, le devanagari et les caractères CJK, et U+200C / U+200D (ZWNJ, ZWJ) sont autorisés car l'orthographe correcte de plusieurs écritures en a besoin."
      },
      "EidasTimestamp": {
        "type": "object",
        "description": "Résultat eIDAS lorsque include_eidas a été demandé avec une clé live ; absent lorsqu'il n'a pas été demandé. confirmed : le paquet contient manifest.json.tsr (facturé, sauf si la révocation est inconnue). not_obtained : aucun jeton n'a été livré et rien n'a été facturé ; le paquet ne contient que l'ancrage OpenTimestamps (motif token_refused : un jeton a été renvoyé et n'a pas passé la vérification lors de l'émission ; unavailable : aucun jeton n'a pu être obtenu ; time_budget : la demande ne tenait pas dans le temps restant, elle n'a donc pas été effectuée, a été refusée par le service d'horodatage avant toute demande à l'autorité, ou a été interrompue, ce qui se produit sur la route synchrone de caviardage ou à la fin d'une capture longue ; account_limit : le compte n'a pas encore de facture payée et a atteint sa limite eIDAS de 20 horodatages par jour UTC ou de 50 par mois UTC, décomptés ensemble sur les captures, lots, planifications, certifications, comparaisons, caviardages et scellements d'affaires, aucune demande n'a donc été effectuée ; les comptes ayant une facture payée ne sont pas limités). Les lignes terminées avant le 2026-10-01 indiquent uniquement confirmed.",
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "confirmed",
              "not_obtained"
            ]
          },
          "reason": {
            "type": "string",
            "enum": [
              "token_refused",
              "unavailable",
              "time_budget",
              "account_limit"
            ],
            "description": "not_obtained uniquement."
          },
          "serial": {
            "type": "string",
            "nullable": true,
            "description": "confirmed uniquement."
          },
          "issued_at": {
            "type": "string",
            "nullable": true,
            "description": "confirmed uniquement."
          },
          "revocation": {
            "type": "string",
            "enum": [
              "unknown"
            ],
            "description": "confirmed uniquement, et seulement lorsque le statut du certificat de la TSA n'a pas pu être établi lors de l'émission (le jeton a passé tous les autres contrôles). Le jeton est livré et n'est pas facturé."
          },
          "billed": {
            "type": "boolean",
            "enum": [
              false
            ],
            "description": "Présent (false) avec revocation unknown : ce jeton n'a pas été facturé."
          }
        }
      },
      "ExternalArchive": {
        "type": "object",
        "description": "Le résultat Internet Archive (Wayback Machine) d'une capture. Internet Archive est une archive publique tierce.",
        "required": [
          "requested"
        ],
        "properties": {
          "requested": {
            "type": "boolean",
            "description": "false lorsqu'aucune demande Internet Archive n'a été effectuée (external_archive: false, une certification ou une comparaison, une capture réalisée avant l'existence de l'archivage, ou une demande qui ne tenait pas dans le budget de temps). Tous les autres champs sont alors absents."
          },
          "status": {
            "type": "string",
            "enum": [
              "created",
              "recent_existing",
              "created_after_request",
              "existing",
              "pending",
              "rate_limited",
              "failed",
              "unconfirmed",
              "unavailable",
              "skipped",
              "skipped_sensitive",
              "skipped_query"
            ],
            "description": "Résultat actuel. Pour une tâche Save Page Now encore en attente au moment du scellement, le planificateur lit l'état de la tâche environ 5 et 30 minutes après la requête et cette valeur devient le résultat final (created, recent_existing, failed, rate_limited, ou unconfirmed lorsqu'aucune réponse n'est arrivée dans la fenêtre de contrôle). created, recent_existing, created_after_request et existing portent snapshot_url."
          },
          "status_at_sealing": {
            "type": "string",
            "enum": [
              "created",
              "recent_existing",
              "created_after_request",
              "existing",
              "pending",
              "rate_limited",
              "failed",
              "unconfirmed",
              "unavailable",
              "skipped",
              "skipped_sensitive",
              "skipped_query"
            ],
            "description": "L'état scellé dans le paquet (metadata.json, forensic_log.json, chain_of_custody.json et evidence.pdf l'enregistrent). Le paquet lui-même ne change jamais ; égal à status, sauf si un suivi a résolu une tâche en attente."
          },
          "method": {
            "type": "string",
            "nullable": true,
            "enum": [
              "spn2_authenticated",
              "spn_anonymous",
              null
            ]
          },
          "requested_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "snapshot_url": {
            "type": "string",
            "format": "uri",
            "nullable": true,
            "description": "La copie publique de la Wayback Machine, toujours https://web.archive.org/web/{snapshot_timestamp}/{url}."
          },
          "snapshot_timestamp": {
            "type": "string",
            "nullable": true,
            "description": "Horodatage Wayback YYYYMMDDhhmmss (UTC)."
          },
          "snapshot_offset_seconds": {
            "type": "integer",
            "nullable": true,
            "description": "Heure de l'instantané moins l'heure de la capture, en secondes (négatif : avant la capture)."
          },
          "resolved_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "Date à laquelle le suivi du planificateur a enregistré le résultat définitif ; null lorsque l'état scellé était définitif."
          },
          "job_id": {
            "type": "string",
            "nullable": true,
            "description": "Identifiant de tâche Save Page Now (requêtes authentifiées)."
          },
          "follow_up_pending": {
            "type": "boolean",
            "description": "true tant que le planificateur doit encore lire le statut de la tâche."
          },
          "skip_reason": {
            "type": "string",
            "nullable": true,
            "description": "Raison pour laquelle l'URL n'a pas été envoyée, lorsque status vaut skipped, skipped_sensitive ou skipped_query : credentials_in_url, sensitive_path, opaque_path_segment, sensitive_fragment, sensitive_parameter_name, opaque_parameter_value, query_string (ou le statut lui-même sur les lignes plus anciennes)."
          },
          "resolved": {
            "type": "object",
            "nullable": true,
            "properties": {
              "status": {
                "type": "string",
                "enum": [
                  "created",
                  "recent_existing",
                  "failed",
                  "rate_limited",
                  "unresolved"
                ]
              },
              "job_status": {
                "type": "string",
                "nullable": true,
                "description": "success, error ou pending tel que lu dans la tâche Save Page Now, null lorsqu'il n'a pas pu être lu."
              },
              "snapshot_url": {
                "type": "string",
                "nullable": true,
                "description": "https://web.archive.org/web/<14 digits>/<URL>, null lorsqu'elle est absente ou non valide."
              },
              "snapshot_timestamp": {
                "type": "string",
                "nullable": true
              },
              "checked_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "reason": {
                "type": "string",
                "nullable": true,
                "enum": [
                  "no_request_time",
                  "capture_deleted",
                  "capture_failed",
                  null
                ],
                "description": "Raison de l'absence de réponse de la tâche : no_request_time (la ligne n'a pas d'heure de requête à partir de laquelle planifier les lectures), capture_deleted ou capture_failed (la capture a été placée dans la corbeille ou a échoué alors que la tâche était en attente, elle n'a donc pas été suivie ; une capture restaurée depuis la corbeille dans les 30 minutes suivant la requête est de nouveau suivie). null dans les autres cas."
              }
            },
            "description": "L'enregistrement de suivi (historique), sous forme de projection fixe : snapshot_url uniquement lorsqu'il s'agit d'une URL d'instantané Wayback valide de snapshot_timestamp ; reason uniquement lorsque le suivi s'est terminé sans réponse de la tâche."
          }
        }
      },
      "CaptureStorage": {
        "type": "object",
        "description": "Durée de conservation, copie de sauvegarde et (captures automatiques) historique des versions.",
        "properties": {
          "retention": {
            "type": "object",
            "properties": {
              "years": {
                "type": "integer",
                "example": 10
              },
              "counted_from": {
                "type": "string",
                "format": "date-time",
                "nullable": true,
                "description": "La plus tardive des deux dates entre created_at et la dernière utilisation connue : la dernière vérification ayant constaté que la page était inchangée, ou le moment où une capture plus récente de la même planification a remplacé celle-ci."
              },
              "earliest_deletion_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true,
                "description": "counted_from plus years, approximatif : la suppression a lieu lors d'un passage ultérieur du planificateur, et des vérifications ultérieures peuvent la décaler. null en cas de conservation pour litige."
              },
              "legal_hold": {
                "type": "boolean"
              }
            }
          },
          "duplicate_copy": {
            "type": "object",
            "properties": {
              "status": {
                "type": "string",
                "enum": [
                  "none",
                  "pending",
                  "stored",
                  "not_copied"
                ],
                "description": "stored : une seconde copie du paquet (et de sa preuve OTS mise à niveau) est conservée dans un bucket distinct, dans une autre région de l'UE, et supprimée avec l'original. pending : copie en cours (quelques minutes). none : paquets de test, et paquets antérieurs à l'existence du double tant qu'ils ne sont pas copiés."
              },
              "copied_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              }
            }
          },
          "version": {
            "type": "object",
            "description": "Captures automatiques uniquement.",
            "properties": {
              "superseded_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "superseded_by": {
                "type": "string",
                "nullable": true
              },
              "unchanged_first_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true,
                "description": "Premier contrôle (on_change) ayant constaté que le texte de la page n'avait pas changé depuis cette capture."
              },
              "unchanged_last_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "unchanged_checks": {
                "type": "integer",
                "description": "Vérifications sans changement enregistrées (au plus une par heure)."
              }
            }
          }
        }
      },
      "WebhookEventType": {
        "type": "string",
        "enum": [
          "capture.completed",
          "capture.failed",
          "certify.completed",
          "certify.failed",
          "redaction.completed",
          "redaction.failed",
          "monitor.change_detected",
          "capture.anchor_confirmed",
          "schedule.paused"
        ],
        "description": "Un événement du catalogue (tag Webhooks)."
      },
      "WebhookSubscriptionFilters": {
        "type": "object",
        "description": "Au plus l'un des champs host et schedule_id. L'abonnement ne reçoit alors que les événements des lignes ayant cet hôte (nom d'hôte en minuscules de l'URL capturée) ou cette planification. `{}` pour aucun filtre.",
        "properties": {
          "host": {
            "type": "string",
            "example": "www.example.com"
          },
          "schedule_id": {
            "type": "string",
            "example": "sch_a1B2c3D4e5F6g7H8"
          }
        }
      },
      "WebhookSubscription": {
        "type": "object",
        "properties": {
          "subscription_id": {
            "type": "string",
            "example": "whs_a1B2c3D4e5F6g7H8"
          },
          "url": {
            "type": "string",
            "description": "Endpoint https auquel les événements sont envoyés par POST."
          },
          "events": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/WebhookEventType"
            }
          },
          "filters": {
            "$ref": "#/components/schemas/WebhookSubscriptionFilters"
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "disabled"
            ]
          },
          "env": {
            "type": "string",
            "enum": [
              "live",
              "test"
            ],
            "description": "L'environnement de l'identifiant qui l'a créé. Un abonnement live reçoit uniquement les événements des lignes live, un abonnement de test uniquement ceux des lignes de test."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "created_by": {
            "type": "string",
            "enum": [
              "key",
              "session"
            ],
            "description": "key : créé avec une clé API ; la révocation de cette clé désactive l'abonnement (disabled_reason key_revoked)."
          },
          "consecutive_failures": {
            "type": "integer",
            "description": "Livraisons en échec depuis le dernier succès."
          },
          "last_success_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "last_failure_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "last_error": {
            "type": "string",
            "nullable": true,
            "description": "Classe d'erreur de la dernière livraison en échec (timeout, connection_failed, blocked_address, redirect_not_followed, http_<status>)."
          },
          "disabled_reason": {
            "type": "string",
            "nullable": true,
            "enum": [
              "gone",
              "failing",
              "key_revoked",
              null
            ],
            "description": "gone : l'endpoint a répondu 410. failing : toutes les livraisons ont échoué pendant 5 jours sans aucun succès. key_revoked : la clé API qui l'a créé a été révoquée. PATCH status active le réactive."
          },
          "disabled_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        },
        "required": [
          "subscription_id",
          "url",
          "events",
          "filters",
          "description",
          "status",
          "env",
          "created_at",
          "created_by",
          "consecutive_failures",
          "last_success_at",
          "last_failure_at",
          "last_error",
          "disabled_reason",
          "disabled_at"
        ]
      },
      "WebhookSubscriptionCreated": {
        "type": "object",
        "properties": {
          "subscription_id": {
            "type": "string",
            "example": "whs_a1B2c3D4e5F6g7H8"
          },
          "url": {
            "type": "string",
            "description": "Endpoint https auquel les événements sont envoyés par POST."
          },
          "events": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/WebhookEventType"
            }
          },
          "filters": {
            "$ref": "#/components/schemas/WebhookSubscriptionFilters"
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "disabled"
            ]
          },
          "env": {
            "type": "string",
            "enum": [
              "live",
              "test"
            ],
            "description": "L'environnement de l'identifiant qui l'a créé. Un abonnement live reçoit uniquement les événements des lignes live, un abonnement de test uniquement ceux des lignes de test."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "created_by": {
            "type": "string",
            "enum": [
              "key",
              "session"
            ],
            "description": "key : créé avec une clé API ; la révocation de cette clé désactive l'abonnement (disabled_reason key_revoked)."
          },
          "secret": {
            "type": "string",
            "description": "whsec_ suivi de base64 standard. Affiché UNE SEULE FOIS : chaque livraison vers cet abonnement est signée avec ce secret (Standard Webhooks). Enregistrez-le maintenant ; il ne peut pas être relu. Un rejeu Idempotency-Key dans les 24 heures renvoie le même corps, secret compris."
          }
        },
        "required": [
          "subscription_id",
          "url",
          "events",
          "filters",
          "description",
          "status",
          "env",
          "created_at",
          "created_by",
          "secret"
        ]
      },
      "Event": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "evt_a1B2c3D4e5F6g7H8"
          },
          "type": {
            "$ref": "#/components/schemas/WebhookEventType"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "env": {
            "type": "string",
            "enum": [
              "live",
              "test"
            ]
          },
          "data": {
            "type": "object",
            "description": "Le corps de l'événement tel que le reçoivent les abonnements, sans subscription_id et sans liens présignés (package_url, package_url_expires_at, upgraded_proof_url, compare.package_url)."
          }
        },
        "required": [
          "id",
          "type",
          "created_at",
          "env",
          "data"
        ]
      },
      "PrivateNote": {
        "type": "string",
        "nullable": true,
        "maxLength": 200,
        "description": "Votre note privée sur la capture (par exemple « capture mobile »), 200 caractères au maximum, en texte brut (règle PlainText). Elle est stockée uniquement dans l'enregistrement de la capture : elle ne fait PAS partie du paquet de preuves scellé (ni dans le ZIP, ni dans le PDF, ni dans le manifeste, ni dans aucune empreinte) et n'a aucune valeur probante ; elle peut donc être modifiée ou effacée ultérieurement avec PATCH /v1/captures/{capture_id} sans incidence sur la preuve. Seul l'auteur de la capture peut la lire. Omettez-la, ou envoyez null ou une chaîne vide, si vous ne souhaitez pas de note.",
        "example": "mobile capture"
      },
      "CaptureEvent": {
        "type": "object",
        "description": "Un événement de la chaîne des événements de capture du compte. hash = SHA-256 (hex) du JSON canonique des champs hachés (v, chain, seq, type, capture_id, manifest_sha256, actor, access, detail, at, prev_hash ; clés triées, sans espaces, valeurs absentes sous forme de null). prev_hash est l'empreinte de l'événement précédent, 64 zéros pour seq 1.",
        "properties": {
          "v": {
            "type": "integer",
            "enum": [
              1
            ]
          },
          "chain": {
            "type": "string",
            "description": "Le compte : l'uid pour les captures personnelles, org_<org_id> pour les captures d'un cabinet."
          },
          "seq": {
            "type": "integer",
            "minimum": 1
          },
          "type": {
            "type": "string",
            "enum": [
              "trash",
              "restore",
              "erase",
              "purge",
              "retention_deletion",
              "account_deletion",
              "company_deletion",
              "legal_hold_on",
              "legal_hold_off",
              "s3_legal_hold",
              "import"
            ]
          },
          "capture_id": {
            "type": "string"
          },
          "manifest_sha256": {
            "type": "string",
            "nullable": true,
            "description": "SHA-256 du manifest.json de la capture, lu avant toute suppression."
          },
          "actor": {
            "type": "string",
            "description": "L'uid du compte qui agit, ou system pour les tâches périodiques du serveur."
          },
          "access": {
            "type": "string",
            "description": "author, company_owner, matter_owner ou system."
          },
          "detail": {
            "type": "string",
            "nullable": true,
            "description": "legal_hold_on / legal_hold_off : l'origine de la conservation (manual, bulk, matter:<id>, derived:<capture_id>) ou, pour une levée par le système (acteur system), son code de motif subscription_ended ; s3_legal_hold : le résultat S3 (on, off, failed, unsupported, none) ; import : le type du paquet importé. Aucun texte libre."
          },
          "at": {
            "type": "string",
            "format": "date-time",
            "description": "Date à laquelle l'événement a été écrit dans la chaîne. Jamais antérieure au at de l'événement précédent."
          },
          "prev_hash": {
            "type": "string"
          },
          "hash": {
            "type": "string"
          },
          "occurred_at": {
            "type": "string",
            "format": "date-time",
            "description": "Date à laquelle l'action elle-même a été validée (transaction du changement d'état). Non hachée. Normalement égale à at à quelques millisecondes près ; plus tardive uniquement lorsque l'ajout à la chaîne a été relancé par le serveur (l'action est enregistrée dans la même transaction que le changement, aucune n'est donc perdue)."
          }
        }
      },
      "CaptureEventAnchor": {
        "type": "object",
        "description": "Un ancrage OpenTimestamps quotidien de la chaîne : head_hash est l'empreinte de l'événement to_seq, qui couvre cryptographiquement tous les événements antérieurs.",
        "properties": {
          "anchor_id": {
            "type": "string"
          },
          "day": {
            "type": "string",
            "format": "date"
          },
          "from_seq": {
            "type": "integer"
          },
          "to_seq": {
            "type": "integer"
          },
          "head_hash": {
            "type": "string"
          },
          "ots_proof_base64": {
            "type": "string",
            "nullable": true,
            "description": "La preuve OpenTimestamps (fichier .ots, en base64) portant sur head_hash."
          },
          "ots_status": {
            "type": "string",
            "enum": [
              "pending",
              "bitcoin",
              "unconfirmed"
            ],
            "description": "pending : ots_proof_base64 contient l'engagement des calendriers (ots verify interroge les calendriers) ; bitcoin : il a été remplacé par la preuve mise à niveau vers le bloc Bitcoin (bitcoin_block_height) ; unconfirmed : aucune preuve Bitcoin n'a été renvoyée dans un délai de 14 jours, la preuve stockée se vérifie toujours via les calendriers."
          },
          "bitcoin_block_height": {
            "type": "integer",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ImportSummary": {
        "type": "object",
        "description": "Les trois niveaux d'un paquet vérifié.",
        "properties": {
          "kind_label_key": {
            "type": "string",
            "description": "Clé de libellé du type de paquet, import_kind_<kind>."
          },
          "integrity": {
            "type": "string",
            "enum": [
              "verified",
              "failed"
            ]
          },
          "time_anchor": {
            "type": "string",
            "enum": [
              "eidas_qualified",
              "eidas_qualified_revocation_unknown",
              "ots_bitcoin_attested_unconfirmed",
              "ots_pending",
              "none"
            ]
          },
          "provenance": {
            "type": "string",
            "nullable": true,
            "enum": [
              "issued_by_proofsnap",
              "unverified",
              "registered_by_extension_account",
              "not_registered",
              "unknown",
              null
            ]
          },
          "registered_at": {
            "type": "string",
            "format": "date-time",
            "description": "Date à laquelle le registre de l'extension a enregistré le paquet (provenance registered_by_extension_account)."
          },
          "issued_at": {
            "type": "string",
            "format": "date-time",
            "description": "Date à laquelle le registre d'émission de l'API a enregistré l'émission (provenance issued_by_proofsnap)."
          }
        }
      },
      "Import": {
        "type": "object",
        "properties": {
          "import_id": {
            "type": "string",
            "example": "imp_a1B2c3D4e5F6g7H8i9J0"
          },
          "status": {
            "type": "string",
            "enum": [
              "uploading",
              "verifying",
              "accepted",
              "rejected",
              "duplicate",
              "expired"
            ]
          },
          "filename": {
            "type": "string"
          },
          "size_bytes": {
            "type": "integer"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "upload_expires_at": {
            "type": "string",
            "format": "date-time",
            "description": "uploading uniquement : terminez avant ce moment (24 heures après la création)."
          },
          "finished_at": {
            "type": "string",
            "format": "date-time",
            "description": "Date à laquelle l'importation a atteint son statut définitif."
          },
          "capture_id": {
            "type": "string",
            "description": "accepted uniquement : la nouvelle capture (même identifiant que l'importation)."
          },
          "duplicate_of": {
            "type": "string",
            "description": "duplicate uniquement : votre capture qui contient déjà ce manifeste."
          },
          "kind": {
            "type": "string",
            "enum": [
              "web_capture_v21",
              "web_capture_legacy",
              "session",
              "file_certification_single",
              "file_certification_combined",
              "api_capture",
              "api_certify",
              "api_compare",
              "api_redaction"
            ],
            "description": "Le type de paquet, une fois détecté."
          },
          "summary": {
            "$ref": "#/components/schemas/ImportSummary"
          },
          "reasons": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "rejected et expired : codes de motif, par exemple file_mismatch, file_missing, unlisted_file, bad_signature, no_hashed_files, unknown_schema, zip_too_large, test_package, ots_digest_mismatch, eidas_token_failed, forensic_chain_broken, size_mismatch, upload_not_completed, verification_incomplete. Uniquement des codes, jamais de noms de fichiers."
          },
          "zip_sha256": {
            "type": "string",
            "description": "SHA-256 du ZIP téléversé, une fois calculé."
          },
          "manifest_sha256": {
            "type": "string",
            "description": "SHA-256 du manifest.json du paquet (accepté et doublon)."
          },
          "matter_id": {
            "type": "string",
            "description": "L'affaire indiquée lors de la création de l'importation."
          },
          "matter_assignment": {
            "type": "string",
            "enum": [
              "assigned",
              "skipped"
            ],
            "description": "accepted avec matter_id : indique si la capture a été ajoutée à l'affaire (skipped lorsque l'affaire était entre-temps scellée, archivée ou plus accessible pour vous ; l'importation reste accepted)."
          },
          "upload": {
            "type": "object",
            "description": "Uniquement avec ?upload=true tant que status vaut uploading.",
            "properties": {
              "upload_id": {
                "type": "string"
              },
              "part_size": {
                "type": "integer"
              },
              "parts": {
                "type": "array",
                "description": "URL PUT présignées des parties pas encore reçues ; envoyez par PUT exactement size_bytes.",
                "items": {
                  "type": "object",
                  "properties": {
                    "part_number": {
                      "type": "integer"
                    },
                    "url": {
                      "type": "string"
                    },
                    "size_bytes": {
                      "type": "integer"
                    }
                  }
                }
              },
              "expires_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "received": {
                "type": "array",
                "nullable": true,
                "items": {
                  "type": "object",
                  "properties": {
                    "part_number": {
                      "type": "integer"
                    },
                    "etag": {
                      "type": "string"
                    },
                    "size_bytes": {
                      "type": "integer"
                    }
                  }
                }
              },
              "completed": {
                "type": "boolean",
                "description": "Le téléversement était déjà terminé : envoyez de nouveau complete."
              }
            }
          }
        }
      },
      "ImportVerification": {
        "type": "object",
        "description": "Le rapport de vérification enregistré lors de l'importation (sans contenu de fichier ni détails en texte libre).",
        "properties": {
          "ok": {
            "type": "boolean"
          },
          "kind": {
            "type": "string",
            "enum": [
              "web_capture_v21",
              "web_capture_legacy",
              "session",
              "file_certification_single",
              "file_certification_combined",
              "api_capture",
              "api_certify",
              "api_compare",
              "api_redaction"
            ]
          },
          "format": {
            "type": "string"
          },
          "kind_label_key": {
            "type": "string"
          },
          "evidence_id": {
            "type": "string",
            "description": "L'identifiant que porte le paquet (ps_..., FC-..., cap_..., cert_..., cmp_..., red_...)."
          },
          "manifest_sha256": {
            "type": "string"
          },
          "zip_sha256": {
            "type": "string"
          },
          "anchor_sha256": {
            "type": "string",
            "description": "L'empreinte couverte par les horodatages : celle du manifeste ou, pour la certification d'un fichier unique, celle du fichier certifié."
          },
          "anchor_source": {
            "type": "string",
            "enum": [
              "manifest",
              "certified_file"
            ]
          },
          "checks": {
            "type": "object",
            "description": "structure, signature, files (listed, verified, presence_only, unhashed, os_junk_ignored), custody, forensic_chain, ots, eidas, test_mode.",
            "additionalProperties": true
          },
          "summary": {
            "$ref": "#/components/schemas/ImportSummary"
          },
          "reasons": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "CaptureImport": {
        "type": "object",
        "description": "Uniquement sur une capture avec source \"import\".",
        "properties": {
          "import_id": {
            "type": "string"
          },
          "package_kind": {
            "type": "string",
            "enum": [
              "web_capture_v21",
              "web_capture_legacy",
              "session",
              "file_certification_single",
              "file_certification_combined",
              "api_capture",
              "api_certify",
              "api_compare",
              "api_redaction"
            ]
          },
          "kind_label_key": {
            "type": "string"
          },
          "package_format": {
            "type": "string"
          },
          "original_evidence_id": {
            "type": "string"
          },
          "original_filename": {
            "type": "string",
            "nullable": true
          },
          "imported_at": {
            "type": "string",
            "format": "date-time"
          },
          "captured_at_basis": {
            "type": "string",
            "enum": [
              "package_claim"
            ],
            "description": "Le captured_at d'un paquet importé est l'heure indiquée par le paquet, jamais une heure observée par ProofSnap."
          },
          "manifest_sha256": {
            "type": "string"
          },
          "zip_sha256": {
            "type": "string"
          },
          "summary": {
            "$ref": "#/components/schemas/ImportSummary"
          },
          "levels": {
            "type": "object",
            "properties": {
              "integrity": {
                "type": "string"
              },
              "time_anchor": {
                "type": "string"
              },
              "provenance": {
                "type": "string",
                "nullable": true
              }
            }
          },
          "verification": {
            "$ref": "#/components/schemas/ImportVerification"
          },
          "provenance": {
            "type": "object",
            "properties": {
              "level": {
                "type": "string"
              },
              "source": {
                "type": "string",
                "nullable": true,
                "enum": [
                  "api_ledger",
                  "extension_registry",
                  null
                ]
              },
              "issued_at": {
                "type": "string",
                "nullable": true
              },
              "registered_at": {
                "type": "string",
                "nullable": true
              },
              "reason": {
                "type": "string"
              }
            }
          }
        }
      },
      "Rule902DisabledWarnings": {
        "type": "array",
        "description": "Présent uniquement lorsque la requête demandait le modèle de certification (include_rule_902: true, ou pour une capture un evidence_pack.custodian non vide) alors que le modèle est désactivé. La requête a été acceptée et l'indicateur ignoré : aucun rule_902_certification.pdf n'est généré.",
        "items": {
          "type": "object",
          "properties": {
            "code": {
              "type": "string",
              "enum": [
                "RULE902_DISABLED"
              ]
            },
            "message": {
              "type": "string",
              "example": "The FRE 902 certification template (rule_902_certification.pdf) is currently disabled. include_rule_902 was accepted and ignored: the package does not contain the template."
            }
          },
          "required": [
            "code",
            "message"
          ]
        }
      }
    }
  }
}
