Salta al contenuto
VIISI Prova 30 giorni

API VIISI

La guida per chi integra

Collegare un gestionale, un badge system o un cruscotto aziendale a VIISI: leggere chi risulta dentro, scaricare le visite di un periodo, ricevere un messaggio quando qualcuno entra. Senza usare la password di nessuno.

In tre minuti

Tre passi: si crea una chiave dal portale, la si mette in un'intestazione, si chiama. Non c'è un flusso di autorizzazione da implementare, e non serve registrare un'applicazione da nessuna parte.

1. La chiave

Nel portale, con un utente amministratore: Configurazione → Integrazioni → Nuova chiave. Si dà un nome — quello del programma che la userà — e si spuntano i permessi che servono.

2. L'indirizzo

https://viisi.ak8.cloud/v1/api

3. La prima chiamata

Richiesta

curl -H "Authorization: Bearer viisi_..." \
  https://viisi.ak8.cloud/v1/api/pubblica/presenti

Risposta

[
  {
    "codice_visita": "V-2609-0007",
    "tipologia": "visitatore",
    "motivo": "Riunione commerciale",
    "ingresso_effettivo": "2026-09-02T08:41:12.204Z",
    "nome": "Mario",
    "cognome": "Rossi",
    "azienda": "Meccanica Rossi Snc",
    "sede": "Stabilimento di Prato",
    "referente": "Marco Bianchi"
  }
]

Tutto quello che segue sono dettagli di questo. Le risposte sono JSON, le date sono in UTC in formato ISO 8601, e i campi che non hanno un valore arrivano a null invece di sparire.

La chiave

È una stringa lunga che comincia per viisi_, si presenta in Authorization: Bearer, e non scade da sola a meno che non le si dia una data.

Si vede una volta sola Nel nostro database c'è la sua impronta, non la chiave. Se si perde non possiamo rileggerla: si revoca e se ne fa un'altra. È meno comodo, ed è l'unico modo perché una copia del database non sia anche un mazzo di chiavi.

Perché non è un token OAuth

Un token firmato si verifica senza interrogare il database, ed è il motivo per cui i token di sessione di VIISI durano quindici minuti: finché non scadono, valgono. Una credenziale che deve restare in un gestionale per un anno non può funzionare così — revocarla richiederebbe una lista di token annullati da consultare a ogni richiesta, cioè esattamente la query che il token firmato serviva a evitare.

Quindi la chiave è una stringa casuale che verifichiamo interrogando il database ogni volta. Il prezzo è una query in più; quello che si compra è che la revoca ha effetto sulla richiesta successiva, non allo scadere di qualcosa.

Dove conservarla

  • In una variabile d'ambiente o nella configurazione del server, non nel codice sorgente.
  • Mai in una pagina web o in un'app che gira sul dispositivo di qualcuno: chiunque apra gli strumenti del browser la legge.
  • Una chiave per programma, non una per tutti: così si revoca quella giusta senza fermare gli altri.

Revocarla

Dal portale, in Configurazione → Integrazioni: dall'API una chiave non si revoca. Una chiave revocata riceve 401 dalla chiamata dopo, comprese quelle già in volo che non hanno ancora raggiunto il server.

Nell'elenco delle chiavi si vede l'ultimo uso: serve a capire se una chiave che nessuno ricorda è ancora collegata a qualcosa prima di toglierla.

Gli ambiti

Il perimetro si dichiara alla creazione e non si allarga dopo. Una chiave nata per leggere le presenze non deve poter creare una visita perché un domani qualcuno ha aggiunto un permesso da qualche parte.

AmbitoPermette
presenze:leggiChi risulta dentro adesso
visite:leggiLe visite di un periodo
visitatori:leggiCercare una persona in rubrica
referenti:leggiL'elenco di chi riceve visite
imprese:leggiLe imprese fornitrici censite
documenti:leggiCosa manca e cosa sta per scadere
documenti:scriviRegistrare una carta e la sua validità
visite:scriviCreare e modificare visite (in arrivo)
imprese:scriviModificare l'anagrafica delle imprese (in arrivo)
webhook:gestisciRegistrare e togliere indirizzi di consegna
Gli ambiti valgono solo sotto /pubblica Le rotte che servono il portale non si aprono alle chiavi, nemmeno a una che ha tutti gli ambiti: rispondono 403. Se lo facessero, ogni funzione aggiunta domani al prodotto diventerebbe pubblica senza che nessuno l'abbia deciso — e un'API pubblica è una promessa che si mantiene per anni.

Le rotte

Sono poche di proposito. Ogni rotta è una promessa, e le promesse in più si pagano quando bisogna mantenerle mentre il prodotto cambia.

GET /pubblica/presenti presenze:leggi

Chi risulta dentro adesso, cioè le visite con l'ingresso registrato e ancora aperte, in ordine di ingresso. È la rotta che serve al cruscotto della portineria. L'elenco vale quanto le uscite registrate, e i dipendenti non ci sono.

Richiesta

curl -H "Authorization: Bearer $VIISI_KEY" \
  $API/pubblica/presenti

Campi

codice_visita, tipologia, motivo,
ingresso_effettivo,
nome, cognome, azienda,
sede, referente
GET /pubblica/visite visite:leggi

Le visite di un periodo. Senza parametri restituisce quelle di oggi; con dal e al quelle dell'intervallo, al massimo cinquecento per volta, dalla più recente.

Richiesta

curl -H "Authorization: Bearer $VIISI_KEY" \
  "$API/pubblica/visite?dal=2026-09-01&al=2026-09-30"

Campi

codice_visita, tipologia, stato, motivo,
data_prevista, ingresso_effettivo,
uscita_effettiva,
nome, cognome, email, azienda,
sede, referente

Le date si scrivono 2026-09-01 oppure per intero 2026-09-01T06:00:00Z. Il filtro guarda l'ingresso vero quando c'è, altrimenti la data prevista: una visita programmata e mai arrivata resta nel giorno in cui era attesa.

stato segue il ciclo della visita:

StatoVuol dire
programmataIn agenda, l'invito non è ancora partito
invitataL'invito è stato mandato al visitatore
in_attesaArrivato al chiosco, aspetta l'assenso del referente
autorizzataIl referente ha detto sì, la persona non è ancora entrata
presenteÈ dentro
uscitaCheck-out fatto dalla persona
chiusa_ufficioChiusa d'ufficio dopo le ore scelte per la sede (12 di serie), perché nessuno ha registrato l'uscita
rifiutataIl referente ha negato l'accesso
annullataDisdetta prima dell'arrivo
scadutaLa finestra prevista è passata senza check-in
no_showAttesa e mai arrivata
Oggi nessuno aspetta un'autorizzazione Tutti i percorsi entrano senza l'assenso del referente, e dal portale non si cambia. Quindi in_attesa, autorizzata e rifiutata oggi non compaiono: il vostro programma li tratti comunque, perché sono valori previsti.
Le due uscite sono diverse uscita è la persona che ha timbrato; chiusa_ufficio è il sistema che chiude, dopo le ore scelte per la sede, chi se n'è andato senza dirlo. Chi calcola le presenze dovrebbe contarle entrambe come uscite, e chi misura la qualità del dato dovrebbe tenerle separate.
GET /pubblica/referenti referenti:leggi

Chi in azienda può ricevere visite, con la sede di appartenenza. Serve a chi deve popolare una tendina nel proprio programma, o a chi sincronizza l'anagrafica dal gestionale del personale.

Richiesta

curl -H "Authorization: Bearer $VIISI_KEY" \
  $API/pubblica/referenti

Risposta

[
  {
    "id": "fd03ddb7-…",
    "nome": "Marco",
    "cognome": "Bianchi",
    "email": "m.bianchi@…",
    "id_esterno": null,
    "sede": "Prato"
  }
]
id_esterno: il campo che serve a voi È lo spazio per l'identificativo che quella stessa persona ha nel vostro sistema. Accoppiare per nome e cognome funziona finché non ci sono due Mario Rossi; per email finché nessuno cambia indirizzo — e l'email di un dipendente cambia più spesso di quanto si creda.

Si valorizza con l'importazione dei referenti, passandolo insieme agli altri campi:

curl -X POST -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"referenti":[
       {"nome":"Marco","cognome":"Bianchi",
        "email":"m.bianchi@example.it","id_esterno":"HR-4471"}
     ]}' \
  $API/referenti/import

La prima volta aggancia, dalla seconda comanda. L'importazione cerca per identificativo e per email insieme, preferendo il primo: così l'esportazione che porta gli identificativi per la prima volta riconosce la rubrica che c'è già invece di ricrearla, e da lì in poi un cambio di indirizzo aggiorna la persona giusta invece di sdoppiarla.

Sul visitatore id_esterno per ora si legge soltanto: arriverà in scrittura insieme a visite:scrivi, che è il momento in cui un gestionale avrà una visita da creare e una persona da accoppiare.

GET /pubblica/visitatori visitatori:leggi

Cerca una persona in rubrica per nome, cognome, email o azienda. Servono almeno due caratteri in testo, e tornano al massimo cento risultati.

Richiesta

curl -H "Authorization: Bearer $VIISI_KEY" \
  "$API/pubblica/visitatori?testo=rossi"

Risposta

[
  {
    "id": "b255ced5-…",
    "nome": "Mario",
    "cognome": "Rossi",
    "email": "mario.rossi@example.it",
    "telefono": null,
    "id_esterno": null,
    "azienda": "Meccanica Rossi Snc",
    "visite": 1
  }
]
La rubrica non si scarica Senza testo la risposta è un elenco vuoto, non l'anagrafica intera. Un'integrazione che deve trovare una persona non ha bisogno di scaricarle tutte, e un'API che lo permette diventa il modo più comodo di portarsi via una rubrica. Chi ha bisogno dell'esportazione completa la fa dal portale, dove resta scritto chi l'ha fatta e quando.

Le persone anonimizzate — per scadenza della conservazione o per richiesta di cancellazione — non compaiono.

Imprese e documenti

Chi fa lavori dentro un'azienda deve avere le carte in ordine: il DURC dell'impresa, la formazione dei suoi tecnici, l'idoneità sanitaria. VIISI tiene le scadenze e le mostra nello scadenzario. Ai tornelli collegati con /pubblica/abilitazione risponde no se le carte non sono in regola. Il chiosco oggi non ferma nessuno (vedi Funzioni, «da attivare»). Se quelle date le tenete già in un gestionale documentale, queste rotte servono a non scriverle due volte.

Due date della stessa cosa divergono sempre Al primo rinnovo registrato da una parte sola, nessuno sa più quale guardare. Meglio che la sorgente resti una — la vostra — e che sia lei a scrivere qui.
GET /pubblica/imprese imprese:leggi

Le imprese fornitrici, con il loro stato di fornitura — attivo, in_pausa, cessato — e il vostro id_esterno, che è come le ritrovate.

GET /pubblica/scadenzario documenti:leggi

Cosa manca e cosa sta per scadere, entro ?giorni= (trenta di default). Include i documenti mai consegnati, non solo quelli in scadenza: un obbligo che nessuno ha mai caricato è più grave di uno che scade fra due settimane, e un elenco di sole scadenze non lo mostrerebbe mai.

Richiesta

curl -H "Authorization: Bearer $VIISI_KEY" \
  "$API/pubblica/scadenzario?giorni=60"

Risposta

[
  {
    "ambito": "impresa",
    "azienda": "Impianti Verdi Srl",
    "persona": null,
    "tipo": "DURC",
    "chiave_tipo": "durc",
    "obbligatorio": true,
    "valido_al": "2026-09-18",
    "giorni": 15,
    "situazione": "in_scadenza"
  }
]

situazione vale mancante, scaduto, in_scadenza o respinto. Le imprese in pausa o cessate non compaiono: chiedere il rinnovo a chi ha finito il lavoro è il modo più rapido di far ignorare i propri messaggi.

POST /pubblica/documenti documenti:scrivi

Registra una carta e fino a quando vale. Si indirizza con i vostri identificativi, non con i nostri:

curl -X POST -H "Authorization: Bearer $VIISI_KEY" \
  -H "Content-Type: application/json" \
  -d '{"tipo":"durc",
       "impresa_id_esterno":"FORN-4471",
       "riferimento":"INAIL_31554702",
       "rilasciato_il":"2026-08-20",
       "valido_al":"2026-12-18"}' \
  $API/pubblica/documenti

tipo è la chiave del catalogo — durc, visura_camerale, formazione_generale, idoneita_sanitaria e le altre, visibili dal portale in Configurazione → Documenti fornitori. Per un documento personale si usa persona_id_esterno invece di impresa_id_esterno.

Il PDF si allega in base64 nel campo file, ed è facoltativo: la scadenza da sola basta a tenere aggiornati lo scadenzario e la risposta di /pubblica/abilitazione. Le richieste ai fornitori partono a mano dal portale.

Se il documento lo tenete già nel vostro archivio — SharePoint, un drive, il gestionale delle qualifiche — potete mandarne l'indirizzo invece del file, nel campo collegamento. Serve a non avere due copie della stessa carta: al primo rinnovo fatto da una parte sola, una delle due diventa quella sbagliata e nessuno sa quale.

curl -X POST -H "Authorization: Bearer $VIISI_KEY"   -H "Content-Type: application/json"   -d '{"tipo":"durc",
       "impresa_id_esterno":"FORN-4471",
       "valido_al":"2026-12-18",
       "collegamento":"https://archivio.esempio.it/durc/4471.pdf"}'   $API/pubblica/documenti

Tre regole, e la risposta dice quale delle due strade avete preso con "conservato" e "collegato":

  • O il file o il collegamento, non tutti e due.
  • Solo https://: punta al documento di una persona, e un indirizzo in chiaro si intercetta come qualunque altra cosa.
  • Non per i tipi che non conservano il file — vedi qui sotto: un collegamento sarebbe il modo di rimettere dentro dalla finestra quello che si è deciso di non tenere.
Un collegamento non è conservazione Se quel file lo spostate o lo cancellate, da noi resta un indirizzo che non apre più niente — e ve ne accorgete il giorno dell'ispezione. Per le carte che dovete poter esibire, mandate il file.
Alcuni documenti non conservano il file, ed è voluto L'idoneità sanitaria dice se una persona è idonea alla mansione: è un dato sulla salute. Di quel documento VIISI registra che esiste e quando scade; il file, se lo mandate, viene scartato. Lo dice la risposta con "conservato": false.

Caricare di nuovo lo stesso tipo per la stessa impresa o persona sostituisce: la versione precedente resta negli archivi ma esce dagli elenchi. È il rinnovo, e non serve cancellare prima.

Tornelli e lettori di badge

Una domanda sola: questo badge apre? Chi vende controllo accessi ha già i lettori e il software; quello che gli manca è la risposta.

POST /pubblica/abilitazione presenze:leggi

Richiesta

curl -X POST -H "Authorization: Bearer $VIISI_KEY" \
  -H "Content-Type: application/json" \
  -d '{"credenziale":"04:A2:BF:19"}' \
  $API/pubblica/abilitazione

Risposta

{
  "riconosciuta": true,
  "abilitata": false,
  "motivo": "Documenti non in regola",
  "nome": "Franco Verdi",
  "azienda": "Impianti Verdi Srl",
  "ostacoli": [
    { "tipo": "DURC",
      "situazione": "scaduto",
      "motivo": "DURC: scaduto il 28/08/2026" }
  ]
}
Un no è un esito, non un errore Risponde sempre 200, anche quando la risposta è «non abilitato». Un 403 manderebbe l'integrazione a cercare un guasto in sé stessa, quando invece ha funzionato tutto.

È una POST benché non cambi niente, e la ragione è che il codice di un tesserino non deve finire in un indirizzo: gli indirizzi si scrivono nei log di ogni proxy attraversato, e un UID letto da lì è un tesserino clonabile.

L'UID si può mandare come lo legge il lettore — 04:A2:BF:19, 04a2bf19, 04-A2-BF-19: separatori e maiuscole non contano. Lo stesso tag letto da due marche di lettore arriva scritto in due modi, e senza normalizzazione sarebbero due credenziali diverse.

Le ragioni di un no

motivoCosa è successo
Credenziale non riconosciutaIl codice non risulta. Non si dice altro: a un lettore non serve, e a chi prova a indovinare servirebbe
Credenziale revocataIl badge è stato disattivato dal portale
Credenziale scaduta il …Aveva una data di fine, ed è passata
Documenti non in regolaLa persona c'è e il badge vale, ma le sue carte no: ostacoli dice quali
Persona non più in anagraficaAnonimizzata o disattivata
Chiedete a ogni passaggio, non una volta al giorno La revoca ha effetto sulla richiesta successiva. Chi scarica l'elenco dei validi e lo tiene in memoria apre il varco a qualcuno che è stato revocato dieci minuti prima — ed è esattamente il caso per cui la revoca esiste.

Gli errori

Sempre nella stessa forma, con il percorso e l'istante: bastano a ritrovare la richiesta nei nostri registri se ci si scrive.

{
  "stato": 403,
  "messaggio": "Alla chiave mancano gli ambiti: visite:leggi.",
  "percorso": "/v1/api/pubblica/visite",
  "istante": "2026-09-02T14:26:12.330Z"
}
CodiceVuol direCosa fare
401 Chiave assente, scaduta o revocata Non ritentare: la chiave non tornerà buona da sola
403 Ambito mancante, o rotta non aperta alle chiavi Il messaggio dice quale ambito manca. Serve una chiave nuova: gli ambiti non si allargano
429 Troppe richieste — il limite è 120 al minuto Aspettare e riprovare. Se serve stabilmente di più, i webhook risolvono il problema alla radice
403 Servizio sospeso — l'abbonamento del cliente non è in regola Non riguarda l'integrazione: si sblocca quando il cliente rinnova o paga (per informazioni, info@viisi.it). L'API torna a rispondere da sola
5xx Un problema nostro Ritentare con attese crescenti. Se dura, scriveteci con l'istante
Non interrogate a ciclo continuo Chiedere «è arrivato qualcuno?» ogni cinque secondi consuma il limite e arriva comunque in ritardo. Per sapere le cose quando succedono ci sono i webhook, che è la sezione qui sotto.

I webhook

Si lascia un indirizzo HTTPS, e VIISI ci scrive quando succede qualcosa. Nessuna interrogazione ripetuta, nessun ritardo.

Si registrano dal portale — Configurazione → Integrazioni → Webhook — oppure dall'API con l'ambito webhook:gestisci:

curl -X POST -H "Authorization: Bearer $VIISI_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://gestionale.example/viisi",
       "eventi":["visita.ingresso","visita.uscita"],
       "descrizione":"Timbrature"}' \
  $API/pubblica/webhook

Con la stessa chiave si elencano (GET), si sospendono (PATCH con {"stato":"NA"}), si tolgono (DELETE), e si leggono le ultime consegne con il loro esito: GET /pubblica/webhook/{id}/consegne. È la rotta che evita la telefonata — si vede da soli se il proprio endpoint ha risposto 500 o se non è stato raggiunto affatto.

La risposta contiene il segreto con cui firmeremo le consegne. Come la chiave, si vede una volta sola.

Solo HTTPS Su HTTP il contenuto viaggia in chiaro, e le visite portano nome, cognome e azienda di persone vere. Un indirizzo che non comincia per https:// viene rifiutato alla registrazione.

Cosa si può ascoltare

EventoQuando
visita.creataUna visita entra in agenda, da chiunque sia stata inserita
visita.ingressoLa persona ha fatto il check-in ed è dentro
visita.uscitaCheck-out registrato
visita.autorizzataIl referente ha dato l'assenso a chi era in attesa. Oggi non succede: tutti i percorsi entrano senza autorizzazione
visita.rifiutataIl referente ha negato l'accesso. Oggi non succede, per lo stesso motivo
visita.invitataÈ partito l'invito al visitatore (in arrivo)
credenziale.rilasciataUn badge o un codice è stato assegnato (in arrivo)
credenziale.revocataUn badge o un codice non vale più (in arrivo)
documento.caricatoUn fornitore ha consegnato una carta (in arrivo)
documento.in_scadenzaUn documento entra nel preavviso (in arrivo)
documento.scadutoUn documento ha superato la sua validità (in arrivo)
Quelli «in arrivo» oggi non partono Ci si può già iscrivere, ma VIISI non li manda ancora. Oggi arrivano visita.creata, visita.ingresso e visita.uscita. Per i documenti, nel frattempo, c'è GET /pubblica/scadenzario.

L'elenco degli eventi a cui ci si può iscrivere si può anche chiedere al programma: GET /pubblica/eventi, con l'ambito webhook:gestisci. Comprende anche quelli in arrivo.

Cosa arriva

Intestazioni

Content-Type: application/json
X-VIISI-Evento:   visita.ingresso
X-VIISI-Istante:  1756822567
X-VIISI-Firma:    sha256=9f2b…
X-VIISI-Consegna: 4c8a1f7e-…
User-Agent:       VIISI-Webhook/1

Corpo

{
  "evento": "visita.ingresso",
  "istante": "2026-09-02T08:41:12.204Z",
  "dati": {
    "codice_visita": "V-2609-0007",
    "stato": "presente",
    "nome": "Mario",
    "cognome": "Rossi",
    "azienda": "Meccanica Rossi Snc",
    "sede": "Prato",
    "referente": "Marco Bianchi",
    "id_esterno": null,
    "id_esterno_referente": null
  }
}

Il vostro endpoint deve rispondere 2xx, e in fretta: dopo dieci secondi chiudiamo e la consideriamo fallita. Se il lavoro è lungo, mettete in coda e rispondete subito — un endpoint lento non deve rallentare un check-in.

Verificare la firma

Il vostro indirizzo è raggiungibile da chiunque lo conosca. La firma è come distinguete una consegna nostra da una di qualcun altro, ed è l'unico controllo che conta davvero.

È un HMAC-SHA256 calcolato sulla stringa istante + "." + corpo, con il segreto del webhook. Il corpo è quello grezzo, byte per byte come è arrivato: se lo si riserializza dopo averlo interpretato, uno spazio di differenza cambia la firma e non torna più niente.

Node.js

const express = require('express');
const crypto = require('crypto');
const app = express();

// Il corpo grezzo, non l'oggetto: express.json() lo consumerebbe.
app.post('/viisi', express.raw({ type: 'application/json' }), (req, res) => {
  const istante = req.headers['x-viisi-istante'];
  const atteso = crypto
    .createHmac('sha256', process.env.VIISI_SEGRETO)
    .update(`${istante}.${req.body}`)
    .digest('hex');

  // timingSafeEqual e non !==: il confronto normale esce al primo
  // carattere diverso, e quel tempo si misura.
  const ricevuta = Buffer.from(req.headers['x-viisi-firma'] ?? '');
  const calcolata = Buffer.from(`sha256=${atteso}`);
  if (ricevuta.length !== calcolata.length ||
      !crypto.timingSafeEqual(ricevuta, calcolata)) {
    return res.sendStatus(401);
  }

  // Più di cinque minuti: è una consegna vecchia rimandata da qualcuno.
  if (Math.abs(Date.now() / 1000 - Number(istante)) > 300) {
    return res.sendStatus(401);
  }

  const fatto = JSON.parse(req.body);
  // Rispondere subito, lavorare dopo.
  res.sendStatus(200);
  accoda(req.headers['x-viisi-consegna'], fatto);
});

Python

import hmac, hashlib, time
from flask import Flask, request, abort

app = Flask(__name__)

@app.post("/viisi")
def viisi():
    istante = request.headers.get("X-VIISI-Istante", "")
    corpo = request.get_data()  # grezzo, non request.json

    atteso = "sha256=" + hmac.new(
        SEGRETO.encode(),
        f"{istante}.".encode() + corpo,
        hashlib.sha256,
    ).hexdigest()

    # compare_digest e non ==: il tempo del confronto non deve dire nulla
    if not hmac.compare_digest(atteso, request.headers.get("X-VIISI-Firma", "")):
        abort(401)
    if abs(time.time() - int(istante)) > 300:
        abort(401)

    fatto = request.get_json()
    accoda(request.headers["X-VIISI-Consegna"], fatto)
    return "", 200

PHP

<?php
// Il corpo grezzo: $_POST lo avrebbe già interpretato.
$corpo   = file_get_contents('php://input');
$istante = $_SERVER['HTTP_X_VIISI_ISTANTE'] ?? '';
$firma   = $_SERVER['HTTP_X_VIISI_FIRMA'] ?? '';

$atteso = 'sha256=' . hash_hmac('sha256', "$istante.$corpo", VIISI_SEGRETO);

// hash_equals e non ===: il confronto a tempo costante
if (!hash_equals($atteso, $firma) || abs(time() - (int)$istante) > 300) {
    http_response_code(401);
    exit;
}

$fatto = json_decode($corpo, true);
http_response_code(200);
// il lavoro vero dopo aver risposto
accoda($_SERVER['HTTP_X_VIISI_CONSEGNA'], $fatto);

C#

// ASP.NET Core — minimal API
app.MapPost("/viisi", async (HttpRequest req) =>
{
    using var lettore = new StreamReader(req.Body);
    var corpo = await lettore.ReadToEndAsync();   // grezzo
    var istante = req.Headers["X-VIISI-Istante"].ToString();
    var firma = req.Headers["X-VIISI-Firma"].ToString();

    using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(Segreto));
    var calcolata = "sha256=" + Convert.ToHexString(
        hmac.ComputeHash(Encoding.UTF8.GetBytes($"{istante}.{corpo}"))).ToLowerInvariant();

    if (!CryptographicOperations.FixedTimeEquals(
            Encoding.UTF8.GetBytes(calcolata), Encoding.UTF8.GetBytes(firma)))
        return Results.Unauthorized();

    var eta = Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - long.Parse(istante));
    if (eta > 300) return Results.Unauthorized();

    Accoda(req.Headers["X-VIISI-Consegna"], JsonDocument.Parse(corpo));
    return Results.Ok();
});
Perché l'istante entra nella firma Firmare il solo corpo renderebbe ogni consegna riutilizzabile per sempre da chi l'ha vista passare: la stessa firma resterebbe valida mesi dopo. Con l'istante dentro, una consegna vecchia si riconosce e si rifiuta — purché lo controlliate, che è il motivo per cui in tutti e quattro gli esempi c'è quella riga.

Ritentativi, doppioni, sospensione

Cosa succede quando il vostro endpoint non risponde, e cosa dovete fare perché non ne nascano guai.

Cinque tentativi

Una consegna che fallisce viene rimandata dopo 1, 5, 15 e 60 minuti. Un endpoint che risponde male durante un aggiornamento non deve far perdere gli ingressi di quei minuti.

Lo stesso fatto può arrivare due volte

Se rispondete 200 ma la risposta si perde per strada, per noi è un fallimento e ritentiamo. Per questo ogni consegna porta X-VIISI-Consegna, che non cambia fra un tentativo e l'altro: tenete gli identificativi già trattati e scartate i ripetuti.

È la parte che si sbaglia più spesso Senza questo controllo, un timeout dalla nostra parte diventa una visita doppia dalla vostra — e il difetto si vede settimane dopo, quando qualcuno nota due timbrature identiche.

Un indirizzo morto si sospende

Dopo venti fallimenti di fila l'indirizzo passa in stato sospeso e smettiamo di scrivergli. Non è una punizione: una coda che cresce senza fine per un endpoint spento rallenta le consegne di tutti gli altri. Si riattiva dal portale, e il conteggio riparte da zero.

Come si vede cosa è successo

Nel portale, per ogni indirizzo, ci sono le ultime consegne con il loro esito: codice HTTP, errore di rete, numero di tentativi. Un 500 dal vostro endpoint e un nome DNS che non si risolve si vedono diversi, ed è quello che evita che la conversazione cominci con «da noi funziona».

L'ordine non è garantito

Due fatti vicini possono arrivare invertiti — un ingresso e un'uscita a pochi secondi, con il primo tentativo fallito. Se l'ordine conta, guardate istante nel corpo, che è il momento del fatto e non quello della consegna.

Cosa promettiamo

Un'integrazione scritta oggi deve funzionare fra due anni senza che nessuno la tocchi. Ecco cosa promettiamo e cosa no.

  • I campi che ci sono restano. Possiamo aggiungerne di nuovi — il vostro programma deve ignorare quelli che non conosce — ma non togliamo né rinominiamo quelli documentati qui.
  • I valori chiusi possono allungarsi. Un stato o un evento nuovo può comparire: trattate l'ignoto senza rompervi, non con un errore.
  • Il prefisso /v1/ non cambia significato. Se un giorno servisse un cambiamento che rompe, sarà /v2/, e la /v1/ resterà per almeno dodici mesi.
  • Le rotte del portale non sono un'API. Cambiano quando cambia l'interfaccia, senza preavviso. Se vi serve qualcosa che non è sotto /pubblica, chiedetecelo: aggiungerlo qui è il modo per non trovarselo rotto.

Domande frequenti

La chiave vede tutte le sedi?

Sì: è una chiave dell'azienda, non di una sede. Il campo sede è in ogni risposta, e si filtra dalla vostra parte.

Posso creare una visita dall'API?

Non ancora. L'ambito visite:scrivi esiste già ed è il posto dove arriverà. Nel frattempo, per far programmare le visite da un assistente — Claude o altri — c'è il collegamento MCP, descritto nel manuale. Il programma di collegamento per ora ve lo mandiamo noi, su richiesta ad assistenza@viisi.it, e usa le credenziali del portale di chi lo installa.

Quanto tempo restano i dati?

I tempi sono gli stessi per tutti, e valgono anche per l'API: se una persona non torna da 24 mesi, nome, recapiti, targa, foto, note e codice fiscale si cancellano (le visite restano, senza questi dati: in /pubblica/visite il nome diventa «Anonimizzato»); le foto si cancellano dopo 7 giorni, salvo quelle dei tesserini permanenti ancora validi. Se il cliente ha bisogno di tempi diversi, li impostiamo noi su sua richiesta. Se il vostro programma deve tenere uno storico con i nomi più a lungo, deve conservarlo lui, e il cliente deve saperlo.

C'è un ambiente di prova?

I trenta giorni di prova sono un'azienda vera con dati vostri: è lì che si collauda un'integrazione, senza toccare quella di produzione. Chi ha già un abbonamento può chiederci una seconda azienda per le prove.

Come vi segnalo un problema?

A assistenza@viisi.it, con l'istante preso dalla risposta d'errore o dalla consegna: con quello ritroviamo la richiesta esatta nei registri.