Market Scanner integration
Dokicasa's Scanner APIs let you obtain deeds (visure), inspections, notes, cadastral maps and address searches. They are asynchronous: this guide shows how to receive results efficiently, avoiding polling when possible.
🎮 Try the API live
Open the interactive Playground: paste your token, fire each call in sandbox and watch socket and polling arrive in real time, test webhooks, and copy ready-to-use code in cURL, Python, Node, JavaScript and PHP.
| Base URL | https://api.dokicasa.it |
| Auth | Authorization: Bearer <api_token> |
| Content-Type | application/json |
Documentation and authentication
Interactive docs for all endpoints are on Swagger, Scanner section: api.dokicasa.it/api/documentation#/Scanner
Every call must be authenticated with a Bearer token:
Authorization: Bearer YOUR_API_TOKENWARNING
The token must never end up in public repos or the frontend: treat it like a password.
Main endpoints
Summary table — the full schema is on Swagger.
| Method | Path | What it does |
|---|---|---|
POST | /api/scanner/{type} | Create a scanner request (visura / inspection / map / list-addresses / …). Returns scanner_request_id. |
GET | /api/scanner-request/{id} | Request status + result. For PDFs you get the link result.pdf_path here (the file is NOT in the JSON: download it from the link); for list-addresses the data is already in the JSON. |
GET | /api/scanner-request/{id}/results | Structured results for "normal" searches (e.g. properties from an address). |
| WS | private-scanner_request.{id} | Private WebSocket channel for realtime updates. |
GET | /api/scanner/health | Service status: Sister is working, slow, down or in scheduled maintenance, also by scan type. Details |
Request states
ENQUEUED → SCANNING → SUCCESS
╲→ ERRORENQUEUED and SCANNING are transient; SUCCESS and ERROR are final.
Query types
Every query is a POST /api/scanner/{type}. Below are the available type values (the parameter schema of each is on Swagger, Scanner section).
Data lookups — the result is JSON in the result field:
type | What it does | Field in result |
|---|---|---|
search-fiscal-code | People with fiscal code from first name, last name and optional province (free) | anagrafiche |
search-company | Companies (legal entities) with fiscal code from company name and optional province (price list) | anagrafiche |
tax-data | Properties owned by a fiscal/VAT code (optional province and municipality) | immobili |
catasto | Properties from sheet / parcel | immobili |
intestatari-catasto | Owners of a parcel | intestatari |
list-addresses | Normalize an address → list of streets | addresses |
immobili-by-name | Nationwide properties from first/last name (2 steps) | immobili |
Documents — no API call returns the PDF: the result.pdf_path field holds the link to download it (see note below):
type | Document |
|---|---|
visura-catasto / visura-tax-data | Cadastral survey (from cadastral data / from fiscal-VAT code) |
ispezione-catasto / ispezione-tax-data | Mortgage inspection |
nota-catasto / nota-tax-data | Transcription note |
mappa-catasto | Cadastral map |
elaborato-planimetrico | Floor-plan document |
elenco-immobili | Property list |
IMPORTANT
catasto ≠ cadastral survey. catasto is the property lookup by sheet/parcel and returns the immobili list (JSON data). The cadastral survey PDF is instead visura-catasto (from cadastral data) or visura-tax-data (from fiscal/VAT code). Likewise the mortgage inspection is ispezione-catasto / ispezione-tax-data.
Current or historical survey
The two surveys do not behave the same way with tipo_visura:
| Call | tipo_visura | Survey you get |
|---|---|---|
visura-catasto (by property) | ANALITICA | historical, detailed |
SINTETICA | historical, summary | |
COMPLETA | current | |
visura-tax-data (by subject) | ANALITICA | SINTETICA | COMPLETA | current, by subject |
visura-tax-data + "storica": true | ANALITICA | historical by subject, detailed |
SINTETICA | historical by subject, summary |
WARNING
For the survey by property, COMPLETA does not mean "more data": it is the only one without history. For the survey by subject, instead, the historical one is requested only with "storica": true: without it, ANALITICA and SINTETICA return the current survey too.
Survey by subject: current or historical
- Current (default, no
storica): the properties the subject owns today in the given province. If they own nothing there today, Sister produces no survey. - Historical (
"storica": true): every property the subject has owned in the province, even if they no longer own it, with the changes of ownership. It is Sister's "Visura storica per soggetto": useful for sellers, deceased persons or to rebuild past holdings.
POST /api/scanner/visura-tax-data
{
"tax_code": "RSSMRA80A01H501U",
"provincia": "LT",
"tipo_visura": "ANALITICA",
"storica": true
}| Rule | Detail |
|---|---|
| Natural and legal persons | tax_code can be a fiscal code or a VAT number (companies) |
ANALITICA or SINTETICA only | with COMPLETA it returns 422 |
| One province per call | the survey covers the whole province given in provincia; if the subject owned properties in several provinces, make one call per province |
| Same price | it costs the same as a regular visura-tax-data |
| Timing | the historical survey goes through Sister's security code, solved automatically: it may take a few extra seconds |
Survey at a date: situazione_al
Sister's form has a "Situazione al" field (situation as of): the survey by subject shows the situation at that date instead of today. With situazione_al (optional, format YYYY-MM-DD) you request it at a past date. Without the parameter the date is today, as always.
POST /api/scanner/visura-tax-data
{
"tax_code": "RSSMRA80A01H501U",
"provincia": "LT",
"tipo_visura": "ANALITICA",
"storica": true,
"situazione_al": "2020-06-30"
}| Rule | Detail |
|---|---|
visura-tax-data only | on other calls the parameter has no effect; works with fiscal code and VAT number |
| Format | YYYY-MM-DD; a future date or another format returns 422 |
With storica | the typical case: who owned property at that date, even if they own nothing today |
| Same price | it costs the same as a regular visura-tax-data |
| Timing | with a date other than today Sister asks for its security code, solved automatically: a few extra seconds |
| Date too far back | if the digitized records hold nothing at that date, the response is Nessun documento trovato. |
| Documents already downloaded | the date is returned as situazione_al in the subject's document list (null = situation on the day of the request) |
Land only or buildings only: search_catasto_type
Like the "Catasto" menu of Sister's search by subject, search_catasto_type chooses which registry to query. Optional: without it, the search covers land and buildings, as always.
| Value | What it returns |
|---|---|
E (default) | land and buildings |
T | land only |
F | buildings only |
POST /api/scanner/tax-data
{
"tax_code": "RSSMRA80A01H501U",
"search_catasto_type": "F"
}| Rule | Detail |
|---|---|
| Where it applies | tax-data, visura-tax-data, immobili-by-name (both steps); fiscal code and VAT number |
| Values | E, T or F; any other value → 422 |
| Format | the immobili rows are the same: only which ones are there changes (catasto is T or F on each row) |
| Same price | it costs the same as the same search without the parameter |
| Documents already downloaded | in the subject's document list the survey reports catasto (E, T or F) |
NOTE
In searches by property (catasto, intestatari-catasto, visura-catasto, nota-catasto…) the same parameter is the registry of the property searched and only accepts F or T.
Province and municipality (optional). In tax-data you can narrow the properties down to a province and, within it, to a municipality. Without them the search covers all of Italy as always.
POST /api/scanner/tax-data
{
"tax_code": "RSSMRA80A01H501U",
"provincia": "RM",
"comune": "Colleferro"
}| Rule | Detail |
|---|---|
provincia | two-letter code (RM); Trento and Bolzano are not on Sister → 422 |
comune | municipality name, together with provincia (the same name exists in several provinces); or its ISTAT or cadastral code (H501), and then the province is not needed |
| Matching | accents, apostrophes and case are ignored (Lurago d Erba = LURAGO D'ERBA) |
| No properties | empty list, like a search with no results |
| Same price | it costs the same as the same search without filters |
List or document? The data lookups tax-data and immobili-by-name return only the subject's current properties, as a JSON list. Sister does not provide a list of past properties: the past is only available as a document, through the historical survey by subject above.
The cadastral map has its own explicit parameter, tipo_richiesta (ATTUALITA | STORICA).
TIP
Inspections and notes require the comune in the body: it is used to resolve the competent land registry office (conservatoria).
IMPORTANT
API calls never return the PDF: they only return the link result.pdf_path. The file is downloaded with a separate GET on that link, passing your Bearer token (authenticated endpoint, not a public anonymous link). The link is stable and permanent (it does not expire): you can store it and reuse it as a reference in your own workflow.
⚠️ With curl the response is binary (Content-Type: application/pdf): without -o curl prints nothing to the terminal (it looks like an empty "200"), but the file is there. Save it:
curl -o visura.pdf "<pdf_path>" -H "Authorization: Bearer <TOKEN>"search-fiscal-code — list of matching people (free)
Returns every person registered in the Land Registry with that first and last name in the given province (or in all of Italy when no province is sent), each with fiscal code, date and place of birth. The call is free: it does not charge the wallet and needs no price-list entry.
POST /api/scanner/search-fiscal-code:
{ "first_name": "MARIO", "last_name": "ROSSI", "provincia": "MI" }| Field | Required | Notes |
|---|---|---|
first_name | ✅ | first name (one of its words is enough: VERONICA also finds MARIA VERONICA) |
last_name | ✅ | last name, matched exactly (compound names too: DE LUCA) |
provincia | — | 2-letter code of the Land Registry office to search. Optional: without it the search is nationwide. TN and BZ are not allowed (tavolare land registry, excluded from the nationwide search too) |
birth_date | — | date of birth YYYY-MM-DD. Optional: narrows down the matches. People with no date of birth in the Land Registry stay in the list |
NOTE
Nationwide or by province? Without provincia the search covers all of Italy: the right choice when you don't know where the properties are. Very common surnames return many rows: if the search fails with too many results, repeat it with the province.
Too many results. Above 400 people Sister returns no list (it happens with very common names even in a single province, e.g. MARIA COLOMBO in MI). The request ends straight away in ERROR with details: "TOO_MANY_RESULTS", with no pointless retries: repeat it with birth_date (and the province, if you left it out).
When done (socket or polling) the result holds the list:
{
"immobili": [],
"anagrafiche": [
{
"cognome": "ROSSI", "nome": "MARIO",
"data_di_nascita": "17/05/1958", "luogo_di_nascita": "MILANO (MI)",
"sesso": "M", "codice_fiscale": "RSSMRA58E17F205X"
},
{
"cognome": "ROSSI", "nome": "MARIO",
"data_di_nascita": "02/11/1971", "luogo_di_nascita": "MONZA (MB)",
"sesso": "M", "codice_fiscale": "RSSMRA71S02F704Y"
}
]
}Use it to pick the right person among namesakes by comparing date and place of birth, and to get the fiscal code to use afterwards with tax-data.
TIP
Typos in the Land Registry. The name search is exact: if a person is registered with a misspelled surname (e.g. RISSI instead of ROSSI), searching ROSSI will not return them. Accents and apostrophes are normalised. The fiscal-code search (tax-data) only compares the fiscal code, so it finds the person even when the name is misspelled in the Registry. Recommended flow: search-fiscal-code to identify the person, then tax-data with their fiscal code.
search-company — companies by company name
The search-fiscal-code twin for legal entities: returns the companies registered in the Land Registry whose name contains the searched words, each with registered office and fiscal code. No deed is requested.
WARNING
Paid. Unlike search-fiscal-code, this search needs a SEARCH-COMPANY entry in your price list. Without it the call returns 403 PARTNER_PRICING_REQUIRED: contact Dokicasa to enable it.
POST /api/scanner/search-company:
{ "company_name": "ESEMPIO IMMOBILIARE", "provincia": "MI" }| Field | Required | Notes |
|---|---|---|
company_name | ✅ | company name, 2-150 characters. Accents and apostrophes are normalised |
provincia | — | 2-letter code. Optional: without it the search is nationwide. Not TN/BZ |
When done (socket or polling) the result holds the list:
{
"immobili": [],
"anagrafiche": [
{ "denominazione": "ESEMPIO IMMOBILIARE SRL", "sede": "MILANO (MI)", "codice_fiscale": "01234567890" },
{ "denominazione": "ESEMPIO IMMOBILIARE S.R.L. IN LIQUIDAZIONE", "sede": "ROMA (RM)", "codice_fiscale": "09876543210" },
{ "denominazione": "ESEMPIO IMMOBILIARE SRL CON SEDE IN MILANO VIA ESEMPIO 1", "sede": "", "codice_fiscale": "" }
]
}Good to know:
- The same company shows up more than once. Deeds spell the name in different ways (
C.I.M.E.P.,C I M E P, with or without the office): different rows with the samecodice_fiscale. Group by fiscal code before showing them. sedeandcodice_fiscalecan be empty on older records.- A single generic word is not enough. With
IMMOBILIAREalone the Registry does not search and the request ends inERRORwith "troppo generica: aggiungere un'altra parola della ragione sociale" (too generic, add another word). Above the Registry's result limit the request also ends inERROR: add a word or send the province. - Once you have the fiscal code, use
tax-datato get the company's properties.
immobili-by-name — two-phase search
immobili-by-name finds a person's properties nationwide starting from first and last name. Since there can be several namesakes, it works in two phases.
Phase 1 — list of namesakes
POST /api/scanner/immobili-by-name with only first and last name:
{ "first_name": "CHRISTIAN", "last_name": "CANNATA" }When it completes (via socket/polling, like every async call) the result holds the list of namesakes found:
{
"needs_selection": true,
"omonimi": [
{
"value": "9800054753#0#CANNATA#CHRISTIAN#CNNCRS91D08E625S#LIVORNO#08/04/1991#LI",
"nome": "CHRISTIAN", "cognome": "CANNATA",
"codice_fiscale": "CNNCRS91D08E625S",
"data_nascita": "08/04/1991", "luogo_nascita": "LIVORNO (LI)", "sesso": "M"
}
// … other namesakes
]
}needs_selection: true→ you must pick one person from the list.- If there is only one namesake the selection is automatic: you get the Phase 2 result directly.
- Phase 1 is free: you pay only once, at Phase 2 (or at Phase 1 when there is a single namesake and the properties come back straight away). Namesakes without a fiscal code also have their
valueand can be selected.
Phase 2 — properties of the chosen person
Call the same endpoint adding selected_value = the value field of the namesake chosen in Phase 1 (copy it verbatim):
{
"first_name": "CHRISTIAN",
"last_name": "CANNATA",
"selected_value": "9800054753#0#CANNATA#CHRISTIAN#CNNCRS91D08E625S#LIVORNO#08/04/1991#LI"
}When it completes the result holds:
{
"omonimo_selezionato": { "value": "…", "nome": "CHRISTIAN", "cognome": "CANNATA" },
"immobili": { "…": "…" },
"has_soppressi": false
}The properties are persisted: the full list (with owners) is retrieved paginated from GET /api/scanner-request/{id}/results.
NOTE
Both phases are regular async calls: the POST returns ENQUEUED, then via socket or polling you reach SUCCESS and read the result. Very common names may exceed Phase 1's result limit: narrow it down with more specific data.
How it works
All scanner calls are asynchronous. The flow is always the same, only how you receive the status update changes.
- Create the request —
POST /api/scanner/{type}→ you get ascanner_request_idand the initial stateENQUEUED. - Wait for completion — pick one of these (in order of preference):
- WebSocket — instant push, no polling.
- Webhook callback — our server calls yours when it finishes.
- Polling (fallback) — only if you can't use the two above.
- Get the result — when the state is
SUCCESS:- PDF (visure, inspections, maps, notes): the file is already in the
GET /api/scanner-request/{id}response. - Structured searches: call
GET /api/scanner-request/{id}/results.
- PDF (visure, inspections, maps, notes): the file is already in the
Realtime updates via WebSocket
⭐ Recommended for frontend apps
Recommended approach for web/mobile: no polling, instant updates, progress bars and push notifications.
Each request publishes events on a dedicated private channel:
private-scanner_request.{id}where {id} is the scanner_request_id returned by the initial POST.
Socket — host and key per environment
The socket is separate per environment: use the right host and key for where you call.
| Environment | API base URL | wsHost (socket) | key | authEndpoint |
|---|---|---|---|---|
| Sandbox / test | https://api-dev.dokicasa.it | api-dev.dokicasa.it | staging | https://api-dev.dokicasa.it/broadcasting/auth |
| Production | https://api.dokicasa.it | socket.dokicasa.it | ra9jcihuz0sx7vtw8yut | https://api.dokicasa.it/broadcasting/auth |
WARNING
Don't mix environments: connecting to the production socket while calling in sandbox (or vice versa) subscribes the channel but you receive no events (it's a different Reverb). The example below uses production values; for sandbox swap key, wsHost and authEndpoint with the Sandbox row.
Full example (JavaScript + Pusher/Reverb)
import Pusher from 'pusher-js'
const apiToken = 'YOUR_API_TOKEN'
const pusher = new Pusher('ra9jcihuz0sx7vtw8yut', {
wsHost: 'socket.dokicasa.it',
wsPort: 443,
wssPort: 443,
forceTLS: true,
cluster: 'mt1',
disableStats: true,
enabledTransports: ['ws', 'wss'],
authEndpoint: 'https://api.dokicasa.it/broadcasting/auth',
auth: { headers: { Authorization: 'Bearer ' + apiToken } },
})
const { scanner_request_id } = await fetch(
'https://api.dokicasa.it/api/scanner/intestatari-catasto',
{
method: 'POST',
headers: {
Authorization: 'Bearer ' + apiToken,
'Content-Type': 'application/json',
},
body: JSON.stringify({
comune: 'Roma', provincia: 'RM',
foglio: '123', particella: '45',
search_catasto_type: 'F',
}),
}
).then((r) => r.json())
const channel = pusher.subscribe(`private-scanner_request.${scanner_request_id}`)
channel.bind('ScannerRequestNotification', ({ message }) => {
const { status, percentage } = message
if (status === 'SCANNING') updateProgressBar(percentage)
if (status === 'SUCCESS') fetchResult(scanner_request_id)
if (status === 'ERROR') handleError(message)
})
async function fetchResult(id) {
const data = await fetch(`https://api.dokicasa.it/api/scanner-request/${id}`, {
headers: { Authorization: 'Bearer ' + apiToken },
}).then((r) => r.json())
console.log('Final result:', data)
}Webhook & signature
⭐ Recommended for backend integrations
Pass a URL when creating the request: when it finishes, Dokicasa POSTs the result to you. Delivery is async with retries — no setup in the panel.
There are two fields you can add to the initial POST body (either or both):
| Field (in POST body) | Payload you receive | When it fires |
|---|---|---|
callback | compact: { id, type, status, percentage, result, created_at, updated_at, finished_at } | when the scan finishes |
webhook | full: { scanner_request: { …whole request object, with paramsandresult… } } | on the completion event |
{
"type": "catasto",
"comune": "Roma", "foglio": "123", "particella": "45", "search_catasto_type": "F",
"callback": "https://your-server.com/dokicasa/scanner-callback",
"webhook": "https://your-server.com/dokicasa/scanner-webhook"
}Municipalities split into cadastral sections
In some municipalities — Rome, Naples, Genoa — sheet and parcel alone are not enough to identify a property: the cadastral section is needed too. Add sezione_comune to the request in those cases.
{
"type": "catasto",
"comune": "Napoli", "provincia": "NA",
"sezione_comune": "SAN FERDINANDO",
"foglio": "123", "particella": "45"
}It is optional and takes part in identifying the property together with foglio, particella, subalterno and comune: two properties sharing a parcel but sitting in different sections stay distinct. Omitting it in a municipality that uses sections may return the wrong property, or none.
The field is named sezione_comune: a sezione key is ignored without error.
The result you receive has a type-dependent shape — see Result format by type.
callback vs webhook — what's the difference
Same mechanism (a signed POST to your URL when processing finishes, with retries): only the payload shape differs.
{
"id": 2615190,
"type": "VISURA-CATASTO",
"status": "SUCCESS",
"percentage": 98,
"result": { "pdf_path": "https://.../sample-visura-catasto", "is_empty": false },
"created_at": "2026-07-02T23:22:59+02:00",
"updated_at": "2026-07-02T23:23:02+02:00",
"finished_at": "2026-07-02T23:23:02+02:00"
}{
"scanner_request": {
"id": 2615190,
"user_id": 5128,
"type": "VISURA-CATASTO",
"params": { "comune": "ROMA", "provincia": "RM", "foglio": "123", "particella": "45", "tipo_visura": "ANALITICA" },
"result": { "pdf_path": "https://.../sample-visura-catasto", "is_empty": false },
"status": "SUCCESS"
}
}callback | webhook | |
|---|---|---|
| Payload | compact, flat fields | full scanner_request object |
Includes input params | No | Yes |
Signature header (HMAC) | Yes | Yes |
| When it fires | when the scan finishes | when the scan finishes |
Which to use
callback→ lightweight payload: you just react to the outcome (id,status,result). The most common choice.webhook→ the whole record, including the original requestparams: handy to reconcile/log without keeping state on your side.
They're independent: pass one, the other, or both (you then receive two separate POSTs). Both signed with the Signature header.
Signature — verify authenticity
Every webhook includes a Signature header: the HMAC-SHA256 of the raw body, signed with your api_token (the same one you use as Bearer). So you don't manage any new key — you verify with your own api key.
Signature = hash_hmac('sha256', <raw_body>, <your_api_token>)$raw = file_get_contents('php://input'); // RAW body, not re-serialized
$expected = hash_hmac('sha256', $raw, $MY_API_TOKEN);
if (!hash_equals($expected, $_SERVER['HTTP_SIGNATURE'] ?? '')) {
http_response_code(401); exit; // invalid signature
}const crypto = require('crypto')
const raw = req.rawBody // RAW body (Buffer/string)
const expected = crypto.createHmac('sha256', MY_API_TOKEN).update(raw).digest('hex')
if (expected !== req.get('Signature')) return res.sendStatus(401)Sign the **raw** body
Compute the HMAC over the exact bytes you received, before parsing/re-serializing the JSON: re-encoding changes whitespace and key order, and the signature won't match.
Idempotency
On network errors delivery is retried: identify the request by its id in the body and always respond 2xx, even on duplicates, to avoid useless retries.
Result format by type
The result field (found in GET /api/scanner-request/{id}, and the same one delivered via socket and webhook) changes shape depending on the type of request. The main shapes are below, with real examples.
`result` vs `/results`
result= the full scan payload (properties, personal data, PDF…), ready to use.GET /api/scanner-request/{id}/results= a paginated view of the persisted properties only (with owners), handy when there are many. It applies to the types that list properties (catasto,tax-data,list-addresses,intestatari-catasto) and returns a Laravel paginator{ current_page, data: [...], links, ... }.
{
"has_soppressi": true,
"immobili": [
{
"foglio": "368", "particella": "435", "subalterno": "5",
"indirizzo": "VIA ESEMPIO n. 11 Interno 1 Piano S1 - T",
"comune": "ROMA", "provincia": "RM", "numero_civico": "11", "piano": "S1",
"categoria": "A02", "classe": "02", "consistenza": "6 vani", "rendita": "1239,50",
"catasto": "F", "codice_belfiore": "H501", "partita": "", "zona_cens": "004",
"scanner_immobili_id": 82659
},
{
"foglio": "368", "particella": "435", "subalterno": "24",
"indirizzo": "VIA ESEMPIO n. 19 Piano T",
"comune": "ROMA", "provincia": "RM", "numero_civico": "19", "piano": "T",
"categoria": "C01", "classe": "10", "consistenza": "46 m2", "rendita": "3594,44",
"catasto": "F", "codice_belfiore": "H501", "partita": "", "zona_cens": "004",
"scanner_immobili_id": 82678
}
]
}{
"intestatari": [
{
"nome_cognome": "ROSSI MARIO", "codice_fiscale": "RSSMRA80A01H501U",
"quota": "1/2", "titolarita": "Proprieta'",
"luogo_nascita": "ROMA", "data_nascita": "01/01/1980",
"comune": "ROMA", "provincia": "RM"
},
{
"nome_cognome": "ESEMPIO IMMOBILIARE SRL", "codice_fiscale": "01234567890",
"quota": "1/2", "titolarita": "Nuda proprieta'",
"luogo_nascita": "", "data_nascita": "",
"comune": "ROMA", "provincia": null
}
]
}{
"immobili": [],
"anagrafiche": [
{
"cognome": "ROSSI", "nome": "MARIO",
"data_di_nascita": "17/05/1980", "luogo_di_nascita": "ROMA (RM)",
"sesso": "M", "codice_fiscale": "RSSMRA80E17H501U"
}
]
}{
"immobili": [],
"anagrafiche": [
{ "denominazione": "ESEMPIO IMMOBILIARE SRL", "sede": "MILANO (MI)", "codice_fiscale": "01234567890" }
]
}{
"addresses": {
"123494##VIA ROMA": " VIA ROMA ",
"337696##VIA ROMAGNOSI": " VIA ROMAGNOSI ",
"12428##VIALE ROMAGNA": " VIALE ROMAGNA "
}
}{
"pdf_path": "https://.../visure/<request_id>.pdf",
"request_id": "…",
"is_nota_negativa": false,
"is_empty": false
}Where to find each individual call's schema
The input parameters and the exact type of each query (foglio/particella, tax_code, tipo_visura, etc.) are on Swagger, Scanner section: api.dokicasa.it/api/documentation#/Scanner. This page instead covers how to receive and interpret the results.
How to test the integration
There is a sandbox environment — https://api-dev.dokicasa.it — that mirrors the production API but makes no real queries: every call returns realistic sample data, consumes no credit and produces no real documents. Compared to production, only the base URL changes: endpoints, parameters, response format, WebSocket and webhooks are identical.
🔑 Test API key (sandbox)
Use this token: it's a user with every call enabled and free, made specifically for testing. In sandbox the origin whitelist is disabled, so you can call from localhost or any test domain.
HfBfYShns3E02no3kkwCRWQugkVNcJCtrRN2BPoI5NxhqsiXaxxnSPpIHY2OSend it as the Authorization: Bearer <token> header on every call.
Already loaded in the Playground
In the interactive Playground this token is pre-filled (and copyable): open it, hit Run and watch socket + polling live, with zero setup.
Endpoints to test
API — base https://api-dev.dokicasa.it
| What | Endpoint |
|---|---|
| Start a search | POST /api/scanner/{type} → { scanner_request_id, status } |
| Status + result | GET /api/scanner-request/{id} · …/{id}/status |
| Paginated results | GET /api/scanner-request/{id}/results |
| Create monitoring | POST /api/monitoring/from-catasto |
| Simulate webhook (sandbox only) | POST /api/user-monitorings/{id}/simulate-webhook |
Socket — sandbox (see also Socket environments)
| Parameter | Value |
|---|---|
wsHost | api-dev.dokicasa.it |
key | staging |
authEndpoint | https://api-dev.dokicasa.it/broadcasting/auth |
| channel · event | private-scanner_request.{id} · ScannerRequestNotification |
Quick try (curl)
curl -X POST https://api-dev.dokicasa.it/api/scanner/list-addresses \
-H "Authorization: Bearer HfBfYShns3E02no3kkwCRWQugkVNcJCtrRN2BPoI5NxhqsiXaxxnSPpIHY2O" \
-H "Content-Type: application/json" \
-d '{"indirizzo":"via roma","comune":"Milano","provincia":"MI"}'
# → { "scanner_request_id": 12345, "status": "ENQUEUED" }
# then: GET /api/scanner-request/12345/status until status = SUCCESSWhat to verify:
- Async flow — the POST returns
ENQUEUED, then via socket (instant) or polling you reachSUCCESS. - Response shape — map the
resultfields (see Result format by type). - Webhook — point
webhook_urlat an inspector (webhook.site); in sandbox the monitoring sends a sample event immediately on creation (or re-trigger it with/simulate-webhook). - Signature — validate the
Signatureheader (see Signature section).
NOTE
In sandbox the data is representative and fixed (it does not vary with the input parameters): it shows you the result structure. When ready, switch to the production base URL and production socket (see the Socket environments table).
TIP
For your first tests use list-addresses or search-fiscal-code: they are fast and immediately show you the shape of result without needing specific cadastral data.
Property monitoring
Beyond one-off searches, you can monitor a property over time: Dokicasa periodically re-checks the cadastral owners and, when they change, sends you a webhook with the variation.
Start a monitoring
POST /api/monitoring/from-catasto — start straight from the cadastral data (no property id needed). If the property isn't in the system yet, it's first resolved with a cadastral search (owners included) and saved, then monitoring is activated.
{
"foglio": "123",
"particella": "45",
"subalterno": "7",
"comune": "Roma",
"provincia": "RM",
"webhook_url": "https://your-server.com/dokicasa/monitoring-changed"
}| Field | Required | Notes |
|---|---|---|
foglio, particella | ✅ | the cadastral "triplet" |
subalterno | — | numeric, optional |
comune | ✅ | cadastral code or town name |
provincia | ✅ | province code (no TN/BZ) |
search_catasto_type | — | F buildings (default) / T land |
sezione_comune | — | for towns with sections (Naples/Rome/Genoa) |
webhook_url | — | URL to receive changes (see below) |
name, note | — | free labels |
Response: { immobile_id, user_monitoring_id, created, scanned, last_monitoring, next_monitoring } (last_monitoring = last run, null if never; next_monitoring = next run).
Owners-change webhook
If you set webhook_url, on every owners change detected by the monitoring Dokicasa makes an async POST to that URL with the full change JSON:
{
"event": "immobile.owners_changed",
"user_monitoring_id": 12345,
"immobile": {
"id": 987, "foglio": "123", "particella": "45",
"subalterno": "7", "comune": "Roma", "provincia": "RM"
},
"changes": [
{
"date": "2026-07-01",
"differences": {
"added": [ { "quota": "1/2", "nome": "Mario", "cognome": "Rossi", "denominazione": null } ],
"removed": [ { "quota": "1/2", "nome": "Luigi", "cognome": "Verdi", "denominazione": null } ],
"modified": [ { "id": "CF:RSSMRA80A01H501U", "before": { "quota": "1/3" }, "after": { "quota": "1/2" } } ]
}
}
]
}NOTE
The webhook fires automatically from the monitoring cron, with retries on network errors. Respond 2xx to acknowledge receipt.
TIP
webhook_url is optional at creation: if you don't pass it, nothing is sent. You can add or change it later (see below).
Test the webhook in **sandbox** (staging)
On the test environment (https://api-dev.dokicasa.it) you don't have to wait for a real owners change:
- On creation, if you pass a
webhook_url, we send a sampleimmobile.owners_changedevent to that URL right away (the create response includessimulated_webhook_sent: true). - You can re-send it any time with
POST /api/user-monitorings/{id}/simulate-webhook.
The payload has the exact same format as the real event: point webhook_url to an inspector (webhook.site) to see it arrive. In production these helpers are disabled — the webhook only fires on a real owners change.
Listing your monitorings
GET /api/users/me/monitorings — returns your monitorings (paginated). Filters via query string:
| Parameter | Values | Notes |
|---|---|---|
filter.type | IMMOBILE (default) / SOGGETTO | monitoring type |
filter.status | ACTIVE, EXPIRED, CANCELLED | multiple allowed, comma-separated |
filter.source | API / CENSIMENTO | monitoring origin |
filter.change_owner | 1 | only those with owner changes |
filter.text | free text | searches notes + address/cadastral data |
sort_ends_at | ASC / DESC | sort by expiry (default: id DESC) |
page | number | page (paginated response) |
Need only the ids? GET /api/users/me/monitorings-ids.
NOTE
This is a read-only endpoint: it always works with your Bearer token, even without a configured origin whitelist. The default filter.type=IMMOBILE returns only property monitorings — for subjects pass filter.type=SOGGETTO.
Managing a monitoring
| Method | Path | What it does |
|---|---|---|
GET | /api/user-monitorings/{id} | Detail of the monitoring: monitoring data, linked property (model), current owners (model.soggetti), change events (events) and the owner reads history (proprietari_history). See example below. |
PATCH | /api/user-monitorings/{id}/webhook | Set or remove (pass null) the webhook_url. Body: { "webhook_url": "https://…" }. |
PATCH | /api/user-monitorings/{id} | Change status: ACTIVE, DISACTIVE (paused), CANCELED. |
POST | /api/user-monitorings/{id}/renew | Renew the monitoring. |
POST | /api/user-monitorings/{id}/simulate-webhook | Sandbox only: immediately sends a sample immobile.owners_changed event to the webhook_url, to test delivery without waiting for a real change. In production it returns 404. |
DELETE | /api/user-monitorings/{id} | Delete the monitoring. |
Example response — monitoring detail
{
"id": 6004,
"user_id": 5101,
"status": "ACTIVE",
"ends_at": "2026-07-10 11:08:01",
"model_type": "App\\Models\\ScannerImmobile",
"model_id": 2013602,
"model": {
"id": 2013602,
"comune": "Nardo'", "provincia": "LE",
"foglio": "129", "particella": "1474", "subalterno": "1",
"categoria": "A03",
"indirizzo_completo": "VIA CAVALIERI TEUTONICI n. 6 Piano T",
"soggetti": [
{
"denominazione": "COLELLA ANNA MARIA",
"codice_fiscale": "CLLNMR35P44F842Z",
"pivot": { "quota": "1/2", "titolarita": "Proprieta'" }
}
]
},
"events": [
{
"date": "2025-12-02",
"differences": {
"added": [ { "quota": "1/2", "denominazione": "BERNES ANGELO" } ],
"removed": [ { "quota": "", "denominazione": "AMATEIS DAVIDE" } ],
"modified": []
}
}
],
"proprietari_history": [
{
"date": "2025-11-02",
"read_at": "2025-11-02 00:00:00",
"scanner_request_id": 2071196,
"proprietari": [
{ "denominazione": "AMATEIS DAVIDE", "codice_fiscale": "MTSDVD82H03D208H", "quota": "", "titolarita": "" },
{ "denominazione": "COLELLA ANNA MARIA", "codice_fiscale": "CLLNMR35P44F842Z", "quota": "1/2", "titolarita": "Proprieta'" }
]
},
{
"date": "2025-12-02",
"read_at": "2025-12-02 00:00:00",
"scanner_request_id": 2241215,
"proprietari": [
{ "denominazione": "BERNES ANGELO", "quota": "1/2", "titolarita": "Proprieta'" },
{ "denominazione": "COLELLA ANNA MARIA", "quota": "1/2", "titolarita": "Proprieta'" }
]
}
]
}| Field | Contents |
|---|---|
model | Detail of the monitored property (municipality, sheet/parcel/sub, category, address…) |
model.soggetti | Current owners (with quota and titolarità in the pivot) |
events | Owner changes: diff per date (added / removed / modified) |
proprietari_history | Owner snapshot at every read (one entry per scanner_request, with date/read_at): the raw, non-deduplicated history of who the owners were at each check |
Use case: address search
You have an address and want the associated cadastral properties. Two phases: address resolution → properties.
list-addresses(address resolution) —POST /api/scanner/list-addresses. Wait via socket/webhook/polling. The normalized address list is already in theresultsfield ofGET /api/scanner-request/{id}— no need to call/results.- Properties from an address — use an address from step 1 as
address_valuein the properties scanner call (exact endpoint on Swagger, Scanner section). - Fetch results — when
SUCCESSarrives, callGET /api/scanner-request/{id}/resultsfor the property list.
Use case: visure, inspections, notes, maps (PDF)
All requests producing a PDF (cadastral visura, inspection, note, map) follow the same pattern.
- Create the request —
POST /api/scanner/{type}(type depends on the document, see Swagger). - Wait for completion — via WebSocket or webhook: state goes
ENQUEUED→SCANNINGup toSUCCESSorERROR. - Automatic retry on transient errors — if a temporary error occurs (line issues with external services), the system retries by itself every ~5 minutes. You don't need to resend anything. Credit is charged only on the first success.
- Get the PDF — the file is returned directly in the
GET /api/scanner-request/{id}response (no/results). The PDF is also sent automatically by email to the address bound to the token, so the end user gets it even without integrating the client-side download.
Service status (Sister)
Cadastral queries go through Sister, the Italian Revenue Agency portal, which now and then slows down, stops or goes into scheduled maintenance (usually announced with a banner like "il servizio è indisponibile dalle 17.00 alle 18:00 circa"). We check it every 10 minutes based on the real duration of scans and we read the maintenance notices published by Sister; you can use the result to warn your users or, when the service is down, route requests to manual handling instead of queueing them.
GET /api/scanner/health
Authorization: Bearer <token>Response fields
| Field | Type | Description |
|---|---|---|
status | string | ok | slow | down | unknown (see table below). |
accepting_requests | boolean | false only with status = down. This is the field to decide on. |
message | string | Ready-to-show message, in Italian. It may change: don't decide on it. |
since | string (ISO 8601) | null | Since when the problem has been going on. During maintenance it is the start time. null with ok. |
sister_checked_at | string (ISO 8601) | null | Last Sister check. |
maintenance | object | null | Scheduled maintenance in progress or announced (the next one). null if there is none. |
maintenance.in_progress | boolean | true if we are now inside the starts_at → ends_at window. |
maintenance.starts_at | string (ISO 8601) | Maintenance start. |
maintenance.ends_at | string (ISO 8601) | Expected end: at this time the Market Scanner restarts by itself. |
maintenance.scanner_blocked | boolean | true if maintenance is in progress and the Market Scanner is suspended. |
maintenance.message | string | Ready-to-show message, in Italian ("sara' in manutenzione dalle 17:00 alle 18:00" / "e' in manutenzione fino alle 18:00"). If the window ends the next day the date is included ("fino alle 02:00 del 18/09"). |
maintenance.sister_message | string | null | Original text of the Sister banner, as is. |
queue.enqueued | integer | Scans waiting (last 2 hours). |
queue.scanning | integer | Scans in progress. |
queue.max_wait_seconds | integer | Wait of the oldest queued request. |
recommended_poll_seconds | integer | How often to call it (300). |
generated_at | string (ISO 8601) | When the response was computed. |
Times are in Italian time (+02:00 / +01:00).
status | Meaning | Suggested action |
|---|---|---|
ok | Service operational. | Nothing. If maintenance is not null, warn about the upcoming maintenance. |
slow | Sister answers but slower than usual: scans complete, just later. | Warn that times are longer. |
down | Sister is not responding, is in scheduled maintenance, or we suspended the service. accepting_requests = false. | Stop new requests and handle them manually. With maintenance.in_progress = true you can tell the user when it restarts (ends_at). |
unknown | Status not available (check not recent, or too few scans to judge). Not an outage. | Treat it as ok. |
Status by scan type: types
Sister sometimes stops only some services (for example mortgage inspections) while the rest keeps working, and a notice does not always come. That is why types gives the status of each scan type, derived from the real outcome of the requests of the last hour. The key is the type you use in POST /api/scanner/{type}.
"types": {
"intestatari-catasto": { "status": "ok", "reason": null, "since": null, "requests": { "total": 57, "with_errors": 0 }, "last_success_at": "2026-09-26T10:41:12+02:00" },
"ispezione-catasto": { "status": "down", "reason": "errors", "since": "2026-09-26T10:12:03+02:00", "requests": { "total": 4, "with_errors": 3 }, "last_success_at": "2026-09-26T10:05:40+02:00" },
"mappa-catasto": { "status": "unknown", "reason": null, "since": null, "requests": { "total": 0, "with_errors": 0 }, "last_success_at": "2026-09-25T17:32:32+02:00" }
}types.<type>.status | Meaning |
|---|---|
ok | Requests of this type succeed (even when a second attempt is needed, normal for surveys with the security code). |
degraded | At least 30% of the searches of the last hour failed even after retries, on more than one server. |
down | Since the last successful request at least 3 different searches failed on at least 2 servers, or a Sister maintenance is in progress (reason: "maintenance"). since = first error. |
unknown | No request of this type in the last hour: we don't know. last_success_at tells when it last worked (last 24 hours). Not an outage. |
- It is informational only: requests are not blocked. You decide whether to warn the user or switch that type to manual.
- Errors caused by the searched data (non-existent municipality or parcel, too many results) do not count: they say the search was wrong, not that Sister is down.
- The same search repeated several times counts once, and an error on a single server is not enough: single-server errors are handled on our side by moving the request to another server.
requests.totalandrequests.with_errorscount the distinct searches of the last hour.
Response examples
{
"status": "ok",
"accepting_requests": true,
"message": "Servizio operativo.",
"since": null,
"sister_checked_at": "2026-09-17T10:40:07+02:00",
"maintenance": null,
"queue": { "enqueued": 3, "scanning": 2, "max_wait_seconds": 14 },
"recommended_poll_seconds": 300,
"generated_at": "2026-09-17T10:41:00+02:00"
}{
"status": "ok",
"accepting_requests": true,
"message": "Servizio operativo.",
"since": null,
"sister_checked_at": "2026-09-17T14:30:05+02:00",
"maintenance": {
"in_progress": false,
"starts_at": "2026-09-17T17:00:00+02:00",
"ends_at": "2026-09-17T18:00:00+02:00",
"scanner_blocked": false,
"message": "Sister (Agenzia delle Entrate) sara' in manutenzione programmata dalle 17:00 alle 18:00.",
"sister_message": "A causa di un intervento programmato, il servizio e' indisponibile dalle 17.00 alle 18:00 circa"
},
"queue": { "enqueued": 5, "scanning": 3, "max_wait_seconds": 22 },
"recommended_poll_seconds": 300,
"generated_at": "2026-09-17T14:31:00+02:00"
}{
"status": "down",
"accepting_requests": false,
"message": "Sister (Agenzia delle Entrate) e' in manutenzione programmata fino alle 18:00: le scansioni riprenderanno al termine.",
"since": "2026-09-17T17:00:00+02:00",
"sister_checked_at": "2026-09-17T17:12:00+02:00",
"maintenance": {
"in_progress": true,
"starts_at": "2026-09-17T17:00:00+02:00",
"ends_at": "2026-09-17T18:00:00+02:00",
"scanner_blocked": true,
"message": "Sister (Agenzia delle Entrate) e' in manutenzione programmata fino alle 18:00: le scansioni riprenderanno al termine.",
"sister_message": "A causa di un intervento programmato, il servizio e' indisponibile dalle 17.00 alle 18:00 circa"
},
"queue": { "enqueued": 0, "scanning": 0, "max_wait_seconds": 0 },
"recommended_poll_seconds": 300,
"generated_at": "2026-09-17T17:12:00+02:00"
}{
"status": "slow",
"accepting_requests": true,
"message": "Sister (Agenzia delle Entrate) e' piu' lento del solito: le scansioni vanno avanti ma impiegano piu' tempo.",
"since": "2026-09-17T12:20:00+02:00",
"sister_checked_at": "2026-09-17T12:40:07+02:00",
"maintenance": null,
"queue": { "enqueued": 12, "scanning": 4, "max_wait_seconds": 95 },
"recommended_poll_seconds": 300,
"generated_at": "2026-09-17T12:41:00+02:00"
}{
"status": "down",
"accepting_requests": false,
"message": "Sister (Agenzia delle Entrate) non risponde: le scansioni restano in attesa.",
"since": "2026-09-17T09:50:00+02:00",
"sister_checked_at": "2026-09-17T10:10:04+02:00",
"maintenance": null,
"queue": { "enqueued": 27, "scanning": 6, "max_wait_seconds": 1260 },
"recommended_poll_seconds": 300,
"generated_at": "2026-09-17T10:11:00+02:00"
}Good to know
- Decide on
accepting_requests(orstatus=down), never on themessagetext. - An announced maintenance blocks nothing:
statusstays whatever it is and scans keep running untilstarts_at. Usemaintenanceto warn the user in advance. - During maintenance
statusisdownand the Market Scanner is suspended:POST /api/scanner/{type}answers400withSISTER_FORCE_BLOCKand the same maintenance in themaintenancefield (see below). Whenends_atis reached the scanner restarts by itself, nothing else is needed. - A maintenance is detected when our scans run into the Sister notice: it may show up shortly before it starts, or directly while already in progress.
- Once
ends_athas passed,maintenancegoes back tonull(or shows the next one, if another is announced). - The time-based
slow/downstatus changes only if the problem lasts at least two checks in a row (10-20 minutes): a single slow scan does not trigger it, and it goes back tookafter two normal checks. Maintenance applies immediately. queuecovers scans of the last 2 hours:max_wait_secondsis the wait of the oldest queued request.- The response is cached for 60 seconds: call it every
recommended_poll_seconds(5 minutes), not more often, from your backend. If the call fails (network, 5xx) keep the last known status. - With
downcaused by Sister not responding (no maintenance), requests you send are still accepted and stay queued until Sister is back: blocking them is your choice. - In the sandbox the Sister status is normally
unknownandmaintenanceisnull.
Requests sent during maintenance
Creation refused — while the Market Scanner is suspended, POST /api/scanner/{type} does not create the request and charges no credits:
// HTTP 400
{
"error": "SISTER_FORCE_BLOCK",
"maintenance": {
"in_progress": true,
"starts_at": "2026-09-17T17:00:00+02:00",
"ends_at": "2026-09-17T18:00:00+02:00",
"scanner_blocked": true,
"message": "Sister (Agenzia delle Entrate) e' in manutenzione programmata fino alle 18:00: le scansioni riprenderanno al termine.",
"sister_message": "A causa di un intervento programmato, il servizio e' indisponibile dalle 17.00 alle 18:00 circa"
}
}maintenance is null when the suspension is not due to scheduled maintenance (manual block on our side): in that case there is no restart time, check GET /api/scanner/health.
Request already running — if maintenance starts while a request is being processed, the request goes to ERROR and its result gives the reason in code:
{
"id": 12345,
"status": "ERROR",
"result": {
"error": "Sister (Agenzia delle Entrate) e' in manutenzione programmata fino alle 18:00: le scansioni riprenderanno al termine.",
"code": "SISTER_MAINTENANCE",
"maintenance": {
"in_progress": true,
"starts_at": "2026-09-17T17:00:00+02:00",
"ends_at": "2026-09-17T18:00:00+02:00",
"scanner_blocked": true,
"message": "Sister (Agenzia delle Entrate) e' in manutenzione programmata fino alle 18:00: le scansioni riprenderanno al termine.",
"sister_message": "A causa di un intervento programmato, il servizio e' indisponibile dalle 17.00 alle 18:00 circa"
}
}
}result.code | Meaning |
|---|---|
SISTER_MAINTENANCE | Sister in scheduled maintenance; maintenance says until when (it may be null if the notice had no times). |
SISTER_UNAVAILABLE | Sister showed an unavailability notice without times; maintenance is null. |
Requests that produce a PDF (visure, inspections, notes, maps) are relaunched by us after the maintenance ends, like other temporary errors (see Use case: visure, inspections, notes, maps). Other requests must be resent after ends_at.
Polling — last resort only
Not recommended
You have two push mechanisms (WebSocket and webhook) that remove the need for polling. Consider them first: polling increases latency and server load with no real benefit.
If you really must poll, query periodically:
GET /api/scanner-request/{id}Practical guidelines:
- Recommended minimum interval: 3 seconds between calls.
- Exponential backoff after a few attempts (e.g. 3s → 6s → 12s).
- A sensible attempt limit (e.g. 5 minutes total), then time out on your side.
- Stop as soon as the state is
SUCCESSorERROR.
Typical response:
{ "id": 12345, "status": "SCANNING", "percentage": 42 }Operational notes
- Always keep the
id(scanner_request_id) returned at creation: you need it for every later operation. - Handle the
ERRORstate explicitly: read the message to understand the cause. - Never share the API token: treat it like a password.
- In production, prefer webhook or socket over polling.