namaiDocumentazioneTorna all'app

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, free sono i posti rimasti e open dice 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:

  1. Limite di frequenza: 5 richieste/ora per IP e 20/ora per sito. Si ottiene 429.
  2. Honeypot: il campo website deve essere assente o vuoto. Se è compilato la richiesta è rifiutata con 400.
  3. 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.
  4. 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 in images degli immobili.
  • Per le foto w può valere 400, 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/:name serve logo, favicon e immagine di condivisione; name ha la forma site-XXXXXXXXXX.<estensione> oppure logo.<estensione> (jpg, jpeg, png, webp, gif, ico). Un nome diverso dà 400.
  • Risposta: i byte dell'immagine con ETag e Cache-Control: public, max-age=31536000, immutable. Con If-None-Match valido: 304. Se il servizio è occupato: 503 con Retry-After: 2.

robots.txt e sitemap.xml

GET /robots.txt
GET /sitemap.xml
  • Sull'host principale robots.txt consente l'indicizzazione e indica la sitemap. Su ogni altro host del sito (per esempio un dominio secondario) risponde Disallow: /.
  • 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.