Skip to content

Market Scanner integration ​

Integrate with an AI assistant
Download or copy the integration prompt for Market Scanner and paste it into Claude Code, Cursor or Copilot: it contains auth, end-to-end examples and a checklist.
Open

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 URLhttps://api.dokicasa.it
AuthAuthorization: Bearer <api_token>
Content-Typeapplication/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:

http
Authorization: Bearer YOUR_API_TOKEN

WARNING

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.

MethodPathWhat 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}/resultsStructured results for "normal" searches (e.g. properties from an address).
WSprivate-scanner_request.{id}Private WebSocket channel for realtime updates.
GET/api/scanner/healthService status: Sister is working, slow, down or in scheduled maintenance, also by scan type. Details

Request states ​

ENQUEUED  →  SCANNING  →  SUCCESS
                       ╲→  ERROR

ENQUEUED 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:

typeWhat it doesField in result
search-fiscal-codePeople with fiscal code from first name, last name and optional province (free)anagrafiche
search-companyCompanies (legal entities) with fiscal code from company name and optional province (price list)anagrafiche
tax-dataProperties owned by a fiscal/VAT code (optional province and municipality)immobili
catastoProperties from sheet / parcelimmobili
intestatari-catastoOwners of a parcelintestatari
list-addressesNormalize an address → list of streetsaddresses
immobili-by-nameNationwide 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):

typeDocument
visura-catasto / visura-tax-dataCadastral survey (from cadastral data / from fiscal-VAT code)
ispezione-catasto / ispezione-tax-dataMortgage inspection
nota-catasto / nota-tax-dataTranscription note
mappa-catastoCadastral map
elaborato-planimetricoFloor-plan document
elenco-immobiliProperty 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:

Calltipo_visuraSurvey you get
visura-catasto (by property)ANALITICAhistorical, detailed
SINTETICAhistorical, summary
COMPLETAcurrent
visura-tax-data (by subject)ANALITICA | SINTETICA | COMPLETAcurrent, by subject
visura-tax-data + "storica": trueANALITICAhistorical by subject, detailed
SINTETICAhistorical 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.
json
POST /api/scanner/visura-tax-data
{
  "tax_code": "RSSMRA80A01H501U",
  "provincia": "LT",
  "tipo_visura": "ANALITICA",
  "storica": true
}
RuleDetail
Natural and legal personstax_code can be a fiscal code or a VAT number (companies)
ANALITICA or SINTETICA onlywith COMPLETA it returns 422
One province per callthe survey covers the whole province given in provincia; if the subject owned properties in several provinces, make one call per province
Same priceit costs the same as a regular visura-tax-data
Timingthe 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.

json
POST /api/scanner/visura-tax-data
{
  "tax_code": "RSSMRA80A01H501U",
  "provincia": "LT",
  "tipo_visura": "ANALITICA",
  "storica": true,
  "situazione_al": "2020-06-30"
}
RuleDetail
visura-tax-data onlyon other calls the parameter has no effect; works with fiscal code and VAT number
FormatYYYY-MM-DD; a future date or another format returns 422
With storicathe typical case: who owned property at that date, even if they own nothing today
Same priceit costs the same as a regular visura-tax-data
Timingwith a date other than today Sister asks for its security code, solved automatically: a few extra seconds
Date too far backif the digitized records hold nothing at that date, the response is Nessun documento trovato.
Documents already downloadedthe 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.

ValueWhat it returns
E (default)land and buildings
Tland only
Fbuildings only
json
POST /api/scanner/tax-data
{
  "tax_code": "RSSMRA80A01H501U",
  "search_catasto_type": "F"
}
RuleDetail
Where it appliestax-data, visura-tax-data, immobili-by-name (both steps); fiscal code and VAT number
ValuesE, T or F; any other value → 422
Formatthe immobili rows are the same: only which ones are there changes (catasto is T or F on each row)
Same priceit costs the same as the same search without the parameter
Documents already downloadedin 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.

json
POST /api/scanner/tax-data
{
  "tax_code": "RSSMRA80A01H501U",
  "provincia": "RM",
  "comune": "Colleferro"
}
RuleDetail
provinciatwo-letter code (RM); Trento and Bolzano are not on Sister → 422
comunemunicipality 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
Matchingaccents, apostrophes and case are ignored (Lurago d Erba = LURAGO D'ERBA)
No propertiesempty list, like a search with no results
Same priceit 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:

bash
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:

json
{ "first_name": "MARIO", "last_name": "ROSSI", "provincia": "MI" }
FieldRequiredNotes
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:

json
{
  "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:

json
{ "company_name": "ESEMPIO IMMOBILIARE", "provincia": "MI" }
FieldRequiredNotes
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:

json
{
  "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 same codice_fiscale. Group by fiscal code before showing them.
  • sede and codice_fiscale can be empty on older records.
  • A single generic word is not enough. With IMMOBILIARE alone the Registry does not search and the request ends in ERROR with "troppo generica: aggiungere un'altra parola della ragione sociale" (too generic, add another word). Above the Registry's result limit the request also ends in ERROR: add a word or send the province.
  • Once you have the fiscal code, use tax-data to 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:

json
{ "first_name": "CHRISTIAN", "last_name": "CANNATA" }

When it completes (via socket/polling, like every async call) the result holds the list of namesakes found:

json
{
  "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 value and 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):

json
{
  "first_name": "CHRISTIAN",
  "last_name": "CANNATA",
  "selected_value": "9800054753#0#CANNATA#CHRISTIAN#CNNCRS91D08E625S#LIVORNO#08/04/1991#LI"
}

When it completes the result holds:

json
{
  "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.

  1. Create the request — POST /api/scanner/{type} → you get a scanner_request_id and the initial state ENQUEUED.
  2. 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.
  3. 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.

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.

EnvironmentAPI base URLwsHost (socket)keyauthEndpoint
Sandbox / testhttps://api-dev.dokicasa.itapi-dev.dokicasa.itstaginghttps://api-dev.dokicasa.it/broadcasting/auth
Productionhttps://api.dokicasa.itsocket.dokicasa.itra9jcihuz0sx7vtw8yuthttps://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) ​

js
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 receiveWhen it fires
callbackcompact: { id, type, status, percentage, result, created_at, updated_at, finished_at }when the scan finishes
webhookfull: { scanner_request: { …whole request object, with paramsandresult… } }on the completion event
json
{
  "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.

json
{
  "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.

json
{
  "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"
}
json
{
  "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"
  }
}
callbackwebhook
Payloadcompact, flat fieldsfull scanner_request object
Includes input paramsNoYes
Signature header (HMAC)YesYes
When it fireswhen the scan finisheswhen 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 request params: 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>)
php
$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
}
js
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, ... }.
json
{
  "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
    }
  ]
}
json
{
  "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
    }
  ]
}
json
{
  "immobili": [],
  "anagrafiche": [
    {
      "cognome": "ROSSI", "nome": "MARIO",
      "data_di_nascita": "17/05/1980", "luogo_di_nascita": "ROMA (RM)",
      "sesso": "M", "codice_fiscale": "RSSMRA80E17H501U"
    }
  ]
}
json
{
  "immobili": [],
  "anagrafiche": [
    { "denominazione": "ESEMPIO IMMOBILIARE SRL", "sede": "MILANO (MI)", "codice_fiscale": "01234567890" }
  ]
}
json
{
  "addresses": {
    "123494##VIA ROMA": " VIA ROMA ",
    "337696##VIA ROMAGNOSI": " VIA ROMAGNOSI ",
    "12428##VIALE ROMAGNA": " VIALE ROMAGNA "
  }
}
json
{
  "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.

HfBfYShns3E02no3kkwCRWQugkVNcJCtrRN2BPoI5NxhqsiXaxxnSPpIHY2O

Send 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

WhatEndpoint
Start a searchPOST /api/scanner/{type} → { scanner_request_id, status }
Status + resultGET /api/scanner-request/{id} · …/{id}/status
Paginated resultsGET /api/scanner-request/{id}/results
Create monitoringPOST /api/monitoring/from-catasto
Simulate webhook (sandbox only)POST /api/user-monitorings/{id}/simulate-webhook

Socket — sandbox (see also Socket environments)

ParameterValue
wsHostapi-dev.dokicasa.it
keystaging
authEndpointhttps://api-dev.dokicasa.it/broadcasting/auth
channel · eventprivate-scanner_request.{id} · ScannerRequestNotification

Quick try (curl) ​

bash
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 = SUCCESS

What to verify:

  1. Async flow — the POST returns ENQUEUED, then via socket (instant) or polling you reach SUCCESS.
  2. Response shape — map the result fields (see Result format by type).
  3. Webhook — point webhook_url at an inspector (webhook.site); in sandbox the monitoring sends a sample event immediately on creation (or re-trigger it with /simulate-webhook).
  4. Signature — validate the Signature header (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.

json
{
  "foglio": "123",
  "particella": "45",
  "subalterno": "7",
  "comune": "Roma",
  "provincia": "RM",
  "webhook_url": "https://your-server.com/dokicasa/monitoring-changed"
}
FieldRequiredNotes
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:

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 sample immobile.owners_changed event to that URL right away (the create response includes simulated_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:

ParameterValuesNotes
filter.typeIMMOBILE (default) / SOGGETTOmonitoring type
filter.statusACTIVE, EXPIRED, CANCELLEDmultiple allowed, comma-separated
filter.sourceAPI / CENSIMENTOmonitoring origin
filter.change_owner1only those with owner changes
filter.textfree textsearches notes + address/cadastral data
sort_ends_atASC / DESCsort by expiry (default: id DESC)
pagenumberpage (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 ​

MethodPathWhat 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}/webhookSet 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}/renewRenew the monitoring.
POST/api/user-monitorings/{id}/simulate-webhookSandbox 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 ​

json
{
  "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'" }
      ]
    }
  ]
}
FieldContents
modelDetail of the monitored property (municipality, sheet/parcel/sub, category, address…)
model.soggettiCurrent owners (with quota and titolarità in the pivot)
eventsOwner changes: diff per date (added / removed / modified)
proprietari_historyOwner 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

You have an address and want the associated cadastral properties. Two phases: address resolution → properties.

  1. list-addresses (address resolution) — POST /api/scanner/list-addresses. Wait via socket/webhook/polling. The normalized address list is already in the results field of GET /api/scanner-request/{id} — no need to call /results.
  2. Properties from an address — use an address from step 1 as address_value in the properties scanner call (exact endpoint on Swagger, Scanner section).
  3. Fetch results — when SUCCESS arrives, call GET /api/scanner-request/{id}/results for the property list.

Use case: visure, inspections, notes, maps (PDF) ​

All requests producing a PDF (cadastral visura, inspection, note, map) follow the same pattern.

  1. Create the request — POST /api/scanner/{type} (type depends on the document, see Swagger).
  2. Wait for completion — via WebSocket or webhook: state goes ENQUEUED → SCANNING up to SUCCESS or ERROR.
  3. 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.
  4. 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.

http
GET /api/scanner/health
Authorization: Bearer <token>

Response fields ​

FieldTypeDescription
statusstringok | slow | down | unknown (see table below).
accepting_requestsbooleanfalse only with status = down. This is the field to decide on.
messagestringReady-to-show message, in Italian. It may change: don't decide on it.
sincestring (ISO 8601) | nullSince when the problem has been going on. During maintenance it is the start time. null with ok.
sister_checked_atstring (ISO 8601) | nullLast Sister check.
maintenanceobject | nullScheduled maintenance in progress or announced (the next one). null if there is none.
maintenance.in_progressbooleantrue if we are now inside the starts_at → ends_at window.
maintenance.starts_atstring (ISO 8601)Maintenance start.
maintenance.ends_atstring (ISO 8601)Expected end: at this time the Market Scanner restarts by itself.
maintenance.scanner_blockedbooleantrue if maintenance is in progress and the Market Scanner is suspended.
maintenance.messagestringReady-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_messagestring | nullOriginal text of the Sister banner, as is.
queue.enqueuedintegerScans waiting (last 2 hours).
queue.scanningintegerScans in progress.
queue.max_wait_secondsintegerWait of the oldest queued request.
recommended_poll_secondsintegerHow often to call it (300).
generated_atstring (ISO 8601)When the response was computed.

Times are in Italian time (+02:00 / +01:00).

statusMeaningSuggested action
okService operational.Nothing. If maintenance is not null, warn about the upcoming maintenance.
slowSister answers but slower than usual: scans complete, just later.Warn that times are longer.
downSister 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).
unknownStatus 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}.

json
"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>.statusMeaning
okRequests of this type succeed (even when a second attempt is needed, normal for surveys with the security code).
degradedAt least 30% of the searches of the last hour failed even after retries, on more than one server.
downSince 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.
unknownNo 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.total and requests.with_errors count the distinct searches of the last hour.

Response examples ​

json
{
  "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"
}
json
{
  "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"
}
json
{
  "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"
}
json
{
  "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"
}
json
{
  "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 (or status = down), never on the message text.
  • An announced maintenance blocks nothing: status stays whatever it is and scans keep running until starts_at. Use maintenance to warn the user in advance.
  • During maintenance status is down and the Market Scanner is suspended: POST /api/scanner/{type} answers 400 with SISTER_FORCE_BLOCK and the same maintenance in the maintenance field (see below). When ends_at is 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_at has passed, maintenance goes back to null (or shows the next one, if another is announced).
  • The time-based slow/down status 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 to ok after two normal checks. Maintenance applies immediately.
  • queue covers scans of the last 2 hours: max_wait_seconds is 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 down caused 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 unknown and maintenance is null.

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:

json
// 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:

json
{
  "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.codeMeaning
SISTER_MAINTENANCESister in scheduled maintenance; maintenance says until when (it may be null if the notice had no times).
SISTER_UNAVAILABLESister 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:

http
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 SUCCESS or ERROR.

Typical response:

json
{ "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 ERROR state 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.