{
  "openapi": "3.0.3",
  "info": {
    "title": "API ProofSnap",
    "version": "2.0.0",
    "description": "L'API ProofSnap carica una pagina web in un browser Chromium lato server e restituisce un pacchetto di prove (ZIP).\n\nOgni pacchetto di acquisizione contiene lo screenshot, l'HTML della pagina, il testo estratto dalla pagina, metadata.json, forensic_log.json, chain_of_custody.json, evidence.pdf e un manifest.json che elenca lo SHA-256 di ogni file. manifest.json è firmato con una chiave RSA-4096 generata per quell'acquisizione (la chiave pubblica è fornita come publickey.pem, la chiave privata viene scartata) e lo SHA-256 di manifest.json viene inviato ai calendari OpenTimestamps per un ancoraggio Bitcoin (manifest.json.ots). Se la richiesta ai calendari non riesce, il pacchetto viene fornito senza la prova e manifest.json non la elenca, quindi gli script offline e il verificatore la segnalano come non presente anziché mancante. Con `include_eidas` su una chiave di produzione, lo stesso hash riceve anche una validazione temporale elettronica qualificata eIDAS (RFC 3161, manifest.json.tsr). Gli screenshot JPEG vengono firmati con C2PA Content Credentials secondo il principio del massimo impegno: quando la firma non riesce, l'acquisizione si completa senza di esse. La cartella verification/ contiene script che verificano il pacchetto offline. Ogni manifest.json dichiara `ots_listed_only_when_present: true` (manifest.json.ots è elencato solo quando è presente nel pacchetto) e `eidas_marker_version: 1`. Registra `eidas_requested: true` solo quando il pacchetto viene fornito con la sua validazione temporale elettronica qualificata. Se il token non può essere ottenuto, il manifest viene risigillato senza di esso. Un pacchetto che registra `eidas_requested: true` ma non contiene manifest.json.tsr ha quindi perso il suo token, e gli script offline riportano FAIL. chain_of_custody.json registra `bitcoin_ots_included` ed elenca, in `page_modifications.items[]`, ogni modifica apportata alla pagina prima dell'acquisizione. manifest.json riporta sealed_at (quando è stato generato, dopo l'hash di ogni file) e gli scostamenti dell'orologio del server di acquisizione misurati tramite SNTP; chain_of_custody.json elenca i passaggi dell'acquisizione nell'ordine in cui sono avvenuti, ciascuno con l'ora in cui si è concluso, e termina con custody_record_written (il calcolo degli hash, la firma e le validazioni temporali avvengono dopo e sono attestati da manifest.json e dalle prove).\n\nmetadata.json registra:\n\n- `browser_identity`: lo user agent e i Client Hints effettivamente inviati.\n- `dns`: una verifica DNS indipendente dell'indirizzo a cui si è connesso il browser, con le risposte di dns.google e cloudflare-dns.com e l'esito: confirmed, not_confirmed, resolvers_unavailable, connected_ip_unknown, skipped_ip_literal (l'URL indica un indirizzo IP, quindi non c'è un nome host da cercare) oppure skipped_internal_host (un nome host interno, che non viene mai inviato ai resolver pubblici).\n- `url_at_load` e `url_at_screenshot`: l'indirizzo subito dopo il caricamento della pagina e al momento in cui ne è stata acquisita l'immagine (una pagina può modificare il proprio indirizzo, il più delle volte la parte dopo #).\n- `redirect_chain`: i reindirizzamenti del documento principale, dal più vecchio, ciascuno con il suo stato HTTP. Gli indirizzi richiesto e finale sono riportati per intero; in ogni passaggio intermedio i valori della query sono sostituiti da [redacted] (`query_values_redacted`).\n- `tls.source`: da dove proviene ciascun campo TLS, `browser_connection` oppure `browser_connection_and_separate_handshake`, quest'ultimo con `at`, `ip` e `port` dell'handshake separato che ha recuperato certificate_chain.pem dopo il caricamento della pagina.\n\nQuando un passaggio richiesto non è stato eseguito (una validazione temporale eIDAS, la firma C2PA, la richiesta all'archivio, WHOIS, la seconda lettura dell'orologio, un WARC troncato dal limite di tempo, o un'attesa o un'espansione dei contenuti saltata a causa del limite di tempo), evidence.pdf lo elenca con un breve motivo e il certificato di provenienza lo indica in una riga.\n\nLe acquisizioni sono asincrone. POST /captures restituisce 202 con un capture_id; interrogare GET /captures/{capture_id} oppure passare `options.webhook_url`. Un'acquisizione completata restituisce un package_url prefirmato valido fino a 24 ore (vedere package_url_expires_at); rileggere l'acquisizione per ottenere un link nuovo.\n\nI percorsi seguenti sono relativi all'URL del server, che contiene già lo stage /v1. I valori `poll_path` restituiti dall'API includono /v1.",
    "contact": {
      "email": "support@getproofsnap.com"
    }
  },
  "servers": [
    {
      "url": "https://api.getproofsnap.com/v1",
      "description": "API ProofSnap (UE)"
    }
  ],
  "security": [
    {
      "ApiKeyAuth": []
    }
  ],
  "tags": [
    {
      "name": "Acquisizioni",
      "description": "Creare, elencare, leggere, eliminare e verificare acquisizioni.\n\n**Limiti di frequenza.** `POST /captures`, `POST /captures/batch`, `POST /compare`, `POST /captures/{capture_id}/redact` e `POST /certify-hash` sono soggetti a un limite di frequenza per account su finestre fisse di 60 secondi: tutte le chiavi API dell'account e la relativa sessione della dashboard condividono un'unica finestra (aggiungere chiavi non aumenta il limite). Un lotto o una certificazione di più hash consuma un'unità per URL o per hash. Limiti predefiniti per finestra in base al piano:\n\n| Piano | Richieste per 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| qualsiasi altro piano | 30 |\n\nAnche le richieste seguenti attingono alla stessa finestra, un'unità per richiesta salvo diversa indicazione: `POST /captures/{capture_id}/verify`, `PATCH /captures/{capture_id}` con l'azione legal_hold, `GET /captures/hosts` (solo quando l'elenco viene ricalcolato, non quando viene restituito un risultato in cache), `POST /legal-holds` (un'unità ogni 10 acquisizioni), `POST /captures/export-jobs`, nonché `POST /imports`, `POST /imports/{import_id}/complete` e `GET /imports/{import_id}?upload=true`.\n\nQuando il limite viene superato, l'API restituisce 429 RATE_LIMITED con gli header `Retry-After`, `X-RateLimit-Limit`, `X-RateLimit-Remaining` e `X-RateLimit-Reset` e `error.details.retry_after_seconds`. Una richiesta che necessita di più unità di quante ne consenta una finestra (una certificazione di più hash superiore al limite per finestra) non può mai riuscire e restituisce 400 RATE_LIMIT_COST_TOO_HIGH con `error.details` `{ max_per_window, window_seconds, requested }`. Suddividerla in richieste più piccole. Una richiesta rifiutata non consuma unità e gli header riportano il budget residuo effettivo.\n\n**Limiti mensili.** Acquisizioni, confronti, oscuramenti e certificazioni con chiavi di produzione vengono conteggiati in un massimo mensile per account (mese solare UTC). La risposta di accettazione contiene gli header `X-Quota-*`. Il raggiungimento del massimo restituisce 402 QUOTA_EXCEEDED con `error.details` `{ reason, used, limit, max_limit, cap, org_member }`, dove `cap` è `plan_max`, `customer`, `member_owner` o `org`. Finché un abbonamento di produzione è scaduto o non pagato, le richieste fatturabili restituiscono 402 PAYMENT_REQUIRED; per un tale account, le acquisizioni (singole e in lotto) vengono rifiutate con questo errore indipendentemente dall'ambiente della chiave, chiavi di prova comprese.\n\n**Acquisizioni in corso e modalità di prova.** Ogni account può eseguire contemporaneamente solo un numero limitato di acquisizioni (developer 10, startup 25, growth 100, legal_solo e legal_seats 25, custom 200, website_archive 25, sandbox 5). Oltre tale limite, l'API restituisce 429 CONCURRENT_CAPTURE_LIMIT; un lotto avvia gli URL che rientrano nel limite ed elenca gli altri in `not_admitted` (ciascuno con `code` CONCURRENT_CAPTURE_LIMIT, più `in_flight_limit`), da inviare di nuovo in seguito. Il lavoro in modalità di prova (una chiave di prova, o una sessione senza fatturazione collegata) è limitato a 200 unità per account per giorno UTC (429 TEST_MODE_DAILY_LIMIT; header `X-Test-Daily-*`) e un'acquisizione in modalità di prova non viene mai inviata all'Internet Archive (`external_archive.skip_reason` \"test_mode\")."
    },
    {
      "name": "Confrontare e oscurare",
      "description": "Ricavare un nuovo pacchetto sigillato da acquisizioni esistenti: un confronto tra due acquisizioni o una copia oscurata di una di esse."
    },
    {
      "name": "Certificazione di hash",
      "description": "Certificare gli hash SHA-256 dei file conservati in proprio. Vengono inviati solo gli hash; i file non raggiungono mai ProofSnap."
    },
    {
      "name": "Acquisizioni automatiche",
      "description": "Acquisizioni ricorrenti (interval o cron) e monitoraggio delle modifiche (on_change). Ogni esecuzione che produce un'acquisizione è un'acquisizione normale: viene conteggiata e rientra nei limiti mensili."
    },
    {
      "name": "Pratiche",
      "description": "Spazi di lavoro per le pratiche legali con un registro della catena di custodia concatenato tramite hash. Disponibili sui piani Legal (legal_solo, legal_seats); gli altri piani ricevono 403 MATTERS_NOT_ENABLED, con due eccezioni. Il titolare di uno studio il cui abbonamento è terminato può ancora elencare, aprire ed esportare le pratiche di quello studio fino alla data di eliminazione dei dati dello studio (data_delete_after); questo accesso è in sola lettura più esportazione, e GET /matters aggiunge allora read_only: true. Un account con il piano Archive only può elencare, aprire ed esportare le proprie pratiche. Solo il titolare della pratica può rinominarla, archiviarla, gestirne i membri, sigillarla, condividerla o esportarla. Il titolare della pratica è chi l'ha creata. In una pratica dello studio, il titolare dello studio diventa titolare della pratica solo dopo che chi l'ha creata ha lasciato lo studio. Un titolare dello studio aggiunto come membro ha solo i diritti di membro. I membri ricevono 403 FORBIDDEN per tali operazioni. Ogni scrittura su una pratica (creazione, PATCH, assegnazione di acquisizioni, condivisione e relativa revoca, sigillatura, esportazione) richiede un indirizzo e-mail confermato (403 EMAIL_NOT_VERIFIED, per una sessione della dashboard e per una chiave API di un account il cui indirizzo non è confermato) e, per un chiamante di produzione (chiave API di produzione, o sessione con fatturazione collegata), viene rifiutata con 402 PAYMENT_REQUIRED finché l'abbonamento (o quello dello studio) è scaduto o non pagato. Una pratica archiviata rifiuta ridenominazioni, nuovi membri, nuove acquisizioni, condivisione ed esportazione (409 MATTER_ARCHIVED) finché non viene riaperta; un chiamante che non può riaprirla (il titolare di uno studio cessato, un account Archive only) può comunque esportarla."
    },
    {
      "name": "Webhook",
      "description": "ProofSnap invia con POST un evento JSON al `webhook_url` indicato con un'acquisizione, una certificazione, un oscuramento o una pianificazione. L'URL deve usare https, non deve contenere credenziali (`user:password@`, 400 INVALID_URL) e non deve puntare a un indirizzo privato, di loopback, link-local, NAT di livello carrier (CGNAT), multicast, riservato o di metadati cloud, comprese le forme IPv6 che incorporano un tale indirizzo IPv4. Il controllo avviene quando la richiesta viene accettata (422 SSRF_BLOCKED) e di nuovo a ogni consegna: il nome host viene risolto, ogni indirizzo viene controllato e la consegna si connette solo a un indirizzo che ha superato il controllo, così un host che in seguito si risolve in un indirizzo bloccato non viene contattato. Se il nome host non può essere risolto, la consegna non viene inviata. Su tutte le route, un webhook_url vuoto o composto solo da spazi equivale all'assenza di webhook. /compare non invia webhook.\n\n**Sottoscrizioni dell'account.** Invece di (o in aggiunta a) un webhook_url per richiesta, è possibile sottoscrivere un endpoint agli eventi dell'account con `POST /webhook-subscriptions`: ogni evento corrispondente dell'account viene inviato a quell'endpoint, indipendentemente dalla chiave, dalla pianificazione o dalla sessione della dashboard che lo ha generato. Ogni sottoscrizione ha un proprio secret (restituito una sola volta, al momento della creazione), elenca i tipi di evento che desidera ricevere e può filtrare per `host` o `schedule_id`; una sottoscrizione live riceve solo gli eventi delle acquisizioni live, e una sottoscrizione di prova solo quelli delle acquisizioni di prova. Una chiave API elenca e modifica solo le sottoscrizioni del proprio ambiente; la sessione della dashboard le vede tutte. Le consegne di una sottoscrizione riportano `subscription_id` al livello superiore e nessun link prefirmato (package_url, package_url_expires_at, upgraded_proof_url e compare.package_url vengono rimossi; monitor.change_detected riporta compare.compare_id): scaricare il pacchetto con GET /captures/{capture_id}. Gli eventi di acquisizione riportano anche `capture_id` al livello superiore. Il webhook-id di una consegna di sottoscrizione è derivato dalla sottoscrizione, dall'acquisizione (o dalla pianificazione) e dall'evento. Si applicano le regole di consegna e i nuovi tentativi descritti di seguito. Inoltre, una risposta 410 disattiva immediatamente la sottoscrizione (disabled_reason gone), consegne che falliscono per 5 giorni senza alcun successo (e almeno un ciclo completo di tentativi falliti) la disattivano (failing), e la revoca della chiave API che l'ha creata la disattiva (key_revoked); PATCH status \"active\" la riattiva. Ogni evento viene inoltre conservato per 30 giorni in `GET /events`.\n\n**Header**\n\nOgni consegna è firmata secondo la specifica [Standard Webhooks](https://www.standardwebhooks.com/), perciò qualsiasi libreria ufficiale Standard Webhooks può verificarla:\n\n| Header | Valore |\n| --- | --- |\n| `Content-Type` | `application/json` |\n| `webhook-id` | `msg_<delivery id>`: identico per lo stesso evento della stessa acquisizione, quindi una consegna duplicata riporta lo stesso id. Usarlo per la deduplicazione: elaborare ogni webhook-id una sola volta. `GET /webhook-events/{capture_id}` restituisce lo stesso id senza il prefisso `msg_` come `delivery_id` |\n| `webhook-timestamp` | Ora Unix, in secondi, dell'invio della consegna. È coperto dalla firma |\n| `webhook-signature` | `v1,<base64>`: HMAC-SHA256 su `<webhook-id>.<webhook-timestamp>.<raw body>`. Durante una rotazione del secret del webhook contiene due voci separate da uno spazio, `v1,<new> v1,<previous>`; accettare la consegna quando una qualsiasi delle voci corrisponde |\n| `User-Agent` | `ProofSnap-Webhook/2.0` |\n\n**Verificare una consegna.** Eseguire la verifica sui byte esatti ricevuti, prima di qualsiasi parsing. Rifiutare una consegna il cui webhook-timestamp si discosta dal proprio orologio di oltre 5 minuti (le librerie ufficiali lo fanno) e deduplicare per webhook-id. La chiave di firma è la parte del secret che segue `whsec_`, decodificata da base64. `webhook_secret` è `whsec_` seguito da base64 standard, la forma che si aspettano le librerie ufficiali: passarlo così com'è.\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\nSenza libreria (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 di firma del webhook.** Ogni chiave API ha un proprio secret (`whsec_...`). Viene mostrato una sola volta, quando si crea la chiave nella dashboard in Sviluppatori (il campo `webhook_secret` della risposta che crea la chiave). Non può essere riletto in seguito: l'elenco delle chiavi non lo restituisce mai. Se lo si perde, o se potrebbe essere stato divulgato, generarne uno nuovo nella dashboard in Sviluppatori (Nuovo secret del webhook sulla chiave). Anche il nuovo secret viene mostrato una sola volta. **Sovrapposizione durante la rotazione:** per 24 ore dopo una rotazione (fino a `previous_secret_expires_at` nella risposta della rotazione) ogni consegna è firmata sia con il nuovo sia con il precedente secret (due voci `v1,` in webhook-signature), così è possibile passare l'endpoint al nuovo secret senza perdere consegne. Una seconda rotazione mentre la sovrapposizione è ancora in corso viene rifiutata con 409 WEBHOOK_SECRET_ROTATION_PENDING e `error.details.previous_secret_expires_at` (il momento da cui è possibile ruotare di nuovo): in questo modo non sono mai attivi più di due secret contemporaneamente, e un secret su cui i destinatari potrebbero ancora fare affidamento non viene mai ritirato in anticipo. Per un secret divulgato, revocare la chiave e crearne una nuova. La revoca della chiave elimina entrambi i secret. Le consegne usano i secret della chiave al momento dell'invio, anche per le attività avviate prima di una rotazione. Una richiesta proveniente da una sessione autenticata della dashboard non ha una chiave API e quindi nessun secret del webhook, perciò un `webhook_url` al suo interno viene rifiutato con 400 `WEBHOOK_REQUIRES_API_KEY` (acquisizioni, batch, oscuramenti, certificazioni di hash, creazione o modifica di un'acquisizione automatica). Le esecuzioni pianificate usano il secret della chiave che ha creato la pianificazione. Quando la chiave è stata revocata, o il suo secret non può essere letto al momento della consegna, l'evento non viene inviato e il tentativo viene registrato con il motivo (consultarlo con `GET /webhook-events/{capture_id}`).\n\n**Consegna.** Ogni tentativo ha un timeout di 10 secondi. I reindirizzamenti non vengono mai seguiti: una risposta 3xx conta come errore, come una risposta fuori da 2xx o un timeout. Il corpo della risposta non viene letto. **Nuovi tentativi:** una consegna che va in timeout, non riesce a connettersi o riceve una risposta fuori da 2xx (tranne 410 Gone) viene inviata di nuovo circa 5 minuti, 30 minuti, 2 ore, 8 ore e 24 ore dopo (tentativi da 2 a 6, circa 35 ore in totale), ogni volta con lo stesso `webhook-id` e un nuovo `webhook-timestamp`. Dopodiché la consegna viene abbandonata. Una consegna ripetuta riporta lo stesso payload SENZA il link di download valido 24 ore (`package_url`, `package_url_expires_at`): scaricare il pacchetto con GET /captures/{capture_id}. Le consegne relative a un'acquisizione cancellata nel frattempo vengono scartate. Un errore temporaneo da parte nostra nella lettura del secret di firma viene ritentato allo stesso modo. Un 410 Gone, un reindirizzamento, un indirizzo bloccato o una chiave mancante o revocata non vengono ritentati. Il corpo di un nuovo tentativo può differire da quello del primo (nessun link di download): deduplicare per webhook-id, non per un hash del corpo. Rispondere rapidamente con un 2xx. Il registro dei tentativi annota una classe di errore generica (connection_failed, timeout, blocked_address, redirect_not_followed, http_<status>), mai l'errore di rete grezzo. certify.completed e redaction.completed vengono inviati in modo asincrono, poco dopo la restituzione della risposta. Ogni tentativo viene registrato; consultare il registro con `GET /webhook-events/{capture_id}`.\n\n**Eventi**\n\n`capture.completed`: un'acquisizione (singola, elemento di un batch, o esecuzione di una pianificazione a intervallo o cron) è terminata. Anche le sottoscrizioni webhook dell'account lo ricevono per le acquisizioni di riferimento e di modifica di una pianificazione on_change (il webhook_url della pianificazione riceve per queste monitor.change_detected). `capture` ha gli stessi campi di un `GET /captures/{capture_id}` completato, più `timestamps.eidas` quando è stata aggiunta una validazione temporale elettronica qualificata, `schedule_id` (null al di fuori di una pianificazione), `test_mode` e `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`: non è stato possibile completare l'acquisizione. `error.message` è un breve motivo pubblico (per un errore imprevisto \"The capture could not be completed.\"); i dettagli interni dell'errore non vengono mai inviati. Ciò comprende un'esecuzione andata in crash o in timeout: viene contrassegnata come fallita da un'attività periodica di pulizia circa 30 minuti dopo l'avvio, e in quel momento viene inviato capture.failed (messaggio \"Capture did not finish (timed out).\"). La stessa attività periodica invia certify.failed o redaction.failed per una certificazione di hash o un oscuramento che non si è concluso. Un'esecuzione che termina dopo essere stata segnalata come fallita non viene addebitata e non invia 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`: una certificazione di hash è terminata. `certificate` ha gli stessi campi della risposta sincrona di `POST /certify-hash`. Consegnato in modo asincrono, poco dopo la restituzione della risposta.\n\n`certify.failed`: non è stato possibile completare una certificazione di hash.\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 oscuramento è terminato. `redaction` ha gli stessi campi della risposta di `POST /captures/{capture_id}/redact`.\n\n`redaction.failed`: non è stato possibile completare un oscuramento. `error.code` è il codice che la richiesta avrebbe restituito (ad esempio REDACTION_LEAK_DETECTED), con `details` quando presenti; un oscuramento contrassegnato come fallito dall'attività periodica di pulizia riporta 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`: una pianificazione `on_change` ha rilevato una modifica del testo della pagina e ha eseguito una nuova acquisizione.\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` e `metadata_verdict` (dal 2026-10-14) riprendono i risultati per categoria del confronto in compare.diff_summary (null in assenza di confronto). Una pianificazione con `alert_on: \"text\"` invia monitor.change_detected solo quando text_verdict è changed (o quando non c'è un risultato del confronto).\n\n`capture.anchor_confirmed` (solo sottoscrizioni e GET /events): l'ancoraggio Bitcoin della prova OpenTimestamps del pacchetto è stato confermato, di solito poche ore dopo l'acquisizione.\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` (solo sottoscrizioni e GET /events): ProofSnap ha sospeso una pianificazione. `reason` è `no_billing` (nessun accesso all'API), `plan_downgrade` (il piano consente meno pianificazioni, nessun on_change o nessuna nuova attività), `failures` (cinque esecuzioni fallite consecutive), `key_revoked` o `invalid_cron`. Una sospensione effettuata dall'utente non è un evento.\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` è `ok` (compare contiene il confronto), `no_baseline` (nessuna acquisizione precedente con cui confrontare) o `failed` (il confronto non è riuscito; compare è null)."
    },
    {
      "name": "Pubblico",
      "description": "Operazioni che non richiedono una chiave API."
    },
    {
      "name": "Importazione di pacchetti",
      "description": "Importare nel proprio account pacchetti di prove ProofSnap esistenti (file ZIP prodotti dall'estensione ProofSnap: acquisizioni web, sessioni multischeda, certificazioni di file singole e combinate; oppure da questa API: acquisizioni, certificazioni, confronti, oscuramenti), in modo che siano gestiti come le acquisizioni API: conservati per 10 anni e disponibili per il blocco per controversia legale, le pratiche e le esportazioni.\n\nSolo piani Legal (legal_solo, legal_seats), con una chiave API di produzione o una sessione della dashboard con fatturazione collegata (una chiave di prova, una sessione senza fatturazione e la Sandbox ricevono 403 IMPORT_NOT_ALLOWED). Un indirizzo e-mail non confermato riceve 403 EMAIL_NOT_VERIFIED, un abbonamento scaduto 402 PAYMENT_REQUIRED. Un'importazione non è un'acquisizione: non viene conteggiata nei limiti mensili e non ha un costo per importazione; viene fatturata solo come dati archiviati.\n\n**Procedura.** 1. `POST /imports` con il nome del file e la sua dimensione esatta in byte (al massimo 2 GiB): la risposta contiene un URL prefirmato per ogni parte da 32 MiB (l'ultima parte è più piccola), valido per 1 ora. 2. Inviare con PUT i byte di ciascuna parte al relativo URL e conservare l'header di risposta `ETag` di ciascuna. 3. `POST /imports/{import_id}/complete` con ogni numero di parte e il relativo ETag. 4. Interrogare `GET /imports/{import_id}` finché lo stato non è definitivo.\n\n**Stati.** `uploading` (in attesa dei caricamenti delle parti e della chiamata complete; un caricamento non completato entro 24 ore diventa `expired`), `verifying` (il pacchetto è in fase di controllo), `accepted` (la verifica è riuscita: `capture_id` è la nuova acquisizione, con lo stesso ID dell'importazione), `rejected` (la verifica non è riuscita: `reasons` elenca i codici; non viene conservato nulla tranne il record dell'importazione), `duplicate` (l'account contiene già un'acquisizione con lo stesso manifest: `duplicate_of`), `expired`.\n\n**Verifica.** Rigorosa: la firma RSA su manifest.json, ogni file elencato presente con lo SHA-256 elencato (e SHA-512 e dimensione, se elencati), nessun file non elencato nel manifest (tranne i file aggiunti dal sistema operativo, come .DS_Store, e le prove derivate dal manifest), almeno un file sottoposto a hash, un ID della prova coerente, la catena di hash del log forense ricalcolata, la prova OpenTimestamps confrontata con il manifest (offline) e la validazione temporale elettronica qualificata eIDAS confrontata con la radice Disig fissata, con i dati di validazione contenuti nel pacchetto. La struttura ZIP viene controllata prima di leggere qualsiasi contenuto (nessuna cifratura, nessun ZIP64, nessun link, nomi sicuri, nessun nome duplicato, dimensioni e CRC-32 di ogni voce). Un pacchetto di prova dell'API (con filigrana) viene rifiutato. I pacchetti API sigillati prima che evidence.pdf venisse sottoposto a hash vengono accettati, con tale file elencato in `verification.checks.files.unhashed`.\n\n**Livelli** (`summary`): `integrity` (verified o failed), `time_anchor` (eidas_qualified, eidas_qualified_revocation_unknown, ots_bitcoin_attested_unconfirmed, ots_pending, none) e `provenance`: per i pacchetti API `issued_by_proofsnap` (il manifest è nel registro di emissione dei pacchetti dell'API) o `unverified`; per i pacchetti dell'estensione `registered_by_extension_account` (un utente dell'estensione con accesso eseguito ha registrato quel manifest al momento della creazione) o `not_registered`; `unknown` quando non è stato possibile eseguire la ricerca. La provenienza non fa mai fallire un'importazione.\n\n**L'acquisizione.** Un'importazione accettata è un'acquisizione con `source` \"import\": è scaricabile (lo ZIP caricato, byte per byte) e verificabile (POST /captures/{capture_id}/verify esegue di nuovo gli stessi controlli), può essere sottoposta a blocco per controversia legale, spostata nel cestino e cancellata, compare nelle pratiche (se importata con `matter_id`, il registro della catena di custodia della pratica registra `package.imported`) e nelle esportazioni, ed è conservata per 10 anni dall'importazione. Il suo `captured_at` è l'ora dichiarata dal pacchetto stesso (`import.captured_at_basis` \"package_claim\"), mai un'ora osservata da ProofSnap; `created_at` è il momento in cui è stato creato il caricamento. I confronti e gli oscuramenti di un pacchetto importato vengono rifiutati (422 IMPORT_NOT_SUPPORTED).\n\n**Limiti.** Al massimo 3 importazioni in corso (in caricamento o in verifica) per account (409 IMPORT_LIMIT), 50 importazioni create per account e giorno UTC (429 RATE_LIMITED, `details.action` \"import_create\"; conta ogni richiesta di creazione, anche una rifiutata in seguito) e il limite di frequenza al minuto del piano per create e complete. Volume di importazione: al massimo 5 GiB al giorno UTC e 20 GiB al mese UTC di dimensioni dichiarate dei pacchetti per account, conteggiati al momento di create (e di complete per un caricamento meno recente); oltre tale soglia 429 IMPORT_DAILY_BYTES_LIMIT o IMPORT_MONTHLY_BYTES_LIMIT con `Retry-After`; un caricamento annullato non viene conteggiato."
    }
  ],
  "paths": {
    "/captures": {
      "post": {
        "tags": [
          "Acquisizioni"
        ],
        "operationId": "createCapture",
        "summary": "Acquisire un URL",
        "description": "Accetta l'acquisizione e restituisce subito 202; l'acquisizione viene eseguita in background. Interrogare `GET /captures/{capture_id}` oppure passare `options.webhook_url`. Il nome host dell'URL viene risolto e verificato prima che l'acquisizione sia accettata.\n\nLimite di frequenza per account su finestre di 60 secondi (tutte le chiavi API dell'account e la relativa sessione della dashboard condividono un'unica finestra). Dopo l'esecuzione del limitatore, le risposte di questa route includono gli header `RateLimit-Policy` e `RateLimit` (basati sull'Internet-Draft IETF draft-ietf-httpapi-ratelimit-headers) oltre ai precedenti `X-RateLimit-Limit`, `X-RateLimit-Remaining` e `X-RateLimit-Reset`. Una risposta 429 aggiunge `Retry-After`.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CaptureRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accettato. Anche la ripetizione idempotente di una richiesta precedente con la stessa Idempotency-Key restituisce 202, con `Idempotent-Replay: true` e il corpo originale.",
            "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 o INVALID_EVIDENCE_PACK_OPTIONS. WEBHOOK_REQUIRES_API_KEY: webhook_url inviato da una sessione della dashboard (i webhook richiedono una chiave 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 (massimo mensile o quota del Sandbox; riporta gli header X-Quota-* o X-Sandbox-*) oppure 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 con una chiave di prova (una chiave ps_live_ di un account senza account di fatturazione collegato funziona come chiave di prova). EMAIL_NOT_VERIFIED: una sessione della dashboard il cui indirizzo e-mail non è confermato. PLAN_ARCHIVE_ONLY: il piano mantiene solo l'archivio e non crea nuovo lavoro. PLAN_API_READ_ONLY: una chiave API di un account Website Archive (la sua chiave è di sola lettura; utilizzare la dashboard). PLAN_API_READ_ONLY con details.reason \"session_outside_dashboard\": una sessione della dashboard di un account Website Archive la cui richiesta non ha l'header Origin o ne ha uno che non è un indirizzo della dashboard ProofSnap (su quel piano il nuovo lavoro si crea solo nella dashboard; si tratta di un vincolo blando, non di un confine di sicurezza).",
            "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 oppure 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: troppe richieste nella finestra corrente di 60 secondi. `error.details.retry_after_seconds` ripete Retry-After. RateLimit riporta r=0 e i secondi mancanti al ripristino della finestra. Inoltre RATE_LIMITED, per una sessione della dashboard Website Archive: il suo tetto giornaliero di 100 unità per giorno UTC, condiviso da acquisizioni singole e batch (un'unità per URL), compare, redact e certify-hash (un'unità per richiesta); una richiesta che non rientra viene rifiutata per intero (details.daily_limit, details.remaining, details.action \"wa_session_captures\"; Retry-After e details.retry_after_seconds indicano i secondi mancanti al giorno UTC successivo, e gli header RateLimit e X-RateLimit-* al minuto vengono omessi). Un breve rifiuto con solo details.retry_after_seconds e Retry-After: 1 indica un aggiornamento concorrente del contatore: riprovare. Un rifiuto non imputabile al chiamante restituisce le unità; un'attività accettata con 202 che fallisce in seguito mantiene la propria unità. Un controllo morbido, non un confine di sicurezza. Un'acquisizione singola la cui Idempotency-Key viene servita dall'archivio (una ripetizione, 409 o 422) viene servita prima del tetto giornaliero e non costa alcuna unità.\n\nCONCURRENT_CAPTURE_LIMIT: troppe acquisizioni dell'account sono in esecuzione contemporaneamente (developer 10, startup 25, growth 100, legal_solo e legal_seats 25, custom 200, website_archive 25, sandbox 5); `details.limit`, `details.in_flight`, `details.requested` e Retry-After indicano quando riprovare.\n\nTEST_MODE_DAILY_LIMIT: le attività in modalità di prova (una chiave di prova, o una sessione senza fatturazione collegata) sono limitate a 200 unità per account e giorno UTC; `details.used`, `details.limit`, `details.reset_at`, gli header `X-Test-Daily-*` e 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 (non è stato possibile avviare l'acquisizione; details.capture_id) o 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": [
          "Acquisizioni"
        ],
        "operationId": "listCaptures",
        "summary": "Elencare le proprie acquisizioni",
        "description": "Righe create dall'utente, dalla più recente, in ogni stato (comprese quelle eliminate, il cestino; filtrare con status). Ogni riga riporta source, redacted_from, label e compared, così il suo tipo può essere mostrato senza rileggerla. Un'acquisizione eseguita per uno studio viene elencata solo finché l'utente può ancora agire su di essa (occupa una postazione di quello studio o ne è titolare); una pagina può quindi contenere meno righe di limit, e next_cursor prosegue dopo l'ultima riga letta. Sono inclusi certificati, confronti e oscuramenti.\n\nRicerca (solo nel proprio elenco): url, host o schedule_id, combinati con from / to e status, trovano il record di una pagina in un dato giorno. Un filtro errato, due tra url / host / schedule_id, oppure from non anteriore a to restituisce 400 MALFORMED_REQUEST.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Dimensione della pagina, da 1 a 200 (predefinito 50). Un valore fuori intervallo restituisce 400 MALFORMED_REQUEST.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "next_cursor della pagina precedente (formato created_at|capture_id, oppure completed_at|capture_id con order=completed_at). Passarlo invariato.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Solo le righe in questo stato: processing, completed, failed o deleted (cestino). Qualsiasi altro valore restituisce 400 MALFORMED_REQUEST.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "url",
            "in": "query",
            "required": false,
            "description": "Solo le acquisizioni di questo URL esatto, così come è stato acquisito (http o https). Al massimo uno tra url, host e schedule_id.",
            "schema": {
              "type": "string",
              "maxLength": 2048
            }
          },
          {
            "name": "host",
            "in": "query",
            "required": false,
            "description": "Solo le acquisizioni il cui URL si trova su questo nome host (senza distinzione tra maiuscole e minuscole, ad esempio www.example.com; i sottodomini sono host distinti). Al massimo uno tra url, host e schedule_id. Le righe create prima del 2026-10-05 riportano l'host dopo l'esecuzione del backfill.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "schedule_id",
            "in": "query",
            "required": false,
            "description": "Solo le acquisizioni eseguite da questa pianificazione. Al massimo uno tra url, host e schedule_id.",
            "schema": {
              "type": "string",
              "pattern": "^sch_[A-Za-z0-9]{8,32}$"
            }
          },
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "Creato in questo momento o dopo: una data (YYYY-MM-DD, mezzanotte UTC) o un timestamp ISO 8601.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "Creato prima di questo momento (escluso): una data (YYYY-MM-DD, mezzanotte UTC) o un timestamp ISO 8601. Deve essere successivo a from. Con schedule_id o url, la riga più recente prima di un dato momento è la versione allora vigente.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "order",
            "in": "query",
            "required": false,
            "description": "created_at (predefinito: prima le più recenti per creazione) oppure completed_at (prima le più recenti per completamento; solo con status=completed, altrimenti 400 MALFORMED_REQUEST). Con completed_at, from e to delimitano completed_at, e next_cursor ha la forma completed_at|capture_id. Funziona con url, host e schedule_id.",
            "schema": {
              "type": "string",
              "enum": [
                "created_at",
                "completed_at"
              ],
              "default": "created_at"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Una pagina di acquisizioni.",
            "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": "Quando l'acquisizione è stata completata (null durante l'elaborazione o dopo un errore)."
                          },
                          "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": "Solo righe completed. Uguale a external_archive di GET /captures/{capture_id}."
                          },
                          "label": {
                            "type": "string",
                            "nullable": true,
                            "description": "L'etichetta della riga (certificati: l'etichetta indicata; oscuramenti: quale acquisizione oscurano)."
                          },
                          "compared": {
                            "type": "array",
                            "nullable": true,
                            "items": {
                              "type": "string"
                            },
                            "description": "Solo confronti: gli ID delle due acquisizioni di origine."
                          },
                          "source": {
                            "type": "string",
                            "nullable": true,
                            "description": "Come è stata creata la riga: api (un'acquisizione web), schedule, certify-hash (un certificato), compare (un confronto) o redact (un oscuramento); null nelle righe precedenti all'introduzione del campo."
                          },
                          "redacted_from": {
                            "type": "string",
                            "nullable": true,
                            "description": "Solo oscuramenti: l'acquisizione da cui è stato ricavato l'oscuramento."
                          },
                          "captured_at": {
                            "type": "string",
                            "nullable": true,
                            "format": "date-time",
                            "description": "Quando la pagina è stata acquisita (righe completate). Per un oscuramento, l'ora di acquisizione della sua acquisizione di origine (redacted_at indica quando è stato eseguito l'oscuramento)."
                          },
                          "redacted_at": {
                            "type": "string",
                            "format": "date-time",
                            "description": "Solo oscuramenti: quando è stato eseguito l'oscuramento. Il captured_at di un oscuramento è l'ora di acquisizione della sua acquisizione di origine."
                          },
                          "compared_at": {
                            "type": "string",
                            "nullable": true,
                            "format": "date-time",
                            "description": "Solo confronti: il momento in cui è stato eseguito il confronto. Il suo captured_at riporta lo stesso valore (mantenuto per i client meno recenti); utilizzare compared_at."
                          },
                          "manifest_hash": {
                            "type": "string",
                            "nullable": true,
                            "description": "sha256:<hex> del manifest.json del pacchetto (righe completate)."
                          },
                          "private_note": {
                            "type": "string",
                            "nullable": true,
                            "description": "La Sua nota privata sull'acquisizione (vedere PrivateNote), mai parte del pacchetto di prove."
                          },
                          "legal_hold": {
                            "type": "boolean",
                            "description": "L'acquisizione è sotto blocco per controversia legale (non viene eliminata finché è true; il blocco viene revocato automaticamente quando l'abbonamento è terminato e sono trascorsi i suoi 30 giorni). Vedere PATCH /captures/{capture_id}."
                          }
                        }
                      }
                    },
                    "next_cursor": {
                      "type": "string",
                      "description": "null quando questa pagina ha meno righe di limit.",
                      "nullable": true
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "MALFORMED_REQUEST: cursor non valido, status non valido (uno tra processing, completed, failed, deleted), limit non valido (da 1 a 200) o filtro di ricerca non valido (url, host, schedule_id, from, to; al massimo uno tra url / host / schedule_id; from precedente a 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": [
          "Acquisizioni"
        ],
        "operationId": "createBatch",
        "summary": "Acquisire più URL",
        "description": "Avvia un'acquisizione per ogni URL con le stesse opzioni. Ogni acquisizione è indipendente: interrogare ciascun capture_id o ricevere un webhook per acquisizione. Consuma un'unità del limite di frequenza e un'unità del limite mensile per URL. Un batch con più URL di quanti ne consenta una finestra del limite di frequenza non può mai riuscire, quindi viene rifiutato con 400 RATE_LIMIT_COST_TOO_HIGH invece di 429. Suddividerlo in richieste più piccole. La ripetizione di un batch completato (stessa Idempotency-Key e stesso corpo) viene servita prima dell'applicazione del limite di frequenza e non costa alcuna unità.\n\nLimite di frequenza per account in finestre di 60 secondi (tutte le chiavi API dell'account e la sua sessione della dashboard condividono una sola finestra). Una volta eseguito il limitatore, le risposte di questa route includono gli header `RateLimit-Policy` e `RateLimit` (basati sulla bozza IETF draft-ietf-httpapi-ratelimit-headers) più gli header legacy `X-RateLimit-Limit`, `X-RateLimit-Remaining` e `X-RateLimit-Reset`. Un 429 aggiunge `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": "Accettato. Anche la ripetizione idempotente di un lotto completato con la stessa Idempotency-Key e lo stesso corpo restituisce 202, con `Idempotent-Replay: true`; viene servita prima dell'applicazione del limite di frequenza e quindi non consuma alcuna unità del limite di frequenza.",
            "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 e received, oppure index, url e un oggetto reason annidato per l'URL non valido), MALFORMED_REQUEST o un errore delle opzioni. WEBHOOK_REQUIRES_API_KEY: webhook_url inviato da una sessione della dashboard (i webhook richiedono una chiave API). RATE_LIMIT_COST_TOO_HIGH: più URL di quanti ne consenta una finestra del limite di frequenza; 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 oppure PAYMENT_REQUIRED. Un batch viene rifiutato con QUOTA_EXCEEDED anche quando l'utilizzo mensile non può essere letto in quel momento; riprovare.",
            "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 con una chiave di prova. EMAIL_NOT_VERIFIED: una sessione della dashboard il cui indirizzo e-mail non è confermato. PLAN_ARCHIVE_ONLY: il piano mantiene solo l'archivio e non crea nuovo lavoro. PLAN_API_READ_ONLY: una chiave API di un account Website Archive (la sua chiave è di sola lettura; utilizzare la dashboard). PLAN_API_READ_ONLY con details.reason \"session_outside_dashboard\": una sessione della dashboard di un account Website Archive la cui richiesta non ha l'header Origin o ne ha uno che non è un indirizzo della dashboard ProofSnap (su quel piano il nuovo lavoro si crea solo nella dashboard; si tratta di un vincolo blando, non di un confine di sicurezza).",
            "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: troppe richieste nella finestra corrente di 60 secondi. `error.details.retry_after_seconds` ripete Retry-After. RateLimit riporta r=0 e i secondi mancanti al ripristino della finestra. Inoltre RATE_LIMITED, per una sessione della dashboard Website Archive: il suo tetto giornaliero di 100 unità per giorno UTC, condiviso da acquisizioni singole e batch (un'unità per URL), compare, redact e certify-hash (un'unità per richiesta); una richiesta che non rientra viene rifiutata per intero (details.daily_limit, details.remaining, details.action \"wa_session_captures\"; Retry-After e details.retry_after_seconds indicano i secondi mancanti al giorno UTC successivo, e gli header RateLimit e X-RateLimit-* al minuto vengono omessi). Un breve rifiuto con solo details.retry_after_seconds e Retry-After: 1 indica un aggiornamento concorrente del contatore: riprovare. Un rifiuto non imputabile al chiamante restituisce le unità; un'attività accettata con 202 che fallisce in seguito mantiene la propria unità. Un controllo morbido, non un confine di sicurezza.\n\nCONCURRENT_CAPTURE_LIMIT: nessun URL del batch ha potuto avviarsi perché l'account ha già in esecuzione il numero massimo di acquisizioni consentito (developer 10, startup 25, growth 100, legal_solo e legal_seats 25, custom 200, website_archive 25, sandbox 5); `details.limit`, `details.in_flight`, `details.requested` e Retry-After. Quando solo alcuni rientrano, il batch viene accettato con 202 ed elenca i restanti in `not_admitted`.\n\nTEST_MODE_DAILY_LIMIT: le attività in modalità di prova (una chiave di prova, o una sessione senza fatturazione collegata) sono limitate a 200 unità per account e giorno UTC, una per URL; `details.used`, `details.limit`, `details.reset_at`, gli header `X-Test-Daily-*` e 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": [
          "Acquisizioni"
        ],
        "operationId": "listCaptureHosts",
        "summary": "Elencare i nomi host acquisiti",
        "description": "Ogni nome host del proprio elenco di acquisizioni, con il numero di acquisizioni e l'ora dell'ultima acquisizione per host, per un filtro per dominio: passare un host come GET /captures?host=. count è il numero di righe che GET /captures?host=<host> restituisce senza status (tra le acquisizioni lette): acquisizioni di prova e di produzione insieme, ogni stato comprese quelle nel cestino (status deleted), così come le mostra l'elenco; le acquisizioni cancellate e le righe senza URL di pagina (certificati) sono escluse. Un'acquisizione viene conteggiata non appena riporta il proprio nome host memorizzato (le acquisizioni create prima del 2026-10-05 lo riportano dopo l'esecuzione del backfill). Un'acquisizione dello studio viene conteggiata solo finché si può ancora agire su di essa (si occupa una postazione di quello studio o se ne è titolari). host è in minuscolo. Ordinato per count (dal più alto), poi per host (dalla A alla Z). Al massimo 1 000 host. Vengono lette le 5 000 acquisizioni più recenti; truncated è true quando ci sono più di 1 000 host o acquisizioni più vecchie di quelle 5 000, quindi un host acquisito solo prima di esse può mancare. L'elenco viene calcolato al massimo una volta ogni 30 secondi per account e può risalire fino a 30 secondi prima (poco di più mentre un'altra richiesta dell'account lo sta calcolando). Il calcolo costa un'unità del limite di frequenza al minuto; una risposta servita da tale cache non costa nulla. Disponibile su tutti i piani, per le chiavi API e la sessione della dashboard.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "I nomi host.",
            "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": "Nome host in minuscolo, il valore accettato da GET /captures?host=."
                          },
                          "count": {
                            "type": "integer",
                            "minimum": 1,
                            "example": 12,
                            "description": "Acquisizioni di questo host tra quelle lette: le righe che GET /captures?host= restituisce senza status, comprese quelle nel cestino."
                          },
                          "last_captured_at": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true,
                            "example": "2026-10-05T07:48:38Z",
                            "description": "Ora dell'ultima acquisizione di questo host (il momento in cui la pagina è stata acquisita, altrimenti il momento in cui l'acquisizione è stata richiesta), UTC, al secondo."
                          }
                        }
                      }
                    },
                    "truncated": {
                      "type": "boolean",
                      "description": "true quando l'elenco può essere incompleto: più di 1000 host, oppure più acquisizioni delle 5000 più recenti che sono state lette."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "MALFORMED_REQUEST: scope non supportato (solo le proprie acquisizioni).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "description": "RATE_LIMITED: il limite di frequenza al minuto è esaurito (il calcolo dell'elenco costa un'unità), oppure l'elenco del Suo account è in corso di calcolo da parte di un'altra richiesta e non è memorizzato alcun risultato precedente; riprovare dopo Retry-After secondi.",
            "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": [
          "Acquisizioni"
        ],
        "operationId": "getCapture",
        "summary": "Ottenere un'acquisizione",
        "description": "Stato e, una volta completato, un package_url nuovo valido fino a 24 ore (vedere package_url_expires_at). Funziona con gli ID di acquisizioni, certificati, confronti e oscuramenti. Leggibile dall'account che lo ha creato, dai membri della pratica a cui appartiene e dal titolare dello studio per cui è stato eseguito (se l'abbonamento dello studio è terminato, fino alla chiusura della sua finestra di download).",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "event",
            "in": "query",
            "required": false,
            "description": "Inviare download quando si scarica il pacchetto: per un'acquisizione in una pratica registra capture.downloaded nel registro degli accessi della pratica. Le semplici interrogazioni dello stato dovrebbero ometterlo.",
            "schema": {
              "type": "string",
              "enum": [
                "download"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "L'acquisizione.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CaptureResult"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "FORBIDDEN: l'acquisizione non è propria.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "CAPTURE_NOT_FOUND: ID sconosciuto o malformato, acquisizione eliminata, oppure il file del pacchetto non è stato trovato.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/AuthUnavailable"
          }
        }
      },
      "delete": {
        "tags": [
          "Acquisizioni"
        ],
        "operationId": "deleteCapture",
        "summary": "Eliminare un'acquisizione",
        "description": "Predefinito: sposta l'acquisizione nel cestino. Può essere ripristinata con PATCH fino a restorable_until (30 giorni); dopo tale termine viene eliminata definitivamente. Con permanent=true il pacchetto archiviato viene cancellato subito e il record viene ridotto a una traccia minima di eliminazione; l'operazione è irreversibile. Nessuna delle due opzioni rimuove i pacchetti già scaricati né la prova OpenTimestamps. Un'acquisizione in una pratica aperta o archiviata viene prima rimossa dalla pratica (operazione registrata nel relativo registro della catena di custodia) quando il chiamante può aprire tale pratica; un chiamante che non può farlo (il suo autore dopo essere stato rimosso dalla pratica) viene rifiutato con 409 MATTER_LINKED, sia per il cestino sia per permanent=true, finché il titolare della pratica non la rimuove dalla pratica; un'acquisizione in una pratica sigillata non può essere eliminata. Un'acquisizione ancora in elaborazione non può essere eliminata (409 CAPTURE_PROCESSING); eliminarla una volta completata o non riuscita. L'eliminazione di un'acquisizione mai completata restituisce la relativa unità mensile.\n\nPeriodo di tolleranza dopo la revoca del blocco per controversia legale: per 14 giorni dopo la revoca di un blocco per controversia legale (`deletion_blocked_until` in GET) l'acquisizione non viene eliminata definitivamente (409 CAPTURE_HOLD_GRACE con `details.until`), né da una persona né dalla pulizia del cestino o dalla conservazione programmata (l'eliminazione dell'account e dello studio dopo la fine dell'abbonamento non attende tale periodo). Lo spostamento nel cestino è consentito; la sua pulizia attende la fine del periodo di tolleranza. Un'eliminazione definitiva rimuove prima qualsiasi legal hold S3 Object Lock ancora presente sugli oggetti archiviati; quando lo storage rifiuta, la richiesta restituisce 409 CAPTURE_LEGAL_HOLD (\"still locked in storage\", riprovare più tardi) e non viene eliminato nulla. Ogni spostamento nel cestino, ripristino ed eliminazione definitiva viene registrato nella catena degli eventi di acquisizione dell'account (GET /captures/events). Un oscuramento o un confronto la cui acquisizione di origine è sottoposta a blocco per controversia legale restituisce `source_under_legal_hold: true` quando viene spostato nel cestino o eliminato.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "permanent",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "true"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Eliminato. Cestino: { capture_id, status: \"deleted\", restorable: true, restorable_until }. Definitivo: { capture_id, status: \"deleted\", permanent: true }. Già nel cestino: { 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": "Presente (true) solo quando si tratta di un oscuramento o di un confronto ricavato da un'acquisizione sottoposta a blocco per controversia legale."
                    },
                    "deletion_blocked_until": {
                      "type": "string",
                      "format": "date-time",
                      "description": "Cestino durante il periodo di tolleranza dopo la revoca: lo svuotamento del cestino attende fino a questo momento."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "FORBIDDEN: l'acquisizione non è propria. FORBIDDEN anche quando l'acquisizione appartiene a una pratica non sigillata e il chiamante (il titolare dello studio che agisce sull'acquisizione di un membro dello studio, non il suo autore) non può aprire tale pratica. PLAN_API_READ_ONLY: una chiave API di un account Website Archive (la sua chiave è di sola lettura; utilizzare la dashboard).",
            "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: l'acquisizione è un allegato di una pratica sigillata. MATTER_LINKED: l'acquisizione è un allegato di una pratica di cui il chiamante non fa più parte; il titolare della pratica deve prima rimuoverla. CAPTURE_PROCESSING: l'acquisizione è ancora in elaborazione (error.details.status). CAPTURE_LEGAL_HOLD: l'acquisizione è sottoposta a blocco per controversia legale e nessuno può eliminarla (né spostarla nel cestino né cancellarla). CAPTURE_HOLD_GRACE: un'eliminazione definitiva durante il periodo di tolleranza successivo alla revoca del blocco per controversia legale dell'acquisizione (LEGAL_HOLD_RELEASE_GRACE_DAYS, 14 giorni); error.details.until indica quando diventa possibile. Lo spostamento nel cestino è consentito durante il periodo di tolleranza.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "INTERNAL_ERROR: non è stato possibile cancellare il pacchetto archiviato; non è stato eliminato nulla.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/AuthUnavailable"
          }
        }
      },
      "patch": {
        "tags": [
          "Acquisizioni"
        ],
        "operationId": "restoreCapture",
        "summary": "Ripristinare un'acquisizione eliminata, impostarne la nota privata, o attivarne o revocarne il blocco per controversia legale",
        "description": "Ripristina un'acquisizione dal cestino. Se al momento dell'eliminazione era stata rimossa da una pratica, viene riaggiunta quando quella pratica è ancora aperta e il chiamante può ancora aprirla; matter_id e matter_relinked compaiono nella risposta solo in quel caso (matter_id null quando il ricollegamento non è avvenuto). Un chiamante che non può più aprire quella pratica riottiene l'acquisizione senza di essa, nel registro della catena di custodia della pratica non viene scritto nulla e la risposta non menziona la pratica. Un'acquisizione che non si trova nel cestino viene restituita invariata.\n\nNota privata: un corpo con `private_note` (una stringa di al massimo 200 caratteri, o null per svuotarla) imposta invece la nota privata dell'acquisizione e non fa nient'altro; non può essere combinato con `action`. La nota viene memorizzata solo nel record dell'acquisizione, al di fuori del pacchetto di prove sigillato, quindi modificarla non altera mai le prove o i loro hash. Solo l'autore dell'acquisizione può impostarla (il titolare dello studio e i membri della pratica ricevono 403), su un'acquisizione in qualsiasi stato tranne quella eliminata definitivamente (404). La risposta è { capture_id, status, private_note }.\n\nBlocco per controversia legale: un corpo `{ \"action\": \"legal_hold\", \"enabled\": true|false, \"reason\"?: string }` attiva (true) o revoca (false) il blocco per controversia legale dell'acquisizione e non fa nient'altro. Un'acquisizione sotto blocco per controversia legale non viene eliminata: DELETE (cestino o definitiva) restituisce 409 CAPTURE_LEGAL_HOLD per chiunque, e né lo svuotamento del cestino né l'eliminazione per scadenza della conservazione la toccano; GET mostra `storage.retention.earliest_deletion_at` null. Un blocco viene mantenuto solo finché l'account ha un piano che conserva i dati (qualsiasi piano a pagamento, Archive only incluso; per un'acquisizione dello studio: lo studio è attivo o conservato come archivio dello studio). Quando l'abbonamento termina, le acquisizioni restano disponibili per 30 giorni; se entro quella data non viene acquistato un piano di questo tipo, ogni blocco per controversia legale dell'account (o dello studio) viene revocato automaticamente in quella data (actor system, reason subscription_ended, registrato come qualsiasi revoca, senza periodo di tolleranza) e le acquisizioni vengono eliminate insieme alle altre. Per mantenere le acquisizioni sotto blocco, passare ad Archive only (o acquistare l'archivio dello studio) prima di quella data, oppure scaricarle. In quei 30 giorni non è possibile attivare nuovi blocchi (l'account non ha un piano a pagamento), mentre quelli esistenti possono ancora essere revocati. Il blocco viene memorizzato solo nel record dell'acquisizione, al di fuori del pacchetto di prove sigillato. `reason` è un testo semplice facoltativo di al massimo 200 caratteri (senza caratteri di controllo, invisibili o di direzione del testo; gli spazi vengono compressi, un valore vuoto significa nessuno); viene conservato con il blocco e cancellato quando questo viene revocato.\n\nChi: attivare un blocco, chiunque possa agire sull'acquisizione (il suo autore, per un'acquisizione dello studio finché occupa ancora una postazione di quello studio, e il titolare dello studio, anche di uno studio il cui abbonamento è terminato mentre è nel periodo di conservazione o conservato come archivio dello studio; l'archivio solo tramite la sessione della dashboard). Revocarlo: su un'acquisizione personale il suo autore, su un'acquisizione dello studio solo il titolare dello studio (una postazione riceve 403 LEGAL_HOLD_OWNER_ONLY). Attivare un blocco richiede un piano a pagamento (qualsiasi piano a pagamento, Archive only incluso): il Sandbox gratuito, così come un account il cui abbonamento è terminato, riceve 403 LEGAL_HOLD_NOT_AVAILABLE con details.reason \"plan\" (un archivio dello studio attivo vale come piano a pagamento per le acquisizioni di quello studio, tramite la sessione della dashboard del titolare), e un'acquisizione in modalità di prova (con filigrana, eseguita con una chiave di prova o senza fatturazione collegata) riceve 403 LEGAL_HOLD_NOT_AVAILABLE con details.reason \"test_capture\". La revoca funziona con tutti i piani, anche dopo la fine dell'abbonamento. Su Website Archive la chiave API è in sola lettura (403 PLAN_API_READ_ONLY), la sessione della dashboard invece può. Un abbonamento con pagamento scaduto o non pagato non impedisce l'operazione. Una sessione della dashboard o una chiave il cui indirizzo email non è confermato riceve 403 EMAIL_NOT_VERIFIED.\n\nRifiutata su un'acquisizione nel cestino (409 CAPTURE_IN_TRASH: ripristinarla prima) o ancora in elaborazione (409 CAPTURE_PROCESSING); un'acquisizione eliminata definitivamente, o la cui eliminazione definitiva è in corso, restituisce 404. Ogni richiesta di blocco per controversia legale costa un'unità del limite di frequenza al minuto dell'account (429 RATE_LIMITED con Retry-After quando è esaurito). Inviare lo stato che l'acquisizione ha già non modifica nulla e restituisce 200 con i campi memorizzati e `changed: false`. Ogni modifica viene registrata nel log di audit come capture.legal_hold (quella di un'acquisizione dello studio nel tenant dello studio), annotata nella cronologia degli accessi dell'acquisizione (`legal_hold_on` / `legal_hold_off`, conservata anche quando l'acquisizione viene poi cancellata) e, quando l'acquisizione appartiene a una pratica, scritta nel registro della catena di custodia della pratica come capture.legal_hold_placed o capture.legal_hold_released con l'id dell'acquisizione come destinazione (il motivo va nel registro degli accessi della pratica, mai nella catena sottoposta a hash; la catena di una pratica sigillata non cambia). La risposta è { capture_id, legal_hold, legal_hold_set_at, legal_hold_reason, changed }.\n\nBlocco per controversia legale v2: attivare o revocare un blocco imposta o rimuove anche un legal hold S3 Object Lock su ogni versione del pacchetto memorizzato, sulla sua prova OpenTimestamps aggiornata e sul duplicato memorizzato separatamente (`s3_legal_hold` in GET; quando lo storage rifiuta o non ha una configurazione Object Lock, il blocco nel record dell'acquisizione si applica comunque e un tentativo fallito viene ripetuto dal server). Una revoca avvia un periodo di tolleranza di 14 giorni durante il quale l'acquisizione non viene eliminata definitivamente (non dopo la fine dell'abbonamento: lo sostituiscono i 30 giorni successivi alla fine). Un blocco attivato dal blocco per controversia legale della sua pratica (`legal_hold_source` \"matter:<id>\") si revoca sulla pratica finché quest'ultimo è attivo: revocarlo qui restituisce 409 LEGAL_HOLD_MATTER (error.details.matter_id per chi può leggere la pratica). Ogni modifica viene registrata nella catena degli eventi di acquisizione dell'account (GET /captures/events).",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "action": {
                    "type": "string",
                    "description": "restore (il valore predefinito per un corpo senza private_note) oppure legal_hold (con enabled e il reason facoltativo).",
                    "enum": [
                      "restore",
                      "legal_hold"
                    ]
                  },
                  "private_note": {
                    "allOf": [
                      {
                        "$ref": "#/components/schemas/PrivateNote"
                      }
                    ],
                    "description": "Imposta (una stringa) o svuota (null o una stringa vuota) la nota privata. Non può essere combinato con action."
                  },
                  "enabled": {
                    "type": "boolean",
                    "description": "Con action legal_hold (lì obbligatorio): true attiva il blocco per controversia legale, false lo revoca."
                  },
                  "reason": {
                    "type": "string",
                    "nullable": true,
                    "maxLength": 200,
                    "description": "Con action legal_hold, facoltativo: perché l'acquisizione è sotto blocco per controversia legale, testo semplice di al massimo 200 caratteri. Memorizzato con il blocco, al di fuori del pacchetto di prove; ignorato in caso di revoca (va allora solo nel log di audit)."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Ripristinata ({ capture_id, status, restored: true, matter_id?, matter_relinked? }), invariata ({ capture_id, status }), nota privata impostata ({ capture_id, status, private_note }), oppure blocco per controversia legale impostato ({ 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": "Azione di blocco per controversia legale: lo stato del blocco dell'acquisizione dopo la richiesta."
                    },
                    "legal_hold_set_at": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true,
                      "description": "Azione di blocco per controversia legale: quando è stato attivato il blocco attuale (null se non ce n'è uno)."
                    },
                    "legal_hold_reason": {
                      "type": "string",
                      "nullable": true,
                      "description": "Azione di blocco per controversia legale: il motivo memorizzato con il blocco attuale, oppure null."
                    },
                    "changed": {
                      "type": "boolean",
                      "description": "Azione di blocco per controversia legale: false quando l'acquisizione era già nello stato richiesto (non è stato modificato né registrato nulla)."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "MALFORMED_REQUEST: JSON non valido, un corpo che non è un oggetto, un'azione diversa da restore o legal_hold, private_note combinato con action, oppure un private_note che non è una stringa o null, supera i 200 caratteri o contiene caratteri di controllo, invisibili o di direzione del testo. Per legal_hold: enabled assente o non booleano, un motivo che non è una stringa, supera i 200 caratteri o non è testo semplice, oppure un campo sconosciuto.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "FORBIDDEN: l'acquisizione non è propria. FORBIDDEN anche quando l'acquisizione appartiene a una pratica non sigillata e il chiamante (il titolare dello studio che agisce sull'acquisizione di un membro dello studio, non il suo autore) non può aprire tale pratica. PLAN_API_READ_ONLY: una chiave API di un account Website Archive (la sua chiave è di sola lettura; utilizzare la dashboard). Per private_note: FORBIDDEN per chiunque non sia l'autore dell'acquisizione (che deve comunque essere ancora autorizzato ad agire su di essa). Per legal_hold: FORBIDDEN quando il chiamante non può agire sull'acquisizione; LEGAL_HOLD_OWNER_ONLY quando un membro dello studio (non il titolare dello studio) revoca il blocco per controversia legale di un'acquisizione dello studio; LEGAL_HOLD_NOT_AVAILABLE quando si attiva un blocco per controversia legale senza un piano a pagamento (details.reason plan) o su un'acquisizione in modalità di prova (details.reason test_capture); EMAIL_NOT_VERIFIED per un indirizzo e-mail non confermato. La regola sulle pratiche indicata sopra non si applica al blocco per controversia legale.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "CAPTURE_NOT_FOUND, anche per un'acquisizione eliminata definitivamente.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Solo blocco per controversia legale. CAPTURE_IN_TRASH: l'acquisizione è nel cestino; ripristinarla prima. CAPTURE_PROCESSING: l'acquisizione è ancora in elaborazione (error.details.status). LEGAL_HOLD_MATTER: il blocco dell'acquisizione è stato attivato dal blocco per controversia legale della sua pratica, ancora attivo; revocarlo sulla pratica. LEGAL_HOLD_SOURCE: il blocco di un oscuramento o di un confronto che segue un'acquisizione da cui è stato ricavato (per un confronto, l'una o l'altra acquisizione confrontata), ancora sottoposta a blocco per controversia legale (attivato su tale acquisizione o tramite la sua pratica): il pacchetto resta sottoposto a blocco finché questo non viene revocato (`details.source_capture_id`). Un confronto resta sottoposto a blocco finché lo è una qualsiasi delle acquisizioni confrontate e viene liberato solo quando nessuna delle due lo è più (`legal_hold_source` \"derived:<capture id>\" indica quella sottoposta a blocco).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Solo blocco per controversia legale. RATE_LIMITED: il limite di frequenza al minuto dell'account è esaurito. `error.details.retry_after_seconds` ripete 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": [
          "Acquisizioni"
        ],
        "operationId": "verifyCapture",
        "summary": "Verificare un pacchetto memorizzato",
        "description": "Legge il pacchetto memorizzato e verifica la firma RSA di manifest.json e lo SHA-256 di ogni file che elenca. I pacchetti più grandi di 256 MiB restituiscono 413 VERIFY_PACKAGE_TOO_LARGE; verificarli offline con gli script inclusi. Ogni richiesta costa un'unità del limite di frequenza al minuto dell'account (429 RATE_LIMITED). Quando il pacchetto contiene manifest.json.tsr, ProofSnap verifica anche il token di validazione temporale eIDAS (checks.eidas): la firma del token e gli attributi firmati, che contenga lo SHA-256 di manifest.json, l'extendedKeyUsage del certificato della TSA, la sua catena di certificati fino alla CA radice fissata Disig QCA4 alla data del token (genTime), e lo stato del certificato della TSA dalla risposta OCSP o dalla CRL registrata al momento dell'emissione in eidas_validation.json (verificata tramite firma, non scaricata in tempo reale). Questa verifica non richiede mai una nuova validazione temporale. Indica inoltre se manifest.json.ots è presente e se l'hash del manifest si trova nel registro di emissione di ProofSnap. Il verdetto dipende solo dalla firma del manifest e dai file elencati; un token che non supera la verifica viene segnalato in checks.eidas e in warnings (EIDAS_TOKEN_FAILED), senza confluire nel verdetto. Un pacchetto emesso con una validazione temporale elettronica qualificata registra eidas_requested: true nel suo manifest.json firmato; quando un tale pacchetto non contiene più manifest.json.tsr, checks.eidas e checks.eidas_disig vengono segnalati come missing con l'avviso EIDAS_TOKEN_MISSING (gli script di verifica inclusi riportano FAIL per lo stesso pacchetto). Consentito al titolare dell'acquisizione, ai membri della sua pratica e al titolare dello studio per cui è stata eseguita. Per verificare le validazioni temporali in modo indipendente, eseguire gli script nella cartella verification/ del pacchetto.\n\nPacchetti importati (source import): lo ZIP memorizzato viene riverificato con i controlli dell'importazione (firma, ogni file, catena di custodia, catena forense, validazioni temporali; la provenienza viene cercata di nuovo nel registro di emissione dell'API per i pacchetti API e ricavata dal record di importazione per i pacchetti dell'estensione). La risposta è { 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 pacchetto oltre 512 MiB, o una verifica che non si conclude in circa 20 secondi, restituisce rechecked: false con un motivo (too_large_for_synchronous_check, time_budget, storage_error), verdict: null e stored_verification (il rapporto registrato al momento dell'importazione).",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Risultato della verifica.",
            "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: sconosciuta, eliminata o senza pacchetto archiviato.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "VERIFY_PACKAGE_TOO_LARGE: il pacchetto memorizzato supera 256 MiB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "RATE_LIMITED: il limite di frequenza al minuto dell'account è esaurito.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "CONFIGURATION_ERROR: non è stato possibile leggere il pacchetto archiviato.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/AuthUnavailable"
          }
        }
      }
    },
    "/captures/{capture_id}/redact": {
      "parameters": [
        {
          "$ref": "#/components/parameters/CaptureId"
        }
      ],
      "post": {
        "tags": [
          "Confrontare e oscurare"
        ],
        "operationId": "redactCapture",
        "summary": "Creare una copia oscurata",
        "description": "Crea un nuovo pacchetto da un'acquisizione completata rimuovendo il contenuto selezionato, lo firma di nuovo (nuova chiave RSA-4096) e lo invia per un ancoraggio OpenTimestamps. L'originale non viene modificato. Sincrono: restituisce 201 quando il pacchetto è pronto. Conteggiato come un'acquisizione.\n\n- Gli oscuramenti a riquadro anneriscono aree dello screenshot. Uno screenshot JPEG nella forma in cui l'acquisizione lo scrive (sequenziale, in scala di grigi o YCbCr) viene oscurato senza decodificarlo in pixel: i blocchi compressi (MCU, da 8 a 32 pixel di lato) toccati da un riquadro vengono sostituiti con blocchi neri e tutti gli altri blocchi restano invariati, quindi il resto dell'immagine non viene ricompresso (`screenshot_method` `jpeg_dct_blocks`, fino a 150 megapixel). L'area annerita è ciascun riquadro arrotondato verso l'esterno a blocchi interi (`painted_rects`), quindi contiene sempre il riquadro. Uno screenshot PNG, o un JPEG di altro tipo, viene decodificato, annerito e ricodificato come screenshot.jpeg, quindi i blocchi 8 x 8 toccati da un riquadro vengono anneriti nel nuovo file (`screenshot_method` `decode_repaint_jpeg`, fino a 60 megapixel). Uno screenshot oltre il proprio limite o con dati immagine illeggibili viene rifiutato con 422 REDACTION_SCREENSHOT_UNREADABLE prima di qualsiasi addebito. Se l'intestazione è leggibile ma i dati immagine no, l'errore viene rilevato dopo la prenotazione dell'unità mensile, che viene poi rilasciata. Gli oscuramenti di testo e di metadati funzionano anche senza riquadri. redaction_manifest.json (`screenshot_boxes`) registra il metodo, `painted_rects`, la griglia dei blocchi e una nota esplicativa per i periti forensi.\n- Gli oscuramenti di testo rimuovono le corrispondenze da page.html e domtextcontent.txt, da ogni valore di testo libero di metadata.json (titolo della pagina e valori meta, record WHOIS / RDAP, soggetto TLS, valori degli header di risposta HTTP) e da ogni valore di testo libero di forensic_log.json e chain_of_custody.json (case_reference, case_id, case_note, i valori dei metadati forniti, il codice iniettato e i relativi errori, le etichette dei pop-up chiusi, legal_context.note). Identificatori, hash, orari e URL in tali record restano invariati. Vengono rimosse anche le occorrenze codificate: riferimenti a caratteri HTML (con nome, decimali, esadecimali, come M&uuml;ller o jan&#64;firma.cz), percent-encoding (mailto:jan%40firma.cz) e sequenze di escape JavaScript (\\u0040, \\x40), comprese le codifiche annidate fino a due livelli; il marcatore sostituisce i caratteri codificati stessi. Il controllo successivo all'oscuramento, che cerca corrispondenze sopravvissute, legge i file JSON valore per valore e cerca ogni testo sia così come è scritto sia decodificato, quindi fallisce su una corrispondenza in una qualsiasi di queste forme. Gli oscuramenti di testo anneriscono anche lo screenshot nei punti in cui il text_layout.json dell'acquisizione di origine colloca un'occorrenza (`text_rule_rects`): vengono annerite le righe stimate del blocco di testo che contengono la corrispondenza, e l'intero blocco solo quando la stima è incerta (conteggiato in `screenshot_boxes.from_text_rules.rects_whole_block` di redaction_manifest.json); le occorrenze che non riesce a collocare sono conteggiate in `text_occurrences_not_located`. Le regole distinguono esattamente maiuscole e minuscole, salvo che sia impostato `case_insensitive`.\n- Gli oscuramenti di metadati rimuovono campi da metadata.json. Due nomi hanno una portata maggiore. `ip_dns` (accettati anche: `ip`, `dns`, `resolved_ips`) rimuove gli indirizzi IP e le risposte DNS del sito di destinazione da metadata.json, forensic_log.json e chain_of_custody.json (gli indirizzi propri della macchina di acquisizione restano), e li rimuove anche, solo come indirizzi interi, da page.html, domtextcontent.txt e dallo screenshot come regole di testo automatiche (registrate solo con la fonte ip_dns, mai con l'indirizzo o un suo hash, e non conteggiate nel limite di text_redactions). `cookie_names` (anche `cookie`, `cookies`, `http_headers.cookie`) rimuove gli header Cookie e Set-Cookie e ogni elenco di nomi di cookie in tali record; i conteggi vengono mantenuti. I valori dei cookie sono già oscurati in ogni acquisizione.\n\nUn riquadro copre solo pixel. Il testo sotto un riquadro resta in page.html, domtextcontent.txt e metadata.json, a meno di aggiungere anche un oscuramento di testo, e un'immagine sotto un riquadro può essere ancora referenziata o incorporata in page.html (img src o srcset, url() CSS, un URI data:); un oscuramento di testo che corrisponde al suo indirizzo rimuove tale riferimento. Il pacchetto oscurato riprende dall'originale solo screenshot.jpeg (o screenshot.png quando nessun riquadro lo ha sostituito), page.html, domtextcontent.txt, metadata.json e forensic_log.json, più i file che scrive ex novo (evidence.pdf, chain_of_custody.json, redaction_manifest.json, i file di verifica e i file del manifest). Tutti gli altri file dell'originale, come images/, image_inventory.json, text_layout.json, page.mhtml, network.har e capture.warc, vengono esclusi ed elencati in redaction_manifest.json sotto dropped_for_leak_protection. Gli oscuramenti di testo si applicano anche a questi nomi di file, perché una copia di immagine prende il nome dal proprio URL. chain_of_custody.json conserva gli eventi dell'originale che li riguardano, contrassegnati artifact_removed_in_redaction e senza gli hash dei file che il pacchetto oscurato non contiene. Viene rimosso anche un eventuale rule_902_certification.pdf dell'originale, perché riporta i file e gli hash dell'originale; la rimozione è elencata in redaction_manifest.json (removed_as_stale) e in chain_of_custody.json. L'URL di acquisizione non viene mai oscurato perché fa parte del manifest firmato: un oscuramento di testo il cui testo compare anche nell'URL di acquisizione o nell'URL finale viene applicato ovunque altrove, il testo resta in quell'URL e la regola è elencata in `rules_in_capture_url` e in redaction_manifest.json (leakage_check.capture_url). Possono essere oscurate solo le acquisizioni di pagine web (un confronto, una certificazione o un oscuramento vengono rifiutati con 422 REDACTION_SOURCE_UNSUPPORTED).\n\nL'oscuramento di un'acquisizione di prova (eseguita con una chiave di prova, nella Sandbox o da un account senza account di fatturazione collegato) è un pacchetto di prova indipendentemente dalla chiave che lo richiede: con filigrana, `test_mode: true` in manifest.json, una riga di prova (key_env test) non conteggiata e che non consuma alcuna unità mensile. include_eidas su un tale oscuramento viene rifiutato con 403 EIDAS_REQUIRES_LIVE_KEY (details.reason).\n\nIl rapporto di oscuramento (evidence.pdf) viene generato nella lingua `language` (predefinita: la lingua dell'acquisizione di origine), compresi corpo, copertina e banner di prova.\n\nPrima di qualsiasi oscuramento, ogni file che l'oscuramento legge o riprende (lo screenshot, page.html, domtextcontent.txt, metadata.json, forensic_log.json, chain_of_custody.json, text_layout.json) e che il manifest di origine elenca con uno SHA-256 viene sottoposto di nuovo a hash (redaction_manifest.json derived_from.source_files_rehashed); una discrepanza o un file mancante restituisce 422 REDACTION_SOURCE_INTEGRITY_FAILED e non viene emesso nulla, quindi un'origine corrotta o alterata non viene mai risigillata con una nuova firma. I file esclusi dal pacchetto oscurato non vengono letti. Un oscuramento sincrono la cui origine ha richiesto troppo tempo per essere letta si interrompe prima di qualsiasi prenotazione, con 500 CAPTURE_TIME_BUDGET_EXCEEDED.\n\nL'oscuramento di un'acquisizione sottoposta a blocco per controversia legale viene anch'esso sottoposto a blocco (legal_hold_source \"derived:<capture id>\"), così come ogni oscuramento di un'acquisizione nel momento in cui il blocco viene attivato su di essa; revocare il blocco dell'acquisizione revoca anche tali blocchi e, finché l'acquisizione resta sottoposta a blocco, questi non possono essere revocati singolarmente (409 LEGAL_HOLD_SOURCE). Il captured_at di una riga di oscuramento è l'ora di acquisizione della sua origine; redacted_at è il momento in cui è stato eseguito l'oscuramento.\n\nL'oscuramento è a prova di errore (fail closed): quando il controllo successivo all'oscuramento trova ancora un termine oscurato in un punto qualsiasi del pacchetto al di fuori dell'URL di acquisizione e dell'URL finale, non viene emesso alcun pacchetto e la richiesta restituisce 422 REDACTION_LEAK_DETECTED (`error.details` `{ files, rule_indexes, metadata_fields, in_capture_url }`; il termine stesso non viene mai riportato). redaction_manifest.json registra `leakage_check.fail_closed: true`.\n\nI valori vengono troncati ai confini dei caratteri (un carattere non viene mai spezzato): stringhe di corrispondenza del testo a 2 000 caratteri, motivi a 200, reason_summary a 500, campi del custode a 200. Motivi, reason_summary e campi del custode sono stampati sul certificato di oscuramento e devono essere testo semplice: un carattere di controllo, invisibile o di direzione del testo viene rifiutato con 400.\n\nBudget di tempo e nuovi tentativi: la richiesta risponde entro il limite di 29 secondi di API Gateway. La sigillatura viene eseguita entro un budget di tempo calcolato dalla richiesta; se la validazione temporale elettronica qualificata (include_eidas) non rientra nel budget, non viene richiesta e il pacchetto viene consegnato con il solo ancoraggio OpenTimestamps (timestamps.eidas stato not_obtained, motivo time_budget, non addebitata), e una richiesta OpenTimestamps che non rientra nel budget viene omessa (chain_of_custody.json registra ots_skipped con motivo time_budget). Inviare una Idempotency-Key affinché un nuovo tentativo (ad esempio dopo un 504 da un gateway o un errore di rete) restituisca lo stesso oscuramento invece di crearne e addebitarne un secondo. Un nuovo tentativo mentre la prima richiesta è ancora in corso restituisce 409 IDEMPOTENCY_IN_PROGRESS con l'ID dell'oscuramento in `error.details.capture_id` (interrogare GET /captures/{capture_id} con tale ID). Un nuovo tentativo successivo restituisce 201 con il corpo originale e `Idempotent-Replay: true`. Un oscuramento non riuscito rilascia la propria chiave, quindi un nuovo tentativo con la stessa chiave viene eseguito di nuovo.\n\nLimite di frequenza per account su finestre di 60 secondi (tutte le chiavi API dell'account e la relativa sessione della dashboard condividono un'unica finestra). Dopo l'esecuzione del limitatore, le risposte di questa route includono gli header `RateLimit-Policy` e `RateLimit` (basati sull'Internet-Draft IETF draft-ietf-httpapi-ratelimit-headers) oltre ai precedenti `X-RateLimit-Limit`, `X-RateLimit-Remaining` e `X-RateLimit-Reset`. Una risposta 429 aggiunge `Retry-After`.\n\nUn pacchetto di prove importato (source import, ID con prefisso `imp_`) non può essere oscurato: 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": "Rettangoli in pixel dello screenshot dell'acquisizione di origine (x e y dall'angolo superiore sinistro, w e h la larghezza e l'altezza). Quando tutti e quattro i valori di un riquadro sono compresi tra 0 e 1, vengono interpretati come frazioni della larghezza e dell'altezza dello screenshot. Un riquadro che si trova interamente fuori dallo screenshot non annerisce nulla e viene elencato in ignored_boxes; un riquadro che oltrepassa un bordo viene tagliato al bordo ed elencato in clipped_boxes. Quando tutti i riquadri cadono fuori e nessun oscuramento di testo o di metadati modifica alcunché, la richiesta viene rifiutata con 400 REDACTION_NO_OPS prima di qualsiasi addebito."
                  },
                  "text_redactions": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "match": {
                          "oneOf": [
                            {
                              "type": "string",
                              "description": "Testo letterale.",
                              "minLength": 1
                            },
                            {
                              "type": "object",
                              "properties": {
                                "pattern": {
                                  "type": "string",
                                  "description": "Espressione regolare, da 1 a 500 caratteri. I pattern con quantificatori annidati come (a+)+ vengono rifiutati con 400.",
                                  "minLength": 1,
                                  "maxLength": 500
                                },
                                "flags": {
                                  "type": "string",
                                  "description": "Uno qualsiasi tra g, i, m, s, u, ciascuno al massimo una volta. y (sticky) viene rifiutato con 400: troverebbe corrispondenze in una sola posizione e lascerebbe le altre occorrenze.",
                                  "pattern": "^[gimsu]*$"
                                }
                              },
                              "required": [
                                "pattern"
                              ]
                            }
                          ]
                        },
                        "case_insensitive": {
                          "type": "boolean",
                          "default": false,
                          "description": "Cerca una stringa letterale senza distinguere tra maiuscole e minuscole (per un'espressione regolare utilizzare il flag i)."
                        },
                        "reason": {
                          "type": "string",
                          "maxLength": 200,
                          "description": "Testo semplice, stampato sul certificato di oscuramento."
                        }
                      },
                      "required": [
                        "match"
                      ]
                    },
                    "maxItems": 200
                  },
                  "metadata_fields": {
                    "type": "array",
                    "description": "Percorsi di campi in metadata.json (un segmento `*` corrisponde a ogni elemento di un array), oppure i nomi `ip_dns`, `cookie_names` e `authorization` (vedere sopra).",
                    "items": {
                      "oneOf": [
                        {
                          "type": "string",
                          "description": "Percorso del campo in metadata.json."
                        },
                        {
                          "type": "object",
                          "properties": {
                            "field": {
                              "type": "string"
                            },
                            "reason": {
                              "type": "string"
                            }
                          },
                          "required": [
                            "field"
                          ]
                        }
                      ]
                    },
                    "maxItems": 100
                  },
                  "reason_summary": {
                    "type": "string",
                    "maxLength": 500,
                    "description": "Testo semplice, stampato sul certificato di oscuramento."
                  },
                  "custodian": {
                    "type": "object",
                    "properties": {
                      "name": {
                        "type": "string"
                      },
                      "title": {
                        "type": "string"
                      }
                    },
                    "description": "Stampato sul rapporto di oscuramento. name e title seguono la regola del testo semplice (vedere PlainText)."
                  },
                  "language": {
                    "type": "string",
                    "description": "Lingua dei PDF leggibili. Uno tra: 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. Maiuscole/minuscole, trattino e regione sono tollerati (\"DE\", \"fr-FR\" e \"PT-BR\" sono accettati); \"pt\" e \"zh\" da soli vengono rifiutati. Un valore non supportato restituisce 400 MALFORMED_REQUEST. Facoltativo: senza di esso (o se vuoto) il rapporto è nella lingua dell'acquisizione di origine."
                  },
                  "include_eidas": {
                    "type": "boolean",
                    "description": "Come in CaptureOptions.include_eidas.",
                    "default": false
                  },
                  "webhook_url": {
                    "type": "string",
                    "description": "URL https che riceve redaction.completed. Viene inviato subito dopo la risposta 201, non prima. L'URL deve essere https e non deve puntare a un indirizzo privato, di loopback, link-local, NAT di livello carrier o di metadati cloud (le stesse regole dell'URL di un'acquisizione; altrimenti 422 SSRF_BLOCKED). Un valore vuoto o composto solo da spazi significa nessun webhook. Richiede una chiave API: se inviato da una sessione della dashboard viene rifiutato con 400 WEBHOOK_REQUIRES_API_KEY, perché i webhook sono firmati con il secret di firma del webhook della chiave.",
                    "format": "uri"
                  },
                  "rule_commitment_salt": {
                    "type": "string",
                    "description": "Facoltativo. Il proprio salt di 32 byte per gli impegni delle regole (redaction_manifest.json rule_commitments), come 64 cifre esadecimali o in base64 / base64url; un salt composto da un unico byte ripetuto viene rifiutato (400). Inviarlo per disporre del salt prima della chiamata: senza di esso il server ne genera uno e lo restituisce solo nella risposta 201, quindi una risposta persa per un timeout (504 dal gateway mentre l'oscuramento si è comunque concluso) perde il salt, e una ripetizione con Idempotency-Key non lo contiene. ProofSnap non lo conserva."
                  }
                },
                "description": "Almeno uno tra boxes, text_redactions o metadata_fields."
              },
              "example": {
                "text_redactions": [
                  {
                    "match": "jane.doe@example.com",
                    "reason": "personal data"
                  }
                ],
                "reason_summary": "Personal data of a third party"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Pacchetto oscurato pronto. Anche una ripetizione idempotente di un oscuramento precedente con la stessa Idempotency-Key restituisce 201, con `Idempotent-Replay: true`: il corpo originale, con lo stato attuale e un package_url appena prefirmato (omesso quando il pacchetto non può essere prefirmato; leggerlo con 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": "Numero di azioni di oscuramento applicate."
                    },
                    "original_signature_verified": {
                      "type": "boolean",
                      "description": "Se la firma del manifest originale è stata verificata prima dell'oscuramento."
                    },
                    "leakage_warnings": {
                      "type": "integer",
                      "description": "Sempre 0: un oscuramento il cui controllo successivo trova un termine sopravvissuto viene rifiutato con 422 REDACTION_LEAK_DETECTED invece di essere emesso. Mantenuto per compatibilità."
                    },
                    "package_url": {
                      "type": "string",
                      "format": "uri"
                    },
                    "package_url_expires_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "verify_url": {
                      "type": "string",
                      "description": "Link al verificatore pubblico ProofSnap.",
                      "format": "uri"
                    },
                    "ignored_boxes": {
                      "type": "array",
                      "items": {
                        "type": "integer",
                        "minimum": 0
                      },
                      "description": "Indici (a partire da zero) nei boxes della richiesta dei riquadri che si trovavano interamente fuori dallo screenshot e non hanno annerito nulla. Registrati anche in redaction_manifest.json (screenshot_boxes)."
                    },
                    "clipped_boxes": {
                      "type": "array",
                      "items": {
                        "type": "integer",
                        "minimum": 0
                      },
                      "description": "Indici (a partire da zero) nei boxes della richiesta dei riquadri che oltrepassavano il bordo dello screenshot e sono stati tagliati in corrispondenza di esso. Registrati anche in redaction_manifest.json (screenshot_boxes)."
                    },
                    "screenshot_method": {
                      "type": "string",
                      "nullable": true,
                      "enum": [
                        "jpeg_dct_blocks",
                        "decode_repaint_jpeg",
                        null
                      ],
                      "description": "Come sono stati anneriti i riquadri: jpeg_dct_blocks (i blocchi compressi del JPEG sotto ciascun riquadro sostituiti con blocchi neri, tutti gli altri blocchi invariati) oppure decode_repaint_jpeg (lo screenshot decodificato, annerito e ricodificato). null quando nessun riquadro ha annerito nulla. Registrato anche in 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": "Le aree effettivamente annerite, in pixel dello screenshot, una per ogni riquadro applicato nell'ordine dei riquadri della richiesta, esclusi quelli ignorati. Con jpeg_dct_blocks ciascuna è il riquadro arrotondato per eccesso a blocchi interi, quindi contiene il riquadro; con decode_repaint_jpeg è il riquadro limitato allo screenshot e arrotondato per eccesso a blocchi interi 8 x 8 del nuovo file, che vengono anneriti dopo la ricodifica in modo che la compressione non possa trasportare colori adiacenti all'interno del riquadro. Registrato anche in redaction_manifest.json (screenshot_boxes.painted_rects)."
                    },
                    "text_rule_rects": {
                      "type": "integer",
                      "minimum": 0,
                      "description": "Aree distinte dello screenshot annerite per i soli oscuramenti di testo, escluse le aree che contengono un riquadro (un'area individuata da due regole conta una volta): nei blocchi di testo del text_layout.json dell'origine il cui testo corrisponde a una regola, le righe stimate che contengono la corrispondenza, oppure l'intero blocco quando la stima è incerta. Elencate in redaction_manifest.json (screenshot_boxes.from_text_rules)."
                    },
                    "text_occurrences_not_located": {
                      "type": "integer",
                      "minimum": 0,
                      "description": "Occorrenze degli oscuramenti di testo nel testo della pagina che non è stato possibile collocare sullo screenshot e che lì non sono state annerite (testo non elencato nel text_layout.json dell'origine, come il testo all'interno di un'immagine; un blocco troppo grande per essere annerito per un solo termine; aree oltre il limite di un oscuramento; uno screenshot che non è stato possibile annerire). Le occorrenze in un blocco interamente al di fuori dello screenshot (lì non c'è nulla da annerire) non sono conteggiate qui; redaction_manifest.json le elenca come occurrences_not_on_screenshot. Mai più delle occorrenze nel testo della pagina. Controllare lo screenshot e aggiungere riquadri. redaction_manifest.json (leakage_check.screenshot_text_advisory) contiene i conteggi per regola."
                    },
                    "text_variants_remaining": {
                      "type": "object",
                      "description": "Indicativo, mai un errore: per ogni regola di testo letterale, le occorrenze rimaste nel domtextcontent.txt e nel testo del page.html di questo pacchetto che la regola non ha individuato ma che corrispondono ignorando maiuscole/minuscole e segni diacritici (ad esempio \"boskovické\" per la regola sensibile alle maiuscole \"Boskovic\"), e se il termine compare nell'URL di acquisizione o nell'URL finale, che non vengono mai oscurati. `total` somma, per ogni regola, il maggiore dei due conteggi (entrambi i file contengono lo stesso testo della pagina). Solo conteggi, mai il testo. Le regole regex non vengono conteggiate. Riportato anche in redaction_manifest.json (leakage_check.text_variants_advisory). Presente quando la richiesta contiene oscuramenti di testo.",
                      "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": "Presente quando la richiesta contiene oscuramenti di testo. Indici (a partire da zero) delle regole di testo il cui testo compare anche nell'URL dell'acquisizione o nell'URL finale. Questi URL non vengono mai modificati: il testo è stato rimosso ovunque altrove e lì resta visibile. Vuoto quando nessuna regola vi corrisponde. Compare anche in redaction_manifest.json (leakage_check.capture_url, `remains_in_capture_url: true` per regola) e nel rapporto di oscuramento."
                    },
                    "rule_commitment_salt": {
                      "type": "string",
                      "pattern": "^[0-9a-f]{64}$",
                      "description": "Presente quando la richiesta contiene oscuramenti di testo. Il salt di 32 byte (hex; il rule_commitment_salt della richiesta, se è stato inviato) dei rule_commitments di redaction_manifest.json: per ogni regola di testo, lo SHA-256 del salt della regola seguito dai byte UTF-8 del JSON canonico della regola {\"match\": <text, or {\"pattern\", \"flags\"}>, \"case_insensitive\": <bool>}, dove il salt della regola è un HMAC-SHA256 con questo salt come chiave, calcolato sul testo UTF-8 \"rule:<rule_index>\" (così una regola può essere dimostrata rivelando il suo salt, senza rivelare le altre). Le regole non vengono memorizzate e il salt viene restituito solo in questa risposta: non in una ripetizione con Idempotency-Key, in un webhook, nel registro degli eventi o in GET /captures/{capture_id}, e ProofSnap non lo conserva. Conservarlo insieme alla propria documentazione: consente di dimostrare quali regole sono state applicate dall'oscuramento."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "MALFORMED_REQUEST (details.reason; ad esempio un motivo che non è testo semplice o il flag regex y), REDACTION_NO_OPS (nulla fornito, o nessuna corrispondenza) o INVALID_URL (webhook_url non https). WEBHOOK_REQUIRES_API_KEY: webhook_url inviato da una sessione della dashboard (i webhook richiedono una chiave 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 oppure 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: l'acquisizione non è propria. EIDAS_REQUIRES_LIVE_KEY: include_eidas con una chiave di prova, o per un oscuramento di un'acquisizione di prova (in tal caso con details.reason). EMAIL_NOT_VERIFIED: una sessione della dashboard il cui indirizzo e-mail non è confermato. PLAN_ARCHIVE_ONLY: il piano mantiene solo l'archivio e non crea nuovo lavoro. PLAN_API_READ_ONLY: una chiave API di un account Website Archive (la sua chiave è di sola lettura; utilizzare la dashboard). PLAN_API_READ_ONLY con details.reason \"session_outside_dashboard\": una sessione della dashboard di un account Website Archive la cui richiesta non ha l'header Origin o ne ha uno che non è un indirizzo della dashboard ProofSnap (su quel piano il nuovo lavoro si crea solo nella dashboard; si tratta di un vincolo blando, non di un confine di sicurezza).",
            "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: l'acquisizione di origine non è completata. IDEMPOTENCY_IN_PROGRESS: una richiesta con la stessa Idempotency-Key è ancora in esecuzione (details.capture_id è l'ID dell'oscuramento da interrogare con GET /captures/{capture_id}). COMPANY_READ_ONLY: l'origine appartiene a uno studio conservato come archivio dello studio.",
            "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: i dati immagine dello screenshot sono illeggibili oppure lo screenshot supera il limite per l'oscuramento a riquadri (150 megapixel per un JPEG come lo scrive l'acquisizione, 60 megapixel per un PNG o un altro JPEG); gli oscuramenti di testo e di metadati funzionano comunque senza riquadri. REDACTION_LEAK_DETECTED: un termine oscurato è sopravvissuto al di fuori dell'URL dell'acquisizione (details { files, rule_indexes, metadata_fields, in_capture_url }; in_capture_url è true quando il testo della regola rifiutata compare anche nell'URL dell'acquisizione o nell'URL finale, dove non viene mai conteggiato). REDACTION_SOURCE_UNSUPPORTED: l'origine non è un'acquisizione di pagina web (details { source } o { manifest_type }). SSRF_BLOCKED: webhook_url punta a un indirizzo bloccato. REDACTION_SOURCE_INTEGRITY_FAILED: un file del pacchetto di origine non corrisponde allo SHA-256 elencato nel suo manifest firmato, oppure un file elencato manca (details { mismatched, missing }, solo nomi di file); non è stato oscurato né addebitato nulla. IDEMPOTENCY_KEY_REUSED: la Idempotency-Key è già stata usata con un corpo della richiesta diverso (o da un altro ambiente di chiavi).",
            "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: troppe richieste nella finestra corrente di 60 secondi. `error.details.retry_after_seconds` ripete Retry-After. RateLimit riporta r=0 e i secondi mancanti al ripristino della finestra. Inoltre RATE_LIMITED, per una sessione della dashboard Website Archive: il suo tetto giornaliero di 100 unità per giorno UTC, condiviso da acquisizioni singole e batch (un'unità per URL), compare, redact e certify-hash (un'unità per richiesta); una richiesta che non rientra viene rifiutata per intero (details.daily_limit, details.remaining, details.action \"wa_session_captures\"; Retry-After e details.retry_after_seconds indicano i secondi mancanti al giorno UTC successivo, e gli header RateLimit e X-RateLimit-* al minuto vengono omessi). Un breve rifiuto con solo details.retry_after_seconds e Retry-After: 1 indica un aggiornamento concorrente del contatore: riprovare. Un rifiuto non imputabile al chiamante restituisce le unità; un'attività accettata con 202 che fallisce in seguito mantiene la propria unità. Un controllo morbido, non un confine di sicurezza.",
            "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: errore imprevisto del server; riprovare con backoff esponenziale. CAPTURE_TIME_BUDGET_EXCEEDED: l'oscuramento sincrono ha esaurito il proprio budget di tempo ed è stato interrotto prima dell'emissione di qualsiasi pacchetto; non è stato addebitato nulla. Con `details.reason` `source_read`, la lettura del pacchetto di origine ha richiesto la maggior parte del budget: il pacchetto è troppo grande per essere oscurato in questo momento, riprovare più tardi.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/EidasOrAuthUnavailable"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ]
      }
    },
    "/compare": {
      "post": {
        "tags": [
          "Confrontare e oscurare"
        ],
        "operationId": "compareCaptures",
        "summary": "Confrontare due acquisizioni",
        "description": "Confronta due acquisizioni completate (testo della pagina, immagini mostrate nella pagina, metadati) e produce un pacchetto di confronto firmato. Restituisce 202; interrogare GET /captures/{compare_id}, che riporta quindi type, compared e diff_summary. Le immagini vengono confrontate a partire dall'inventario delle immagini sigillato da ciascuna acquisizione di origine (image_inventory.json: l'indirizzo di ogni immagine visualizzata e lo SHA-256 dei suoi byte), mai pixel per pixel; i risultati sono in image_changes.json e in diff_summary.image_changes. Un'origine acquisita prima che le acquisizioni registrassero un inventario delle immagini viene comunque confrontata, senza il confronto delle immagini (diff_summary.image_changes.status not_run). Conteggiato come un'acquisizione. A parità di condizioni (2026-10-14): due acquisizioni eseguite con un profilo dispositivo diverso o con impostazioni di acquisizione diverse vengono rifiutate con 422 COMPARE_PROFILE_MISMATCH (details.differing, details.before, details.after: solo i campi divergenti, CSS e JavaScript iniettati come relativo SHA-256) a meno che allow_profile_mismatch sia true, e due acquisizioni di pagine diverse con 422 COMPARE_URL_MISMATCH (details.before e details.after: url e final_url come origine e percorso) a meno che allow_url_mismatch sia true. Il controllo viene eseguito prima sulle due righe di acquisizione (prima di qualsiasi lettura o prenotazione), poi sui metadata.json e forensic_log.json sigillati di entrambi i pacchetti nell'ambito del controllo delle origini descritto di seguito; una discrepanza rilevata solo durante l'esecuzione del confronto fa fallire il confronto con tale codice e restituisce l'unità mensile. Il testo viene confrontato riga per riga con un diff di righe deterministico (senza timeout): diff_summary conta righe (text_unit line). Possono essere confrontate solo le acquisizioni web (eseguite tramite l'API o da una pianificazione): una certificazione di hash, un confronto o un pacchetto oscurato vengono rifiutati con 422 COMPARE_SOURCE_UNSUPPORTED (`details.capture_id`, `details.source`) prima che venga letto alcun pacchetto e prima di qualsiasi prenotazione. Prima di qualsiasi confronto o prenotazione, viene ricalcolato lo SHA-256 di ogni file elencato con uno SHA-256 nel manifest.json di ciascun pacchetto di origine; un'origine con un file alterato o mancante, o un manifest che non elenca alcun file con uno SHA-256, viene rifiutata con 422 COMPARE_SOURCE_INTEGRITY_FAILED (nessun confronto, nessuna riga, nessuna unità mensile). Vengono confrontati solo i file elencati: un file all'interno di un pacchetto di origine che il relativo manifest non elenca con uno SHA-256 (ad esempio un text_layout.json, image_inventory.json, page.html o domtextcontent.txt aggiunto) viene trattato come assente. Il controllo viene eseguito sulla richiesta quando entrambi i pacchetti di origine sono di 50 MB o meno e si conclude entro circa 10 secondi; in caso contrario la richiesta viene accettata e l'esecuzione del confronto ripete il controllo, e un'origine non conforme fa quindi fallire il confronto (GET /captures/{compare_id} mostra lo stato failed con error.code COMPARE_SOURCE_INTEGRITY_FAILED) e restituisce l'unità mensile. Un confronto completato registra questo controllo in compare.json `sources[].source_files_reverified` e in comparison_method.json `source_verification`. /compare non invia webhook: un webhook_url nel corpo viene ignorato. Interrogare GET /captures/{compare_id}.\n\nUn confronto che coinvolge un'acquisizione di prova (eseguita con una chiave di prova, nella Sandbox o da un account senza account di fatturazione collegato) è un pacchetto di prova indipendentemente dalla chiave che lo richiede: con filigrana, `test_mode: true` in manifest.json, `test_mode: true` per tale origine in compare.json `sources[]` e una riga di prova (key_env test) non conteggiata e che non consuma alcuna unità mensile. include_eidas con un'origine di prova viene rifiutato con 403 EIDAS_REQUIRES_LIVE_KEY (details.reason).\n\nLimite di frequenza per account su finestre di 60 secondi (tutte le chiavi API dell'account e la relativa sessione della dashboard condividono un'unica finestra). Dopo l'esecuzione del limitatore, le risposte di questa route includono gli header `RateLimit-Policy` e `RateLimit` (basati sull'Internet-Draft IETF draft-ietf-httpapi-ratelimit-headers) oltre ai precedenti `X-RateLimit-Limit`, `X-RateLimit-Remaining` e `X-RateLimit-Reset`. Una risposta 429 aggiunge `Retry-After`.\n\nInviare una Idempotency-Key affinché un nuovo tentativo (ad esempio dopo un 504) restituisca la prima risposta invece di una seconda esecuzione addebitata (24 ore, stesse regole di POST /captures). La chiave viene acquisita prima del controllo delle origini, quindi un nuovo tentativo mentre la prima richiesta è ancora in corso restituisce 409 IDEMPOTENCY_IN_PROGRESS; una richiesta rifiutata dal controllo delle origini o dalla quota rilascia la propria chiave, quindi un nuovo tentativo con la stessa chiave viene controllato di nuovo.\n\nUn pacchetto di prove importato (source import, ID con prefisso `imp_`) non può essere confrontato: 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": "L'acquisizione precedente."
                  },
                  "capture_id_2": {
                    "type": "string",
                    "description": "L'acquisizione successiva. Deve essere diversa da capture_id_1."
                  },
                  "language": {
                    "type": "string",
                    "description": "Lingua del PDF di confronto. Solo codice esatto; qualsiasi altro valore ripiega silenziosamente sull'inglese.",
                    "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": "Come in CaptureOptions.include_eidas.",
                    "default": false
                  },
                  "include_rule_902": {
                    "type": "boolean",
                    "description": "Aggiunge un modello di certificazione non firmato (rule_902_certification.pdf, vedere CaptureOptions.include_rule_902) per il confronto. Non disponibile per acquisizioni eseguite in modo diverso: con allow_profile_mismatch true e una discrepanza di profilo o impostazioni, 422 RULE902_NOT_AVAILABLE_FOR_MISMATCH. Attualmente disattivato: il modello di certificazione è temporaneamente disattivato, quindi questo campo viene accettato e ignorato (nessun rule_902_certification.pdf viene generato o elencato nel pacchetto) e la risposta contiene l'avviso RULE902_DISABLED.",
                    "default": false
                  },
                  "allow_profile_mismatch": {
                    "type": "boolean",
                    "default": false,
                    "description": "Confronta due acquisizioni anche se sono state eseguite con un profilo dispositivo diverso (options.viewport mobile o desktop, viewport_width, viewport_height, device_scale_factor; a livello sigillato: dimensioni dell'area visibile, fattore di scala del dispositivo, emulazione mobile, user agent senza numeri di versione) o con impostazioni di acquisizione diverse che cambiano il rendering della pagina (dark_mode, block_ads, block_resources, inject_css, inject_js tramite SHA-256, expand_collapsed, dismiss_consent, dismiss_popups, pagina intera). Senza questa opzione una tale coppia viene rifiutata con 422 COMPARE_PROFILE_MISMATCH. Con essa, diff_summary.text_verdict e images_verdict sono undetermined, changed è null salvo che differisca un campo dei metadati di contenuto, e include_rule_902 viene rifiutato."
                  },
                  "allow_url_mismatch": {
                    "type": "boolean",
                    "default": false,
                    "description": "Confronta due acquisizioni di pagine diverse. Due acquisizioni sono considerate della stessa pagina quando un indirizzo dell'una (l'url richiesto, l'url e il final_url registrati dal suo metadata.json) coincide con uno dell'altra dopo la normalizzazione (host in minuscolo, http e https equivalenti, porta predefinita, frammento, barra finale e parametri di query volatili esclusi). Senza questa opzione, le pagine diverse vengono rifiutate con 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": "Accettato.",
            "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 non valido, un ID mancante, lo stesso ID due volte, oppure allow_profile_mismatch / allow_url_mismatch non booleano.",
            "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 oppure 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: un'acquisizione non è propria. EIDAS_REQUIRES_LIVE_KEY: include_eidas con una chiave di prova, o con un'acquisizione di origine di prova (in tal caso con details.reason). EMAIL_NOT_VERIFIED: una sessione della dashboard il cui indirizzo e-mail non è confermato. PLAN_ARCHIVE_ONLY: il piano mantiene solo l'archivio e non crea nuovo lavoro. PLAN_API_READ_ONLY: una chiave API di un account Website Archive (la sua chiave è di sola lettura; utilizzare la dashboard). PLAN_API_READ_ONLY con details.reason \"session_outside_dashboard\": una sessione della dashboard di un account Website Archive la cui richiesta non ha l'header Origin o ne ha uno che non è un indirizzo della dashboard ProofSnap (su quel piano il nuovo lavoro si crea solo nella dashboard; si tratta di un vincolo blando, non di un confine di sicurezza).",
            "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: entrambe le acquisizioni devono essere completate. COMPANY_READ_ONLY: un'origine appartiene a uno studio conservato come archivio dello studio (da esso non viene creato nulla di nuovo).",
            "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 file di un'acquisizione di origine non corrisponde allo SHA-256 indicato dal relativo manifest firmato, oppure manca; non è stato confrontato né addebitato nulla. `details.capture_id`, `details.mismatched`, `details.missing` indicano la prima origine non conforme; `details.reason` è presente quando il manifest di origine stesso non è utilizzabile (ad esempio \"the source manifest lists no file with a SHA-256\" o \"the source manifest lists no files\"); `details.sources[]` elenca ogni origine non conforme con gli stessi campi. Verificare l'acquisizione di origine (POST /captures/{capture_id}/verify). COMPARE_SOURCE_UNSUPPORTED: un'origine non è un'acquisizione web (una certificazione di hash, un confronto o un pacchetto oscurato); non è stato letto, confrontato né addebitato nulla. `details.capture_id` indica tale origine e `details.source` il suo tipo, quando la riga lo registra. Confrontare invece le acquisizioni web originali. COMPARE_PROFILE_MISMATCH: le due acquisizioni sono state eseguite con un profilo dispositivo diverso o con impostazioni di acquisizione diverse; `details.differing` elenca i nomi dei campi (a livello di riga: 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; a livello sigillato: viewport_width, viewport_height, device_scale_factor, is_mobile, user_agent e le stesse impostazioni), `details.before` e `details.after` i relativi valori. Inviare allow_profile_mismatch: true per confrontare comunque. COMPARE_URL_MISMATCH: le due acquisizioni riguardano pagine diverse; `details.before` e `details.after` riportano url e final_url come origine e percorso. Inviare allow_url_mismatch: true per confrontare comunque. RULE902_NOT_AVAILABLE_FOR_MISMATCH: include_rule_902 con allow_profile_mismatch per acquisizioni eseguite in modo diverso; `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: troppe richieste nella finestra corrente di 60 secondi. `error.details.retry_after_seconds` ripete Retry-After. RateLimit riporta r=0 e i secondi mancanti al ripristino della finestra. Inoltre RATE_LIMITED, per una sessione della dashboard Website Archive: il suo tetto giornaliero di 100 unità per giorno UTC, condiviso da acquisizioni singole e batch (un'unità per URL), compare, redact e certify-hash (un'unità per richiesta); una richiesta che non rientra viene rifiutata per intero (details.daily_limit, details.remaining, details.action \"wa_session_captures\"; Retry-After e details.retry_after_seconds indicano i secondi mancanti al giorno UTC successivo, e gli header RateLimit e X-RateLimit-* al minuto vengono omessi). Un breve rifiuto con solo details.retry_after_seconds e Retry-After: 1 indica un aggiornamento concorrente del contatore: riprovare. Un rifiuto non imputabile al chiamante restituisce le unità; un'attività accettata con 202 che fallisce in seguito mantiene la propria unità. Un controllo morbido, non un confine di sicurezza.",
            "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": [
          "Certificazione di hash"
        ],
        "operationId": "certifyHash",
        "summary": "Certificare hash di file",
        "description": "Inviare uno SHA-256 (hash) o fino a 100 (hashes). Il pacchetto contiene manifest.json (con i Suoi hash), manifest.sig, publickey.pem, evidence.pdf, gli script di verification/ e, quando disponibile, manifest.json.ots.\n\n- Un hash: sincrono, restituisce 200 con il certificato completato.\n- Più hash con combined: true: un unico certificato che elenca ogni hash, sincrono, restituisce 200.\n- Più hash senza combined: un certificato indipendente per ogni hash, avviato in background; restituisce 202 con i certify_ids. Interrogare GET /captures/{certify_id}; con webhook_url ciascuno invia certify.completed.\n\nOgni certificato conta come un'acquisizione ai fini della misurazione dei consumi, del limite mensile e del limite di frequenza (N hash senza combined contano N; N superiore al limite per finestra restituisce 400 RATE_LIMIT_COST_TOO_HIGH). Un `label` viene stampato sul certificato: viene troncato a 200 caratteri e deve essere testo semplice (senza caratteri di controllo, invisibili o di direzione del testo, altrimenti 400). In un batch, un errore relativo a un elemento lo indica in `error.details.index`.\n\nLimite di frequenza per account in finestre di 60 secondi (tutte le chiavi API dell'account e la sua sessione della dashboard condividono una sola finestra). Una volta eseguito il limitatore, le risposte di questa route includono gli header `RateLimit-Policy` e `RateLimit` (basati sulla bozza IETF draft-ietf-httpapi-ratelimit-headers) più gli header legacy `X-RateLimit-Limit`, `X-RateLimit-Remaining` e `X-RateLimit-Reset`. Un 429 aggiunge `Retry-After`.\n\nInviare una Idempotency-Key in modo che un nuovo tentativo (ad esempio dopo un 504) restituisca la prima risposta invece di una seconda esecuzione addebitata (24 ore, stesse regole di POST /captures).",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "hash": {
                    "type": "string",
                    "description": "SHA-256 come 64 caratteri esadecimali (senza distinzione tra maiuscole e minuscole). Usare hash oppure hashes."
                  },
                  "label": {
                    "type": "string",
                    "description": "Nome mostrato per il file. I valori più lunghi vengono accorciati a 200 caratteri."
                  },
                  "hashes": {
                    "type": "array",
                    "items": {
                      "oneOf": [
                        {
                          "type": "string",
                          "description": "SHA-256 in esadecimale."
                        },
                        {
                          "type": "object",
                          "properties": {
                            "hash": {
                              "type": "string"
                            },
                            "label": {
                              "type": "string"
                            }
                          },
                          "required": [
                            "hash"
                          ]
                        }
                      ]
                    },
                    "minItems": 1,
                    "maxItems": 100
                  },
                  "combined": {
                    "type": "boolean",
                    "description": "Certificare tutti gli hash in un unico certificato.",
                    "default": false
                  },
                  "algorithm": {
                    "type": "string",
                    "enum": [
                      "sha256"
                    ],
                    "default": "sha256"
                  },
                  "language": {
                    "type": "string",
                    "description": "Lingua dei PDF leggibili. Uno tra: 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. Maiuscole/minuscole, trattino e regione sono tollerati (\"DE\", \"fr-FR\" e \"PT-BR\" sono accettati); \"pt\" e \"zh\" da soli vengono rifiutati. Un valore non supportato restituisce 400 MALFORMED_REQUEST."
                  },
                  "case_reference": {
                    "type": "string",
                    "description": "Stampato sul certificato. Segue la regola del testo semplice (vedere PlainText); al massimo 200 caratteri.",
                    "maxLength": 200
                  },
                  "include_eidas": {
                    "type": "boolean",
                    "description": "Come in CaptureOptions.include_eidas.",
                    "default": false
                  },
                  "include_rule_902": {
                    "type": "boolean",
                    "description": "Aggiunge un modello di certificazione non firmato (rule_902_certification.pdf, vedere CaptureOptions.include_rule_902). Attualmente disattivato: il modello di certificazione è temporaneamente disattivato, quindi questo campo viene accettato e ignorato (nessun rule_902_certification.pdf viene generato o elencato nel pacchetto) e la risposta contiene l'avviso RULE902_DISABLED.",
                    "default": false
                  },
                  "webhook_url": {
                    "type": "string",
                    "description": "URL https che riceve certify.completed. L'URL deve essere https e non deve puntare a un indirizzo privato, di loopback, link-local, NAT di livello carrier o di metadati cloud (le stesse regole dell'URL di un'acquisizione; altrimenti 422 SSRF_BLOCKED). Un valore vuoto o composto solo da spazi significa nessun webhook. Richiede una chiave API: se inviato da una sessione della dashboard viene rifiutato con 400 WEBHOOK_REQUIRES_API_KEY, perché i webhook sono firmati con il secret di firma del webhook della chiave.",
                    "format": "uri"
                  }
                }
              },
              "example": {
                "hash": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
                "label": "contract.pdf"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Certificato pronto (un hash, o combinato).",
            "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": "Avviati più certificati indipendenti. Un certificato che il limite mensile o la quota del Sandbox rifiutano al momento dell'esecuzione fallisce: la sua riga (GET /captures/{certify_id}) ha status failed ed 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": "ID dei certificati creati (compreso uno il cui avvio non è riuscito)."
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "processing"
                      ]
                    },
                    "certificates": {
                      "type": "array",
                      "description": "Una voce per ogni hash inviato, in ordine. status failed: non è stato possibile avviare quel certificato (la sua riga, se è stata creata, è contrassegnata come failed); gli altri proseguono.",
                      "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": "Quante voci di certificates non sono state avviate. Quando tutte non riescono, la risposta è invece 500 CONFIGURATION_ERROR."
                    },
                    "warnings": {
                      "$ref": "#/components/schemas/Rule902DisabledWarnings"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "MALFORMED_REQUEST (details.reason indica il campo, ad esempio \"label must be a string\"; details.index indica l'elemento di un lotto), INVALID_HASH (details.index in un lotto) o INVALID_URL (webhook_url non https, o con credenziali). WEBHOOK_REQUIRES_API_KEY: webhook_url inviato da una sessione della dashboard (i webhook richiedono una chiave API). RATE_LIMIT_COST_TOO_HIGH: più hash (senza combined) di quanti ne consenta una finestra del limite di frequenza; 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, oppure QUOTA_EXCEEDED nei percorsi sincroni.",
            "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\" quando una chiave di produzione o una sessione funziona come prova perché non è collegato alcun account di fatturazione). EMAIL_NOT_VERIFIED: una sessione della dashboard il cui indirizzo e-mail non è confermato, o una chiave API di un tale account. PLAN_ARCHIVE_ONLY: il piano mantiene solo l'archivio e non crea nuovo lavoro. PLAN_API_READ_ONLY: una chiave API di un account Website Archive (la sua chiave è di sola lettura; utilizzare la dashboard). PLAN_API_READ_ONLY con details.reason \"session_outside_dashboard\": una sessione della dashboard di un account Website Archive la cui richiesta non ha l'header Origin o ne ha uno che non è un indirizzo della dashboard ProofSnap (su quel piano il nuovo lavoro si crea solo nella dashboard; si tratta di un vincolo blando, non di un confine di sicurezza).",
            "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 punta a un indirizzo privato, di loopback, link-local o comunque bloccato.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "RATE_LIMITED: troppe richieste nella finestra corrente di 60 secondi. `error.details.retry_after_seconds` ripete Retry-After. RateLimit riporta r=0 e i secondi mancanti al ripristino della finestra. Inoltre RATE_LIMITED, per una sessione della dashboard Website Archive: il suo tetto giornaliero di 100 unità per giorno UTC, condiviso da acquisizioni singole e batch (un'unità per URL), compare, redact e certify-hash (un'unità per richiesta); una richiesta che non rientra viene rifiutata per intero (details.daily_limit, details.remaining, details.action \"wa_session_captures\"; Retry-After e details.retry_after_seconds indicano i secondi mancanti al giorno UTC successivo, e gli header RateLimit e X-RateLimit-* al minuto vengono omessi). Un breve rifiuto con solo details.retry_after_seconds e Retry-After: 1 indica un aggiornamento concorrente del contatore: riprovare. Un rifiuto non imputabile al chiamante restituisce le unità; un'attività accettata con 202 che fallisce in seguito mantiene la propria unità. Un controllo morbido, non un confine di sicurezza.",
            "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 certificazione non è riuscita (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 certificazione sincrona (un hash, o combined) non si è conclusa entro circa 25 s. Potrebbe comunque completarsi: details.certify_id e details.poll_path indicano la riga da interrogare con 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": [
          "Webhook"
        ],
        "operationId": "listWebhookEvents",
        "summary": "Registro delle consegne dei webhook",
        "description": "Tutti i tentativi di consegna per un'acquisizione, un certificato o un oscuramento, dal più vecchio.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Registro delle consegne.",
            "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 del webhook della riga (options.webhook_url di un'acquisizione, oppure l'URL indicato con un certificato o un oscuramento; null quando non c'è). Per il titolare dello studio per cui è stata eseguita un'acquisizione, che legge il registro di una postazione, solo schema, host, porta e percorso (query e frammento rimossi, perché possono contenere un token); tale lettura viene conservata nella cronologia degli accessi dell'acquisizione (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 quando consegnato. Altrimenti una classe generica, mai l'errore di rete grezzo: connection_failed, timeout, blocked_address (al momento della consegna l'host si è risolto in un indirizzo privato o comunque bloccato), redirect_not_followed (l'endpoint ha risposto con 3xx; i reindirizzamenti non vengono mai seguiti), http_<status> (una risposta fuori da 2xx), oppure una frase che spiega perché l'evento non è stato inviato (nessuna chiave API, chiave revocata o non trovata, nessun secret del webhook, lettura del secret non riuscita)."
                          },
                          "delivery_id": {
                            "type": "string",
                            "description": "L'header webhook-id di questa consegna senza il prefisso `msg_`."
                          }
                        }
                      }
                    },
                    "attempt_count": {
                      "type": "integer"
                    },
                    "last_success": {
                      "type": "string",
                      "description": "attempted_at dell'ultimo tentativo riuscito.",
                      "nullable": true
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "FORBIDDEN: l'acquisizione non è propria.",
            "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": [
          "Acquisizioni automatiche"
        ],
        "operationId": "createSchedule",
        "summary": "Creare una pianificazione",
        "description": "interval e cron: ogni esecuzione produce un'acquisizione e, con webhook_url, invia capture.completed o capture.failed.\n\non_change: ogni esecuzione recupera prima la pagina (HTTP semplice, oppure un rendering nel browser con js_render) e calcola l'hash del suo testo normalizzato. Questo controllo non è un'acquisizione e non viene addebitato. La prima esecuzione produce un'acquisizione di riferimento. Quando l'hash cambia, viene eseguita una nuova acquisizione, confrontata con la precedente, e monitor.change_detected viene inviato a webhook_url. Una pagina che restituisce un errore o quasi nessun testo conta come nessuna modifica.\n\nDopo 5 esecuzioni fallite consecutive una pianificazione si sospende da sola. Quando viene raggiunto il limite mensile o l'abbonamento ha un pagamento scaduto, le esecuzioni vengono saltate e la pianificazione resta attiva.\n\nLimiti per piano:\n\n| Piano | Pianificazioni attive | Cadenza minima | on_change |\n| --- | --- | --- | --- |\n| sandbox | 0 | non disponibile | no |\n| developer | 3 | 300 s | sì |\n| startup | 25 | 300 s | sì |\n| growth | 200 | 300 s | sì |\n| custom | illimitate | 300 s | sì |\n| legal_solo | 3 | 300 s | sì |\n| legal_seats | 3 | 300 s | sì |\n| website_archive | 30 | 86400 s | sì |\n| archive_only | 0 | non disponibile | no |\n\nIdempotency-Key: un nuovo tentativo con la stessa chiave e lo stesso corpo entro 24 ore riceve il primo 201 (header Idempotent-Replay: true) invece di una seconda pianificazione; la stessa chiave con un altro corpo restituisce 422 IDEMPOTENCY_KEY_REUSED.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ScheduleRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Creato.",
            "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) o un errore delle opzioni di acquisizione. WEBHOOK_REQUIRES_API_KEY: webhook_url inviato da una sessione della dashboard (i webhook richiedono una chiave API).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "SCHEDULE_LIMIT_REACHED (details.max_active, details.active) oppure ON_CHANGE_REQUIRES_PLAN. EMAIL_NOT_VERIFIED: una sessione della dashboard il cui indirizzo email non è confermato. PLAN_ARCHIVE_ONLY: il piano conserva solo l'archivio e non crea nuove attività. PLAN_API_READ_ONLY: chiave API di un account Website Archive (la sua chiave è in sola lettura; usare la dashboard). PLAN_API_READ_ONLY con details.reason \"session_outside_dashboard\": una sessione della dashboard di un account Website Archive la cui richiesta non ha un header Origin oppure ne ha uno che non è un indirizzo della dashboard ProofSnap (su quel piano le nuove attività si creano solo nella dashboard; un controllo morbido, non un confine di sicurezza).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "SSRF_BLOCKED: url o webhook_url punta a un indirizzo privato, di loopback, link-local o comunque bloccato.",
            "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": [
          "Acquisizioni automatiche"
        ],
        "operationId": "listSchedules",
        "summary": "Elencare le pianificazioni",
        "description": "Fino a 100 delle Sue pianificazioni.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Pianificazioni.",
            "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": [
          "Acquisizioni automatiche"
        ],
        "operationId": "getSchedule",
        "summary": "Ottenere una pianificazione",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "La pianificazione.",
            "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": [
          "Acquisizioni automatiche"
        ],
        "operationId": "updateSchedule",
        "summary": "Sospendere, riprendere o modificare una pianificazione",
        "description": "Inviare status per sospendere o riprendere, e/o qualsiasi campo di ScheduleRequest da modificare. Un campo di primo livello inviato sostituisce quello memorizzato, compreso un null esplicito (webhook_url, timezone e js_render si svuotano così); un campo omesso mantiene il valore memorizzato; il risultato viene nuovamente convalidato nel suo insieme. `options` fa eccezione alla modifica campo per campo: viene sostituito come mappa intera, non unito chiave per chiave, quindi inviare le opzioni complete desiderate (leggerle prima con GET). Un'opzione omessa dalla mappa viene rimossa. La ripresa verifica il limite di pianificazioni attive, azzera il conteggio degli errori e calcola la prossima esecuzione a partire da ora.\n\nDa una sessione della dashboard non è possibile impostare un webhook_url (400 WEBHOOK_REQUIRES_API_KEY); inviare null o una stringa vuota lo svuota. Una pianificazione creata da una sessione della dashboard non ha mai avuto una chiave API con cui firmare, quindi modificarla elimina anche qualsiasi webhook_url memorizzato in precedenza, e nemmeno una chiave API può aggiungerne uno (400 WEBHOOK_REQUIRES_API_KEY): il pianificatore firma i webhook di una pianificazione con il secret di firma del webhook della chiave con cui la pianificazione è stata creata. Per ricevere webhook, creare la pianificazione con una chiave API.\n\nModificare url, mode o js_render di una pianificazione on_change (o passare una pianificazione a on_change o da on_change) ne azzera il riferimento (l'hash di sondaggio memorizzato e l'ultima acquisizione), così l'esecuzione successiva esegue una nuova acquisizione di riferimento invece di confrontare la nuova destinazione con quella vecchia. Il corpo della richiesta deve essere un oggetto JSON (altrimenti 400 MALFORMED_REQUEST).",
        "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": "Fuso orario IANA per cron. null o una stringa vuota ripristina UTC."
                  },
                  "webhook_url": {
                    "type": "string",
                    "description": "Inviare null o una stringa vuota per rimuoverlo. L'URL deve essere https e non deve puntare a un indirizzo privato, di loopback, link-local, NAT di livello carrier o di metadati cloud (le stesse regole dell'URL di un'acquisizione; altrimenti 422 SSRF_BLOCKED). Richiede una chiave API da entrambe le parti: se inviato da una sessione della dashboard, o inviato con una chiave API per una pianificazione creata nella dashboard, viene rifiutato con 400 WEBHOOK_REQUIRES_API_KEY, perché i webhook sono firmati con il secret di firma del webhook della chiave con cui è stata creata la pianificazione.",
                    "nullable": true
                  },
                  "js_render": {
                    "type": "boolean",
                    "nullable": true,
                    "description": "null equivale a false."
                  },
                  "options": {
                    "allOf": [
                      {
                        "$ref": "#/components/schemas/CaptureOptions"
                      }
                    ],
                    "description": "Sostituisce per intero le opzioni memorizzate (non le unisce chiave per chiave). Inviare la mappa completa; un'opzione omessa viene rimossa. null ripristina le opzioni predefinite. options.webhook_url non viene mai memorizzato (vedere webhook_url)."
                  },
                  "max_capture_age_seconds": {
                    "type": "integer",
                    "nullable": true,
                    "minimum": 86400,
                    "maximum": 34560000,
                    "description": "Solo on_change: un'acquisizione completa almeno ogni N secondi anche senza modifiche (da 1 a 400 giorni, più di interval_seconds). null la rimuove. Passare la modalità a interval o cron la elimina."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "La pianificazione aggiornata.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Schedule"
                }
              }
            }
          },
          "400": {
            "description": "MALFORMED_REQUEST (compreso il caso in cui non c'è nulla da aggiornare), INVALID_URL, INVALID_CRON o INTERVAL_TOO_SHORT. WEBHOOK_REQUIRES_API_KEY: webhook_url inviato da una sessione della dashboard, oppure inviato con una chiave API per una pianificazione creata nella dashboard (non ha alcuna chiave il cui secret di firma del webhook possa firmare i webhook).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "SCHEDULE_LIMIT_REACHED oppure ON_CHANGE_REQUIRES_PLAN. EMAIL_NOT_VERIFIED: una sessione della dashboard il cui indirizzo email non è confermato (ripresa di una pianificazione sospesa; sospensione e modifica restano possibili). PLAN_API_READ_ONLY: chiave API di un account Website Archive, su ogni PATCH (sospensione e modifica incluse). PLAN_API_READ_ONLY con details.reason \"session_outside_dashboard\": una sessione della dashboard di un account Website Archive la cui richiesta non ha un header Origin oppure ne ha uno che non è un indirizzo della dashboard ProofSnap (su quel piano le nuove attività si creano solo nella dashboard; un controllo morbido, non un confine di sicurezza). PLAN_ARCHIVE_ONLY: ripresa di una pianificazione sul piano 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 o webhook_url punta a un indirizzo privato, di loopback, link-local o comunque bloccato.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/AuthUnavailable"
          }
        }
      },
      "delete": {
        "tags": [
          "Acquisizioni automatiche"
        ],
        "operationId": "deleteSchedule",
        "summary": "Eliminare una pianificazione",
        "description": "Interrompe le esecuzioni future. Le acquisizioni già eseguite vengono mantenute.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Eliminato.",
            "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: chiave API di un account Website Archive (la sua chiave è in sola lettura; usare la dashboard).",
            "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": [
          "Acquisizioni automatiche"
        ],
        "operationId": "listScheduleRuns",
        "summary": "Elencare le esecuzioni di una pianificazione",
        "description": "Le 50 acquisizioni più recenti eseguite da questa pianificazione (i controlli on_change che non hanno rilevato modifiche non sono elencati).",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Esecuzioni.",
            "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": [
          "Pratiche"
        ],
        "operationId": "listMatters",
        "summary": "Elencare le pratiche",
        "description": "Fino a 200 pratiche di cui si è membri. Le pratiche di uno studio che si è lasciato non vengono elencate.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Pratiche.",
            "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: il piano non è un piano Legal.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/AuthUnavailable"
          }
        }
      },
      "post": {
        "tags": [
          "Pratiche"
        ],
        "operationId": "createMatter",
        "summary": "Creare una pratica",
        "description": "Il chiamante ne diventa titolare. Il registro della catena di custodia inizia con una voce matter.created. I campi di testo vengono ripuliti dagli spazi alle estremità e accorciati a 200 caratteri.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 200,
                    "description": "Testo semplice: gli spazi vengono compressi e rimossi alle estremità, al massimo 200 caratteri, senza caratteri di controllo, di direzione del testo (bidi) o invisibili (sono ammessi i caratteri di unione U+200C e U+200D, necessari all'ortografia hindi e persiana) (altrimenti 400 MALFORMED_REQUEST). Obbligatorio. Mostrato nella pagina pubblica di verifica della pratica."
                  },
                  "client": {
                    "type": "string",
                    "maxLength": 200,
                    "description": "Testo semplice: gli spazi vengono compressi e rimossi alle estremità, al massimo 200 caratteri, senza caratteri di controllo, di direzione del testo (bidi) o invisibili (sono ammessi i caratteri di unione U+200C e U+200D, necessari all'ortografia hindi e persiana) (altrimenti 400 MALFORMED_REQUEST). Un valore vuoto o null significa non impostato (memorizzato come null). Un valore che non è una stringa viene rifiutato.",
                    "nullable": true
                  },
                  "case_number": {
                    "type": "string",
                    "maxLength": 200,
                    "description": "Testo semplice: gli spazi vengono compressi e rimossi alle estremità, al massimo 200 caratteri, senza caratteri di controllo, di direzione del testo (bidi) o invisibili (sono ammessi i caratteri di unione U+200C e U+200D, necessari all'ortografia hindi e persiana) (altrimenti 400 MALFORMED_REQUEST). Un valore vuoto o null significa non impostato (memorizzato come null). Un valore che non è una stringa viene rifiutato. Mostrato nella pagina pubblica di verifica della pratica.",
                    "nullable": true
                  },
                  "opposing_party": {
                    "type": "string",
                    "maxLength": 200,
                    "description": "Testo semplice: gli spazi vengono compressi e rimossi alle estremità, al massimo 200 caratteri, senza caratteri di controllo, di direzione del testo (bidi) o invisibili (sono ammessi i caratteri di unione U+200C e U+200D, necessari all'ortografia hindi e persiana) (altrimenti 400 MALFORMED_REQUEST). Un valore vuoto o null significa non impostato (memorizzato come null). Un valore che non è una stringa viene rifiutato.",
                    "nullable": true
                  },
                  "jurisdiction": {
                    "type": "string",
                    "description": "Testo libero. Testo semplice: gli spazi vengono compressi e rimossi alle estremità, al massimo 200 caratteri, nessun carattere di controllo, di direzione del testo (bidi) o invisibile (i caratteri di unione U+200C e U+200D, necessari all'ortografia hindi e persiana, sono ammessi) (altrimenti 400 MALFORMED_REQUEST). Vuoto o null significa non impostato (memorizzato come null). Un valore che non è una stringa viene rifiutato. Mostrato nella pagina pubblica di verifica della pratica.",
                    "maxLength": 200,
                    "nullable": true
                  }
                },
                "required": [
                  "name"
                ]
              },
              "example": {
                "name": "Smith v. Jones",
                "case_number": "2026-CV-0142"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Creato.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Matter"
                }
              }
            }
          },
          "400": {
            "description": "MISSING_FIELD: name è assente o vuoto. MALFORMED_REQUEST: JSON non valido. MALFORMED_REQUEST: un campo di testo che non è una stringa, supera i 200 caratteri o contiene caratteri di controllo o di direzione del testo.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "PAYMENT_REQUIRED: chiamante di produzione il cui abbonamento (o quello dello studio) è scaduto o non pagato."
          },
          "403": {
            "description": "MATTERS_NOT_ENABLED: il piano non è un piano Legal. EMAIL_NOT_VERIFIED: una sessione della dashboard il cui indirizzo e-mail non è confermato, o una chiave API di un tale account.",
            "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": [
          "Pratiche"
        ],
        "operationId": "getMatter",
        "summary": "Ottenere una pratica",
        "description": "La pratica con le sue acquisizioni (fino a 500), il suo registro della catena di custodia e fino a 100 eventi di accesso. `members` riporta l'email di ogni voce di `member_uids` e `actor_emails` associa gli uid citati nel registro della catena di custodia e in audit_stream di questa pratica (autori, e la destinazione di `member.added` / `member.removed`, quindi anche i membri rimossi) alla loro email; un'email è null, oppure l'uid manca dalla mappa, quando non può essere letta. Solo i chiamanti che possono aprire la pratica ricevono questi campi; `/matter-verify` non riporta mai email. Un chiamante che può revocare il link di condivisione pubblico (il titolare della pratica, il titolare di uno studio cessato o archiviato) riceve anche `share`: `{ share_url, created_at, expires_at }` del link attivo (`expires_at` è sempre null: i link funzionano fino alla revoca), oppure null quando non ce n'è uno; gli altri membri non ricevono mai il campo. Le voci di audit_stream riportano source_ip e user_agent solo per il titolare della pratica; i membri ricevono le voci senza questi due campi.\n\nUna pratica di uno studio cessato riporta `read_only: true`.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "La pratica.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Matter"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "members": {
                          "type": "array",
                          "description": "Una voce per ogni voce di `member_uids`, nello stesso ordine.",
                          "items": {
                            "type": "object",
                            "properties": {
                              "uid": {
                                "type": "string"
                              },
                              "email": {
                                "type": "string",
                                "format": "email",
                                "nullable": true,
                                "description": "Null quando non è possibile leggere l'account."
                              }
                            }
                          }
                        },
                        "actor_emails": {
                          "type": "object",
                          "description": "`{ uid: email }` per le persone citate in custody_log e audit_stream di questa pratica (escluso l'attore di sistema). Un uid la cui email non può essere letta è assente.",
                          "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": "Solo per un chiamante che può revocare il link. Null quando non esiste alcun link attivo.",
                          "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": "Sempre null: i link di condivisione non scadono."
                            }
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "MATTERS_NOT_ENABLED: il piano non è un piano 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": [
          "Pratiche"
        ],
        "operationId": "updateMatter",
        "summary": "Aggiornare una pratica",
        "description": "Solo titolare. I campi di testo vengono ripuliti dagli spazi alle estremità e accorciati a 200 caratteri. name è obbligatorio: quando viene inviato deve essere una stringa non vuota, e un nome vuoto o composto solo da spazi, null o un valore non stringa viene rifiutato con 400 e non viene modificato nulla. Membri: add_member_email aggiunge una persona che ha già un account ProofSnap e appartiene allo studio della pratica; l'assenza di account, un indirizzo non confermato e un account esterno allo studio ricevono la stessa identica risposta 404 NOT_FOUND; remove_member_uid rimuove un membro (non il titolare). Quando vengono inviati entrambi, viene applicato solo add_member_email. Una pratica ha al massimo 50 membri (una pratica al completo rifiuta prima della ricerca dell'indirizzo). Le modifiche dei membri vengono scritte nel registro della catena di custodia. Una modifica di name, client, case_number, opposing_party, jurisdiction o status viene scritta nel registro della catena di custodia come matter.updated; il suo target è una stringa JSON con fields (i nomi dei campi modificati) e, se lo stato è cambiato, il nuovo stato. I valori a testo libero non vengono scritti nel registro della catena di custodia pubblico e concatenato tramite hash: i valori vecchi e nuovi vanno nella cronologia degli accessi (audit_stream, voce matter.updated). In una pratica archiviata una ridenominazione o add_member_email viene rifiutata con 409 MATTER_ARCHIVED, a meno che la stessa richiesta non la riapra (status \"open\"); remove_member_uid e la riapertura continuano a funzionare. Ogni invito viene conteggiato in un limite giornaliero di 50 per titolare che invita (inviti allo studio dalla dashboard e add_member_email delle pratiche insieme, rifiuti compresi, poiché ciascuno comporta la ricerca di un indirizzo); al raggiungimento del limite la richiesta restituisce 429 RATE_LIMITED. Un errore lato server non viene conteggiato.\n\nBlocco per controversia legale: `{ \"legal_hold\": true|false }` da solo imposta il blocco per controversia legale della pratica (solo titolare della pratica) e lo applica alle sue acquisizioni nella stessa richiesta, con gli effetti del blocco per controversia legale di un'acquisizione (legal hold S3 Object Lock, voci della catena di custodia capture.legal_hold_placed / capture.legal_hold_released, la catena degli eventi di acquisizione). La risposta è la pratica più `changed` (il flag è cambiato), `captures_held` o `captures_released`, `skipped` ([{ capture_id, code }], ad esempio un'acquisizione nel cestino) e `truncated` (più di 1 000 acquisizioni: inviare di nuovo lo stesso valore per proseguire). Le acquisizioni assegnate alla pratica in seguito, mentre il blocco è attivo, vengono sottoposte a blocco al momento dell'assegnazione. Un'unità del limite di frequenza per richiesta più una ogni 10 acquisizioni modificate.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "description": "Deve contenere almeno un carattere diverso da uno spazio. Ometterlo per mantenere il nome attuale. Stessa regola del testo semplice: spazi compressi, al massimo 200 caratteri, nessun carattere di controllo, di direzione del testo o invisibile (U+200C e U+200D sono ammessi).",
                    "maxLength": 200
                  },
                  "client": {
                    "type": "string",
                    "nullable": true,
                    "description": "Testo semplice: gli spazi vengono compressi e rimossi alle estremità, al massimo 200 caratteri, senza caratteri di controllo, di direzione del testo (bidi) o invisibili (sono ammessi i caratteri di unione U+200C e U+200D, necessari all'ortografia hindi e persiana) (altrimenti 400 MALFORMED_REQUEST). null o una stringa composta solo da spazi svuota il campo (memorizzato come null). Un valore che non è una stringa viene rifiutato con 400.",
                    "maxLength": 200
                  },
                  "case_number": {
                    "type": "string",
                    "nullable": true,
                    "description": "Testo semplice: gli spazi vengono compressi e rimossi alle estremità, al massimo 200 caratteri, senza caratteri di controllo, di direzione del testo (bidi) o invisibili (sono ammessi i caratteri di unione U+200C e U+200D, necessari all'ortografia hindi e persiana) (altrimenti 400 MALFORMED_REQUEST). null o una stringa composta solo da spazi svuota il campo (memorizzato come null). Un valore che non è una stringa viene rifiutato con 400.",
                    "maxLength": 200
                  },
                  "opposing_party": {
                    "type": "string",
                    "nullable": true,
                    "description": "Testo semplice: gli spazi vengono compressi e rimossi alle estremità, al massimo 200 caratteri, senza caratteri di controllo, di direzione del testo (bidi) o invisibili (sono ammessi i caratteri di unione U+200C e U+200D, necessari all'ortografia hindi e persiana) (altrimenti 400 MALFORMED_REQUEST). null o una stringa composta solo da spazi svuota il campo (memorizzato come null). Un valore che non è una stringa viene rifiutato con 400.",
                    "maxLength": 200
                  },
                  "jurisdiction": {
                    "type": "string",
                    "nullable": true,
                    "description": "Testo semplice: gli spazi vengono compressi e rimossi alle estremità, al massimo 200 caratteri, senza caratteri di controllo, di direzione del testo (bidi) o invisibili (sono ammessi i caratteri di unione U+200C e U+200D, necessari all'ortografia hindi e persiana) (altrimenti 400 MALFORMED_REQUEST). null o una stringa composta solo da spazi svuota il campo (memorizzato come null). Un valore che non è una stringa viene rifiutato con 400.",
                    "maxLength": 200
                  },
                  "status": {
                    "type": "string",
                    "description": "Per sigillare, usare POST /matters/{matter_id}/seal.",
                    "enum": [
                      "open",
                      "archived"
                    ]
                  },
                  "add_member_email": {
                    "type": "string",
                    "format": "email"
                  },
                  "remove_member_uid": {
                    "type": "string"
                  },
                  "legal_hold": {
                    "type": "boolean",
                    "description": "Blocco per controversia legale della pratica, inviato da solo (combinato con un altro campo: 400). true sottopone a blocco ogni acquisizione attuale della pratica e ogni acquisizione assegnata a essa finché il blocco è attivo (legal_hold_source \"matter:<id>\"; un'acquisizione già sottoposta a blocco mantiene il proprio). false revoca solo i blocchi attivati dalla pratica; un blocco manuale o massivo resta. Solo titolare della pratica. Non bloccato da un abbonamento scaduto. L'attivazione richiede un piano a pagamento (403 LEGAL_HOLD_NOT_AVAILABLE, details.reason plan) e le acquisizioni in modalità di prova vengono saltate (codice LEGAL_HOLD_NOT_AVAILABLE in `skipped`); false da solo è accettato anche dopo la fine del piano Legal."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "La pratica aggiornata.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Matter"
                }
              }
            }
          },
          "400": {
            "description": "MALFORMED_REQUEST: name vuoto, composto solo da spazi o non stringa; un campo di testo che non è una stringa, supera i 200 caratteri o contiene caratteri di controllo o di direzione del testo; status non valido; nulla da aggiornare; già membro; oppure rimozione del titolare.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "PAYMENT_REQUIRED: chiamante di produzione il cui abbonamento (o quello dello studio) è scaduto o non pagato."
          },
          "403": {
            "description": "MATTERS_NOT_ENABLED, FORBIDDEN (non titolare), LEGAL_HOLD_NOT_AVAILABLE (legal_hold true senza un piano a pagamento), oppure EMAIL_NOT_VERIFIED: una sessione della dashboard il cui indirizzo e-mail non è confermato, o una chiave API di un tale account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "MATTER_NOT_FOUND, oppure NOT_FOUND (nessun account idoneo per l'indirizzo e-mail: inesistente, non confermato o esterno allo studio, con un'unica risposta per tutti i casi; oppure remove_member_uid non è un membro).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "MATTER_SEALED, oppure MATTER_ARCHIVED (ridenominazione o nuovo membro in una pratica archiviata). COMPANY_READ_ONLY: la pratica appartiene a uno studio cessato o conservato come archivio dello studio (chiamante con un piano Legal; apertura, esportazione e revoca delle condivisioni continuano a funzionare).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "RATE_LIMITED: è stato raggiunto il limite giornaliero di 50 inviti di membri per titolare che invita (gli inviti allo studio dalla dashboard e add_member_email di una pratica vengono conteggiati insieme; details.daily_limit, details.action \"member_invite\"). Retry-After indica quando termina il giorno UTC.",
            "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": [
          "Pratiche"
        ],
        "operationId": "assignMatterCaptures",
        "summary": "Aggiungere o rimuovere acquisizioni",
        "description": "Qualsiasi membro può chiamare questa route, ma vengono spostate solo le acquisizioni create dal chiamante; le altre vengono restituite in skipped, così come le acquisizioni già assegnate a un'altra pratica e quelle già presenti in questa pratica (o, con unassign, non presenti). Le voci che non sono ID di acquisizione vengono ignorate e ne vengono elaborate al massimo 100. Ogni modifica viene scritta nel registro della catena di custodia. Una pratica archiviata non accetta nuove acquisizioni (409 MATTER_ARCHIVED); unassign continua a funzionare.",
        "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 rimuove le acquisizioni dalla pratica.",
                    "default": false
                  }
                },
                "required": [
                  "capture_ids"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Risultato.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "matter_id": {
                      "type": "string"
                    },
                    "assigned": {
                      "type": "integer",
                      "description": "Numero di acquisizioni modificate."
                    },
                    "skipped": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "unassign": {
                      "type": "boolean"
                    },
                    "legal_holds_placed": {
                      "type": "integer",
                      "description": "Presente quando il blocco per controversia legale della pratica è attivo: su quante delle acquisizioni assegnate ha attivato il blocco."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "MALFORMED_REQUEST: nessun ID di acquisizione valido.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "PAYMENT_REQUIRED: chiamante di produzione il cui abbonamento (o quello dello studio) è scaduto o non pagato."
          },
          "403": {
            "description": "MATTERS_NOT_ENABLED, oppure EMAIL_NOT_VERIFIED: una sessione della dashboard il cui indirizzo e-mail non è confermato, o una chiave API di un tale account.",
            "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, oppure MATTER_ARCHIVED (aggiunta a una pratica archiviata). COMPANY_READ_ONLY: la pratica appartiene a uno studio cessato o conservato come archivio dello studio (chiamante con un piano Legal; apertura, esportazione e revoca delle condivisioni continuano a funzionare).",
            "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": [
          "Pratiche"
        ],
        "operationId": "sealMatter",
        "summary": "Sigillare una pratica",
        "description": "Solo titolare. Ancora l'hash dell'ultima voce attuale del registro della catena di custodia con OpenTimestamps e, quando nel deployment è configurato eIDAS e il chiamante è di produzione (una chiave API di produzione, o una sessione della dashboard con fatturazione collegata), con una marca temporale eIDAS (la marca temporale eIDAS del sigillo non viene addebitata; se non riesce, la sigillatura prosegue senza di essa). Un chiamante di prova (chiave API di prova o sessione Sandbox) viene sigillato solo con OpenTimestamps e il sigillo registra eidas_status not_obtained con eidas_reason test_key; la richiesta non prevede alcuna opzione eIDAS, quindi non si tratta di un errore. La pratica diventa quindi di sola lettura: nessuna modifica, nessun cambiamento delle acquisizioni e le sue acquisizioni non possono essere eliminate. Una pratica che contiene ancora un'acquisizione nel cestino non viene sigillata (409 MATTER_HAS_TRASHED_CAPTURES): ripristinare l'acquisizione o rimuoverla prima dalla pratica.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Sigillata.",
            "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: il registro della catena di custodia della pratica non contiene ancora alcuna voce.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "PAYMENT_REQUIRED: chiamante di produzione il cui abbonamento (o quello dello studio) è scaduto o non pagato."
          },
          "403": {
            "description": "MATTERS_NOT_ENABLED: il piano non è un piano Legal. FORBIDDEN: non titolare. EMAIL_NOT_VERIFIED: una sessione della dashboard il cui indirizzo e-mail non è confermato, o una chiave API di un tale account.",
            "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 (già sigillata), MATTER_CHANGED (il registro della catena di custodia è cambiato durante la sigillatura; riprovare) o MATTER_HAS_TRASHED_CAPTURES (un'acquisizione della pratica è nel cestino; ripristinarla o rimuoverla prima dalla pratica). COMPANY_READ_ONLY: la pratica appartiene a uno studio cessato o conservato come archivio dello studio (chiamante con un piano Legal; apertura, esportazione e revoca delle condivisioni continuano a funzionare).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "TIMESTAMP_SERVICE_UNAVAILABLE (l'ancoraggio non è riuscito, nulla è cambiato; riprovare) oppure CONFIGURATION_ERROR.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "EIDAS_SEAL_RETRY: la marca temporale eIDAS del sigillo non è arrivata in tempo (o è ancora in fase di emissione). Non è stato sigillato nulla. Riprovare dopo Retry-After secondi: il nuovo tentativo riceve la marca temporale già emessa per quest'ultima voce della catena di custodia, senza una seconda marca. SERVICE_UNAVAILABLE: il gateway non ha potuto verificare le credenziali; riprovare con backoff.",
            "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": [
          "Pratiche"
        ],
        "operationId": "exportMatter",
        "summary": "Esportare una pratica",
        "description": "Solo titolare. Crea un unico ZIP con custody_report.pdf, custody_log.json, matter.json, README.txt, access_log.json (quando ci sono eventi di accesso) e il pacchetto di prove di ogni acquisizione. Con la numerazione Bates attiva (impostazione predefinita) aggiunge copie con numerazione Bates di ciascun evidence.pdf in exhibits/ (allegati numerati a partire dalla prova più vecchia) e, con formats.loadfile, un file di caricamento Concordance/Opticon. Con formats.pdfa le copie per il deposito vengono anche convertite in PDF/A quando il convertitore è disponibile (in un'esportazione sincrona solo entro un budget di tempo); solo le copie conteggiate in pdfa_count sono in PDF/A, le altre sono PDF che non dichiarano la conformità PDF/A, e README.txt menziona PDF/A solo quando pdfa_count è superiore a 0.\n\nUna pratica con un massimo di 20 acquisizioni viene esportata in modo sincrono (200). Una pratica più grande viene esportata in background: la risposta è 202 con un job_id; interrogare GET /matters/{matter_id}/export?job_id=. Le acquisizioni che non rientrano nel limite di dimensione (120 MB in modalità sincrona; 800 MB e 300 acquisizioni in background) sono elencate nei rapporti ma non incluse.\n\nFunziona su pratiche aperte e sigillate. Una pratica archiviata viene rifiutata con 409 MATTER_ARCHIVED (riaprirla prima), tranne che per un chiamante che non può riaprirla: il titolare di uno studio cessato durante il relativo periodo di conservazione e un account Archive only. Ogni esportazione viene registrata come export.generated nella cronologia degli accessi. Ogni esportazione viene conteggiata in un limite giornaliero di 50 per utente; un'esportazione in background non riuscita restituisce la propria 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": "Solo lettere e cifre, convertite in maiuscolo, fino a 12. Predefinito: ricavato dal numero di causa o dal nome della pratica."
                      },
                      "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": "Include il file di caricamento per l'e-discovery (richiede la numerazione Bates).",
                        "default": true
                      },
                      "pdfa": {
                        "type": "boolean",
                        "description": "Converte in PDF/A le copie per il deposito con numerazione Bates (richiede la numerazione Bates). Nei limiti del possibile: una copia non convertita resta un PDF senza dichiarazione PDF/A; pdfa_count nella risposta indica quante copie sono state convertite.",
                        "default": true
                      }
                    }
                  },
                  "export_locale": {
                    "type": "string",
                    "description": "Lingua di custody_report.pdf, dell'indice degli allegati e di 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 (il trattino è accettato al posto del trattino basso). Gli altri valori ripiegano sull'inglese."
                  },
                  "declaration": {
                    "type": "object",
                    "description": "Accettato per compatibilità. I suoi campi di testo (name, title, party, case_caption, executed_location) seguono la regola del testo semplice (vedere PlainText): un valore con un carattere non ammesso viene rifiutato con 400 MALFORMED_REQUEST. I valori troppo lunghi vengono accorciati (name, title, executed_location 120, party 160, case_caption 200 caratteri)."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Esportazione pronta.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "matter_id": {
                      "type": "string"
                    },
                    "url": {
                      "type": "string",
                      "description": "Link di download prefirmato, valido 24 ore.",
                      "format": "uri"
                    },
                    "expires_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "capture_count": {
                      "type": "integer"
                    },
                    "captures_included": {
                      "type": "integer"
                    },
                    "captures_skipped": {
                      "type": "integer",
                      "description": "Acquisizioni elencate nei rapporti ma non incluse perché è stato raggiunto un limite di dimensione."
                    },
                    "custody_events": {
                      "type": "integer"
                    },
                    "exhibits_count": {
                      "type": "integer",
                      "description": "Copie degli allegati con numerazione Bates."
                    },
                    "bates_prefix": {
                      "type": "string",
                      "nullable": true
                    },
                    "searchable": {
                      "type": "boolean",
                      "description": "Sempre false: alle copie per il deposito non viene aggiunto alcun livello di testo. Mantenuto per una struttura della risposta stabile."
                    },
                    "pdfa_count": {
                      "type": "integer",
                      "description": "Copie per il deposito in exhibits/ che sono in formato PDF/A. 0 significa che nessuna copia è in PDF/A né dichiara di esserlo."
                    },
                    "loadfile": {
                      "type": "boolean"
                    },
                    "size_bytes": {
                      "type": "integer"
                    },
                    "generated_at": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          },
          "202": {
            "description": "Esportazione avviata in background.",
            "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": "Include lo stage /v1."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "MALFORMED_REQUEST: un'opzione di esportazione non è valida (details.reason).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "PAYMENT_REQUIRED: chiamante di produzione il cui abbonamento (o quello dello studio) è scaduto o non pagato."
          },
          "403": {
            "description": "MATTERS_NOT_ENABLED: il piano non è un piano Legal. FORBIDDEN: non titolare. EMAIL_NOT_VERIFIED: una sessione della dashboard il cui indirizzo e-mail non è confermato, o una chiave API di un tale account.",
            "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: riaprire prima la pratica (PATCH status \"open\").",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "CONFIGURATION_ERROR: non è stato possibile creare o avviare l'esportazione.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/AuthUnavailable"
          },
          "504": {
            "description": "UPSTREAM_TIMEOUT: l'esportazione sincrona non si è conclusa entro circa 25 s (details.reason). Riprovare.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "Pratiche"
        ],
        "operationId": "getMatterExport",
        "summary": "Controllare lo stato di un'esportazione in background",
        "description": "Solo titolare. Un job ancora in elaborazione 30 minuti dopo l'avvio viene segnalato come failed con l'errore \"export timed out\". Un job termina una sola volta: un job segnalato come failed non diventa mai ready in seguito e un job ready non viene mai segnalato come failed.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "job_id",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Stato del job. Quando è pronto, url e i conteggi vengono valorizzati.",
            "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 mancante.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "MATTERS_NOT_ENABLED: il piano non è un piano Legal. FORBIDDEN: non titolare.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "MATTER_NOT_FOUND, oppure NOT_FOUND per un job sconosciuto.",
            "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": [
          "Pratiche"
        ],
        "operationId": "shareMatter",
        "summary": "Creare un link di verifica pubblico",
        "description": "Solo titolare. Restituisce un link che chiunque può aprire per vedere il nome della pratica, il numero di causa, la giurisdizione, lo stato, il registro della catena di custodia e il sigillo (vedere GET /matter-verify). Non viene condiviso alcun contenuto delle acquisizioni. Una nuova chiamata restituisce lo stesso link. Il link funziona finché non viene revocato.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Link di condivisione.",
            "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: chiamante di produzione il cui abbonamento (o quello dello studio) è scaduto o non pagato."
          },
          "403": {
            "description": "MATTERS_NOT_ENABLED: il piano non è un piano Legal. FORBIDDEN: non titolare. EMAIL_NOT_VERIFIED: una sessione della dashboard il cui indirizzo e-mail non è confermato, o una chiave API di un tale account.",
            "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: una pratica archiviata non può essere condivisa. COMPANY_READ_ONLY: la pratica appartiene a uno studio cessato o conservato come archivio dello studio (chiamante con un piano Legal; apertura, esportazione e revoca delle condivisioni continuano a funzionare).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/AuthUnavailable"
          }
        }
      },
      "delete": {
        "tags": [
          "Pratiche"
        ],
        "operationId": "revokeMatterShare",
        "summary": "Revocare il link pubblico",
        "description": "Solo titolare. Il link smette di funzionare. Un POST successivo ne crea uno nuovo. Il titolare di uno studio cessato (o conservato come archivio dello studio) e il titolare di una pratica con il piano Archive only possono revocare il link senza un piano Legal (la creazione di un link resta una funzione dei piani Legal).",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Revocato.",
            "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: chiamante di produzione il cui abbonamento (o quello dello studio) è scaduto o non pagato."
          },
          "403": {
            "description": "MATTERS_NOT_ENABLED, FORBIDDEN (non titolare), oppure EMAIL_NOT_VERIFIED: una sessione della dashboard il cui indirizzo e-mail non è confermato, o una chiave API di un tale account.",
            "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": [
          "Pubblico"
        ],
        "operationId": "health",
        "summary": "Controllo dello stato",
        "description": "Sempre 200 finché l'API risponde. Con deep=1 (o true) la risposta aggiunge dependencies: se ciascun servizio a valle è configurato e, per eIDAS, una verifica in tempo reale del servizio di marca temporale. Viene verificato solo eIDAS; gli altri riportano solo la configurazione. status diventa degraded quando eIDAS è configurato e la sua verifica non riesce. Una verifica che non risponde in tempo viene segnalata in dependencies (probe_timeout, oppure eidas.error timeout), mai come errore di /health. deep=1 è soggetto a un limite di frequenza per IP del client (10 al minuto): oltre tale soglia la richiesta restituisce 429 RATE_LIMITED con Retry-After.",
        "security": [],
        "parameters": [
          {
            "name": "deep",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "1",
                "true"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Stato del servizio.",
            "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": "Millisecondi trascorsi dall'avvio dell'istanza che risponde."
                    },
                    "version": {
                      "type": "string"
                    },
                    "dependencies": {
                      "type": "object",
                      "properties": {
                        "eidas": {
                          "type": "object",
                          "properties": {
                            "configured": {
                              "type": "boolean"
                            },
                            "healthy": {
                              "type": "boolean",
                              "description": "null quando non configurato.",
                              "nullable": true
                            },
                            "error": {
                              "type": "string",
                              "description": "Codice fisso quando la verifica eIDAS non ha segnalato uno stato integro: timeout (nessuna risposta entro 15 s), unavailable (il servizio di marca temporale ha risposto di non essere integro), error (non è stato possibile eseguire la verifica). I dettagli vengono registrati lato server, mai restituiti.",
                              "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": "Solo quando il controllo delle dipendenze stesso non è riuscito: probe_timeout quando non ha risposto entro 20 s, altrimenti probe_failed. La risposta resta 200."
                        }
                      },
                      "description": "Solo con deep=1."
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/ledger/{hash}": {
      "get": {
        "tags": [
          "Pubblico"
        ],
        "operationId": "ledgerLookup",
        "summary": "Cercare l'hash di un manifest",
        "description": "Verifica se ProofSnap ha registrato l'emissione di un pacchetto il cui manifest.json ha questo SHA-256. Acquisizioni, certificati, confronti e oscuramenti vengono registrati al momento dell'emissione, con l'hash del manifest consegnato (dopo il passaggio della validazione temporale elettronica qualificata); i pacchetti emessi prima dell'esistenza del registro di emissione non vi compaiono. issued: false significa soltanto che non esiste alcuna registrazione.",
        "security": [],
        "parameters": [
          {
            "name": "hash",
            "in": "path",
            "required": true,
            "description": "SHA-256 di manifest.json, 64 caratteri esadecimali, maiuscoli o minuscoli, con prefisso facoltativo sha256:.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Risultato della ricerca.",
            "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": "Assente quando issued è false."
                    },
                    "type": {
                      "type": "string",
                      "enum": [
                        "capture",
                        "certify",
                        "compare",
                        "redaction"
                      ],
                      "nullable": true,
                      "description": "Assente quando issued è false."
                    },
                    "eidas": {
                      "type": "boolean",
                      "nullable": true,
                      "description": "Se il pacchetto emesso contiene una validazione temporale elettronica qualificata eIDAS (manifest.json.tsr). Assente quando issued è false; null per i record scritti prima del 2026-10-01."
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "NOT_FOUND: l'hash non è composto da 64 caratteri esadecimali.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/matter-verify": {
      "get": {
        "tags": [
          "Pubblico"
        ],
        "operationId": "matterVerify",
        "summary": "Vista pubblica di una pratica condivisa",
        "description": "Dati in sola lettura dietro un link di condivisione di una pratica: dettagli della pratica, il registro della catena di custodia (così la catena di hash può essere ricalcolata) e il sigillo con la sua prova OpenTimestamps e il token eIDAS. Nessun contenuto delle acquisizioni, link di download o registro degli accessi. attested_by.organization è il nome, impostato dal titolare, dello studio a cui appartiene la pratica (una pratica dello studio), oppure null; una pratica personale non indica alcuna organizzazione. custody_log[].actor_org è lo studio a cui apparteneva l'autore quando la voce è stata scritta (registrato con la voce, al di fuori del payload sottoposto a hash), solo su una pratica dello studio; le voci scritte prima che questo dato venisse registrato mostrano null. Limite di frequenza per IP del client (30 richieste ogni 60 secondi per impostazione predefinita).",
        "security": [],
        "parameters": [
          {
            "name": "token",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_-]{16,64}$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Vista della pratica.",
            "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 nei sigilli creati prima del 2026-10-01 senza un token eIDAS."
                            },
                            "eidas_reason": {
                              "type": "string",
                              "enum": [
                                "token_refused",
                                "unavailable",
                                "test_key",
                                "account_limit"
                              ],
                              "description": "Solo quando eidas_status è not_obtained. test_key: il sigillo è stato creato da un chiamante di prova, quindi non è stata richiesta alcuna marca temporale eIDAS. account_limit: l'account non ha ancora una fattura pagata e ha raggiunto il proprio limite eIDAS (20 al giorno UTC, 50 al mese UTC), quindi non è stata richiesta alcuna marca temporale eIDAS."
                            },
                            "eidas_verification_status": {
                              "type": "string",
                              "nullable": true,
                              "description": "verified quando il token ha superato la verifica di ProofSnap al momento della sigillatura."
                            },
                            "eidas_revocation_status": {
                              "type": "string",
                              "nullable": true,
                              "enum": [
                                "good",
                                "revoked_after_gen_time",
                                "unknown",
                                "not_checked",
                                null
                              ],
                              "description": "Stato del certificato della TSA stabilito al momento della sigillatura (non scaricato in tempo reale)."
                            },
                            "eidas_validation": {
                              "type": "object",
                              "nullable": true,
                              "additionalProperties": true,
                              "description": "Il contenuto di eidas_validation.json registrato al momento della sigillatura (catena della TSA, radice fissata, risposta OCSP, CRL, record di verifica) affinché il token in tsr_base64 possa essere verificato offline. null nei sigilli meno recenti."
                            }
                          },
                          "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": "Sempre null."
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "NOT_FOUND: token non valido, sconosciuto o revocato.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/captures/export-jobs": {
      "post": {
        "tags": [
          "Acquisizioni"
        ],
        "operationId": "startCaptureExportJob",
        "summary": "Esportare tutte le acquisizioni in un unico download (job asincrono)",
        "description": "Avvia un job che raccoglie tutte le Sue acquisizioni in un unico download, suddiviso in parti di circa 2 GB al massimo. Ogni parte è un normale ZIP: README.txt (in inglese) più README.<language>.txt (lo stesso testo tradotto, quando language non è en; entrambi UTF-8 con byte order mark e terminazioni di riga CRLF; la riga 1 di ciascuno rimanda all'altro), captures.csv (UTF-8 con byte order mark, CRLF; ogni acquisizione dell'esportazione con le colonne member (l'email dell'autore in un'esportazione dello studio, vuota nella propria esportazione), capture_id, created_at (quando è stata ricevuta la richiesta), captured_at (il momento dell'acquisizione; vuoto per un'acquisizione fallita), url, status (completed, failed o unavailable), export_part, file, ots_file (captures/<capture_id>.upgraded.ots quando quella parte contiene la prova, altrimenti vuoto), test_mode (true per un'acquisizione eseguita con una chiave di prova: con filigrana, non costituisce prova) e note (perché un pacchetto non è incluso, anche per ogni riga unavailable); lo stesso elenco in ogni parte, e quando le parti non concordano fa fede l'elenco della parte più alta) e captures/<capture_id>.zip, il pacchetto di prove sigillato originale byte per byte, più captures/<capture_id>.upgraded.ots quando è memorizzata una prova OpenTimestamps aggiornata. Ogni pacchetto interno si verifica autonomamente con il suo verification/verify.sh o verify.ps1. Un pacchetto più grande di 2 GB non viene incluso (elencato come unavailable con una nota; scaricarlo separatamente con GET /captures/{capture_id}). Le acquisizioni fallite sono elencate solo in captures.csv; le acquisizioni nel cestino e quelle cancellate non vengono esportate. Le voci di ogni parte sono datate con l'ora dell'esportazione (UTC, anche come campo timestamp esteso). Interrogare GET /captures/export-jobs/{job_id} finché status è ready. **Riutilizzo:** quando l'ultima esportazione è pronta, ha lo stesso ambito (e la stessa org) e la stessa lingua, le resta almeno un giorno della finestra di download, i Suoi diritti sono invariati e nessuna acquisizione è stata aggiunta, completata, spostata nel cestino, ripristinata o cancellata da quando è stata pianificata, la richiesta restituisce 200 con quel job (reused: true): non viene ricostruito nulla e non viene consumata alcuna unità giornaliera. Un'esportazione pronta più recente con lo stesso ambito sostituisce la precedente (il job precedente diventa expired, expired_reason superseded). Chi può avviarla: una chiave di produzione o la sessione della dashboard, su tutti i piani compresi Sandbox, Archive only e le chiavi in sola lettura (Website Archive). Una chiave di prova riceve 403 FORBIDDEN (l'esportazione contiene acquisizioni di produzione), e GET /captures/export-jobs/{job_id} restituisce 404 EXPORT_JOB_NOT_FOUND a una chiave di prova per ogni job. La propria esportazione (scope own) contiene le acquisizioni personali e quelle eseguite per lo studio attuale o per uno studio di cui si è titolari, anche tramite una chiave mentre quello studio è un archivio dello studio attivo (le acquisizioni delle altre postazioni di un archivio si raggiungono solo tramite la sessione della dashboard). scope company (solo il titolare dello studio, anche per uno studio cessato entro la sua finestra di download; un archivio dello studio attivo solo tramite la sessione della dashboard) esporta le acquisizioni di ogni postazione eseguite per lo studio più le proprie, con l'email dell'autore nella colonna member. Un job in esecuzione per account (409 EXPORT_JOB_RUNNING lo indica), 5 job avviati per account e giorno UTC (un job fallito restituisce la sua unità, uno riutilizzato non ne consuma), il limite di frequenza al minuto del Suo piano. Al massimo le 20 000 acquisizioni più recenti per job (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": "Con scope company: uno degli studi di cui si è titolari (attuale o ultimo), quando si è titolari di entrambi."
                  },
                  "language": {
                    "type": "string",
                    "default": "en",
                    "description": "Lingua della traduzione del README: README.<language>.txt (la traduzione completa) accanto al README.txt in inglese in ogni parte; en fornisce solo README.txt. Uno tra: 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. Maiuscole/minuscole, trattino e regione sono tollerati (\"PT-BR\" corrisponde a pt_BR); \"pt\" e \"zh\" da soli e qualsiasi altro valore restituiscono 400 MALFORMED_REQUEST. Un'esportazione pronta viene riutilizzata solo per la stessa lingua. La dashboard invia la lingua della propria interfaccia."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "L'esportazione pronta è ancora attuale e viene riutilizzata: nessun nuovo job, nessuna unità giornaliera. Leggerne i link con 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": "Il job è stato avviato.",
            "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: il corpo non è un oggetto JSON, oppure scope / org non è valido.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "FORBIDDEN: la richiesta è stata effettuata con una chiave di prova, oppure con scope company da una persona che non è titolare di uno studio che si trova entro la propria finestra di download.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "EXPORT_JOB_RUNNING: un job dell'account è ancora in elaborazione; details.job_id e details.poll_path lo identificano.",
            "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": [
          "Acquisizioni"
        ],
        "operationId": "getCaptureExportJob",
        "summary": "Stato e link di download di un job di esportazione delle acquisizioni",
        "description": "Lo stato e l'avanzamento del job; una volta pronto, un link prefirmato per parte. Un link è valido fino a url_expires_at: al massimo 24 ore, mai oltre download_expires_at e mai oltre le credenziali che lo hanno firmato (può essere meno di un'ora); interrogare di nuovo dopo url_expires_at per ottenere link nuovi. download_expires_at cade 7 giorni dopo la mezzanotte UTC precedente all'avvio della prima parte del job (da 6 a 7 giorni dopo che il job è diventato pronto); dopo tale termine le parti vengono eliminate. Il job perde anticipatamente il download (status expired, le parti eliminate, expired_reason) quando un'esportazione più recente con lo stesso ambito lo ha sostituito, un'acquisizione che contiene è stata cancellata, i dati del suo studio sono stati eliminati, oppure i Suoi diritti non lo coprono più (verificato a ogni lettura di un job pronto). Un job che non segnala alcun avanzamento per 30 minuti viene chiuso come failed da questa chiamata e quanto ha scritto viene eliminato. La prima risposta con link in un'ora viene registrata nel log di audit (capture.export_job.links_issued). Solo l'account che ha avviato il job lo vede; un'esportazione dello studio che contiene un archivio dello studio solo tramite la sessione della dashboard.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "job_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^cexp_[A-Za-z0-9]{16}$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Il job.",
            "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 mentre vengono scritti il piano e le parti; ready con i link di download; failed (avviare un nuovo job, l'unità del giorno è stata restituita); expired quando la finestra di download è terminata o il download è stato ritirato (expired_reason; le parti vengono eliminate)."
                    },
                    "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 lingua del README del job (README.<language>.txt oltre a README.txt; en solo per README.txt)."
                    },
                    "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 quando l'account ha più di 20 000 acquisizioni esportabili: il job contiene le 20 000 più recenti."
                    },
                    "counts": {
                      "type": "object",
                      "nullable": true,
                      "description": "Impostato quando il job è pronto.",
                      "properties": {
                        "captures": {
                          "type": "integer",
                          "description": "Righe di captures.csv."
                        },
                        "packaged": {
                          "type": "integer",
                          "description": "Pacchetti contenuti nelle parti."
                        },
                        "failed": {
                          "type": "integer",
                          "description": "Acquisizioni non riuscite (nessun pacchetto, elencate solo in captures.csv)."
                        },
                        "unavailable": {
                          "type": "integer",
                          "description": "Pacchetti non inclusi: non è stato possibile leggerli durante l'esportazione, oppure superano 2 GB (captures.csv indica quali)."
                        }
                      }
                    },
                    "parts": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "part",
                          "bytes",
                          "count",
                          "download_url"
                        ],
                        "properties": {
                          "part": {
                            "type": "integer",
                            "minimum": 1,
                            "example": 1,
                            "description": "Numero della parte, da 1 al numero di parti."
                          },
                          "bytes": {
                            "type": "integer",
                            "example": 1834201112,
                            "description": "Dimensione in byte dello ZIP della parte."
                          },
                          "count": {
                            "type": "integer",
                            "example": 412,
                            "description": "Pacchetti di acquisizione di questa parte."
                          },
                          "filename": {
                            "type": "string",
                            "example": "proofsnap-captures-2026-10-07T1430Z-part-1-of-3.zip",
                            "description": "Nome del file con cui viene salvato il download (Content-Disposition): proofsnap-captures-<data e ora UTC di avvio del job>, con -part-N-of-M quando ci sono più parti."
                          },
                          "download_url": {
                            "type": "string",
                            "format": "uri",
                            "nullable": true,
                            "description": "GET prefirmato dello ZIP della parte, valido fino a url_expires_at. null quando il job è scaduto."
                          }
                        }
                      }
                    },
                    "download_expires_at": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true,
                      "description": "Fine della finestra di download: 7 giorni dopo la mezzanotte UTC precedente l'avvio della prima parte (dopo tale termine il ciclo di vita dello storage elimina le parti)."
                    },
                    "url_expires_at": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true,
                      "description": "Quando i valori download_url di questa risposta smettono di funzionare: al massimo 24 ore, mai oltre download_expires_at, mai oltre le credenziali di firma. Interrogare di nuovo dopo tale momento per ottenere link nuovi."
                    },
                    "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": "Impostato quando status è expired: window_ended (download_expires_at è trascorso), superseded (un'esportazione più recente con lo stesso ambito), rights_changed (i Suoi diritti non coprono più il job), capture_erased (un'acquisizione che contiene è stata cancellata), company_deleted (i dati del suo studio sono stati eliminati)."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "EXPORT_JOB_NOT_FOUND: job inesistente, job di un altro account, record scaduto (8 giorni dopo la sua conclusione) oppure richiesta effettuata con una chiave di prova.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/AuthUnavailable"
          }
        }
      }
    },
    "/webhook-subscriptions": {
      "get": {
        "tags": [
          "Webhook"
        ],
        "operationId": "listWebhookSubscriptions",
        "summary": "Elencare le sottoscrizioni webhook",
        "description": "Le sottoscrizioni webhook dell'account, dalla più recente, attive e disattivate, senza secret. Chiave API o sessione della dashboard; può usarla anche una chiave in sola lettura (Website Archive).",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Sottoscrizioni.",
            "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": [
          "Webhook"
        ],
        "operationId": "createWebhookSubscription",
        "summary": "Sottoscrivere gli eventi dell'account",
        "description": "Sottoscrive un endpoint https agli eventi dell'account: ogni evento corrispondente dell'account gli viene inviato con POST, firmato con il secret proprio della sottoscrizione (restituito una sola volta in questa risposta). Richiede un indirizzo email confermato (403 EMAIL_NOT_VERIFIED). Consentito anche con un abbonamento con pagamento scaduto (non viene addebitato nulla). Limiti di sottoscrizioni ATTIVE per account: 25 su developer, startup, growth, custom, legal_solo e legal_seats, 10 su website_archive, 0 su archive_only e sandbox (409 WEBHOOK_SUBSCRIPTION_LIMIT). L'URL segue le regole di webhook_url (https, nessuna credenziale, nessun indirizzo privato o riservato; controllato di nuovo a ogni consegna). Idempotency-Key è supportata: un nuovo tentativo con la stessa chiave e lo stesso corpo entro 24 ore riceve la prima risposta, secret incluso, invece di una seconda sottoscrizione. Chiave API o sessione della dashboard; può usarla anche una chiave in sola lettura (Website Archive).",
        "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": "Testo semplice, al massimo 200 caratteri."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Creato. `secret` viene mostrato solo qui.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/XRequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookSubscriptionCreated"
                }
              }
            }
          },
          "400": {
            "description": "MALFORMED_REQUEST (events, filters, description, campo sconosciuto), MISSING_FIELD (url) o 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 punta a un indirizzo bloccato) oppure 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": [
          "Webhook"
        ],
        "operationId": "getWebhookSubscription",
        "summary": "Ottenere una sottoscrizione webhook",
        "description": "Una sottoscrizione con il relativo stato di consegna, senza il secret. Chiave API o sessione della dashboard; può essere utilizzata anche una chiave di sola lettura (Website Archive).",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "La sottoscrizione.",
            "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: sconosciuta, eliminata o di un altro account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/AuthUnavailable"
          }
        }
      },
      "patch": {
        "tags": [
          "Webhook"
        ],
        "operationId": "updateWebhookSubscription",
        "summary": "Modificare o riattivare una sottoscrizione webhook",
        "description": "Modifica eventi, filtri (null o {} li cancella) o descrizione, oppure riattiva una sottoscrizione disattivata con status \"active\" (azzera i contatori degli errori; soggetto al limite delle sottoscrizioni attive). L'URL non può essere modificato: creare una nuova sottoscrizione. Richiede un indirizzo e-mail confermato. Chiave API o sessione della dashboard; può essere utilizzata anche una chiave di sola lettura (Website Archive).",
        "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": "La sottoscrizione modificata.",
            "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: sconosciuta, eliminata o di un altro account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "WEBHOOK_SUBSCRIPTION_LIMIT (riattivazione oltre il limite).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/AuthUnavailable"
          }
        }
      },
      "delete": {
        "tags": [
          "Webhook"
        ],
        "operationId": "deleteWebhookSubscription",
        "summary": "Eliminare una sottoscrizione webhook",
        "description": "Elimina la sottoscrizione, il relativo secret e le consegne in coda. 404 per una sottoscrizione sconosciuta o già eliminata (considerarla rimossa). Chiave API o sessione della dashboard; può essere utilizzata anche una chiave di sola lettura (Website Archive).",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "responses": {
          "204": {
            "description": "Eliminato.",
            "headers": {
              "X-Request-ID": {
                "$ref": "#/components/headers/XRequestId"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "WEBHOOK_SUBSCRIPTION_NOT_FOUND: sconosciuta, eliminata o di un altro account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/AuthUnavailable"
          }
        }
      }
    },
    "/events": {
      "get": {
        "tags": [
          "Webhook"
        ],
        "operationId": "listEvents",
        "summary": "Registro degli eventi",
        "description": "Tutti gli eventi emessi per l'account negli ultimi 30 giorni, che una sottoscrizione li abbia ricevuti o meno, dal più recente. Una chiave API vede gli eventi dell'ambiente in cui opera (una chiave di produzione gli eventi di produzione, una chiave di prova gli eventi di prova); una sessione della dashboard vede gli eventi di produzione. Utilizzare questa route per recuperare eventi arretrati o per eseguire il polling invece di sottoscriversi. Chiave API o sessione della dashboard; può essere utilizzata anche una chiave di sola lettura (Website Archive).",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "type",
            "in": "query",
            "required": false,
            "description": "Solo questo tipo di evento.",
            "schema": {
              "$ref": "#/components/schemas/WebhookEventType"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Da 1 a 100, valore predefinito 25.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          },
          {
            "name": "starting_after",
            "in": "query",
            "required": false,
            "description": "Un ID evento: la pagina inizia dopo di esso (passare next_cursor). Un ID di un altro account o ambiente, o più vecchio di 30 giorni, restituisce 400.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Una pagina di eventi.",
            "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": "Da passare come starting_after per la pagina successiva; null nell'ultima pagina."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "MALFORMED_REQUEST (type, limit o starting_after).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "503": {
            "$ref": "#/components/responses/AuthUnavailable"
          }
        }
      }
    },
    "/whoami": {
      "get": {
        "tags": [
          "Webhook"
        ],
        "operationId": "whoami",
        "summary": "Chi sono",
        "description": "Che cosa sono le credenziali: piano, ambiente, se la chiave API è in sola lettura, e id, nome e prefisso della chiave (null per una sessione della dashboard). Le integrazioni lo usano per testare una connessione e per etichettarla. Nessun indirizzo email e nessun id dell'account. Chiave API o sessione della dashboard; può usarlo anche una chiave in sola lettura (Website Archive).",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Le credenziali.",
            "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 per una chiave API di un account Website Archive (può leggere e sottoscrivere, non creare acquisizioni)."
                    },
                    "env": {
                      "type": "string",
                      "enum": [
                        "live",
                        "test"
                      ],
                      "description": "L'ambiente in cui operano le credenziali."
                    },
                    "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": [
          "Acquisizioni"
        ],
        "operationId": "listCaptureEvents",
        "summary": "Catena degli eventi di acquisizione (eliminazioni e blocchi per controversia legale)",
        "description": "Il registro con alterazioni rilevabili di ciò che è accaduto alle acquisizioni dell'account: cestino, ripristino, eliminazione definitiva (erase), svuotamento del cestino, eliminazione per scadenza della conservazione, eliminazione insieme all'account o allo studio, blocco per controversia legale attivato e revocato (una revoca da parte del sistema dopo la fine dell'abbonamento ha actor `system` e detail `subscription_ended`), il risultato S3 Object Lock e l'importazione di un pacchetto di prove (type `import`, detail = il tipo di pacchetto). Dal più vecchio. La catena sopravvive alle acquisizioni e ai dati dell'account: contiene solo ID e hash dei manifest, nessun contenuto delle acquisizioni e nessun testo libero.\n\nCome verificarla: (1) per ogni evento calcolare lo SHA-256 sul JSON canonico dei campi sottoposti a hash (verification.hashed_fields; chiavi ordinate, nessuno spazio, valori assenti come null) e confrontarlo con `hash`; (2) controllare che ogni `prev_hash` sia uguale all'`hash` dell'evento precedente (64 zeri per seq 1) e che seq non abbia lacune; (3) per un ancoraggio, controllare che `head_hash` sia uguale all'`hash` dell'evento `to_seq`, scrivere `ots_proof_base64` in un file (decodificato da base64) ed eseguire `ots verify -d <head_hash> anchor.ots` (client OpenTimestamps): dimostra che la catena fino a quell'evento esisteva non oltre il blocco Bitcoin. Gli ancoraggi vengono creati una volta al giorno per ogni account con eventi di un giorno precedente.\n\nChiave API o sessione della dashboard, sul proprio account; `scope=company` fornisce al titolare di uno studio la catena delle acquisizioni dello studio (`org` ne sceglie uno quando il chiamante è titolare di più studi; uno studio conservato come archivio dello studio solo tramite la sessione della dashboard). Una chiave di prova riceve 403 FORBIDDEN: la catena copre le acquisizioni di produzione e non può essere filtrata senza spezzarla.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Da 1 a 500, valore predefinito 100.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "default": 100
            }
          },
          {
            "name": "starting_after",
            "in": "query",
            "required": false,
            "description": "Un seq di evento: la pagina inizia dopo di esso (passare 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'id dello studio.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Una pagina della catena.",
            "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 o scope non valido.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "FORBIDDEN: la richiesta è stata effettuata con una chiave di prova, oppure con scope=company da una persona che non è titolare dello studio.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/AuthUnavailable"
          }
        }
      }
    },
    "/legal-holds": {
      "post": {
        "tags": [
          "Acquisizioni"
        ],
        "operationId": "bulkLegalHold",
        "summary": "Attivare o revocare in massa i blocchi per controversia legale",
        "description": "Attiva (enabled true) o revoca (false) il blocco per controversia legale di molte acquisizioni in una volta. selector contiene esattamente uno dei seguenti elementi: `capture_ids` (da 1 a 500 ID), `matter_id` (le acquisizioni di una pratica che il chiamante può aprire), `host` (le acquisizioni del chiamante relative a quel nome host) oppure `schedule_id` (le acquisizioni del chiamante eseguite da una delle sue pianificazioni). Ogni acquisizione passa per le regole di PATCH /captures/{capture_id} con l'azione legal_hold e ottiene un proprio risultato: applied, unchanged (già in quello stato) oppure rifiutata con il codice di errore (ad esempio FORBIDDEN, CAPTURE_IN_TRASH, CAPTURE_PROCESSING, LEGAL_HOLD_OWNER_ONLY, LEGAL_HOLD_MATTER, LEGAL_HOLD_SOURCE per un oscuramento la cui acquisizione di origine è ancora sotto blocco, CAPTURE_NOT_FOUND, LEGAL_HOLD_NOT_AVAILABLE per un'acquisizione in modalità di prova). Gli oscuramenti ricavati da ciascuna acquisizione ne seguono il blocco, anche per un'acquisizione segnalata come unchanged; se l'operazione non si conclude entro la richiesta, viene completata entro pochi minuti. Attivare il blocco richiede un piano a pagamento: il Sandbox gratuito riceve 403 LEGAL_HOLD_NOT_AVAILABLE (details.reason \"plan\") per l'intera richiesta; la revoca funziona con tutti i piani. Un blocco attivato qui ha legal_hold_source \"bulk\"; revocarlo in massa o uno alla volta funziona allo stesso modo. Al massimo 500 acquisizioni per richiesta: una pratica, un host o una pianificazione che ne contiene di più viene troncata (`truncated: true`, inviare di nuovo la richiesta). Il legal hold S3 Object Lock delle prime 25 acquisizioni modificate viene applicato durante la richiesta, il resto entro pochi minuti (`s3_legal_hold.status` pending). Costo: un'unità del limite di frequenza ogni 10 acquisizioni (almeno una); una richiesta che richiede più di quanto consente una finestra restituisce 400 RATE_LIMIT_COST_TOO_HIGH. Chiave API o sessione della dashboard; su Website Archive la chiave API è in sola lettura (403 PLAN_API_READ_ONLY). Una sessione della dashboard o una chiave il cui indirizzo email non è confermato riceve 403 EMAIL_NOT_VERIFIED. Non bloccato da un abbonamento con pagamento scaduto.",
        "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": "Testo semplice conservato con ogni blocco per controversia legale attivato (non con una revoca); mai incluso nelle catene di hash."
                  },
                  "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": "Risultati per singola acquisizione.",
            "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": "Il codice di errore di un rifiuto."
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "MALFORMED_REQUEST (corpo, selector, più di 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 (attivazione senza un piano a pagamento, details.reason plan).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "MATTER_NOT_FOUND (matter_id che il chiamante non può aprire), 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": [
          "Importazione di pacchetti"
        ],
        "operationId": "createImport",
        "summary": "Avviare un'importazione di pacchetti",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "description": "Crea un'importazione e ne avvia il caricamento. Inviare la dimensione esatta dello ZIP in byte; la risposta contiene un URL PUT prefirmato per ogni parte da 32 MiB (l'ultima più piccola), valido per 1 ora (`upload.expires_at`; mai più a lungo delle credenziali di firma del server). Inviare con PUT i byte di ciascuna parte al relativo URL, conservare l'header `ETag` di ogni risposta, quindi chiamare POST /imports/{import_id}/complete entro 24 ore. Con `matter_id` l'acquisizione accettata viene aggiunta a quella pratica: occorre poter aggiungere acquisizioni a essa in quel momento (una pratica aperta a cui si ha accesso; l'importazione di un membro dello studio solo verso una pratica di quello studio). Ogni creazione viene conteggiata nel limite giornaliero di 50.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "filename",
                  "size_bytes"
                ],
                "additionalProperties": false,
                "properties": {
                  "filename": {
                    "type": "string",
                    "maxLength": 200,
                    "description": "Il nome del file da mostrare (testo semplice: senza caratteri di controllo, invisibili o di direzione del testo)."
                  },
                  "size_bytes": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 2147483648,
                    "description": "Dimensione esatta dello ZIP in byte, al massimo 2 GiB."
                  },
                  "matter_id": {
                    "type": "string",
                    "description": "Facoltativo: aggiunge l'acquisizione accettata a questa pratica."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Importazione creata; caricare le parti.",
            "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 prefirmato: inviare con PUT esattamente size_bytes byte (l'URL firma il Content-Length), quindi leggere l'header di risposta ETag. Non serve nessun altro header."
                              },
                              "size_bytes": {
                                "type": "integer",
                                "description": "La lunghezza esatta in byte di questa parte: part_size per ogni parte tranne l'ultima, il resto per l'ultima (la parte n inizia a (n - 1) x part_size)."
                              }
                            }
                          }
                        },
                        "expires_at": {
                          "type": "string",
                          "format": "date-time"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "MALFORMED_REQUEST (corpo, filename, size_bytes, matter_id, campo sconosciuto).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "PAYMENT_REQUIRED: l'abbonamento è scaduto o non pagato.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "IMPORT_NOT_ALLOWED (details.reason plan: solo piani Legal; live_credentials_required: una chiave di prova, una sessione senza fatturazione collegata o la Sandbox), EMAIL_NOT_VERIFIED, FORBIDDEN (importazione di un membro dello studio verso una pratica di un altro studio).",
            "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 importazioni in corso; 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 superiore a 2 GiB (details.max_bytes).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "RATE_LIMITED: il limite al minuto, oppure il limite giornaliero di 50 importazioni (details.action import_create, details.daily_limit).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/AuthUnavailable"
          }
        }
      },
      "get": {
        "tags": [
          "Importazione di pacchetti"
        ],
        "operationId": "listImports",
        "summary": "Elencare le proprie importazioni",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "description": "Le Sue importazioni, dalla più recente. Le importazioni definitive vengono conservate per 30 giorni (l'acquisizione di un'importazione accettata resta). Un caricamento oltre le sue 24 ore viene mostrato come expired.",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 25
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "next_cursor della pagina precedente."
          }
        ],
        "responses": {
          "200": {
            "description": "Una pagina di importazioni.",
            "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": "ID dell'importazione (imp_ seguito da 16 a 24 lettere e cifre). Un ID malformato restituisce 404 IMPORT_NOT_FOUND.",
          "schema": {
            "type": "string",
            "pattern": "^imp_[A-Za-z0-9]{16,24}$"
          }
        }
      ],
      "get": {
        "tags": [
          "Importazione di pacchetti"
        ],
        "operationId": "getImport",
        "summary": "Ottenere un'importazione",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "description": "L'importazione e, una volta definitivo, il suo risultato. Interrogarla dopo POST /imports/{import_id}/complete; la verifica richiede di solito pochi secondi, un pacchetto di 2 GiB fino a qualche minuto. Con ?upload=true su un caricamento ancora in corso la risposta aggiunge upload: nuovi URL prefirmati (parts) per le parti che lo storage non ha ancora e le parti già ricevute (received: part_number, etag, size_bytes; null quando non è stato possibile elencarle, e in tal caso ogni parte viene prefirmata di nuovo), oppure completed: true quando il caricamento era già stato completato (inviare di nuovo complete). Usarlo per riprendere dopo un'interruzione di rete, un URL di parte scaduto (durano al massimo 1 ora, la finestra di caricamento 24 ore) o un ricaricamento della pagina. ?upload=true costa un'unità del limite di frequenza al minuto.",
        "responses": {
          "200": {
            "description": "L'importazione.",
            "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 (anche per l'importazione di un altro account).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "RATE_LIMITED (solo con ?upload=true).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/AuthUnavailable"
          }
        },
        "parameters": [
          {
            "name": "upload",
            "in": "query",
            "required": false,
            "description": "true (o 1): aggiunge nuovi URL delle parti e le parti ricevute di un caricamento in corso.",
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "1"
              ]
            }
          }
        ]
      },
      "delete": {
        "tags": [
          "Importazione di pacchetti"
        ],
        "operationId": "cancelImport",
        "summary": "Annullare un caricamento",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "description": "Annulla un'importazione il cui caricamento è ancora in corso: il caricamento viene interrotto, le parti eliminate e l'importazione rimossa (in seguito GET restituisce 404).",
        "responses": {
          "200": {
            "description": "Annullato.",
            "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: l'importazione non è più in caricamento (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": "ID dell'importazione (imp_ seguito da 16 a 24 lettere e cifre). Un ID malformato restituisce 404 IMPORT_NOT_FOUND.",
          "schema": {
            "type": "string",
            "pattern": "^imp_[A-Za-z0-9]{16,24}$"
          }
        }
      ],
      "post": {
        "tags": [
          "Importazione di pacchetti"
        ],
        "operationId": "completeImport",
        "summary": "Completare il caricamento e verificare",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "description": "Completa il caricamento e avvia la verifica. parts è facoltativo: inviare ogni numero di parte (da 1 a N, in ordine) con l'ETag restituito dal PUT della parte, oppure ometterlo e il server legge le parti caricate dallo storage (utile in un browser che non può leggere l'header ETag). Verifica che la dimensione memorizzata sia uguale a size_bytes. Interrogare GET /imports/{import_id}. Un elenco di parti rifiutato dallo storage, o una parte non ancora caricata (details.missing_parts), restituisce 400 e il caricamento resta aperto: caricare ciò che manca (GET /imports/{import_id}?upload=true fornisce nuovi URL) e inviare di nuovo la richiesta. Inviare di nuovo complete dopo la perdita di una risposta è sicuro: un complete ancora in esecuzione restituisce 409, uno interrotto viene ripreso dopo 2 minuti e un caricamento già completato prosegue con la verifica. Una dimensione diversa da size_bytes comporta l'eliminazione del caricamento (422 IMPORT_SIZE_MISMATCH; l'importazione viene rifiutata, crearne una nuova).",
        "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'header di risposta ETag del PUT della parte, con o senza virgolette."
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Verifica avviata.",
            "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 (parti non in ordine o non corrispondenti alle parti caricate; oppure, senza parts, una parte non ancora caricata: 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: l'importazione non è più in caricamento (details.status, ad esempio verifying o 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": "Inviare la chiave API come `Authorization: Bearer ps_live_...` (il nome dello schema non distingue tra maiuscole e minuscole). Un header mancante o malformato restituisce 401 MISSING_API_KEY. Una chiave sconosciuta, revocata o scaduta, oppure un account senza accesso all'API, restituisce 401 INVALID_API_KEY con `WWW-Authenticate: Bearer realm=\"ProofSnap API\", error=\"invalid_token\"`. Il 403 è riservato a credenziali valide che non sono autorizzate a fare qualcosa (FORBIDDEN, limiti del piano). Una chiave `ps_live_` di un account senza un account di fatturazione collegato (cliente Stripe) funziona come chiave di prova: le acquisizioni hanno la filigrana e non vengono mai addebitate, e include_eidas viene rifiutato con 403 EIDAS_REQUIRES_LIVE_KEY. Il gateway memorizza nella cache la decisione relativa a una chiave per un massimo di 60 secondi, quindi una chiave revocata può continuare a funzionare per un massimo di 60 secondi. Se le credenziali non possono essere verificate a causa di un errore temporaneo del backend, il gateway restituisce invece 503 SERVICE_UNAVAILABLE; questa risposta non viene memorizzata nella cache, quindi riprovare con backoff. La challenge `WWW-Authenticate` viene restituita invariata su un 401 del gateway (la verifica delle credenziali); su un 401 di un handler di route il gateway consegna la stessa challenge rinominata in `x-amzn-Remapped-WWW-Authenticate`."
      }
    },
    "headers": {
      "XRequestId": {
        "description": "ID della richiesta, utile anche nelle richieste di assistenza. Inviato nelle risposte di successo; i corpi di errore lo riportano come request_id.",
        "schema": {
          "type": "string"
        }
      },
      "RateLimitLimit": {
        "description": "Richieste consentite nella finestra corrente di 60 secondi. Header legacy, mantenuto accanto a RateLimit / RateLimit-Policy.",
        "schema": {
          "type": "integer"
        }
      },
      "RateLimitRemaining": {
        "description": "Richieste rimanenti nella finestra corrente. Header legacy, mantenuto accanto a RateLimit / RateLimit-Policy.",
        "schema": {
          "type": "integer"
        }
      },
      "RateLimitReset": {
        "description": "Ora ISO 8601 in cui termina la finestra corrente. Header storico, mantenuto accanto a RateLimit / RateLimit-Policy.",
        "schema": {
          "type": "string",
          "format": "date-time"
        }
      },
      "RetryAfter": {
        "description": "Secondi di attesa prima di riprovare.",
        "schema": {
          "type": "integer"
        }
      },
      "QuotaLimit": {
        "description": "Acquisizioni incluse nel piano al mese (chiavi di produzione sui piani con quota mensile).",
        "schema": {
          "type": "integer"
        }
      },
      "QuotaRemaining": {
        "description": "Acquisizioni incluse rimanenti in questo mese.",
        "schema": {
          "type": "integer"
        }
      },
      "QuotaUsed": {
        "description": "Acquisizioni completate in questo mese.",
        "schema": {
          "type": "integer"
        }
      },
      "QuotaCap": {
        "description": "Il massimo mensile che si applica al chiamante (valore predefinito del piano, tetto proprio o limite dello studio).",
        "schema": {
          "type": "integer"
        }
      },
      "QuotaOrgLimit": {
        "description": "Solo membri dello studio: il limite mensile per l'intero studio, se il titolare ne ha impostato uno.",
        "schema": {
          "type": "integer"
        }
      },
      "QuotaOrgUsed": {
        "description": "Solo membri dello studio: acquisizioni ammesse per l'intero studio in questo mese.",
        "schema": {
          "type": "integer"
        }
      },
      "SandboxLimit": {
        "description": "Sandbox (sessione della dashboard senza piano): quota complessiva di acquisizioni di prova con filigrana (50).",
        "schema": {
          "type": "integer"
        }
      },
      "SandboxRemaining": {
        "description": "Sandbox: quota rimanente.",
        "schema": {
          "type": "integer"
        }
      },
      "SandboxUsed": {
        "description": "Sandbox: quota utilizzata.",
        "schema": {
          "type": "integer"
        }
      },
      "IdempotentReplay": {
        "description": "\"true\" quando questa risposta è la ripetizione di una richiesta precedente con la stessa Idempotency-Key.",
        "schema": {
          "type": "string",
          "enum": [
            "true"
          ]
        }
      },
      "WWWAuthenticate": {
        "description": "Challenge RFC 6750 inviata con ogni 401. Per un 401 restituito da un handler di route anziché dal gateway, API Gateway lo consegna rinominato in `x-amzn-Remapped-WWW-Authenticate`. Una chiave API non valida, revocata o scaduta, oppure un token di sessione errato o revocato (INVALID_API_KEY): `Bearer realm=\"ProofSnap API\", error=\"invalid_token\"`. Un header Authorization mancante o malformato (MISSING_API_KEY): `Bearer realm=\"ProofSnap API\"`.",
        "schema": {
          "type": "string"
        },
        "example": "Bearer realm=\"ProofSnap API\", error=\"invalid_token\""
      },
      "RateLimitPolicy": {
        "description": "Policy del limite di frequenza, secondo la bozza IETF draft-ietf-httpapi-ratelimit-headers (forma structured field; basata sulla bozza, può cambiare con essa): `\"default\";q=<requests per window>;w=60`.",
        "schema": {
          "type": "string"
        },
        "example": "\"default\";q=120;w=60"
      },
      "RateLimit": {
        "description": "Stato attuale del limite di frequenza, secondo l'Internet-Draft IETF draft-ietf-httpapi-ratelimit-headers (basato sulla bozza): `\"default\";r=<requests left>;t=<seconds until the window resets>`. Inviato nelle risposte delle route soggette a limite di frequenza (acquisizioni singole e in lotto, compare, redact, certify-hash) dopo l'esecuzione del limitatore, non solo con 429 (una richiesta rifiutata prima del limitatore, ad esempio per l'autenticazione, non lo contiene).",
        "schema": {
          "type": "string"
        },
        "example": "\"default\";r=117;t=42"
      }
    },
    "parameters": {
      "CaptureId": {
        "name": "capture_id",
        "in": "path",
        "required": true,
        "description": "ID di acquisizione, certificato, confronto, oscuramento o pacchetto importato. Gli ID iniziano con cap_, cert_, cmp_, red_ o imp_. Un ID malformato restituisce 404 CAPTURE_NOT_FOUND.",
        "schema": {
          "type": "string",
          "example": "cap_a1B2c3D4e5F6g7H8"
        }
      },
      "ScheduleId": {
        "name": "schedule_id",
        "in": "path",
        "required": true,
        "description": "ID della pianificazione. Una pianificazione di un altro account restituisce 404 SCHEDULE_NOT_FOUND.",
        "schema": {
          "type": "string",
          "example": "sch_a1B2c3D4e5F6g7H8"
        }
      },
      "MatterId": {
        "name": "matter_id",
        "in": "path",
        "required": true,
        "description": "ID della pratica. Una pratica di cui non si è membri restituisce 404 MATTER_NOT_FOUND.",
        "schema": {
          "type": "string",
          "example": "mat_a1B2c3D4e5F6g7H8"
        }
      },
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": false,
        "description": "Chiave facoltativa del client che rende sicuro un nuovo tentativo. Vengono utilizzati solo i primi 255 caratteri e le chiavi sono associate al proprio account per 24 ore. Un nuovo tentativo con la stessa chiave e lo stesso corpo restituisce il corpo della risposta originale con `Idempotent-Replay: true` (202 per un'acquisizione singola, un lotto o un confronto, 201 per un oscuramento; `status` viene aggiornato allo stato attuale dell'acquisizione, del confronto o dell'oscuramento, e di ogni acquisizione di un lotto); non viene creata né addebitata alcuna nuova acquisizione. La stessa chiave con un corpo diverso restituisce 422 IDEMPOTENCY_KEY_REUSED. Un nuovo tentativo mentre la prima richiesta è ancora in fase di accettazione restituisce 409 IDEMPOTENCY_IN_PROGRESS con l'ID originale in `error.details.capture_id` (il batch_id per un lotto).",
        "schema": {
          "type": "string",
          "maxLength": 255
        }
      },
      "SubscriptionId": {
        "name": "subscription_id",
        "in": "path",
        "required": true,
        "description": "ID della sottoscrizione webhook. Una sottoscrizione di un altro account restituisce 404 WEBHOOK_SUBSCRIPTION_NOT_FOUND.",
        "schema": {
          "type": "string",
          "example": "whs_a1B2c3D4e5F6g7H8"
        }
      }
    },
    "responses": {
      "RateLimited": {
        "description": "RATE_LIMITED: troppe richieste nella finestra corrente di 60 secondi. `error.details.retry_after_seconds` ripete Retry-After. RateLimit riporta r=0 e i secondi mancanti al ripristino della finestra.",
        "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: errore imprevisto del server. Riprovare con backoff esponenziale.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "AuthUnavailable": {
        "description": "SERVICE_UNAVAILABLE: il gateway non ha potuto verificare le credenziali (errore temporaneo del backend). Non memorizzato nella cache: riprovare con backoff.",
        "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: è stato richiesto include_eidas ma la validazione temporale elettronica qualificata non è configurata in questo deployment; riprovare senza include_eidas. SERVICE_UNAVAILABLE: il gateway non ha potuto verificare le credenziali (guasto temporaneo del backend, non memorizzato in cache); riprovare con backoff.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Unauthorized": {
        "description": "401, due codici. MISSING_API_KEY: l'header Authorization è assente o malformato (`WWW-Authenticate: Bearer realm=\"ProofSnap API\"`). INVALID_API_KEY: le credenziali sono state inviate ma non sono valide: chiave API sconosciuta, revocata o scaduta, account senza accesso all'API oppure token di sessione non valido o revocato (`WWW-Authenticate: Bearer realm=\"ProofSnap API\", error=\"invalid_token\"`). Non riprovare con le stesse credenziali. 403 significa che le credenziali sono valide ma non consentono questa operazione (FORBIDDEN, limiti del piano).",
        "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": "Tutti gli errori hanno questa forma. Le risposte di errore contengono l'header `X-Request-ID` con lo stesso valore di `request_id`.\n\nDimensione della richiesta: un corpo della richiesta può essere al massimo di 6 MB (il limite di payload di Lambda; API Gateway stesso si ferma a 10 MB). Un corpo più grande viene rifiutato prima di raggiungere il codice dell'API, quindi quella risposta non ha questa forma.\n\n| HTTP | code | Significato |\n| --- | --- | --- |\n| 400 | `MALFORMED_REQUEST` | Il corpo non è JSON valido (messaggio \"Request body is not valid JSON\") oppure la richiesta non è valida: quando `details.reason` indica quale campo e perché, `message` ripete tale motivo, altrimenti è \"The request is invalid.\" |\n| 400 | `MISSING_FIELD` | Manca un campo obbligatorio. |\n| 400 | `INVALID_URL` | L'URL non è un URL http(s) valido, supera i 2 048 caratteri, oppure un webhook_url non è https. |\n| 400 | `INVALID_HASH` | certify-hash: un hash non è composto da 64 caratteri esadecimali. |\n| 400 | `INVALID_TIMESTAMP_TYPE` | options.timestamp_type non è \"bitcoin\". |\n| 400 | `INVALID_VIEWPORT` | options.viewport non è \"desktop\" né \"mobile\". |\n| 400 | `METADATA_TOO_LARGE` | options.metadata ha più di 10 chiavi, un valore che non è una stringa o un valore di oltre 500 caratteri. |\n| 400 | `INVALID_EVIDENCE_PACK_OPTIONS` | options.evidence_pack non è valido. |\n| 400 | `INVALID_BATCH` | Il campo urls del lotto non è un array da 2 a 50 URL validi. |\n| 400 | `REDACTION_NO_OPS` | L'oscuramento non contiene operazioni, oppure nessuna di esse ha trovato corrispondenze. |\n| 400 | `INVALID_CRON` | Impossibile analizzare cron_expr. |\n| 400 | `INTERVAL_TOO_SHORT` | La cadenza della pianificazione è inferiore al minimo del piano. |\n| 400 | `RATE_LIMIT_COST_TOO_HIGH` | La richiesta necessita di più unità del limite di frequenza di quante ne consenta una finestra; `details.max_per_window`, `details.window_seconds`, `details.requested`. Suddividerla. |\n| 400 | `WEBHOOK_REQUIRES_API_KEY` | È stato inviato un `webhook_url` da una sessione della dashboard. I webhook sono firmati con il secret di firma del webhook di una chiave API, quindi richiedono una chiave API. |\n| 401 | `MISSING_API_KEY` | Header Authorization assente o malformato. `WWW-Authenticate: Bearer realm=\"ProofSnap API\"`. |\n| 401 | `INVALID_API_KEY` | Chiave sconosciuta, revocata o scaduta, nessun accesso all'API, oppure token di sessione non valido o revocato. `WWW-Authenticate: Bearer realm=\"ProofSnap API\", error=\"invalid_token\"`. |\n| 402 | `QUOTA_EXCEEDED` | Raggiunto il massimo mensile o la quota della Sandbox (presente anche nell'`error` di una certificazione in background rifiutata per questo motivo). |\n| 402 | `PAYMENT_REQUIRED` | L'abbonamento di produzione è scaduto o non pagato (presente anche nell'`error` di un'acquisizione pianificata saltata per questo motivo). |\n| 403 | `FORBIDDEN` | La risorsa appartiene a qualcun altro, oppure l'azione è riservata al titolare. Messaggio: \"You do not have access to this resource.\" (details.reason fornisce dettagli quando è utile). |\n| 403 | `EMAIL_NOT_VERIFIED` | Una sessione della dashboard il cui indirizzo e-mail non è confermato ha tentato di creare qualcosa di persistente, a pagamento o che invita persone (acquisizioni, lotti, confronti, oscuramenti, chiavi API, secret di firma del webhook, checkout, cambi di piano, cambi di postazioni, certify-hash, pianificazioni, inviti allo studio, pratiche e ogni loro modifica, membri delle pratiche, acquisizioni assegnate alle pratiche, condivisioni e relativa revoca, sigilli, esportazioni, blocchi per controversia legale). Le letture restano consentite. Messaggio: \"Confirm your email address first. Use the link we sent you, or send a new one from the dashboard.\" Una chiave API viene rifiutata allo stesso modo quando l'indirizzo e-mail del suo account non è confermato. |\n| 403 | `EIDAS_REQUIRES_LIVE_KEY` | include_eidas inviato con una chiave di prova (una chiave ps_live_ di un account senza account di fatturazione collegato conta come tale), oppure per un confronto o un oscuramento di un'acquisizione di prova (`details.reason`). Una chiave di produzione o una sessione che funziona come prova perché non è collegato alcun account di fatturazione riceve `details.reason` \"no_billing_account\". |\n| 403 | `MATTERS_NOT_ENABLED` | Le pratiche richiedono un piano Legal. |\n| 403 | `SCHEDULE_LIMIT_REACHED` | Raggiunto il limite di pianificazioni attive del piano (oppure il piano non prevede pianificazioni). |\n| 403 | `ON_CHANGE_REQUIRES_PLAN` | Il piano non consente il monitoraggio delle modifiche (on_change). |\n| 403 | `PLAN_ARCHIVE_ONLY` | Il piano Archive only mantiene le prove leggibili, scaricabili e verificabili; non crea acquisizioni, pianificazioni, certificazioni, confronti né oscuramenti. |\n| 403 | `PLAN_NOT_AVAILABLE` | Questo account non può scegliere il piano: Archive only si acquista dopo la fine di un abbonamento API a pagamento (finché le prove sono ancora conservate) oppure si ottiene cambiando piano da developer, startup, growth, website_archive o legal_solo; l'archivio dello studio solo dal titolare di uno studio il cui abbonamento è terminato, entro la relativa finestra di download. `details.reason` indica il motivo. |\n| 403 | `PLAN_API_READ_ONLY` | Piano Website Archive: la chiave API è di sola lettura (elenco, download, verifica, webhook). Creare acquisizioni, pianificazioni e certificazioni nella dashboard, oppure utilizzare un piano API. |\n| 403 | `LEGAL_HOLD_OWNER_ONLY` | Solo il titolare dello studio può revocare il blocco per controversia legale di un'acquisizione dello studio (un membro dello studio può attivarlo). |\n| 403 | `LEGAL_HOLD_NOT_AVAILABLE` | Attivare un blocco per controversia legale richiede un piano a pagamento (`details.reason` \"plan\": la Sandbox gratuita, o un account il cui abbonamento è terminato) e un'acquisizione di produzione (`details.reason` \"test_capture\": un'acquisizione in modalità di prova con filigrana). La revoca di un blocco per controversia legale non viene mai rifiutata per questo motivo. |\n| 403 | `IMPORT_NOT_ALLOWED` | L'importazione di pacchetti richiede un piano Legal (`details.reason` \"plan\") e una chiave API di produzione o una sessione della dashboard con fatturazione collegata (`details.reason` \"live_credentials_required\"). |\n| 404 | `CAPTURE_NOT_FOUND` | L'acquisizione non esiste, è stata eliminata o l'ID è malformato. |\n| 404 | `MATTER_NOT_FOUND` | La pratica non esiste oppure non se ne è membri. |\n| 404 | `IMPORT_NOT_FOUND` | L'importazione non esiste, appartiene a un altro account o l'ID è malformato. |\n| 404 | `WEBHOOK_SUBSCRIPTION_NOT_FOUND` | La sottoscrizione webhook non esiste, è stata eliminata o appartiene a un altro account. |\n| 404 | `SCHEDULE_NOT_FOUND` | La pianificazione non esiste o appartiene a un altro account. |\n| 404 | `NOT_FOUND` | Altra risorsa non trovata (formato dell'hash del registro di emissione, token di condivisione, job di esportazione, membro). |\n| 404 | `EXPORT_JOB_NOT_FOUND` | Un job di esportazione delle acquisizioni non esiste, appartiene a un altro account, è scaduto oppure è stato richiesto con una chiave di prova. |\n| 405 | `METHOD_NOT_ALLOWED` | Il metodo HTTP non è consentito per questa route. |\n| 409 | `CAPTURE_PROCESSING` | L'acquisizione è ancora in elaborazione e non può ancora essere eliminata (né può esserne modificato il blocco per controversia legale). |\n| 409 | `CAPTURE_LEGAL_HOLD` | L'acquisizione è sottoposta a blocco per controversia legale e non può essere eliminata. |\n| 409 | `CAPTURE_IN_TRASH` | Blocco per controversia legale: l'acquisizione è nel cestino; ripristinarla prima. |\n| 409 | `CAPTURE_HOLD_GRACE` | Eliminazione definitiva durante il periodo di tolleranza successivo alla revoca del blocco per controversia legale dell'acquisizione; `details.until`. Lo spostamento nel cestino è consentito. |\n| 409 | `LEGAL_HOLD_MATTER` | Il blocco dell'acquisizione è stato attivato dal blocco per controversia legale della sua pratica, ancora attivo: revocarlo sulla pratica (`details.matter_id` per chi può leggere la pratica). |\n| 409 | `LEGAL_HOLD_SOURCE` | Il blocco di un oscuramento o di un confronto segue un'acquisizione da cui è stato ricavato (per un confronto, l'una o l'altra acquisizione confrontata), ancora sottoposta a blocco per controversia legale (attivato su tale acquisizione o tramite la sua pratica): il pacchetto resta sottoposto a blocco finché questo non viene revocato (`details.source_capture_id`). |\n| 409 | `COMPANY_READ_ONLY` | L'abbonamento dello studio è terminato (oppure è conservato come archivio dello studio): le sue pratiche possono essere aperte ed esportate e i relativi link di condivisione revocati, nient'altro, e dalle sue acquisizioni non viene ricavato nulla di nuovo. |\n| 409 | `WEBHOOK_SUBSCRIPTION_LIMIT` | Raggiunto il limite di sottoscrizioni webhook attive del piano (25 sui piani API e Legal, 10 su Website Archive, 0 su Archive only e Sandbox); `details.limit`, `details.active`. |\n| 409 | `WEBHOOK_SECRET_ROTATION_PENDING` | Il precedente secret di firma del webhook è ancora nel suo periodo di sovrapposizione di 24 ore; `details.previous_secret_expires_at`. |\n| 409 | `IDEMPOTENCY_IN_PROGRESS` | La prima richiesta con questa Idempotency-Key è ancora in fase di accettazione. |\n| 409 | `COMPARE_NOT_READY` | Entrambe le acquisizioni devono essere completate prima del confronto. |\n| 409 | `REDACTION_NOT_READY` | L'acquisizione di origine deve essere completata prima dell'oscuramento. |\n| 409 | `MATTER_SEALED` | La pratica (o la pratica dell'acquisizione) è sigillata e non può essere modificata. |\n| 409 | `MATTER_SEALING` | È in corso una sigillatura della pratica (al massimo 60 s): nel frattempo nessuna assegnazione di acquisizioni, ridenominazione, modifica dei membri, spostamento nel cestino di un'acquisizione collegata né seconda sigillatura. `Retry-After`. |\n| 409 | `MATTER_ARCHIVED` | La pratica è archiviata: nessuna nuova acquisizione, ridenominazione, nuovo membro, condivisione o esportazione; riaprirla prima (PATCH status \"open\"). La rimozione di un'acquisizione o di un membro continua a funzionare. |\n| 409 | `MATTER_CHANGED` | Il registro della catena di custodia è cambiato durante la sigillatura; non è stato sigillato nulla, riprovare. |\n| 409 | `MATTER_LINKED` | Eliminazione dell'acquisizione: l'acquisizione è un allegato di una pratica di cui il chiamante non fa più parte. Il titolare della pratica deve prima rimuoverla dalla pratica. |\n| 409 | `MATTER_HAS_TRASHED_CAPTURES` | Sigillatura della pratica: un'acquisizione della pratica è nel cestino. Ripristinarla o rimuoverla dalla pratica, quindi sigillare di nuovo. |\n| 409 | `MEMBER_HAS_INDIVIDUAL_SUBSCRIPTION` | Invito allo studio: l'abbonamento API personale della persona invitata si rinnova ancora. La persona deve disdirlo, poi il titolare la invita di nuovo. |\n| 409 | `IMPORT_LIMIT` | Sono già in corso 3 importazioni (in caricamento o in verifica); `details.limit`, `details.in_progress`. |\n| 409 | `IMPORT_STATE` | L'importazione non è in uno stato che lo consente (complete o cancel di un'importazione non più in caricamento); `details.status`. |\n| 409 | `EXPORT_JOB_RUNNING` | Un'esportazione delle acquisizioni è ancora in preparazione; `details.job_id` e `details.poll_path` la identificano. Avviarne una nuova quando sarà terminata. |\n| 413 | `IMPORT_TOO_LARGE` | Il pacchetto supera 2 GiB; `details.max_bytes`. |\n| 413 | `VERIFY_PACKAGE_TOO_LARGE` | POST /captures/{capture_id}/verify: il pacchetto archiviato supera 256 MiB; verificarlo offline con gli script inclusi. `details.size_bytes`, `details.limit_bytes`. |\n| 422 | `IDEMPOTENCY_KEY_REUSED` | Idempotency-Key riutilizzata con un corpo diverso o un ambiente della chiave diverso (produzione o prova). |\n| 422 | `SSRF_BLOCKED` | L'URL punta a un indirizzo privato, di loopback, link-local o altrimenti bloccato. |\n| 422 | `TARGET_UNREACHABLE` | Il nome host dell'URL non ha record DNS oppure non è stato possibile risolverlo. |\n| 422 | `REDACTION_SCREENSHOT_UNREADABLE` | Non è stato possibile decodificare lo screenshot per l'oscuramento a riquadro. |\n| 422 | `REDACTION_LEAK_DETECTED` | Un termine oscurato è sopravvissuto all'oscuramento al di fuori dell'URL di acquisizione; non è stato emesso nulla. `details.files`, `details.rule_indexes`, `details.metadata_fields`, `details.in_capture_url`. |\n| 422 | `REDACTION_SOURCE_UNSUPPORTED` | Possono essere oscurate solo le acquisizioni di pagine web. `details.source` o `details.manifest_type`. |\n| 422 | `REDACTION_SOURCE_INTEGRITY_FAILED` | Un file dell'acquisizione di origine non corrisponde allo SHA-256 indicato dal relativo manifest firmato, oppure manca; non è stato emesso nulla. `details.mismatched`, `details.missing`. |\n| 422 | `COMPARE_PROFILE_MISMATCH` | POST /compare: le due acquisizioni sono state eseguite con un profilo dispositivo diverso o con impostazioni di acquisizione diverse; `details.differing`, `details.before`, `details.after`. Inviare allow_profile_mismatch: true per confrontare comunque (il risultato per testo e immagini è allora indeterminato). |\n| 422 | `COMPARE_URL_MISMATCH` | POST /compare: le due acquisizioni riguardano pagine diverse; `details.before`, `details.after` (url e final_url come origine e percorso). Inviare allow_url_mismatch: true per confrontare comunque. |\n| 422 | `RULE902_NOT_AVAILABLE_FOR_MISMATCH` | POST /compare: include_rule_902 per acquisizioni eseguite in modo diverso (allow_profile_mismatch); `details.differing`. |\n| 422 | `COMPARE_SOURCE_INTEGRITY_FAILED` | Un file di un'acquisizione di origine di POST /compare non corrisponde allo SHA-256 indicato dal relativo manifest firmato, oppure manca; non è stato confrontato né addebitato nulla. `details.capture_id`, `details.mismatched`, `details.missing`, `details.sources[]`. |\n| 422 | `REDACTION_PATTERN_TOO_SLOW` | Un pattern regex di text_redactions è stato eseguito troppo a lungo su questa pagina (backtracking catastrofico); non è stato emesso né addebitato nulla. `details.rule_index`. Semplificare il pattern. |\n| 422 | `IMPORT_SIZE_MISMATCH` | Il pacchetto caricato non ha la dimensione size_bytes dichiarata; il caricamento è stato scartato. `details.expected`, `details.actual`. |\n| 422 | `IMPORT_NOT_SUPPORTED` | Confronto o oscuramento di un pacchetto di prove importato. |\n| 429 | `RATE_LIMITED` | Troppe richieste nella finestra corrente, oppure è stato raggiunto un limite giornaliero per creazione di pratiche (200), sigillatura (20), esportazione (50), inviti di membri (50) o importazioni di pacchetti (50, `import_create`) (`details.daily_limit`, `details.action`). `Retry-After` indica quando riprovare. |\n| 429 | `CONCURRENT_CAPTURE_LIMIT` | Troppe acquisizioni dell'account in esecuzione contemporaneamente (developer 10, startup 25, growth 100, legal_solo e legal_seats 25, custom 200, website_archive 25, sandbox 5). Un lotto avvia tanti URL quanti ne rientrano nel limite ed elenca gli altri in `not_admitted`; senza slot liberi l'intera richiesta viene rifiutata. `details.limit`, `details.in_flight`, `details.requested`, `Retry-After`. |\n| 429 | `TEST_MODE_DAILY_LIMIT` | Il lavoro in modalità di prova (una chiave di prova o una sessione senza fatturazione collegata; con filigrana, non addebitato) è limitato a 200 unità per account e giorno UTC (un lotto conta i propri URL). `details.used`, `details.limit`, `details.reset_at`, `Retry-After`. |\n| 429 | `IMPORT_DAILY_BYTES_LIMIT` | Importazione di pacchetti: è stato raggiunto il volume di importazione dell'account per il giorno UTC (5 GiB di dimensioni dichiarate). `details.period`, `details.limit_bytes`, `details.used_bytes`, `details.requested_bytes`, `details.reset_at`. |\n| 429 | `IMPORT_MONTHLY_BYTES_LIMIT` | Importazione di pacchetti: è stato raggiunto il volume di importazione dell'account per il mese UTC (20 GiB). Stessi dettagli. |\n| 500 | `INTERNAL_ERROR` | Errore imprevisto. Riprovare con backoff. |\n| 500 | `CAPTURE_FAILED` | Non è stato possibile avviare l'acquisizione oppure (nell'`error` di un'acquisizione non riuscita) completarla. |\n| 500 | `CAPTURE_TIME_BUDGET_EXCEEDED` | Nell'`error` di un'acquisizione non riuscita, o per un oscuramento sincrono: l'esecuzione ha esaurito il proprio budget di tempo e non è stato emesso né addebitato alcun pacchetto. |\n| 500 | `CAPTURE_SCREENSHOT_TIMEOUT` | Nell'`error` di un'acquisizione non riuscita: il browser non ha terminato il rendering dello screenshot: tutti i metodi di ripresa (in un solo scatto, a fasce più piccole, lo screenshot nativo del browser) sono andati in timeout, così come l'unico nuovo tentativo in un browser riavviato quando il budget di tempo lo consentiva. Non è stato addebitato nulla; riprovare. |\n| 500 | `CERTIFY_FAILED` | Nell'`error` di una certificazione non riuscita: non è stato possibile completare la certificazione. |\n| 500 | `REDACTION_FAILED` | Nell'`error` di un oscuramento non riuscito: non è stato possibile completare l'oscuramento. Un oscuramento rifiutato per un motivo noto riporta invece il codice di tale motivo (ad esempio REDACTION_LEAK_DETECTED). |\n| 500 | `TIMESTAMP_SERVICE_UNAVAILABLE` | Sigillatura della pratica: l'ancoraggio OpenTimestamps non è riuscito; non è cambiato nulla, riprovare. |\n| 500 | `CONFIGURATION_ERROR` | Un passaggio lato server non è riuscito o non è configurato (`details.reason`). |\n| 503 | `SERVICE_UNAVAILABLE` | L'autenticazione è temporaneamente non disponibile (non è stato possibile verificare le credenziali). Non memorizzato in cache; riprovare. |\n| 503 | `EIDAS_UNAVAILABLE` | È stato richiesto include_eidas ma la marca temporale eIDAS non è configurata. |\n| 503 | `EIDAS_SEAL_RETRY` | Sigillatura della pratica: la marca temporale eIDAS non è arrivata in tempo; non è stato sigillato nulla. Riprovare dopo `Retry-After`; il nuovo tentativo riutilizza la marca temporale già emessa. |\n| 503 | `LIMIT_CHECK_UNAVAILABLE` | Sigillatura della pratica: non è stato possibile verificare il relativo limite giornaliero; non è stato fatto nulla. Riprovare. |\n| 504 | `UPSTREAM_TIMEOUT` | Un passaggio di elaborazione sincrono (certify-hash, esportazione della pratica) non ha risposto in tempo. Potrebbe ancora completarsi: interrogare quanto indicato in `details` (`certify_id`, `poll_path`), oppure riprovare. |\n| 404 | `ORG_INVITE_NOT_FOUND` | Un invito allo studio sconosciuto, indirizzato a un'altra persona, non più in sospeso oppure il cui studio non è più attivo (details.reason `company_inactive`). |\n| 409 | `ORG_INVITE_NO_SEATS` | Accettazione di un invito allo studio: tutte le postazioni dello studio sono occupate; l'invito resta aperto. |\n| 409 | `ORG_INVITE_IN_OTHER_ORG` | Accettazione di un invito allo studio: il chiamante occupa una postazione di un altro studio; l'invito resta aperto. |\n| 409 | `ORG_INVITE_HAS_SUBSCRIPTION` | Accettazione di un invito allo studio: l'abbonamento API personale del chiamante si rinnova ancora; l'invito resta aperto. |\n| 409 | `ORG_OWNER_CANNOT_LEAVE` | Il titolare dello studio ha tentato di lasciare il proprio studio. |\n| 410 | `ORG_INVITE_EXPIRED` | Accettazione di un invito allo studio più vecchio di 7 giorni. |",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "description": "Codice di errore leggibile dalle macchine (UPPER_SNAKE_CASE)."
              },
              "message": {
                "type": "string",
                "description": "Messaggio leggibile."
              },
              "details": {
                "type": "object",
                "additionalProperties": true,
                "description": "Contesto aggiuntivo facoltativo, ad esempio `reason`, `field` o `retry_after_seconds`."
              }
            },
            "required": [
              "code",
              "message"
            ]
          },
          "request_id": {
            "type": "string",
            "description": "ID della richiesta."
          }
        },
        "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": "Opzioni di acquisizione. Tutti i campi sono facoltativi.",
        "properties": {
          "scroll_full_page": {
            "type": "boolean",
            "description": "Acquisisce l'intera pagina scorrevole. false acquisisce solo l'area visibile (viewport).",
            "default": true
          },
          "expand_collapsed": {
            "type": "boolean",
            "description": "Apre le sezioni compresse prima dello screenshot: elementi <details> chiusi, pulsanti di fisarmonica e di espansione (aria-expanded con aria-controls) e controlli testuali \"see more\" / \"show replies\". Con protezioni: nulla all'interno di navigazioni, intestazioni, menu o finestre di dialogo, nessun link verso un altro URL, nessun invio di moduli, nessuna etichetta che modifica uno stato (like, follow, buy, delete, ...), ogni controllo al massimo una volta, e un controllo che tenta di lasciare la pagina interrompe l'espansione e l'URL originale viene ricaricato. L'esito è registrato per ogni controllo in forensic_log.json (page_modifications.expand_collapsed: trovati, aperti, lasciati chiusi, motivi di esclusione, richieste avviate, altezza della pagina prima e dopo) con un riepilogo in metadata.json (expand_collapsed: requested, opened_total, left_closed_total); evidence.pdf e i certificati indicano solo ciò che è stato effettivamente aperto.",
            "default": true
          },
          "viewport": {
            "type": "string",
            "description": "desktop è 1920x1080 con pixel ratio 1; mobile è 390x844 con pixel ratio 3 e uno user agent di Safari per dispositivi mobili. Altri valori restituiscono 400 INVALID_VIEWPORT.",
            "enum": [
              "desktop",
              "mobile"
            ],
            "default": "desktop"
          },
          "viewport_width": {
            "type": "number",
            "minimum": 320,
            "maximum": 3840,
            "description": "Larghezza personalizzata dell'area visibile in pixel CSS. Deve essere inviata insieme a viewport_height, altrimenti 400 MALFORMED_REQUEST."
          },
          "viewport_height": {
            "type": "number",
            "minimum": 240,
            "maximum": 4320,
            "description": "Altezza personalizzata dell'area visibile in pixel CSS. Deve essere inviata insieme a viewport_width."
          },
          "device_scale_factor": {
            "type": "number",
            "minimum": 1,
            "maximum": 3,
            "description": "Rapporto pixel del dispositivo dello screenshot."
          },
          "wait_for_selector": {
            "type": "string",
            "description": "Attende fino a 10 secondi questo selettore CSS prima dell'acquisizione. Se non compare, l'acquisizione prosegue senza errori. Registrato in forensic_log.json page_modifications.",
            "maxLength": 200
          },
          "wait_ms": {
            "type": "number",
            "minimum": 0,
            "maximum": 15000,
            "description": "Attesa aggiuntiva in millisecondi dopo il caricamento della pagina."
          },
          "screenshot_format": {
            "type": "string",
            "description": "jpeg fornisce screenshot.jpeg; png fornisce uno screenshot.png senza perdita di qualità. Le C2PA Content Credentials vengono aggiunte solo agli screenshot JPEG.",
            "enum": [
              "jpeg",
              "png"
            ],
            "default": "jpeg"
          },
          "url_stamp": {
            "type": "boolean",
            "description": "Aggiunge sopra l'immagine della pagina una fascia di intestazione con l'URL acquisito, l'ora di acquisizione UTC (il momento in cui il browser ha prodotto lo screenshot; per una pagina lunga ripresa a fasce, il momento in cui è stata prodotta l'ultima fascia) e l'ID della prova, contrassegnata come annotazione software. La fascia viene aggiunta in cima all'immagine, quindi nessun pixel della pagina viene coperto. chain_of_custody.json registra screenshot_url_header.raw_screenshot_sha256 con raw_screenshot_source: per una pagina ripresa in un solo pezzo (browser_image, browser_png, browser_jpeg_joined, browser_jpeg_reencoded) lo SHA-256 dell'immagine della pagina prodotta dal browser, prima della fascia; per una pagina lunga ripresa a fasce e poi unita (joined_bands, joined_bands_lossless, joined_bands_reencoded) lo SHA-256 dell'immagine unita così come ProofSnap l'ha scritta con la fascia, prima di qualsiasi Content Credentials C2PA. Quando non è possibile aggiungere la fascia (con essa l'immagine supererebbe i 65 535 px che un JPEG può contenere, il disegno non è riuscito oppure, solo nel percorso di screenshot di riserva, l'immagine della pagina supera i 32 000 000 pixel), lo screenshot viene consegnato senza: metadata.json lo registra in screenshot.url_stamp_skipped e chain_of_custody.json in screenshot_url_header (applied false, con il motivo).",
            "default": false
          },
          "network_log": {
            "type": "boolean",
            "description": "Include network.har (HAR con header di richiesta e di risposta e tempi, senza i corpi) e network.original.har.",
            "default": true
          },
          "warc": {
            "type": "boolean",
            "description": "Include capture.warc (ISO 28500) con le transazioni HTTP del caricamento della pagina. I corpi delle risposte vengono letti entro un limite di tempo (al massimo 30 s, e solo finché la parte facoltativa del budget di tempo dell'acquisizione lo consente, dopo il tempo riservato a eIDAS, C2PA e alla lettura finale dell'orologio); un corpo interrotto da tale limite viene memorizzato vuoto con WARC-Truncated: time, una risposta non raggiunta viene omessa e warc_captured in chain_of_custody.json (e warc in forensic_log.json) registra truncated, truncation_reason, response_pairs, records_total e i conteggi.",
            "default": false
          },
          "external_archive": {
            "type": "boolean",
            "description": "Chiede al servizio Save Page Now dell'Internet Archive di archiviare l'URL (l'Internet Archive conserva tale copia pubblicamente). Il risultato è classificato in base alla richiesta che ha prodotto lo snapshot e al suo orario rispetto alla richiesta: created (creato per questa richiesta), recent_existing (l'Internet Archive ha risposto con uno snapshot precedente), created_after_request (esiste uno snapshot più recente ma la richiesta non è stata confermata), existing (esiste solo uno snapshot più vecchio), pending, rate_limited, failed (l'Internet Archive ha rifiutato la richiesta), unconfirmed (nessuna risposta l'ha confermata o rifiutata e non è noto alcuno snapshot), unavailable, skipped, skipped_sensitive o skipped_query. Gli URL che sembrano contenere credenziali o che riportano parametri, nomi di parametri o segmenti di percorso simili a token non vengono mai inviati. Un URL con query string viene inviato solo quando questa opzione è impostata esplicitamente a true; con il valore predefinito viene saltato (skipped_query). Il risultato è registrato in forensic_log.json (external_archive, con snapshot_offset_seconds rispetto all'acquisizione) e in chain_of_custody.json, ed è restituito come external_archive da GET /captures/{capture_id}. Impostare false per gli URL privati. La richiesta viene inviata non appena la pagina è caricata, in parallelo al resto dell'acquisizione. Un errore non fa fallire l'acquisizione.",
            "default": true
          },
          "dismiss_consent": {
            "type": "boolean",
            "description": "Imposta cookie di consenso \"accetta tutto\" prima di caricare la pagina e tenta di chiudere i banner di consenso ai cookie. Questo modifica la pagina, perciò l'opzione è disattivata per impostazione predefinita; quanto eseguito viene registrato in forensic_log.json consent_dismissal.",
            "default": false
          },
          "dismiss_popups": {
            "type": "boolean",
            "description": "Chiude i pop-up in sovrimpressione di newsletter, promozioni e simili tramite il loro pulsante di chiusura. Il numero di pop-up chiusi e i pulsanti utilizzati vengono registrati in forensic_log.json page_modifications. Ogni pop-up chiuso è registrato in forensic_log.json page_modifications (popups_dismissed, popups_dismissed_controls); evidence.pdf indica quanti ne sono stati chiusi.",
            "default": false
          },
          "dark_mode": {
            "type": "boolean",
            "description": "Esegue il rendering con lo schema di colori scuro. Modifica l'aspetto della pagina; registrato in forensic_log.json page_modifications. Dichiarato in evidence.pdf e in forensic_log.json page_modifications.",
            "default": false
          },
          "block_ads": {
            "type": "boolean",
            "description": "Blocca le richieste verso host pubblicitari e di tracciamento noti. Omette contenuti; registrato in forensic_log.json page_modifications.",
            "default": false
          },
          "block_resources": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "image",
                "font",
                "media",
                "stylesheet"
              ]
            },
            "description": "Tipi di risorse da bloccare. Omette contenuti; registrato in forensic_log.json page_modifications."
          },
          "inject_css": {
            "type": "string",
            "description": "CSS aggiunto alla pagina prima dell'acquisizione. Modifica la pagina; memorizzato integralmente in forensic_log.json page_modifications. Sempre memorizzato integralmente in forensic_log.json page_modifications (injected_css) con injected_css_status `applied` o `failed` e injected_css_error. evidence.pdf indica che è stato iniettato CSS personalizzato; non stampa il codice.",
            "maxLength": 10000
          },
          "inject_js": {
            "type": "string",
            "description": "JavaScript eseguito nella pagina prima dell'acquisizione. Modifica la pagina. Sempre memorizzato integralmente in forensic_log.json page_modifications (injected_js) con injected_js_status e injected_js_error: `applied` (è stato eseguito fino alla fine), `invalid` (non è stato compilato, ad esempio per un errore di sintassi, quindi non è mai stato eseguito e non ha modificato la pagina), `failed` (ha generato un'eccezione durante l'esecuzione, quindi potrebbe aver modificato la pagina prima di interrompersi) o `timed_out` (l'acquisizione ha smesso di attenderlo dopo 5 secondi; lo script potrebbe aver continuato a essere eseguito e a modificare la pagina). evidence.pdf indica l'esito e rimanda a forensic_log.json, e il certificato di provenienza elenca il codice iniettato tra le modifiche della pagina, salvo che fosse `invalid`; nessuno dei due stampa il codice.",
            "maxLength": 10000
          },
          "locale": {
            "type": "string",
            "description": "Impostazioni locali del browser da emulare, ad esempio de o de-DE (imposta la lingua del browser e Accept-Language). Solo emulazione: non viene utilizzato alcun proxy, quindi i contenuti che dipendono dall'IP del visitatore riflettono comunque la regione del server di acquisizione. Registrato in metadata.json geo.",
            "pattern": "^[a-z]{2}(-[A-Z]{2})?$"
          },
          "timezone": {
            "type": "string",
            "description": "Fuso orario del browser da emulare (nome IANA, ad esempio Europe/Berlin). Solo emulazione."
          },
          "language": {
            "type": "string",
            "description": "Lingua dei PDF leggibili. Uno tra: 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. Maiuscole/minuscole, trattino e regione sono tollerati (\"DE\", \"fr-FR\" e \"PT-BR\" sono accettati); \"pt\" e \"zh\" da soli vengono rifiutati. Un valore non supportato restituisce 400 MALFORMED_REQUEST.",
            "default": "en",
            "example": "de"
          },
          "timestamp_type": {
            "type": "string",
            "description": "È accettato solo \"bitcoin\" (l'ancoraggio OpenTimestamps viene sempre tentato). Qualsiasi altro valore restituisce 400 INVALID_TIMESTAMP_TYPE. Utilizzare include_eidas per una validazione temporale elettronica qualificata.",
            "enum": [
              "bitcoin"
            ],
            "default": "bitcoin"
          },
          "include_eidas": {
            "type": "boolean",
            "description": "Richiede inoltre una validazione temporale elettronica qualificata eIDAS (RFC 3161) sullo SHA-256 di manifest.json, fornita come manifest.json.tsr (più eidas_validation.json quando vengono restituiti dati di validazione). Solo chiavi di produzione: una chiave di prova restituisce 403 EIDAS_REQUIRES_LIVE_KEY. Se nel deployment non è configurato alcun servizio eIDAS, la richiesta viene rifiutata con 503 EIDAS_UNAVAILABLE prima di qualsiasi addebito. Prima di utilizzare un token, ProofSnap lo verifica (firma, hash contenuto, catena di certificati fino alla radice CA Disig QCA4, stato del certificato della TSA) e registra tale verifica in eidas_validation.json (verification). Se la richiesta della marca temporale non riesce durante l'acquisizione, o se il token non supera tale verifica, il pacchetto viene consegnato senza di essa, con il solo ancoraggio OpenTimestamps, la marca non viene addebitata e timestamps.eidas (riga dell'acquisizione, GET /captures/{capture_id}, webhook completed) indica lo stato not_obtained con il motivo (token_refused, unavailable o time_budget). Dallo screenshot in poi, i passaggi facoltativi dell'acquisizione (i corpi WARC, le risposte dell'archivio e WHOIS, la firma C2PA, la seconda lettura dell'orologio) lasciano tempo alla marca temporale e sono i primi a essere interrotti. Addebitato per marca temporale.",
            "default": false
          },
          "include_rule_902": {
            "type": "boolean",
            "description": "Aggiunge rule_902_certification.pdf: un modello di certificazione [FRE 902(13)/(14)](https://www.govinfo.gov/content/pkg/USCODE-2023-title28/html/USCODE-2023-title28-app-federalru-dup2-rule902.htm) e [28 U.S.C. 1746](https://www.govinfo.gov/content/pkg/USCODE-2023-title28/html/USCODE-2023-title28-partV-chap115-sec1746.htm) non firmato, che una persona deve compilare e firmare. Il suo SHA-256 è elencato in manifest.json. Viene aggiunto anche quando evidence_pack.custodian ha un valore non vuoto. Se la generazione non riesce, l'acquisizione si completa senza di esso. Attualmente disattivato: il modello di certificazione è temporaneamente disattivato, quindi questo campo viene accettato e ignorato (nessun rule_902_certification.pdf viene generato o elencato nel pacchetto) e la risposta contiene l'avviso RULE902_DISABLED.",
            "default": false
          },
          "case_reference": {
            "allOf": [
              {
                "$ref": "#/components/schemas/PlainText"
              }
            ],
            "description": "L'etichetta della pratica o della causa. Gli spazi vengono compressi. Segue la regola del testo semplice (vedere PlainText). Registrata in metadata.json, forensic_log.json e chain_of_custody.json, e stampata su evidence.pdf.",
            "maxLength": 200
          },
          "case_id": {
            "allOf": [
              {
                "$ref": "#/components/schemas/PlainText"
              }
            ],
            "description": "L'identificativo della causa nel Suo sistema. Registrato come case_reference; segue la regola del testo semplice (vedere PlainText).",
            "maxLength": 200
          },
          "case_note": {
            "allOf": [
              {
                "$ref": "#/components/schemas/PlainText"
              }
            ],
            "description": "Nota a testo libero. Segue la regola del testo semplice (vedere PlainText). Registrata in metadata.json, forensic_log.json e chain_of_custody.json (legal_context.note).",
            "maxLength": 500
          },
          "webhook_url": {
            "type": "string",
            "description": "URL https che riceve capture.completed o capture.failed. Vedere Webhook. L'URL deve essere https e non deve puntare a un indirizzo privato, di loopback, link-local, NAT di livello carrier o di metadati cloud (le stesse regole dell'URL di un'acquisizione; altrimenti 422 SSRF_BLOCKED). Un valore vuoto o composto solo da spazi significa nessun webhook. Richiede una chiave API: se inviato da una sessione della dashboard viene rifiutato con 400 WEBHOOK_REQUIRES_API_KEY, perché i webhook sono firmati con il secret di firma del webhook della chiave.",
            "format": "uri"
          },
          "metadata": {
            "type": "object",
            "additionalProperties": {
              "type": "string",
              "maxLength": 500
            },
            "maxProperties": 10,
            "description": "Fino a 10 tag di tipo stringa (valori fino a 500 caratteri), restituiti con l'acquisizione. Le violazioni restituiscono 400 METADATA_TOO_LARGE."
          },
          "evidence_pack": {
            "type": "object",
            "properties": {
              "enabled": {
                "type": "boolean",
                "description": "false esclude la pagina di certificazione da evidence.pdf.",
                "default": true
              },
              "jurisdiction": {
                "type": "string",
                "description": "Registrato in forensic_log.json options. Non ha altri effetti.",
                "enum": [
                  "US-Federal",
                  "EU-eIDAS"
                ],
                "default": "US-Federal"
              },
              "custodian": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Accettato; non stampato nel modello, i cui campi del dichiarante restano vuoti per la persona che lo firma.",
                    "maxLength": 200
                  },
                  "law_firm": {
                    "type": "string",
                    "description": "Accettato; non stampato nel modello, i cui campi del dichiarante restano vuoti per la persona che lo firma.",
                    "maxLength": 200
                  },
                  "role": {
                    "type": "string",
                    "description": "Accettato; non utilizzato nel modello.",
                    "maxLength": 200
                  },
                  "case_caption": {
                    "type": "string",
                    "description": "Accettato; non utilizzato nel modello.",
                    "maxLength": 200
                  }
                },
                "additionalProperties": false,
                "description": "Ogni valore segue la regola del testo semplice (vedere PlainText); altrimenti 400 INVALID_EVIDENCE_PACK_OPTIONS."
              }
            },
            "description": "I valori non validi restituiscono 400 INVALID_EVIDENCE_PACK_OPTIONS."
          }
        }
      },
      "CaptureRequest": {
        "type": "object",
        "required": [
          "url"
        ],
        "properties": {
          "url": {
            "type": "string",
            "description": "Pagina da acquisire. Solo http o https, al massimo 2 048 caratteri. Gli indirizzi privati, di loopback, link-local e di metadati vengono rifiutati con 422 SSRF_BLOCKED, e un nome host senza record DNS restituisce 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": "Percorso dello stato comprensivo dello stage /v1, ad esempio /v1/captures/cap_..."
          },
          "message": {
            "type": "string"
          },
          "metadata": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          },
          "private_note": {
            "type": "string",
            "description": "La nota privata così come memorizzata (normalizzata), presente solo quando ne è stata inviata una."
          },
          "warnings": {
            "$ref": "#/components/schemas/Rule902DisabledWarnings"
          }
        }
      },
      "CaptureResult": {
        "type": "object",
        "description": "Un'acquisizione, un certificato, un confronto o un oscuramento. I campi dipendono dallo stato.",
        "properties": {
          "capture_id": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "processing",
              "completed",
              "failed"
            ]
          },
          "url": {
            "type": "string",
            "description": "URL acquisito (null per certificati e confronti).",
            "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 per i confronti, import per un pacchetto di prove importato.",
            "enum": [
              "comparison",
              "import"
            ]
          },
          "compared": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Solo sui confronti: gli ID delle due acquisizioni di origine."
          },
          "diff_summary": {
            "$ref": "#/components/schemas/DiffSummary"
          },
          "estimated_completion_seconds": {
            "type": "integer",
            "description": "Solo processing: stima approssimativa, almeno 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": "Il motivo pubblico dell'errore (vedere la tabella Error). CAPTURE_SCREENSHOT_TIMEOUT: il browser non ha completato lo screenshot; CAPTURE_TIME_BUDGET_EXCEEDED: l'esecuzione ha esaurito l'intero tempo a disposizione; nessuno dei due viene addebitato. Una riga di confronto (cmp_) può fallire anche con COMPARE_SOURCE_INTEGRITY_FAILED (un pacchetto di origine alterato dopo l'accettazione della richiesta), COMPARE_PROFILE_MISMATCH o COMPARE_URL_MISMATCH (rilevati solo nei record sigillati che l'esecuzione confronta) oppure RULE902_NOT_AVAILABLE_FOR_MISMATCH; l'unità viene restituita. Una certificazione di hash eseguita in background fallisce con QUOTA_EXCEEDED quando al momento dell'esecuzione è stato raggiunto il massimo mensile. CERTIFY_FAILED e REDACTION_FAILED: non è stato possibile completare una certificazione di hash o un oscuramento; riprovare la richiesta."
              },
              "message": {
                "type": "string"
              }
            },
            "description": "Solo failed."
          },
          "failed_at": {
            "type": "string",
            "description": "Solo failed.",
            "format": "date-time"
          },
          "captured_at": {
            "type": "string",
            "description": "Quando la pagina è stata acquisita (solo righe completate). Per un oscuramento, l'ora di acquisizione della sua acquisizione di origine (redacted_at indica quando è stato eseguito l'oscuramento).",
            "format": "date-time"
          },
          "redacted_at": {
            "type": "string",
            "format": "date-time",
            "description": "Solo oscuramenti: quando è stato eseguito l'oscuramento. Il captured_at di un oscuramento è l'ora di acquisizione della sua acquisizione di origine."
          },
          "compared_at": {
            "type": "string",
            "nullable": true,
            "format": "date-time",
            "description": "Solo confronti: il momento in cui è stato eseguito il confronto. Il suo captured_at riporta lo stesso valore (mantenuto per i client meno recenti); utilizzare compared_at."
          },
          "package_url": {
            "type": "string",
            "description": "Solo completed: link di download prefirmato dello ZIP del pacchetto, valido fino a 24 ore (vedere package_url_expires_at); rileggere l'acquisizione per ottenere un link nuovo.",
            "format": "uri"
          },
          "package_url_expires_at": {
            "type": "string",
            "description": "Solo completed: quando package_url smette di funzionare, al massimo 24 ore dopo questa risposta (un link firmato con credenziali temporanee del server può smettere prima); rileggere l'acquisizione per ottenere un link nuovo.",
            "format": "date-time"
          },
          "verification_url": {
            "type": "string",
            "description": "Solo completed: link al verificatore pubblico di ProofSnap.",
            "format": "uri"
          },
          "manifest_hash": {
            "type": "string",
            "description": "Solo completed: sha256:<hex> di 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> dello screenshot firmato.",
                "nullable": true
              }
            },
            "description": "Solo completed."
          },
          "file_count": {
            "type": "integer",
            "description": "Solo completed."
          },
          "size_bytes": {
            "type": "integer",
            "description": "Solo completed."
          },
          "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": "Solo completed, quando l'acquisizione è finita su un muro di consenso ai cookie o su una pagina interstiziale invece che sulla pagina. Rieseguire con dismiss_consent: true."
          },
          "external_archive": {
            "allOf": [
              {
                "$ref": "#/components/schemas/ExternalArchive"
              }
            ],
            "description": "Solo completed. La richiesta a Internet Archive eseguita con l'opzione external_archive: il risultato attuale (status), lo stato sigillato nel pacchetto (status_at_sealing) e il link allo snapshot. { requested: false } quando non è stata fatta alcuna richiesta."
          },
          "storage": {
            "$ref": "#/components/schemas/CaptureStorage"
          },
          "private_note": {
            "type": "string",
            "nullable": true,
            "description": "La Sua nota privata (vedere PrivateNote). Restituita solo all'autore dell'acquisizione; mai parte del pacchetto di prove."
          },
          "legal_hold": {
            "type": "boolean",
            "description": "L'acquisizione è sotto blocco per controversia legale: non viene eliminata finché è true (PATCH /captures/{capture_id} con action legal_hold). Revocata automaticamente, e l'acquisizione eliminata, quando l'abbonamento è terminato e sono trascorsi i suoi 30 giorni senza un piano che conservi i dati."
          },
          "legal_hold_set_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "Quando è stato attivato il blocco per controversia legale attuale; null in assenza di blocco."
          },
          "legal_hold_reason": {
            "type": "string",
            "nullable": true,
            "description": "Il motivo memorizzato con il blocco per controversia legale attuale; null in assenza di blocco o di motivo."
          },
          "legal_hold_set_by_email": {
            "type": "string",
            "nullable": true,
            "description": "Solo titolare dello studio, su un'acquisizione dello studio sottoposta a blocco per controversia legale: l'indirizzo e-mail di chi ha attivato il blocco."
          },
          "legal_hold_source": {
            "type": "string",
            "nullable": true,
            "description": "Da dove proviene il blocco per controversia legale attuale: \"manual\" (PATCH /captures/{capture_id}), \"bulk\" (POST /legal-holds), \"matter:<matter_id>\" (il blocco per controversia legale della pratica) oppure \"derived:<capture_id>\" (un oscuramento o un confronto sotto blocco perché lo è un'acquisizione da cui è stato ricavato). null in assenza di blocco."
          },
          "legal_hold_released_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "Quando è stato revocato l'ultimo blocco per controversia legale (inizio del periodo di tolleranza dopo la revoca); null finché è sotto blocco o se non è mai stato revocato, e dopo la revoca automatica alla fine di un abbonamento (senza periodo di tolleranza)."
          },
          "deletion_blocked_until": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "Fine del periodo di tolleranza dopo una revoca: fino ad allora l'acquisizione non viene eliminata definitivamente (DELETE definitivo 409 CAPTURE_HOLD_GRACE, nessuna pulizia del cestino, nessuna eliminazione per scadenza della conservazione); lo spostamento nel cestino è possibile. L'eliminazione dell'account e dello studio dopo la fine dell'abbonamento non lo attende. null quando non è in corso alcun periodo di tolleranza (anche mentre è attivo un blocco per controversia legale: questo impedisce l'eliminazione senza scadenza)."
          },
          "s3_legal_hold": {
            "type": "object",
            "nullable": true,
            "description": "Il legal hold S3 Object Lock degli oggetti del pacchetto memorizzato (ZIP, prova OpenTimestamps aggiornata e duplicato memorizzato separatamente, ogni versione). status: on, off, pending (in fase di applicazione), failed (ritentato dal server), unsupported (lo storage non ha una configurazione Object Lock; il blocco nel record dell'acquisizione si applica comunque), none (nessun pacchetto memorizzato, ad esempio un'acquisizione fallita).",
            "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": "Oscuramenti e confronti: true quando un'acquisizione da cui sono stati ricavati è sotto blocco per controversia legale. Un pacchetto derivato segue il blocco della sua origine: finché un'origine è sotto blocco, lo è anche l'oscuramento o il confronto (legal_hold_source derived:<capture_id>) e il suo blocco non può essere revocato autonomamente (409 LEGAL_HOLD_SOURCE). null per le altre acquisizioni."
          },
          "import": {
            "$ref": "#/components/schemas/CaptureImport"
          },
          "source": {
            "type": "string",
            "description": "Come è stata eseguita l'acquisizione: api, schedule, certify-hash, compare, redact, import. GET /captures/{capture_id} lo restituisce solo per un pacchetto importato (\"import\"); le righe di GET /captures lo riportano sempre."
          }
        }
      },
      "Timestamps": {
        "type": "object",
        "properties": {
          "bitcoin_ots": {
            "type": "object",
            "properties": {
              "status": {
                "type": "string",
                "description": "Stato della prova Bitcoin OpenTimestamps in manifest.json.ots. pending_confirmation: la prova è in attesa di un blocco Bitcoin (di solito qualche ora; i controlli iniziano circa 2 ore dopo il completamento dell'acquisizione e si ripetono a intervalli crescenti). verified: un blocco Bitcoin attesta l'hash del manifest (block_height, block_time). not_present: il pacchetto non contiene manifest.json.ots (i calendari non erano raggiungibili quando il pacchetto è stato sigillato). unconfirmed: nessuna conferma trovata entro 7 giorni di controlli; la prova nel pacchetto può ancora essere aggiornata e verificata con il client OpenTimestamps (ots upgrade, ots verify). package_missing: il pacchetto di prove non è più presente nello storage, quindi la prova non può più essere verificata. manifest_mismatch: il manifest.json nel pacchetto memorizzato non corrisponde all'hash del manifest registrato al momento della sigillatura; la validazione temporale non è confermata per esso. Entrambi sono definitivi. Il pacchetto stesso non viene mai modificato. not_checked: l'acquisizione è stata completata prima che esistessero i controlli automatici di conferma e non è mai stata verificata; il suo pacchetto può comunque contenere un manifest.json.ots che il client OpenTimestamps può aggiornare e verificare. Le risposte dei webhook e di creazione vengono inviate al completamento, quindi indicano pending_confirmation (o not_present quando i calendari non hanno risposto); leggere GET /captures/{capture_id} per lo stato attuale. Un pacchetto viene sigillato con la prova in sospeso: a quel punto l'hash del manifest è stato inviato ai calendari OpenTimestamps. Una volta confermata nel blocco Bitcoin {block_height} ({block_time} UTC), la prova dimostra che manifest.json esisteva non oltre quel momento; scaricare la prova aggiornata (upgraded_proof_url) e collocarla accanto a manifest.json come manifest.json.upgraded.ots, e verify.sh e verify.ps1 la verificano automaticamente.",
                "enum": [
                  "pending_confirmation",
                  "verified",
                  "not_present",
                  "unconfirmed",
                  "package_missing",
                  "manifest_mismatch",
                  "not_checked"
                ]
              },
              "block_height": {
                "type": "integer",
                "nullable": true,
                "description": "Altezza del blocco Bitcoin che attesta la prova. Valorizzata quando status è verified."
              },
              "block_time": {
                "type": "string",
                "format": "date-time",
                "nullable": true,
                "description": "Ora di quel blocco Bitcoin. Solo GET /captures/{capture_id}, impostato quando status è verified."
              },
              "verified_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true,
                "description": "Quando ProofSnap ha confermato l'ancoraggio. Solo GET /captures/{capture_id}, impostato quando status è verified."
              },
              "upgraded_proof_url": {
                "type": "string",
                "format": "uri",
                "nullable": true,
                "description": "Solo GET /captures/{capture_id}. Una volta verificato l'ancoraggio, ProofSnap memorizza la prova OpenTimestamps aggiornata dai calendari (contiene l'attestazione del blocco Bitcoin, quindi si verifica offline senza contattare alcun calendario). Questo è un download prefirmato della prova, valido fino a 24 ore come package_url; leggere di nuovo l'acquisizione per ottenere un link nuovo. null quando non è disponibile (non ancora verificata, o memorizzata più tardi da un backfill in background). Il pacchetto sigillato non viene mai modificato: salvare il file accanto al manifest.json del pacchetto come manifest.json.upgraded.ots. I pacchetti sigillati dal 2026-09-29 la verificano con gli script inclusi (verification/verify.sh e verify.ps1 la usano al posto di manifest.json.ots); per un pacchetto più vecchio eseguire `ots verify manifest.json.upgraded.ots -f manifest.json`. Le esportazioni delle pratiche la contengono come captures/<capture_id>.upgraded.ots accanto a captures/<capture_id>.zip. Eliminata insieme al pacchetto."
              }
            }
          },
          "eidas": {
            "$ref": "#/components/schemas/EidasTimestamp"
          }
        }
      },
      "DiffSummary": {
        "type": "object",
        "properties": {
          "text_insertions": {
            "type": "integer",
            "description": "Righe aggiunte (lo stesso valore di text_lines_added; dal 2026-10-14 il testo è conteggiato in righe, vedere text_unit). I confronti eseguiti in precedenza contano caratteri. Deprecato (2026-10-14): mantenuto per i client esistenti, non rimosso; utilizzare text_lines_added.",
            "deprecated": true
          },
          "text_deletions": {
            "type": "integer",
            "description": "Righe rimosse (lo stesso valore di text_lines_removed; righe dal 2026-10-14, caratteri in precedenza). Deprecato (2026-10-14): mantenuto per i client esistenti, non rimosso; utilizzare text_lines_removed.",
            "deprecated": true
          },
          "text_unit": {
            "type": "string",
            "enum": [
              "line"
            ],
            "description": "L'unità dei conteggi del testo: line (ogni riga di domtextcontent.txt con il suo carattere di a capo). Assente nei confronti eseguiti prima del 2026-10-14, che contavano i caratteri."
          },
          "text_lines_added": {
            "type": "integer",
            "description": "Righe aggiunte (righe del testo After nelle sequenze aggiunte del diff di righe)."
          },
          "text_lines_removed": {
            "type": "integer",
            "description": "Righe rimosse (righe del testo Before nelle sequenze rimosse del diff di righe)."
          },
          "text_lines_before": {
            "type": "integer",
            "description": "Righe del testo Before."
          },
          "text_lines_after": {
            "type": "integer",
            "description": "Righe del testo After."
          },
          "text_changed_percent": {
            "type": "number",
            "description": "100 x (righe rimosse + righe aggiunte) / (righe prima + righe dopo), con un decimale."
          },
          "text_diff_capped": {
            "type": "boolean",
            "description": "true quando il diff per righe ha raggiunto il suo budget di elaborazione e una parte dei testi è stata conteggiata come sostituita per intero (sovrastima, non nasconde mai una modifica)."
          },
          "metadata_changes": {
            "type": "array",
            "items": {
              "type": "object"
            },
            "description": "{ key, before, after, class } per ogni campo di metadata.json che differisce, esattamente questi: url, final_url, page_title, http_status, http_headers.content-type, http_headers.server, dns.connected_ip, resolved_ips (confrontati come elenco ordinato), tls.issuer, tls.valid_to, e i campi del profilo del dispositivo viewport_width, viewport_height, device_scale_factor, is_mobile e user_agent. class è content (url, final_url, page_title, http_status, http_headers.content-type: conteggiati come modifica), infrastructure (http_headers.server, dns.connected_ip, resolved_ips, tls.issuer, tls.valid_to: quale server ha consegnato la pagina, solo segnalati) oppure device_profile (come l'acquisizione ha renderizzato la pagina, solo segnalati). Un confronto eseguito prima del 2026-10-10 non ha class."
          },
          "meta_content": {
            "type": "integer",
            "description": "Campi dei metadati di classe content che differiscono; vengono conteggiati in changed."
          },
          "meta_infra": {
            "type": "integer",
            "description": "Campi dei metadati di classe infrastructure che differiscono (software del server, IP connesso, IP risolti, emittente e scadenza TLS). Una rete di distribuzione dei contenuti risponde da indirizzi a rotazione, quindi due caricamenti di una pagina invariata possono differire qui; riportati a titolo informativo, mai conteggiati in changed."
          },
          "meta_device_profile": {
            "type": "integer",
            "description": "Campi del profilo dispositivo che differiscono (area visibile, scala, emulazione mobile, user agent); riportati a titolo informativo, mai conteggiati in changed."
          },
          "image_changes": {
            "type": "object",
            "description": "Il confronto delle immagini, ricavato dall'inventario delle immagini sigillato da ciascuna acquisizione di origine (image_inventory.json). status ran, oppure not_run con reason source_without_inventory (un'origine acquisita prima che le acquisizioni ne registrassero uno), source_inventory_failed o source_inventory_unreadable; nient'altro viene usato al suo posto. I risultati completi per immagine si trovano in image_changes.json del pacchetto.",
            "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": "Perché le immagini non sono state confrontate: source_without_inventory (un'origine eseguita prima che le acquisizioni registrassero un inventario delle immagini; le nuove acquisizioni ne registrano uno), source_inventory_failed (non è stato possibile registrare l'inventario quando è stata eseguita quell'acquisizione) oppure source_inventory_unreadable; null quando il confronto è stato eseguito."
              },
              "images_changed": {
                "type": "integer",
                "nullable": true,
                "description": "Immagini del sito (servite dal dominio registrabile della pagina stessa o, dal 2026-10-14, da un dominio che la pagina dichiara come proprio og:image o icona, oppure che serve la maggior parte delle immagini della pagina in entrambe le acquisizioni: image_changes.json site_image_hosts) sostituite, aggiunte o rimosse, la parte immagini di changed; le immagini spostate non sono incluse. null quando il confronto non è stato eseguito. Deprecato (2026-10-14): mantenuto per i client esistenti, non rimosso; utilizzare 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 quando un inventario di origine ha raggiunto il limite di voci; vedere inventory_cutoff."
              },
              "site_images_changed": {
                "type": "integer",
                "description": "Lo stesso di images_changed (conteggiato)."
              },
              "other_images_changed": {
                "type": "integer",
                "description": "Immagini di altri domini e pixel di tracciamento sostituiti, rimossi o aggiunti (mai conteggiati)."
              },
              "site_image_hosts": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "Domini registrabili considerati propri del sito oltre a quello della pagina (site_image_hosts di image_changes.json)."
              },
              "inventory_cutoff": {
                "type": "object",
                "nullable": true,
                "properties": {
                  "before_only_index_limit": {
                    "type": "integer",
                    "nullable": true,
                    "description": "Un'immagine presente solo in Before è determinabile (removed) quando il suo indice nell'inventario Before è inferiore a questo valore; null: nessun limite (l'inventario After è completo)."
                  },
                  "after_only_index_limit": {
                    "type": "integer",
                    "nullable": true,
                    "description": "Un'immagine presente solo in After è determinabile (added) quando il suo indice nell'inventario After è inferiore a questo valore; null: nessun limite (l'inventario Before è completo)."
                  }
                },
                "description": "Dal secondo audit del 2026-10-14 (algoritmo 3 di image_changes.json) un oggetto, deciso in base all'indice nel documento: quando l'ALTRO inventario ha raggiunto il limite di voci, un'immagine trovata su un solo lato è decidibile solo se il suo indice è inferiore al numero di voci dell'altro inventario meno un margine (il maggiore tra 10 voci e il 5 per cento di tale numero); altrimenti è indeterminata (undetermined_reason beyond_inventory_limit). null quando nessun inventario è troncato. I confronti eseguiti in precedenza riportano un numero (la posizione relativa nel documento della regola precedente)."
              }
            }
          },
          "changed": {
            "type": "boolean",
            "nullable": true,
            "description": "true quando il testo visibile differisce, un campo di metadati di contenuto differisce (meta_content), oppure un'immagine del sito stesso è stata sostituita, rimossa o aggiunta (image_changes.images_changed). I campi di infrastruttura e del profilo del dispositivo, le immagini spostate (gli stessi byte a un altro indirizzo), le immagini di terze parti e i pixel di tracciamento vengono segnalati ma non lo impostano. null (dal 2026-10-14) quando nulla di quanto conteggiato è cambiato in modo affidabile ma il risultato del testo o delle immagini è indeterminato (acquisizioni renderizzate in modo diverso, allow_profile_mismatch). Il pacchetto indica la regola in comparison_method.json counted_as_change. Deprecato (2026-10-14): mantenuto per i lettori esistenti, non rimosso; usare verdict (changed | unchanged | undetermined) e i campi per tipo text_verdict, images_verdict, metadata_verdict.",
            "deprecated": true
          },
          "text_verdict": {
            "type": "string",
            "enum": [
              "changed",
              "unchanged",
              "undetermined"
            ],
            "description": "undetermined quando le due acquisizioni sono state renderizzate con un profilo del dispositivo o impostazioni di acquisizione diversi (allow_profile_mismatch)."
          },
          "images_verdict": {
            "type": "string",
            "enum": [
              "changed",
              "unchanged",
              "undetermined",
              "not_run"
            ]
          },
          "metadata_verdict": {
            "type": "string",
            "enum": [
              "changed",
              "unchanged"
            ],
            "description": "Ricavato dai campi dei metadati di classe content (indirizzo, indirizzo finale, titolo, stato HTTP, tipo di contenuto)."
          },
          "verdict": {
            "type": "string",
            "enum": [
              "changed",
              "unchanged",
              "undetermined"
            ],
            "description": "Il risultato complessivo in parole: changed quando changed è true, undetermined quando changed è null."
          },
          "undetermined_reason": {
            "type": "string",
            "nullable": true,
            "enum": [
              "render_profile_mismatch",
              "capture_settings_mismatch",
              null
            ]
          },
          "text_changes": {
            "type": "array",
            "items": {
              "type": "object"
            },
            "description": "Solo GET: fino a 60 frammenti di testo modificati."
          },
          "change_regions": {
            "type": "array",
            "items": {
              "type": "object"
            },
            "description": "Solo GET: fino a 60 modifiche del testo collocate sugli screenshot (in base al text_layout.json delle origini), in pixel dello screenshot. Ogni modifica riporta counterpart_y_px (la sua posizione verticale sull'altro screenshot, ripresa dal blocco di testo invariato più vicino) e report_region (il numero della zona di modifica in cui evidence.pdf la mostra, al massimo 10 zone, oppure null) come in change_regions.json."
          },
          "change_regions_info": {
            "type": "object",
            "nullable": true
          },
          "order_reversed": {
            "type": "boolean",
            "description": "Solo GET: true quando l'acquisizione indicata come capture_id_1 (Before) è stata eseguita dopo quella indicata come capture_id_2 (After), come order_reversed in compare.json. Il confronto mantiene l'ordine indicato; il rapporto e la dashboard lo segnalano."
          },
          "render_profile_mismatch": {
            "type": "boolean",
            "description": "Solo GET: le due acquisizioni sono state eseguite con un profilo dispositivo diverso (compare.json)."
          },
          "capture_settings_mismatch": {
            "type": "boolean",
            "description": "Solo GET: le due acquisizioni sono state eseguite con impostazioni di acquisizione diverse (compare.json)."
          },
          "capture_settings_differing": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "setting": {
                  "type": "string"
                },
                "before": {},
                "after": {}
              }
            },
            "description": "Solo GET: le impostazioni che differiscono, { setting, before, after }."
          },
          "profile_mismatch_allowed": {
            "type": "boolean",
            "description": "Solo GET: il confronto è stato eseguito con allow_profile_mismatch per acquisizioni eseguite in modo diverso. Deprecato (2026-10-14): mantenuto per i client esistenti, non rimosso; utilizzare render_profile_mismatch e capture_settings_mismatch.",
            "deprecated": true
          },
          "url_mismatch": {
            "type": "boolean",
            "description": "Solo GET: le due acquisizioni non hanno alcun indirizzo in comune (confronto eseguito con allow_url_mismatch)."
          }
        },
        "description": "Riepilogo del confronto."
      },
      "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": "Coppie indeterminate i cui due lati hanno restituito lo stesso stato di errore HTTP (ad esempio 404 in entrambe le acquisizioni); incluse in undetermined, conteggiate nel rapporto e non elencate."
          },
          "occurrences": {
            "type": "integer",
            "description": "Tutti i risultati di questa provenienza (sito o terze parti): occorrenze nella pagina."
          },
          "distinct": {
            "type": "object",
            "description": "Il numero di immagini diverse tra i risultati, in totale e per categoria (un'immagine mostrata più volte conta una sola volta): ogni risultato corrisponde allo SHA-256 del suo lato After, o del suo lato Before quando non ne ha uno, oppure all'indirizzo di quel lato quando non riporta uno 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": "Conteggi per categoria, come occorrenze nella pagina (un'immagine mostrata tre volte conta tre volte): unchanged, replaced (stesso indirizzo, byte diversi), moved (indirizzo diverso, stessi byte; segnalato, non conteggiato come modifica), removed, added, undetermined (un'immagine senza SHA-256 su uno dei due lati, mai conteggiata come removed o added); inoltre le immagini abbinate il cui testo alternativo o la cui posizione tra le immagini della pagina è cambiata, le coppie indeterminate con lo stesso errore HTTP su entrambi i lati e le immagini distinte."
      },
      "BatchRequest": {
        "type": "object",
        "required": [
          "urls"
        ],
        "properties": {
          "urls": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uri",
              "maxLength": 2048
            },
            "minItems": 2,
            "maxItems": 50,
            "description": "Da 2 a 50 URL, ciascuno convalidato come CaptureRequest.url. Al momento dell'accettazione del lotto non viene eseguito alcun controllo DNS sui nomi host; un URL che non supera il controllo DNS o dell'indirizzo fallisce in seguito con stato failed (e capture.failed)."
          },
          "options": {
            "$ref": "#/components/schemas/CaptureOptions"
          },
          "private_note": {
            "allOf": [
              {
                "$ref": "#/components/schemas/PrivateNote"
              }
            ],
            "description": "Una nota privata memorizzata su ogni acquisizione del lotto. La propria nota privata sull'acquisizione (ad esempio \"mobile capture\"), al massimo 200 caratteri, testo semplice (la regola PlainText). Viene memorizzata solo nel record dell'acquisizione: NON fa parte del pacchetto di prove sigillato (non è nello ZIP, nel PDF, nel manifest né in alcun hash) e non ha alcun valore probatorio, quindi può essere modificata o cancellata in seguito con PATCH /v1/captures/{capture_id} senza incidere sulla prova. Solo l'autore dell'acquisizione la legge. Ometterla, oppure inviare null o una stringa vuota, per non avere alcuna nota."
          }
        }
      },
      "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 quando non è stato possibile avviare l'acquisizione.",
                  "enum": [
                    "processing",
                    "failed"
                  ]
                }
              }
            }
          },
          "not_admitted": {
            "type": "array",
            "description": "Solo quando è stato raggiunto il limite di acquisizioni contemporanee dell'account: gli URL non avviati, nell'ordine di invio. Non costano nulla (nessuna unità del limite di frequenza né unità mensile); inviarli di nuovo in seguito. Non presente quando tutti gli URL sono stati avviati.",
            "items": {
              "type": "object",
              "properties": {
                "url": {
                  "type": "string"
                },
                "code": {
                  "type": "string",
                  "description": "Perché l'URL non è stato avviato.",
                  "enum": [
                    "CONCURRENT_CAPTURE_LIMIT"
                  ]
                }
              }
            }
          },
          "in_flight_limit": {
            "type": "integer",
            "description": "Solo insieme a not_admitted: il numero di acquisizioni che l'account può eseguire contemporaneamente (developer 10, startup 25, growth 100, legal_solo e 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: firma valida e ogni file elencato presente con il suo SHA-256 registrato. modified: firma non valida oppure un file mancante o modificato. invalid: non è stato possibile verificare il pacchetto. null solo per un pacchetto importato che questa volta non è stato verificato di nuovo (`rechecked: false`, con `reason` e la verifica registrata al momento dell'importazione in `stored_verification`).",
            "enum": [
              "match",
              "modified",
              "invalid",
              null
            ]
          },
          "reason": {
            "type": "string",
            "description": "Solo invalid; per un pacchetto importato con `rechecked: false`, perché non è stato verificato di nuovo (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": "Solo la presenza di manifest.json.ots; la prova non viene verificata qui.",
                    "enum": [
                      "present",
                      "not_present"
                    ]
                  }
                }
              },
              "eidas_disig": {
                "type": "object",
                "properties": {
                  "status": {
                    "type": "string",
                    "description": "Presenza di manifest.json.tsr. Il token stesso viene verificato in checks.eidas. missing: manifest.json registra eidas_requested (il pacchetto è stato emesso con il token), ma manifest.json.tsr non è presente nel pacchetto.",
                    "enum": [
                      "present",
                      "missing"
                    ]
                  }
                },
                "nullable": true,
                "description": "null quando non c'è manifest.json.tsr e manifest.json non registra eidas_requested."
              },
              "proofsnap_issuance": {
                "type": "object",
                "properties": {
                  "status": {
                    "type": "string",
                    "description": "issued_by_proofsnap: l'hash del manifest si trova nel registro di emissione di ProofSnap. unverified: non si trova (i pacchetti emessi prima dell'esistenza del registro non vi compaiono). unknown: la ricerca non è riuscita.",
                    "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 quando non c'è manifest.json.tsr e manifest.json non registra eidas_requested. Il token di validazione temporale eIDAS così come verificato da ProofSnap, su richiesta, a partire dal pacchetto memorizzato. Si tratta di una verifica eseguita da ProofSnap; il campo note indica come verificare il token in modo indipendente.",
                "properties": {
                  "status": {
                    "type": "string",
                    "enum": [
                      "verified",
                      "verified_revocation_unknown",
                      "failed",
                      "unavailable",
                      "missing"
                    ],
                    "description": "missing: manifest.json registra eidas_requested: true, quindi il pacchetto è stato emesso con una validazione temporale elettronica qualificata, ma manifest.json.tsr non è presente (la risposta riporta allora l'avviso EIDAS_TOKEN_MISSING). verified: tutti i controlli sono stati superati e lo stato del certificato della TSA registrato al momento dell'emissione è noto (good, oppure revocato dopo l'ora del token per un motivo diverso dalla compromissione della chiave). verified_revocation_unknown: tutti i controlli eseguiti sono stati superati, ma non è stato possibile stabilire lo stato del certificato della TSA dai dati registrati (revocation.status unknown o not_checked). failed: almeno un controllo non è stato superato, vedere failures (la risposta riporta allora l'avviso EIDAS_TOKEN_FAILED). unavailable: nessun verdetto sul token (reason: not configured, timeout, input_rejected quando il verificatore ha rifiutato l'input, ad esempio dati di convalida troppo grandi, inconclusive); non se ne deduce nulla sul token."
                  },
                  "reason": {
                    "type": "string",
                    "description": "Solo unavailable: timeout, input_rejected, inconclusive, oppure una breve spiegazione."
                  },
                  "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'ora indicata nel token (TSTInfo genTime)."
                  },
                  "tsa_name": {
                    "type": "string",
                    "nullable": true,
                    "description": "Ricavato dal certificato del firmatario (nome comune e organizzazione), mai un valore predefinito."
                  },
                  "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": "Il momento alla cui data è stata verificata la catena: il genTime del token, così un certificato scaduto dopo l'emissione del token non ne determina il fallimento."
                  },
                  "chain": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "subject": {
                          "type": "string"
                        },
                        "sha256": {
                          "type": "string"
                        }
                      }
                    }
                  },
                  "revocation": {
                    "type": "object",
                    "description": "Stato del certificato della TSA in base ai dati registrati al momento dell'emissione (basis recorded_at_issuance), verificato tramite firma e richiesto come valido al genTime. Non scaricato in tempo reale.",
                    "properties": {
                      "status": {
                        "type": "string",
                        "enum": [
                          "good",
                          "revoked",
                          "revoked_after_gen_time",
                          "unknown",
                          "not_checked"
                        ],
                        "description": "revoked_after_gen_time: revocato dopo l'ora del token per un motivo diverso dalla compromissione della chiave (il token resta verificato). not_checked: nessun dato di revoca è stato registrato o verificato."
                      },
                      "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": "Perché lo stato è sconosciuto."
                      }
                    }
                  },
                  "checks": {
                    "type": "object",
                    "description": "Ogni controllo: passed, failed, not_applicable (nonce: nessuno registrato per un token memorizzato), unknown (revoca senza dati utilizzabili) o not_checked (un errore precedente ha interrotto il controllo).",
                    "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": "Solo input_rejected: che cosa ha rifiutato il verificatore."
                  }
                }
              }
            },
            "description": "Assente quando verdict è invalid. Il verdetto dipende solo da manifest_signature e declared_files; eidas è riportato accanto e, in caso di esito negativo, in warnings."
          },
          "warnings": {
            "type": "array",
            "description": "Presente solo quando un elemento esterno al verdetto richiede attenzione. EIDAS_TOKEN_FAILED: manifest.json.tsr non ha superato la verifica in checks.eidas. EIDAS_TOKEN_MISSING: manifest.json registra eidas_requested, ma manifest.json.tsr non è presente nel pacchetto.",
            "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": "Pagina da acquisire, convalidata come CaptureRequest.url.",
            "format": "uri",
            "maxLength": 2048
          },
          "mode": {
            "type": "string",
            "description": "interval: ogni interval_seconds. cron: secondo cron_expr. on_change: controlla la pagina ogni interval_seconds ed esegue l'acquisizione solo quando il suo testo è cambiato.",
            "enum": [
              "interval",
              "cron",
              "on_change"
            ]
          },
          "interval_seconds": {
            "type": "integer",
            "description": "Obbligatorio per interval e on_change. Almeno il minimo del piano (300 su developer, startup, growth, custom, legal_solo e legal_seats; 86400 su website_archive), altrimenti 400 INTERVAL_TOO_SHORT.",
            "minimum": 300
          },
          "cron_expr": {
            "type": "string",
            "description": "Obbligatorio per cron (al massimo 200 caratteri). Un'espressione non interpretabile restituisce 400 INVALID_CRON; due esecuzioni consecutive più ravvicinate del minimo del piano restituiscono 400 INTERVAL_TOO_SHORT.",
            "maxLength": 200,
            "example": "0 9 * * 1-5"
          },
          "timezone": {
            "type": "string",
            "description": "Fuso orario IANA utilizzato per valutare cron_expr. Predefinito UTC.",
            "example": "Europe/Berlin"
          },
          "webhook_url": {
            "type": "string",
            "description": "URL https. Le esecuzioni interval e cron inviano capture.completed o capture.failed; on_change invia monitor.change_detected. L'URL deve essere https e non deve puntare a un indirizzo privato, di loopback, link-local, NAT di livello carrier o di metadati cloud (le stesse regole dell'URL di un'acquisizione; altrimenti 422 SSRF_BLOCKED). Un valore vuoto o composto solo da spazi significa nessun webhook. Una pianificazione ha un solo webhook_url, al livello superiore: un options.webhook_url non viene memorizzato (quando non viene inviato alcun webhook_url di livello superiore, viene usato come tale), quindi una pianificazione on_change non invia mai anche capture.completed. Richiede una chiave API: se inviato da una sessione della dashboard viene rifiutato con 400 WEBHOOK_REQUIRES_API_KEY, perché i webhook sono firmati con il secret di firma del webhook della chiave.",
            "format": "uri"
          },
          "js_render": {
            "type": "boolean",
            "description": "Solo on_change: controlla la pagina con un rendering completo nel browser invece di un semplice recupero HTTP (per le pagine che richiedono JavaScript).",
            "default": false
          },
          "options": {
            "$ref": "#/components/schemas/CaptureOptions"
          },
          "max_capture_age_seconds": {
            "type": "integer",
            "nullable": true,
            "minimum": 86400,
            "maximum": 34560000,
            "description": "Solo on_change: esegue anche un'acquisizione completa (che invia capture.completed) quando l'acquisizione più recente della pianificazione è più vecchia di questo valore, anche se la pagina non è cambiata. Per le regole che richiedono una copia almeno ogni N giorni, ad esempio 7776000 per 90 giorni. Da 1 giorno a 400 giorni e più di interval_seconds; null (predefinito) esegue acquisizioni solo in caso di modifica. PATCH con null la rimuove."
          },
          "alert_on": {
            "type": "string",
            "enum": [
              "any",
              "text"
            ],
            "nullable": true,
            "description": "Solo on_change (2026-10-14): quale modifica rilevata invia monitor.change_detected. any (predefinito, il comportamento precedente): ogni acquisizione di modifica. text: solo quando il confronto con l'acquisizione precedente rileva che il testo visibile è cambiato (diff_summary.text_verdict changed); un'acquisizione di modifica il cui confronto indica che il testo non è cambiato o non è stato possibile determinarlo non invia nulla (l'acquisizione viene comunque eseguita, addebitata e invia capture.completed alle sottoscrizioni). In assenza di un risultato del confronto (nessun riferimento, il confronto non è riuscito) invia l'avviso. 400 MALFORMED_REQUEST con un'altra modalità o un altro valore. null per le pianificazioni interval e 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": "Il webhook della pianificazione (per una pianificazione meno recente che lo conservava in options.webhook_url, quel valore viene mostrato qui e options non lo contiene mai). Richiede una chiave API: se inviato da una sessione della dashboard viene rifiutato con 400 WEBHOOK_REQUIRES_API_KEY, perché i webhook sono firmati con il secret di firma del webhook della chiave."
          },
          "options": {
            "type": "object",
            "description": "Opzioni di acquisizione con i valori predefiniti applicati."
          },
          "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": "Perché la pianificazione è stata sospesa automaticamente: max_failures (5 esecuzioni fallite consecutive), no_billing, plan_downgrade (limiti del piano, o un piano Archive only), invalid_cron o key_revoked. null quando è attiva o sospesa dall'utente.",
            "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": "L'acquisizione COMPLETATA più recente della pianificazione (la sua versione attuale; last_capture_id è la più recente avviata)."
          },
          "current_captured_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "alert_on": {
            "type": "string",
            "enum": [
              "any",
              "text"
            ],
            "nullable": true,
            "description": "on_change: any oppure text (any per una pianificazione creata prima dell'opzione); null per interval e 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": "Voci del registro della catena di custodia."
          },
          "custody_head_hash": {
            "type": "string",
            "description": "sha256:<hex> della voce più recente del registro della catena di custodia.",
            "nullable": true
          },
          "seal": {
            "$ref": "#/components/schemas/MatterSeal",
            "description": "null fino alla sigillatura."
          },
          "member_uids": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "created_by": {
            "type": "string",
            "description": "Il titolare.",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "read_only": {
            "type": "boolean",
            "description": "Presente (true) su una pratica di uno studio il cui abbonamento è terminato o che è conservato come archivio dello studio: solo apertura, esportazione e revoca della condivisione."
          },
          "legal_hold": {
            "type": "boolean",
            "description": "Il blocco per controversia legale della pratica è attivo (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 testa della catena di custodia che è stata sigillata e inviata a OpenTimestamps."
          },
          "ots": {
            "type": "string",
            "description": "Prova OpenTimestamps, in esadecimale."
          },
          "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": "Il token ha superato la verifica di ProofSnap al momento della sigillatura (verificatore condiviso, stessi controlli del token di un'acquisizione)."
              },
              "revocation_status": {
                "type": "string",
                "enum": [
                  "good",
                  "revoked_after_gen_time",
                  "unknown",
                  "not_checked"
                ],
                "description": "Stato del certificato della TSA stabilito al momento della sigillatura dalla risposta OCSP o dalla CRL raccolta allora."
              },
              "validation": {
                "type": "object",
                "nullable": true,
                "description": "Il contenuto di eidas_validation.json registrato al momento della sigillatura, per una verifica offline del token: tsaCertChain, rootCaPem, ocspResponse, crlResponse (omesso e indicato in omitted quando troppo grande), ocspUrl, ocspFetchedAt, ocspStatus, crlUrl, verification. I sigilli creati prima del 2026-10-01 non lo hanno.",
                "additionalProperties": true
              }
            },
            "nullable": true,
            "description": "Validazione temporale eIDAS sullo stesso hash quando la distribuzione ha eIDAS configurato, il chiamante è in produzione e un token è stato ottenuto e ha superato la verifica, altrimenti null (vedere 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": "Assente nei sigilli creati prima del 2026-10-01. not_obtained: il sigillo contiene solo l'ancoraggio OpenTimestamps (eidas_reason token_refused, unavailable o test_key). Un timeout della marca temporale eIDAS non produce mai un sigillo: in tal caso la richiesta restituisce 503 EIDAS_SEAL_RETRY."
          },
          "eidas_reason": {
            "type": "string",
            "enum": [
              "token_refused",
              "unavailable",
              "test_key",
              "account_limit"
            ],
            "description": "Solo not_obtained. test_key: sigillato da un chiamante di prova (chiave API di prova o sessione Sandbox), quindi non è stata richiesta alcuna validazione temporale eIDAS. account_limit: l'account non ha ancora una fattura pagata e ha raggiunto il suo limite eIDAS (20 al giorno UTC, 50 al mese 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": "Ad esempio matter.created, matter.updated, capture.added, capture.removed, capture.legal_hold_placed, capture.legal_hold_released, member.added, member.removed, package.imported (un pacchetto di prove importato con matter_id; target è l'ID dell'acquisizione)."
          },
          "target": {
            "type": "string",
            "nullable": true
          },
          "prev_hash": {
            "type": "string"
          },
          "entry_hash": {
            "type": "string"
          },
          "details": {
            "type": "object",
            "description": "Solo package.imported: manifest_sha256, zip_sha256, package_kind, verdict, levels. Memorizzati con la voce ma NON coperti da entry_hash.",
            "additionalProperties": true
          }
        },
        "description": "Un anello della catena di hash. entry_hash copre seq, ts, actor, action, target e 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": "Ad esempio capture.downloaded, export.generated, share.created, share.revoked."
          },
          "target": {
            "type": "string",
            "nullable": true
          },
          "source_ip": {
            "type": "string",
            "nullable": true,
            "description": "Solo titolare della pratica (assente per i membri)."
          },
          "user_agent": {
            "type": "string",
            "nullable": true,
            "description": "Solo titolare della pratica (assente per i membri)."
          }
        },
        "description": "Evento di accesso. Non fa parte della catena di hash."
      },
      "CertifyResult": {
        "type": "object",
        "properties": {
          "certify_id": {
            "type": "string",
            "example": "cert_a1B2c3D4e5F6g7H8"
          },
          "status": {
            "type": "string",
            "enum": [
              "completed"
            ]
          },
          "hash_sha256": {
            "type": "string",
            "description": "Solo certificato singolo."
          },
          "combined": {
            "type": "boolean",
            "description": "Solo certificato combinato."
          },
          "file_count": {
            "type": "integer",
            "description": "Solo certificato combinato."
          },
          "files": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "name": {
                  "type": "string",
                  "description": "L'etichetta inviata.",
                  "nullable": true
                },
                "sha256": {
                  "type": "string"
                }
              }
            },
            "description": "Solo certificato combinato."
          },
          "label": {
            "type": "string",
            "description": "La Sua etichetta, oppure \"N files\" per un certificato combinato.",
            "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 è nel pacchetto e attende un blocco Bitcoin. not_present: i calendari OpenTimestamps non erano raggiungibili, il pacchetto non contiene manifest.json.ots e manifest.json non lo elenca."
                  },
                  "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": "Link al verificatore pubblico ProofSnap.",
            "format": "uri"
          },
          "warnings": {
            "$ref": "#/components/schemas/Rule902DisabledWarnings"
          }
        }
      },
      "PlainText": {
        "type": "string",
        "description": "Regola del testo semplice per i campi brevi a testo libero che vengono stampati su documenti destinati al giudice o su pagine pubbliche (case_reference, case_id, case_note, valori di evidence_pack.custodian, case_reference di certify-hash, custode dell'oscuramento, dichiarazione di esportazione, nomi di studio e di pratica). Il valore viene normalizzato in NFC, gli spazi vengono compressi e rimossi alle estremità, e la lunghezza è contata in caratteri (code point Unicode). Rifiutati: caratteri di controllo (C0, C1), incorporamenti, override e isolamenti bidi (da U+202A a U+202E, da U+2066 a U+2069), segni di direzione (U+200E, U+200F, U+061C), caratteri a larghezza zero e invisibili (U+200B, da U+2060 a U+2064, U+FEFF, U+00AD, U+034F, U+180E, da U+206A a U+206F, da U+FFF9 a U+FFFB), i caratteri di riempimento hangul (U+115F, U+1160, U+3164, U+FFA0), i caratteri tag (da U+E0000 a U+E007F), i surrogati isolati e i caratteri a uso privato. Sono accettate le lettere di tutte le scritture, comprese le sillabe hangul, l'arabo, l'ebraico, il devanagari e i caratteri CJK, e U+200C / U+200D (ZWNJ, ZWJ) sono ammessi perché la corretta ortografia di diverse scritture li richiede."
      },
      "EidasTimestamp": {
        "type": "object",
        "description": "Esito eIDAS quando include_eidas è stato richiesto con una chiave di produzione; assente quando non è stato richiesto. confirmed: il pacchetto contiene manifest.json.tsr (addebitato, a meno che revocation non sia unknown). not_obtained: non è stato fornito alcun token e non è stato addebitato nulla; il pacchetto contiene solo l'ancoraggio OpenTimestamps (reason token_refused: un token è stato restituito ma non ha superato la verifica al momento dell'emissione; unavailable: non è stato possibile ottenere alcun token; time_budget: la richiesta non rientrava nel tempo rimasto, quindi non è stata eseguita, è stata rifiutata dal servizio di validazione temporale prima di qualsiasi richiesta all'autorità, oppure è stata interrotta, il che accade sulla route di oscuramento sincrona o al termine di un'acquisizione lunga; account_limit: l'account non ha ancora una fattura pagata e ha raggiunto il suo limite eIDAS di 20 marche al giorno UTC o 50 al mese UTC, conteggiate complessivamente su acquisizioni, batch, pianificazioni, certificazioni, confronti, oscuramenti e sigilli delle pratiche, quindi non è stata fatta alcuna richiesta; gli account con una fattura pagata non sono soggetti a limiti). Le righe completate prima del 2026-10-01 riportano solo confirmed.",
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "confirmed",
              "not_obtained"
            ]
          },
          "reason": {
            "type": "string",
            "enum": [
              "token_refused",
              "unavailable",
              "time_budget",
              "account_limit"
            ],
            "description": "Solo not_obtained."
          },
          "serial": {
            "type": "string",
            "nullable": true,
            "description": "Solo confirmed."
          },
          "issued_at": {
            "type": "string",
            "nullable": true,
            "description": "Solo confirmed."
          },
          "revocation": {
            "type": "string",
            "enum": [
              "unknown"
            ],
            "description": "Solo confirmed, e solo quando non è stato possibile stabilire lo stato del certificato della TSA al momento dell'emissione (il token ha superato tutti gli altri controlli). Il token viene fornito e non viene addebitato."
          },
          "billed": {
            "type": "boolean",
            "enum": [
              false
            ],
            "description": "Presente (false) insieme a revocation unknown: questo token non è stato addebitato."
          }
        }
      },
      "ExternalArchive": {
        "type": "object",
        "description": "Il risultato Internet Archive (Wayback Machine) di un'acquisizione. Internet Archive è un archivio pubblico di terze parti.",
        "required": [
          "requested"
        ],
        "properties": {
          "requested": {
            "type": "boolean",
            "description": "false quando non è stata fatta alcuna richiesta a Internet Archive (external_archive: false, una certificazione o un confronto, un'acquisizione eseguita prima che esistesse l'archiviazione, oppure la richiesta non rientrava nel tempo a disposizione). Tutti gli altri campi sono allora assenti."
          },
          "status": {
            "type": "string",
            "enum": [
              "created",
              "recent_existing",
              "created_after_request",
              "existing",
              "pending",
              "rate_limited",
              "failed",
              "unconfirmed",
              "unavailable",
              "skipped",
              "skipped_sensitive",
              "skipped_query"
            ],
            "description": "Risultato attuale. Per un job Save Page Now ancora in sospeso al momento della sigillatura, lo scheduler legge lo stato del job circa 5 e 30 minuti dopo la richiesta e questo valore diventa l'esito finale (created, recent_existing, failed, rate_limited, oppure unconfirmed quando non è arrivata alcuna risposta entro la finestra di controllo). created, recent_existing, created_after_request ed existing riportano 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": "Lo stato sigillato nel pacchetto (lo registrano metadata.json, forensic_log.json, chain_of_custody.json ed evidence.pdf). Il pacchetto stesso non cambia mai; coincide con status, a meno che un follow-up non abbia risolto un job in sospeso."
          },
          "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 copia pubblica della Wayback Machine, sempre https://web.archive.org/web/{snapshot_timestamp}/{url}."
          },
          "snapshot_timestamp": {
            "type": "string",
            "nullable": true,
            "description": "Timestamp Wayback YYYYMMDDhhmmss (UTC)."
          },
          "snapshot_offset_seconds": {
            "type": "integer",
            "nullable": true,
            "description": "Ora dello snapshot meno l'ora dell'acquisizione, in secondi (negativo: prima dell'acquisizione)."
          },
          "resolved_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "Quando il follow-up del pianificatore ha registrato il risultato definitivo; null quando lo stato sigillato era definitivo."
          },
          "job_id": {
            "type": "string",
            "nullable": true,
            "description": "ID del job Save Page Now (richieste autenticate)."
          },
          "follow_up_pending": {
            "type": "boolean",
            "description": "true finché il pianificatore deve ancora leggere lo stato del job."
          },
          "skip_reason": {
            "type": "string",
            "nullable": true,
            "description": "Perché l'URL non è stato inviato, quando status è skipped, skipped_sensitive o skipped_query: credentials_in_url, sensitive_path, opaque_path_segment, sensitive_fragment, sensitive_parameter_name, opaque_parameter_value, query_string (oppure lo stato stesso nelle righe meno recenti)."
          },
          "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 o pending così come letti dal job Save Page Now, null quando non è stato possibile leggerlo."
              },
              "snapshot_url": {
                "type": "string",
                "nullable": true,
                "description": "https://web.archive.org/web/<14 digits>/<URL>, null quando assente o non valido."
              },
              "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": "Perché non c'è una risposta del job: no_request_time (la riga non ha un'ora della richiesta da cui pianificare le letture), capture_deleted o capture_failed (l'acquisizione è stata spostata nel cestino o è fallita mentre il job era in sospeso, quindi non è stata seguita; un'acquisizione ripristinata dal cestino entro 30 minuti dalla richiesta viene seguita di nuovo). null negli altri casi."
              }
            },
            "description": "Il record di follow-up (cronologia), come proiezione fissa: snapshot_url solo quando è un URL di snapshot Wayback valido di snapshot_timestamp; reason solo quando il follow-up si è concluso senza una risposta del job."
          }
        }
      },
      "CaptureStorage": {
        "type": "object",
        "description": "Conservazione, copia duplicata e (acquisizioni pianificate) cronologia delle versioni.",
        "properties": {
          "retention": {
            "type": "object",
            "properties": {
              "years": {
                "type": "integer",
                "example": 10
              },
              "counted_from": {
                "type": "string",
                "format": "date-time",
                "nullable": true,
                "description": "Il più recente tra created_at e l'ultimo utilizzo noto: l'ultimo controllo che ha trovato la pagina invariata, oppure il momento in cui un'acquisizione più recente della stessa pianificazione ha sostituito questa."
              },
              "earliest_deletion_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true,
                "description": "counted_from più years, approssimativo: l'eliminazione avviene in un passaggio successivo del pianificatore, e controlli successivi possono spostarla. null sotto blocco per controversia legale."
              },
              "legal_hold": {
                "type": "boolean"
              }
            }
          },
          "duplicate_copy": {
            "type": "object",
            "properties": {
              "status": {
                "type": "string",
                "enum": [
                  "none",
                  "pending",
                  "stored",
                  "not_copied"
                ],
                "description": "stored: una seconda copia del pacchetto (e della sua prova OTS aggiornata) è conservata in un bucket separato in un'altra regione UE ed eliminata insieme all'originale. pending: copia in corso (minuti). none: pacchetti di prova e pacchetti precedenti all'esistenza del duplicato, finché non vengono copiati."
              },
              "copied_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              }
            }
          },
          "version": {
            "type": "object",
            "description": "Solo acquisizioni pianificate.",
            "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": "Primo controllo (on_change) che ha rilevato il testo della pagina invariato rispetto a questa acquisizione."
              },
              "unchanged_last_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "unchanged_checks": {
                "type": "integer",
                "description": "Controlli senza modifiche registrati (al massimo uno all'ora)."
              }
            }
          }
        }
      },
      "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 evento del catalogo (tag Webhooks)."
      },
      "WebhookSubscriptionFilters": {
        "type": "object",
        "description": "Al massimo uno tra host e schedule_id. La sottoscrizione riceve quindi solo gli eventi delle righe con quell'host (nome host in minuscolo dell'URL acquisito) o di quella pianificazione. `{}` per nessun filtro.",
        "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 a cui gli eventi vengono inviati con 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'ambiente delle credenziali che l'hanno creata. Una sottoscrizione live riceve solo gli eventi delle righe live, una sottoscrizione di prova solo quelli delle righe di prova."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "created_by": {
            "type": "string",
            "enum": [
              "key",
              "session"
            ],
            "description": "key: creata con una chiave API; la revoca di quella chiave disattiva la sottoscrizione (disabled_reason key_revoked)."
          },
          "consecutive_failures": {
            "type": "integer",
            "description": "Consegne non riuscite dall'ultimo successo."
          },
          "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 di errore dell'ultima consegna non riuscita (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 ha risposto con 410. failing: ogni consegna è fallita per 5 giorni senza alcun successo. key_revoked: la chiave API che l'ha creata è stata revocata. PATCH status active la riattiva."
          },
          "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 a cui gli eventi vengono inviati con 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'ambiente delle credenziali che l'hanno creata. Una sottoscrizione live riceve solo gli eventi delle righe live, una sottoscrizione di prova solo quelli delle righe di prova."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "created_by": {
            "type": "string",
            "enum": [
              "key",
              "session"
            ],
            "description": "key: creata con una chiave API; la revoca di quella chiave disattiva la sottoscrizione (disabled_reason key_revoked)."
          },
          "secret": {
            "type": "string",
            "description": "whsec_ seguito da base64 standard. Mostrato UNA SOLA VOLTA: ogni consegna a questa sottoscrizione è firmata con esso (Standard Webhooks). Conservarlo subito; non può essere riletto. Una ripetizione con Idempotency-Key entro 24 ore restituisce lo stesso corpo, secret incluso."
          }
        },
        "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": "Il corpo dell'evento così come lo ricevono le sottoscrizioni, senza subscription_id e senza link prefirmati (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": "La Sua nota privata sull'acquisizione (ad esempio \"mobile capture\"), al massimo 200 caratteri, testo semplice (la regola PlainText). Viene memorizzata solo nel record dell'acquisizione: NON fa parte del pacchetto di prove sigillato (non è nello ZIP, nel PDF, nel manifest né in alcun hash) e non ha alcun valore probatorio, quindi può essere modificata o svuotata in seguito con PATCH /v1/captures/{capture_id} senza incidere sulle prove. Solo l'autore dell'acquisizione la legge. Ometterla, oppure inviare null o una stringa vuota, per nessuna nota.",
        "example": "mobile capture"
      },
      "CaptureEvent": {
        "type": "object",
        "description": "Un evento della catena degli eventi di acquisizione dell'account. hash = SHA-256 (hex) del JSON canonico dei campi sottoposti a hash (v, chain, seq, type, capture_id, manifest_sha256, actor, access, detail, at, prev_hash; chiavi ordinate, nessuno spazio, valori assenti come null). prev_hash è l'hash dell'evento precedente, 64 zeri per seq 1.",
        "properties": {
          "v": {
            "type": "integer",
            "enum": [
              1
            ]
          },
          "chain": {
            "type": "string",
            "description": "L'account: l'uid per le acquisizioni personali, org_<org_id> per le acquisizioni di uno studio."
          },
          "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 del manifest.json dell'acquisizione, letto prima di qualsiasi eliminazione."
          },
          "actor": {
            "type": "string",
            "description": "L'uid dell'account che agisce, oppure system per le attività periodiche di pulizia del server."
          },
          "access": {
            "type": "string",
            "description": "author, company_owner, matter_owner oppure system."
          },
          "detail": {
            "type": "string",
            "nullable": true,
            "description": "legal_hold_on / legal_hold_off: l'origine del blocco per controversia legale (manual, bulk, matter:<id>, derived:<capture_id>), oppure per una revoca da parte del sistema (actor system) il suo codice motivo subscription_ended; s3_legal_hold: il risultato S3 (on, off, failed, unsupported, none); import: il tipo del pacchetto importato. Nessun testo libero."
          },
          "at": {
            "type": "string",
            "format": "date-time",
            "description": "Quando l'evento è stato scritto nella catena. Mai anteriore all'at dell'evento precedente."
          },
          "prev_hash": {
            "type": "string"
          },
          "hash": {
            "type": "string"
          },
          "occurred_at": {
            "type": "string",
            "format": "date-time",
            "description": "Quando l'azione stessa è stata confermata (la transazione della modifica di stato). Non sottoposto a hash. Normalmente coincide con at entro pochi millisecondi; è successivo solo quando l'aggiunta alla catena è stata ritentata dal server (l'azione viene registrata nella stessa transazione della modifica, quindi nessuna va persa)."
          }
        }
      },
      "CaptureEventAnchor": {
        "type": "object",
        "description": "Un ancoraggio OpenTimestamps giornaliero della catena: head_hash è l'hash dell'evento to_seq, che vincola crittograficamente tutti gli eventi precedenti.",
        "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 prova OpenTimestamps (file .ots, base64) su head_hash."
          },
          "ots_status": {
            "type": "string",
            "enum": [
              "pending",
              "bitcoin",
              "unconfirmed"
            ],
            "description": "pending: ots_proof_base64 contiene l'impegno dei calendari (ots verify interroga i calendari); bitcoin: è stato sostituito dalla prova aggiornata al blocco Bitcoin (bitcoin_block_height); unconfirmed: nessuna prova Bitcoin è stata restituita entro 14 giorni, la prova memorizzata si verifica comunque tramite i calendari."
          },
          "bitcoin_block_height": {
            "type": "integer",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ImportSummary": {
        "type": "object",
        "description": "I tre livelli di un pacchetto verificato.",
        "properties": {
          "kind_label_key": {
            "type": "string",
            "description": "Chiave dell'etichetta del tipo di pacchetto, 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": "Quando il registro dell'estensione ha registrato il pacchetto (provenance registered_by_extension_account)."
          },
          "issued_at": {
            "type": "string",
            "format": "date-time",
            "description": "Quando il registro di emissione dell'API ha registrato l'emissione (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": "Solo uploading: completare prima di questo momento (24 ore dopo la creazione)."
          },
          "finished_at": {
            "type": "string",
            "format": "date-time",
            "description": "Quando l'importazione ha raggiunto lo stato definitivo."
          },
          "capture_id": {
            "type": "string",
            "description": "Solo accepted: la nuova acquisizione (lo stesso id dell'importazione)."
          },
          "duplicate_of": {
            "type": "string",
            "description": "Solo duplicate: la Sua acquisizione che contiene già questo manifest."
          },
          "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": "Il tipo di pacchetto, una volta rilevato."
          },
          "summary": {
            "$ref": "#/components/schemas/ImportSummary"
          },
          "reasons": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "rejected ed expired: codici motivo, ad esempio 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. Solo codici, mai nomi di file."
          },
          "zip_sha256": {
            "type": "string",
            "description": "SHA-256 dello ZIP caricato, una volta calcolato."
          },
          "manifest_sha256": {
            "type": "string",
            "description": "SHA-256 del manifest.json del pacchetto (accettato e duplicato)."
          },
          "matter_id": {
            "type": "string",
            "description": "La pratica indicata al momento della creazione dell'importazione."
          },
          "matter_assignment": {
            "type": "string",
            "enum": [
              "assigned",
              "skipped"
            ],
            "description": "accepted con matter_id: se l'acquisizione è stata aggiunta alla pratica (saltata quando nel frattempo la pratica era stata sigillata, archiviata o non era più accessibile all'utente; l'importazione resta accepted)."
          },
          "upload": {
            "type": "object",
            "description": "Solo con ?upload=true finché status è uploading.",
            "properties": {
              "upload_id": {
                "type": "string"
              },
              "part_size": {
                "type": "integer"
              },
              "parts": {
                "type": "array",
                "description": "URL PUT prefirmati delle parti non ancora ricevute; inviare con PUT esattamente 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": "Il caricamento era già stato completato: inviare di nuovo complete."
              }
            }
          }
        }
      },
      "ImportVerification": {
        "type": "object",
        "description": "Il rapporto di verifica registrato al momento dell'importazione (nessun contenuto dei file, nessun dettaglio in testo libero).",
        "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'id riportato dal pacchetto (ps_..., FC-..., cap_..., cert_..., cmp_..., red_...)."
          },
          "manifest_sha256": {
            "type": "string"
          },
          "zip_sha256": {
            "type": "string"
          },
          "anchor_sha256": {
            "type": "string",
            "description": "L'hash coperto dalle validazioni temporali: quello del manifest oppure, per la certificazione di un singolo file, quello del file certificato."
          },
          "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": "Solo su un'acquisizione con 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": "Il captured_at di un pacchetto importato è l'ora dichiarata dal pacchetto, mai un'ora osservata da 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": "Presente solo quando la richiesta chiedeva il modello di certificazione (include_rule_902: true, oppure per un'acquisizione un evidence_pack.custodian non vuoto) mentre il modello è disattivato. La richiesta è stata accettata e il flag ignorato: non viene generato alcun rule_902_certification.pdf.",
        "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"
          ]
        }
      }
    }
  }
}
