API del sito vetrina (/site/*)
Il sito vetrina di un'agenzia (https://<sottodominio>.namai.it o dominio personalizzato) legge i propri dati da queste rotte. Puoi usarle anche per costruire un front-end o un'integrazione tua.
- Nessuna credenziale. L'agenzia si riconosce dall'host: vedi autenticazione.
- Solo dati pubblici. Un immobile compare solo se è attivo, in vendita o in locazione e non escluso dall'agenzia nelle impostazioni del sito. Prezzi e indirizzi nascosti dall'agenzia non vengono mai inviati.
- Stesso host. Le chiamate vanno fatte all'host del sito, non a
api.namai.it. - Formato di errori, paginazione, lingue e cache: convenzioni.
| Rotta | Scopo |
|---|---|
GET /site/config |
Configurazione pubblica del sito |
GET /site/properties |
Elenco immobili con filtri |
GET /site/properties/:id |
Dettaglio di un immobile |
GET /site/lookups |
Valori per i filtri |
GET /site/events |
Elenco eventi (open house) con fasce orarie |
GET /site/events/summary |
Ci sono eventi? |
POST /site/events/:id/booking |
Prenota una fascia |
GET /site/events/booking/:token |
Dati di una prenotazione |
POST /site/events/booking/:token/cancel |
Annulla una prenotazione |
POST /site/contact |
Invia una richiesta di contatto |
GET /site/image/property/:documentId |
Foto di un immobile |
GET /site/image/asset/:name |
Logo, favicon e immagini del sito |
GET /robots.txt, GET /sitemap.xml |
Indicizzazione |
Negli esempi HOST è l'host del sito, ad esempio demo.namai.it.
Configurazione
GET /site/config
Restituisce tutto ciò che serve a presentare il sito: modello, lingue, tema, pagine e sezioni, footer, SEO, dati dell'agenzia e delle sedi.
curl -s https://HOST/site/config
| Campo | Contenuto |
|---|---|
agencyId, businessname |
Agenzia |
template |
vetrina, boutique o portale |
locales, defaultLocale |
Lingue attive e lingua predefinita (it, en, es) |
theme |
Colori, abbinamento font, angoli, tema scuro consentito |
brand |
Logo, favicon, immagine di condivisione |
pages |
Pagine con indirizzo, titolo, voce di menu e sezioni (copertina, ricerca, in evidenza, elenco immobili, chi siamo, sedi, servizi, testimonianze, galleria, testo libero, invito all'azione, modulo contatti) |
properties |
Impostazioni della pagina immobili (menu, titolo, introduzione, filtri predefiniti) |
footer, seo, legal |
Footer, SEO, indirizzi di privacy e cookie policy |
agency |
REA, assicurazione, associazione di categoria, telefono, email e, per le informative, partita IVA, codice fiscale, PEC, indirizzo, città, provincia, CAP (vat, taxcode, pec, address, city, district, zipcode; possono mancare o essere null) |
locations |
Sedi: descrizione, indirizzo, città, provincia, CAP |
primaryHost |
Host principale del sito (gli altri possono reindirizzarvi) |
turnstileSiteKey |
Chiave pubblica per il widget anti-spam dei form |
hasEvents |
true se esiste almeno un evento pubblicato e non concluso |
Cache: max-age=60, s-maxage=300.
Elenco immobili
GET /site/properties
Restituisce solo immobili in vendita o in locazione.
| Parametro | Tipo | Significato |
|---|---|---|
city |
string | Uno o più codici ISTAT del comune, separati da virgola |
district |
string | Provincia |
propertyTypeId |
string | Uno o più id di tipologia, separati da virgola |
propertyCategoryId |
string | Uno o più id di categoria, separati da virgola |
minPrice, maxPrice |
intero ≥ 0 | Intervallo di prezzo in euro |
minSquareMeters, maxSquareMeters |
intero ≥ 0 | Intervallo di superficie in mq |
sort |
enum | recent (predefinito), price_asc, price_desc, sqm_desc |
limit |
intero | Predefinito 12, massimo 24 |
offset |
intero | Predefinito 0 |
lang |
enum | it (predefinito), en, es |
curl -s "https://HOST/site/properties?city=058091&minPrice=100000&maxPrice=300000&sort=price_asc&limit=12&lang=it"
{
"rows": [
{
"id": 41,
"code": "VE-0041",
"adType": 2,
"propertyCategoryId": 1,
"propertyTypeId": 11,
"city": "058091",
"dscity": "Roma",
"district": "RM",
"zipcode": "00100",
"address": null,
"fullAddress": null,
"squareMeters": 96,
"price": 245000,
"priceOnRequest": false,
"ad": { "title": "Luminoso trilocale", "description": "…", "lang": "it" },
"images": ["/site/image/property/318", "/site/image/property/319"],
"slug": "luminoso-trilocale-roma-41",
"offerStatus": null
}
],
"total": 37
}
Valori illustrativi, risposta abbreviata. Il dettaglio ha lo stesso formato.
Filtri non validi. Un valore non valido (ad esempio sort=foo o minPrice=abc) oggi produce un errore 500 invece di 400.
Campi dell'immobile
| Campo | Tipo | Note |
|---|---|---|
id, code |
number, string | Identificativo e riferimento |
adType |
number | 1 = Locazione, 2 = Vendita |
propertyCategoryId, propertyTypeId, propertyStateId, propertyAvailabilityId |
number | null | Decodifica con /site/lookups (categoria e tipologia) |
agencyLocationId |
number | null | Sede, da incrociare con locations della configurazione |
address, fullAddress |
string | null | null se l'agenzia non vuole mostrare l'indirizzo |
city, dscity, district, zipcode |
string | null | city è il codice ISTAT, dscity il nome |
squareMeters |
number | null | Superficie in mq |
parts |
array | null | Parti dell'immobile (descrizione, percentuale commerciale, mq, prezzo unitario, coefficiente) |
information |
object | null | Piano, locali, livelli, giardino, classe energetica, ascensore, condizione, orientamento |
heating, areaServices, buildState, omiZone |
Riscaldamento, servizi in zona (codici), stato di costruzione, zona OMI | |
price |
number | null | null se priceOnRequest è true |
priceOnRequest |
boolean | L'agenzia non pubblica il prezzo |
ad |
object | null | { title, description, lang } nella lingua richiesta, con ripiego |
images |
string[] | Percorsi relativi delle foto pubblicate, nell'ordine scelto dall'agenzia |
slug |
string | Testo per l'indirizzo della pagina dell'immobile |
offerStatus |
enum | null | in_progress (proposta in corso), accepted (proposta accettata), sold (venduto/affittato). Serve ai badge del sito |
timeins |
string | null | Data di inserimento |
Gli immobili archiviati non compaiono; quelli con una proposta in corso, accettata o conclusa restano visibili con il relativo offerStatus.
Dettaglio immobile
GET /site/properties/:id?lang=it
Restituisce lo stesso oggetto dell'elenco per l'immobile indicato. 404 con 000000:immobile non trovato se l'id non è numerico, non esiste, non è pubblico o è stato escluso dal sito.
Filtri
GET /site/lookups
{
"propertyCategory": [{ "id": 1, "descriptionIt": "Residenziale" }],
"propertyType": [{ "id": 11, "descriptionIt": "Appartamento", "categoryId": 1 }],
"city": [{ "code": "058091", "name": "Roma" }]
}
Le città sono solo quelle in cui l'agenzia ha immobili pubblici, quindi un filtro costruito da qui non porta mai a un elenco vuoto. Le descrizioni sono in italiano. Cache: s-maxage=3600.
Eventi (open house)
Gli eventi sono appuntamenti collegati a un immobile, divisi in fasce orarie con un numero di posti. Un evento compare solo se è pubblicato, non annullato, non concluso e se il suo immobile è pubblico sul sito.
Elenco
GET /site/events?propertyId=41&lang=it
| Parametro | Significato |
|---|---|
propertyId |
Facoltativo: solo gli eventi di quell'immobile (id non numerico: 404). Senza, ogni evento porta la scheda sintetica dell'immobile in property. In ogni caso al massimo 24 eventi |
lang |
Lingua della scheda dell'immobile |
[
{
"id": 7,
"propertyId": 41,
"typeDescription": "Open house",
"typeColor": "#2563eb",
"title": "Open house di sabato",
"description": "Visita libera dell'appartamento.",
"startsAt": "2026-10-24T08:00:00.000Z",
"endsAt": "2026-10-24T10:00:00.000Z",
"slotMinutes": 30,
"bookingEnabled": true,
"bookable": true,
"slots": [
{ "start": "2026-10-24T08:00:00.000Z", "end": "2026-10-24T08:30:00.000Z", "free": 3, "open": true },
{ "start": "2026-10-24T08:30:00.000Z", "end": "2026-10-24T09:00:00.000Z", "free": 0, "open": true }
]
}
]
bookingEnabled: l'agenzia ha attivato le prenotazioni dal sito.bookable: esiste almeno una fascia prenotabile adesso.- Per ogni fascia,
freesono i posti rimasti eopendice se le prenotazioni sono ancora aperte (la fascia non è iniziata e non è scaduta la chiusura prenotazioni).
Cache: s-maxage=30, perché i posti cambiano a ogni prenotazione.
Ci sono eventi?
GET /site/events/summary
{ "hasEvents": true }
Prenotare
POST /site/events/:id/booking
Content-Type: application/json
| Campo | Obbligatorio | Vincoli |
|---|---|---|
slotStart |
sì | Inizio della fascia, uguale a uno dei start dell'elenco |
name |
sì | 1–200 caratteri |
email |
sì | Indirizzo valido, max 200 |
phone |
no | max 20 |
note |
no | max 1000 |
privacy |
sì | Deve essere true |
locale |
no | it, en, es: lingua delle email di conferma |
turnstileToken, website, renderedAt |
sì / no / no | Anti-spam |
curl -s -X POST https://HOST/site/events/7/booking \
-H "Content-Type: application/json" \
-d '{
"slotStart": "2026-10-24T08:00:00.000Z",
"name": "Maria Rossi",
"email": "[email protected]",
"privacy": true,
"locale": "it",
"turnstileToken": "<token del widget>",
"renderedAt": "1760000000000"
}'
{ "status": "confirmed", "slotStart": "2026-10-24T08:00:00.000Z", "slotEnd": "2026-10-24T08:30:00.000Z" }
Chi prenota riceve un'email di conferma con un link per annullare (l'invio è «best effort»: se la posta non parte la prenotazione vale comunque). La prenotazione compare anche nella coda Richieste dell'agenzia con origine «Prenotazione evento», già associata al contatto se il visitatore è noto.
Errori principali:
| Stato | error |
Quando |
|---|---|---|
400 |
000042:Fascia oraria non valida |
slotStart non è una data o non è una fascia dell'evento |
400 |
000041:Le prenotazioni per questa fascia sono chiuse |
Fascia iniziata o chiusura prenotazioni superata |
400 |
000000:richiesta non valida / 000000:verifica anti-spam non superata |
Anti-spam |
404 |
000040:Evento non trovato |
Evento inesistente, non pubblicato, annullato o con immobile non pubblico |
409 |
000043:La fascia scelta è al completo |
Nessun posto libero |
409 |
000044:Hai già una prenotazione per questo evento |
Stessa persona già prenotata |
429 |
000000:troppe richieste, riprova più tardi |
Limite di frequenza |
Consultare e annullare una prenotazione
Il link ricevuto per email contiene un token di annullamento, che è l'unica credenziale.
GET /site/events/booking/:token
{ "title": "Open house di sabato", "status": "confirmed", "slotStart": "2026-10-24T08:00:00.000Z", "slotEnd": "2026-10-24T08:30:00.000Z" }
status è confirmed oppure cancelled. Token sconosciuto: 404 con 000040:Prenotazione non trovata. La risposta ha Cache-Control: no-store.
POST /site/events/booking/:token/cancel?lang=it
Risposta: { "status": "cancelled" }. Chi aveva prenotato riceve un'email di annullamento e la richiesta collegata in coda viene chiusa. L'operazione è idempotente (annullare due volte dà lo stesso esito). Dopo l'inizio della fascia non si può più annullare dal link: 400 con 000041:La fascia è già iniziata. Questa rotta è soggetta al limite di frequenza.
Richiesta di contatto
POST /site/contact
Content-Type: application/json
| Campo | Obbligatorio | Vincoli |
|---|---|---|
firstname, lastname |
sì | 1–120 caratteri |
email |
sì | Indirizzo valido, max 200 |
phone |
no | max 15 |
message |
no | max 2000 |
privacy |
sì | Deve essere true |
propertyId |
no | Se la richiesta nasce dalla scheda di un immobile |
turnstileToken |
sì | Token del widget Cloudflare Turnstile |
website |
no | Honeypot: deve restare vuoto |
renderedAt |
no | Millisecondi epoch di quando il form è stato mostrato |
curl -s -X POST https://HOST/site/contact \
-H "Content-Type: application/json" \
-d '{
"firstname": "Maria",
"lastname": "Rossi",
"email": "[email protected]",
"phone": "3331234567",
"message": "Vorrei visitare l'\''appartamento.",
"privacy": true,
"propertyId": 41,
"turnstileToken": "<token del widget>",
"renderedAt": "1760000000000"
}'
{ "ok": true }
Cosa succede in namai:
- la richiesta entra nella coda Richieste dell'agenzia con origine «Sito web», oggetto «Richiesta dal sito» (con il riferimento dell'immobile, se c'è) e assegnata all'utente scelto nelle impostazioni del sito;
- non viene creato un contatto: se email o telefono coincidono con un contatto già registrato la richiesta gli viene associata, altrimenti resta «Da associare» e sarà l'agente a decidere se creare il contatto o una ricerca;
- l'operazione è registrata nel Registro sicurezza.
Errori: 400 per validazione, honeypot compilato o anti-spam non superato; 429 per il limite di frequenza; 503 se l'agenzia non ha un utente o una sede a cui attribuire la richiesta.
La risposta ha Cache-Control: no-store.
Protezione anti-spam
Le scritture pubbliche (contatto, prenotazione e annullamento) hanno quattro difese, applicate nell'ordine:
- Limite di frequenza: 5 richieste/ora per IP e 20/ora per sito. Si ottiene
429. - Honeypot: il campo
websitedeve essere assente o vuoto. Se è compilato la richiesta è rifiutata con400. - Tempo di permanenza: se invii
renderedAt(millisecondi dall'epoch in cui il form è stato mostrato), la richiesta deve arrivare tra 3 secondi e 2 ore dopo. Fuori da questo intervallo:400. Il valore non è firmato. - Cloudflare Turnstile:
turnstileTokenè verificato lato server. Se la verifica non riesce, o non è configurata sul server, la richiesta è rifiutata (400,000000:verifica anti-spam non superata).
Il widget Turnstile si carica con la chiave turnstileSiteKey restituita da /site/config. Un'integrazione che non può mostrare il widget non può inviare richieste di contatto o prenotazioni: contatta l'agenzia per un'integrazione dedicata.
Immagini
GET /site/image/property/:documentId?w=800
GET /site/image/asset/:name
documentIdè l'ultimo segmento dei percorsi inimagesdegli immobili.- Per le foto
wpuò valere400,800,1200,1920; altrimenti si ottiene l'originale. Le foto sono servite compresse. - Le foto di un immobile archiviato non sono più raggiungibili (
404). asset/:nameserve logo, favicon e immagine di condivisione;nameha la formasite-XXXXXXXXXX.<estensione>oppurelogo.<estensione>(jpg, jpeg, png, webp, gif, ico). Un nome diverso dà400.- Risposta: i byte dell'immagine con
ETageCache-Control: public, max-age=31536000, immutable. ConIf-None-Matchvalido:304. Se il servizio è occupato:503conRetry-After: 2.
robots.txt e sitemap.xml
GET /robots.txt
GET /sitemap.xml
- Sull'host principale
robots.txtconsente l'indicizzazione e indica la sitemap. Su ogni altro host del sito (per esempio un dominio secondario) rispondeDisallow: /. - La sitemap elenca le pagine del sito, la pagina immobili e fino a 5.000 immobili pubblici, nella lingua predefinita.
Pagine HTML
Le richieste GET/HEAD che accettano HTML e non riguardano /v1/, /cus/, /site/, /docs/, /assets/ e /health ricevono l'applicazione del sito con la configurazione e i dati SEO già inseriti. Un host secondario con reindirizzamento attivo riceve un 301 verso primaryHost. Per i front-end personalizzati questo comportamento non è rilevante: usa le rotte JSON qui sopra.
Il sito include due pagine generate dai dati dell'agenzia: informativa sulla privacy e cookie policy (/privacy e /cookie; /privacy e /cookies in inglese, /privacidad e /cookies in spagnolo, con il prefisso /:lang per le lingue diverse da quella predefinita). Una pagina creata dall'agenzia con lo stesso indirizzo ha la precedenza. Con legal.privacyUrl e legal.cookieUrl l'agenzia può invece indicare pagine proprie.