Servizio REST per l'estrazione automatizzata di dati catastali dal portale SISTER dell'Agenzia delle Entrate. Utilizza Playwright per pilotare un browser headless e FastAPI per esporre gli endpoint.
Disclaimer legale — Questo progetto è uno strumento indipendente e non è affiliato, approvato o supportato dall'Agenzia delle Entrate. L'utente è l'unico responsabile del rispetto dei termini di servizio del portale SISTER e della normativa vigente. L'uso di automazione sul portale potrebbe violare i termini d'uso del servizio.
Warning
Per poter attivare le API bisogna prima registrarsi e chiedere l'accesso ai servizi sister utilizzando l'Area Personale di Agenzia delle Entrate e poi cercando "sister" tra i servizi disponibili. L'operazione è veloce.
- Panoramica
- Architettura
- Prerequisiti
- Avvio rapido
- Configurazione
- Endpoint API
- Esempi d'uso
- Logging e debug
- Dettagli tecnici
- Sviluppo e contribuzione
- Risoluzione dei problemi
- Autore
- Licenza
Visura API permette di interrogare i dati catastali italiani tramite una semplice interfaccia HTTP. Il flusso operativo è diviso in due fasi:
| Fase | Endpoint | Descrizione |
|---|---|---|
| 1 — Immobili | POST /visura |
Cerca gli immobili associati a foglio + particella |
| 2 — Intestati | POST /visura/intestati |
Recupera i titolari di uno specifico subalterno |
Entrambe le richieste vengono accodate ed eseguite sequenzialmente su un singolo browser autenticato al portale SISTER. I risultati si recuperano in polling con GET /visura/{request_id}.
- Autenticazione SPID automatizzata via provider Sielte ID (CIE Sign) con push notification
- Coda sequenziale — le richieste vengono processate una alla volta per non sovraccaricare il portale
- Ri-autenticazione automatica — alla scadenza della sessione, il servizio tenta prima un recovery diretto e, solo se necessario, un nuovo login SPID
- Keep-alive — la sessione viene mantenuta attiva con un light keep-alive ogni 30 secondi e un refresh profondo ogni 5 minuti
- Graceful shutdown — su
SIGINT/SIGTERMil servizio effettua il logout dal portale prima di chiudere il browser - Logging HTML completo — ogni pagina visitata dal browser viene salvata su disco per debug e audit
- Docker-ready — immagine pronta con tutte le dipendenze di sistema per Chromium headless
Il login automatizzato funziona esclusivamente con il provider Sielte ID (CIE Sign). Il flusso prevede l'approvazione via push notification sull'app MySielteID. Altri provider SPID non sono supportati e richiederebbero modifiche alla funzione login() in utils.py.
- Alcune città presentano strutture catastali particolari (sezioni urbane, mappe speciali) che possono causare risultati parziali.
- Se la particella non esiste nel catasto, il portale restituisce "NESSUNA CORRISPONDENZA TROVATA" e l'API ritorna una lista vuota con il campo
errorvalorizzato. - Gli immobili con partita "Soppressa" vengono inclusi nei risultati ma senza intestati.
Client HTTP
│
▼
┌──────────────────────────────────────────────────────┐
│ FastAPI (main.py) │
│ │
│ ┌─────────────┐ ┌──────────────────────────────┐ │
│ │ Endpoints │──│ VisuraService │ │
│ │ REST │ │ • asyncio.Queue │ │
│ └─────────────┘ │ • response_store (dict) │ │
│ │ • worker sequenziale │ │
│ └──────────┬───────────────────┘ │
│ │ │
│ ┌───────────────────────────▼───────────────────┐ │
│ │ BrowserManager │ │
│ │ • Playwright browser (Chromium headless) │ │
│ │ • Keep-alive task │ │
│ │ • Session recovery / re-login │ │
│ └───────────────────────────┬───────────────────┘ │
└──────────────────────────────┼───────────────────────┘
│
▼
┌──────────────────────────┐
│ Portale SISTER │
│ sister3.agenziaentrate │
│ .gov.it │
└──────────────────────────┘
| File | Descrizione |
|---|---|
main.py |
Applicazione FastAPI: endpoint, modelli Pydantic, BrowserManager, VisuraService, lifespan |
utils.py |
Automazione browser: login(), logout(), run_visura(), run_visura_immobile(), extract_all_sezioni(), PageLogger, parse_table() |
Dockerfile |
Immagine basata su python:3.11-slim con dipendenze per Chromium |
docker-compose.yaml |
Orchestrazione con healthcheck, volumi per log, restart automatico |
requirements.txt |
Dipendenze Python |
pyproject.toml |
Metadati di progetto e dipendenze opzionali di sviluppo |
- Python 3.11+ (testato fino a 3.13)
- Credenziali SPID tramite provider Sielte ID con app MySielteID configurata
- Convenzione SISTER attiva — l'utente deve avere un account abilitato sul portale SISTER
Per Docker:
- Docker Engine 20+
- Docker Compose v2
git clone https://github.com/zornade/visura-api.git
cd visura-api
cp .env.example .env
# Modifica .env con le tue credenziali SPID
docker-compose up -d
# Verifica che il servizio sia attivo
curl http://localhost:8000/healthgit clone https://github.com/zornade/visura-api.git
cd visura-api
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
playwright install chromium
cp .env.example .env
# Modifica .env con le tue credenziali SPID
uvicorn main:app --host 0.0.0.0 --port 8000All'avvio il servizio:
- Lancia un browser Chromium headless
- Esegue il login SPID — approva la notifica push sull'app MySielteID entro 120 secondi
- Naviga fino alla sezione Visure catastali del portale SISTER
- Avvia il keep-alive e il worker della coda
- Inizia ad accettare richieste su porta 8000
Crea un file .env nella root del progetto (vedi .env.example):
# Obbligatorio — Credenziali SPID (Sielte ID)
ADE_USERNAME=RSSMRA85M01H501Z # Codice fiscale
ADE_PASSWORD=la_tua_password
# Opzionale
LOG_LEVEL=INFO # DEBUG | INFO | WARNING | ERROR| Variabile | Obbligatoria | Default | Descrizione |
|---|---|---|---|
ADE_USERNAME |
✅ | — | Codice fiscale per il login SPID |
ADE_PASSWORD |
✅ | — | Password SPID (Sielte ID) |
LOG_LEVEL |
INFO |
Livello di log su console e file |
GET /health
{
"status": "healthy",
"authenticated": true,
"queue_size": 0
}POST /visura
Cerca tutti gli immobili su una particella catastale. Se tipo_catasto è omesso, vengono accodate due richieste (Terreni + Fabbricati).
Request body:
| Campo | Tipo | Obbligatorio | Default | Descrizione |
|---|---|---|---|---|
provincia |
string |
✅ | — | Nome della provincia (es. "Trieste") |
comune |
string |
✅ | — | Nome del comune (es. "TRIESTE") |
foglio |
string |
✅ | — | Numero foglio |
particella |
string |
✅ | — | Numero particella |
sezione |
string |
null |
Sezione censuaria (se presente) | |
tipo_catasto |
string |
null |
"T" = Terreni, "F" = Fabbricati. Se omesso: entrambi |
Esempio:
curl -X POST http://localhost:8000/visura \
-H "Content-Type: application/json" \
-d '{
"provincia": "Trieste",
"comune": "TRIESTE",
"foglio": "9",
"particella": "166",
"tipo_catasto": "F"
}'Risposta:
{
"request_ids": ["req_F_1709312400000"],
"tipos_catasto": ["F"],
"status": "queued",
"message": "Richieste aggiunte alla coda per TRIESTE F.9 P.166"
}Nota: per i Terreni (
T) gli intestati vengono estratti automaticamente. Per i Fabbricati (F) vengono restituiti solo gli immobili — per ottenere gli intestati di un singolo fabbricato, usa la Fase 2.
POST /visura/intestati
Estrae i titolari (intestati) di uno specifico immobile. Per i Fabbricati è necessario specificare il subalterno.
Request body:
| Campo | Tipo | Obbligatorio | Default | Descrizione |
|---|---|---|---|---|
provincia |
string |
✅ | — | Nome della provincia |
comune |
string |
✅ | — | Nome del comune |
foglio |
string |
✅ | — | Numero foglio |
particella |
string |
✅ | — | Numero particella |
tipo_catasto |
string |
✅ | — | "T" o "F" |
subalterno |
string |
Per F |
null |
Subalterno (obbligatorio per Fabbricati, vietato per Terreni) |
sezione |
string |
null |
Sezione censuaria |
Esempio:
curl -X POST http://localhost:8000/visura/intestati \
-H "Content-Type: application/json" \
-d '{
"provincia": "Trieste",
"comune": "TRIESTE",
"foglio": "9",
"particella": "166",
"tipo_catasto": "F",
"subalterno": "3"
}'Risposta:
{
"request_id": "intestati_F_3_1709312500000",
"tipo_catasto": "F",
"subalterno": "3",
"status": "queued",
"message": "Richiesta intestati aggiunta alla coda per TRIESTE F.9 P.166 Sub.3",
"queue_position": 1
}GET /visura/{request_id}
Recupera lo stato e i dati di una richiesta precedentemente accodata.
| Status | Significato |
|---|---|
processing |
La richiesta è in coda o in esecuzione |
completed |
Dati disponibili nel campo data |
error |
Errore — dettagli nel campo error |
Risposta completata (Fase 1):
{
"request_id": "req_F_1709312400000",
"tipo_catasto": "F",
"status": "completed",
"data": {
"immobili": [
{
"Foglio": "9",
"Particella": "166",
"Sub": "3",
"Categoria": "A/2",
"Classe": "5",
"Consistenza": "4.5",
"Rendita": "500,00",
"Indirizzo": "VIA ROMA 10",
"Partita": "12345"
}
],
"results": [
{
"result_index": 1,
"immobile": { },
"intestati": []
}
],
"total_results": 1,
"intestati": []
},
"error": null,
"timestamp": "2026-03-06T10:30:00"
}Risposta completata (Fase 2 — intestati):
{
"request_id": "intestati_F_3_1709312500000",
"status": "completed",
"data": {
"immobile": {
"Foglio": "9",
"Particella": "166",
"Sub": "3"
},
"intestati": [
{
"Nominativo o denominazione": "ROSSI MARIO",
"Codice fiscale": "RSSMRA85M01H501Z",
"Titolarità": "Proprietà per 1/1"
}
],
"total_intestati": 1
}
}Risposta con nessuna corrispondenza:
{
"request_id": "req_F_1709312400000",
"status": "completed",
"data": {
"immobili": [],
"results": [],
"total_results": 0,
"intestati": [],
"error": "NESSUNA CORRISPONDENZA TROVATA"
}
}POST /sezioni/extract
Estrae le sezioni censuarie per tutte le province e comuni d'Italia. Operazione molto lenta — può richiedere ore.
| Campo | Tipo | Default | Descrizione |
|---|---|---|---|
tipo_catasto |
string |
"T" |
"T" o "F" |
max_province |
int |
200 |
Numero massimo di province da processare (1–200) |
POST /shutdown
Esegue un shutdown controllato: logout dal portale SISTER e chiusura del browser.
# 1. Avvia l'estrazione dei fabbricati
curl -s -X POST http://localhost:8000/visura \
-H "Content-Type: application/json" \
-d '{"provincia":"Roma","comune":"ROMA","foglio":"100","particella":"50","tipo_catasto":"F"}' \
| jq .
# Salva il request_id dalla risposta, poi:
# 2. Polling risultati (ripeti fino a status != "processing")
curl -s http://localhost:8000/visura/req_F_1709312400000 | jq .
# 3. Prendi un subalterno dai risultati e chiedi gli intestati
curl -s -X POST http://localhost:8000/visura/intestati \
-H "Content-Type: application/json" \
-d '{"provincia":"Roma","comune":"ROMA","foglio":"100","particella":"50","tipo_catasto":"F","subalterno":"3"}' \
| jq .
# 4. Polling intestati
curl -s http://localhost:8000/visura/intestati_F_3_1709312500000 | jq .import requests, time
BASE = "http://localhost:8000"
def visura_completa(provincia, comune, foglio, particella, tipo="F", subalterno=None):
# Fase 1: immobili
r = requests.post(f"{BASE}/visura", json={
"provincia": provincia, "comune": comune,
"foglio": foglio, "particella": particella,
"tipo_catasto": tipo
}).json()
rid = r["request_ids"][0]
# Polling
while True:
res = requests.get(f"{BASE}/visura/{rid}").json()
if res["status"] != "processing":
break
time.sleep(5)
if res["status"] == "error":
raise Exception(res["error"])
immobili = res["data"]["immobili"]
print(f"Trovati {len(immobili)} immobili")
if not subalterno or tipo == "T":
return res["data"]
# Fase 2: intestati per uno specifico subalterno
r2 = requests.post(f"{BASE}/visura/intestati", json={
"provincia": provincia, "comune": comune,
"foglio": foglio, "particella": particella,
"tipo_catasto": tipo, "subalterno": subalterno
}).json()
rid2 = r2["request_id"]
while True:
res2 = requests.get(f"{BASE}/visura/{rid2}").json()
if res2["status"] != "processing":
break
time.sleep(5)
return res2["data"]
# Esempio
dati = visura_completa("Roma", "ROMA", "100", "50", tipo="F", subalterno="3")
print(dati)Il servizio produce due livelli di logging:
Scritto su stdout e su file in logs/visura.log. Contiene l'intero flusso operativo: login, navigazione, estrazione dati, errori.
# Avvia con log dettagliati
LOG_LEVEL=DEBUG uvicorn main:app --host 0.0.0.0 --port 8000Ogni pagina visitata dal browser viene salvata come file HTML su disco. Questo permette di ispezionare esattamente ciò che il browser ha visto in ogni punto del flusso — utile per debug, audit e sviluppo.
Struttura directory:
logs/pages/
└── 2026-03-06_16-28-24/ ← session_id (reset ad ogni avvio del server)
├── login/
│ ├── 01_goto_login.html
│ ├── 02_entra_con_spid.html
│ ├── 03_sielte.html
│ ├── ...
│ └── 15_conferma_lettura.html
├── visura/
│ ├── 01_scelta_servizio.html
│ ├── 02_provincia_applicata.html
│ ├── 03_immobile.html
│ ├── 04_ricerca.html
│ ├── 05_conferma_subalterno.html
│ ├── 06_risultati.html
│ └── 07_intestati_r1.html
├── visura_002/ ← seconda visura nella stessa sessione
│ └── ...
├── logout/
│ ├── 01_before_logout.html
│ └── 02_after_logout.html
└── recovery/
└── ...
Ogni file HTML include in testa dei commenti con metadati:
<!-- URL: https://sister3.agenziaentrate.gov.it/Visure/... -->
<!-- Step: ricerca -->
<!-- Timestamp: 2026-03-06T16:30:45 -->Privacy: la directory
logs/pages/è nel.gitignoreperché i file HTML contengono dati personali (codice fiscale, intestatari, indirizzi). Non committare mai questi file.
| Meccanismo | Intervallo | Descrizione |
|---|---|---|
| Light keep-alive | 30 secondi | Mouse move sulla pagina per evitare timeout idle |
| Session refresh | 5 minuti | Naviga a SceltaServizio.do e verifica che la sessione sia ancora attiva |
| Recovery | Su errore | Navigazione diretta → percorso interno → re-login SPID completo |
- Unica
asyncio.Queuecon worker sequenziale - Pausa di 2 secondi tra una richiesta e l'altra
- Pausa di 5 secondi dopo un errore
- I risultati restano in memoria (
response_store) fino al riavvio del servizio - Il client fa polling su
GET /visura/{request_id}— restituisce"processing"finché il risultato non è pronto
Quando uvicorn riceve SIGINT o SIGTERM:
- Il lifespan
shutdownviene invocato da uvicorn logout()clicca "Esci" sul portale SISTERclose()clicca "Torna al portale", chiude il browser context, chiude Chromium
Il browser viene lanciato con handle_sigint=False, handle_sigterm=False per impedire che Chromium intercetti i segnali prima che il logout sia completato.
- Naviga alla pagina di login dell'Agenzia delle Entrate
- Clicca "Entra con SPID" → seleziona provider Sielte ID
- Inserisce codice fiscale (con CapsLock attivo) e password
- Clicca "Prosegui" → seleziona invio notifica push
- Clicca "Autorizza" → attende fino a 120 secondi l'approvazione sull'app MySielteID
- Cerca "SISTER" tra i servizi → clicca "Vai al servizio"
- Verifica assenza di sessione bloccata ("Utente già in sessione")
- Naviga: Conferma → Consultazioni e Certificazioni → Visure catastali → Conferma Lettura
- Naviga a
SceltaServizio.do— seleziona provincia — clicca Applica - Clicca "Immobile" — seleziona tipo catasto (
T/F), comune, compila foglio e particella - Clicca "Ricerca" — gestisce eventuale "conferma assenza subalterno"
- Se "NESSUNA CORRISPONDENZA TROVATA" → ritorna risultato vuoto con
.error - Estrae la tabella immobili (
table.listaIsp4) - Per ogni immobile (radio button): seleziona → clicca "Intestati" → estrae tabella intestatari → torna indietro
- Gli immobili con
Partita = "Soppressa"vengono inclusi ma senza estrazione intestati
git clone https://github.com/zornade/visura-api.git
cd visura-api
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
pip install -e ".[dev]" # pytest, black, ruff
playwright install chromium
cp .env.example .env
# Configura le credenzialimain.py contiene:
- Modelli Pydantic di input (
VisuraInput,VisuraIntestatiInput,SezioniExtractionRequest) - Dataclass interne (
VisuraRequest,VisuraResponse,VisuraIntestatiRequest) - Eccezioni custom (
VisuraError,AuthenticationError,BrowserError,ValidationError) BrowserManager— gestione browser, login, keep-alive, session recoveryVisuraService— coda, worker, store risultati- Lifespan FastAPI (startup/shutdown)
- Tutti gli endpoint REST
utils.py contiene:
PageLogger— salva HTML di ogni pagina visitata, organizzato per sessione/flusso/steplogin(page, username, password)— flusso SPID completo (15 step, ciascuno loggato)logout(page)— cerca e clicca "Esci" con fallback su più selettori CSSrun_visura(page, ...)— visura completa: selezione provincia → estrazione intestatirun_visura_immobile(page, ...)— visura mirata per un singolo fabbricato con subalternoextract_all_sezioni(page, ...)— iterazione su tutte le province/comuni per estrarre sezionifind_best_option_match(page, selector, text)— fuzzy matching a 5 livelli su dropdown<select>parse_table(html)— parsing tabelle HTML con BeautifulSoup → lista di dizionari
Il login è implementato nella funzione login() di utils.py. Per supportare un altro provider:
- Modifica il selettore del provider (attualmente
a[href*="sielte"]) - Adatta il form di inserimento credenziali (ogni provider ha layout diversi)
- Gestisci il metodo di approvazione (push notification, OTP, etc.)
Quando aggiungi nuovi flussi o step, usa PageLogger:
logger = PageLogger("nome_flusso") # Crea logger per questo flusso
await logger.log(page, "nome_step") # Salva HTML della pagina correnteI file vengono numerati automaticamente (01_nome_step.html, 02_...). Flussi ripetuti nella stessa sessione ricevono un suffisso incrementale (visura, visura_002, visura_003, ...).
black . # formattazione automatica
ruff check . # controllo lintingpython -m pytest test_*.py -vdocker-compose up --build # build e avvio
docker-compose logs -f # segui i log
docker-compose down # stop e rimozione containerLeggi CONTRIBUTING.md per il dettaglio completo. In breve:
- Crea un branch dal
maincon un nome descrittivo (fix/...,feat/...) - Ogni modifica significativa deve includere i log
PageLoggernei punti critici - Mai committare file da
logs/— contengono dati personali - Rimuovi le credenziali dai log prima di condividerli in una issue
| Problema | Causa probabile | Soluzione |
|---|---|---|
| Il login non parte | Credenziali mancanti | Verifica ADE_USERNAME e ADE_PASSWORD nel file .env |
| Timeout su "Autorizza" | Push non approvata in tempo | Approva la notifica MySielteID entro 120 secondi |
| "Utente già in sessione" | Sessione precedente non chiusa | Attendi qualche minuto o chiudi manualmente dal portale |
| Sessione scaduta durante visura | Inattività prolungata | Il servizio tenta il recovery automatico; se fallisce, ri-esegue il login |
| "NESSUNA CORRISPONDENZA TROVATA" | Dati catastali inesistenti | Verifica foglio, particella, tipo catasto e comune |
| Risposte lente | Coda piena | Controlla queue_size con GET /health |
| Chromium non si avvia in Docker | Dipendenze di sistema mancanti | Usa il Dockerfile fornito che include tutte le librerie necessarie |
| Log HTML vuoti o mancanti | Errore durante il salvataggio | Controlla i permessi sulla directory logs/pages/ |
Per debug approfondito, ispeziona i file HTML in logs/pages/ — mostrano esattamente cosa vedeva il browser in ogni step.
Sviluppato da zornade.
Distribuito sotto licenza GNU Affero General Public License v3.0.
Ultimo aggiornamento: marzo 2026