Endpoint REST che legge il PDF di un bando di gara o di un disciplinare italiano e restituisce JSON. Non solo i campi come li scrive il documento: accanto arriva il blocco normalizzati, cioè gli stessi dati pronti da inserire in un database — date in ISO 8601, importi come numeri, CIG e CUP in lista, categorie SOA divise in sigla e classifica, criterio di aggiudicazione come una parola sola. Quello che di solito ti tocca scrivere dopo aver comprato un’API, qui è già fatto.
Questo strumento non è ancora disponibile: lo stiamo preparando con la stessa cura del resto del motore.
Guarda un esempio di risultato
Perché esiste: i campi che vendiamo non stanno in nessun open data
Se ti serve solo il CIG, l’importo e la stazione appaltante, non comprare niente: te li dà gratis l’API CIG di ANAC. Misurato il 27/09/2026 su un CIG vero, quella risposta ha 61 campi e fra questi zero requisiti di partecipazione, zero punteggi, zero termine dei chiarimenti, zero sopralluogo. Lo stesso vale per la Pubblicità Legale ANAC (50 chiavi) e per OCDS/BDNCP (74 percorsi). Su 100 bandi ANAC della settimana 20-27/09 la categoria SOA c’era su 100, ma senza la classifica: e senza la classifica non sai se puoi partecipare. TED dà i pesi dei criteri su 96 avvisi italiani su 300 e il termine dei chiarimenti su 0 su 300, e non ha alcun campo SOA.
Quei dati esistono solo dentro il PDF del disciplinare. Questo endpoint serve a tirarli fuori.
Che cosa cambia rispetto al JSON «come sta scritto»
Queste sono stringhe vere, misurate su tre bandi italiani letti il 22/09/2026 (INPS Sondrio 50 pagine, Padova 81, Trecastagni 31). A sinistra ciò che il documento scrive e che il prodotto web consegna tale e quale; a destra ciò che ricevi in normalizzati.
| Nel documento c’è scritto | In normalizzati ricevi |
|---|---|
| «10 marzo 2026», «23.04.2026», «08/02/2026» — tre bandi, tre formati | "2026-03-10", "2026-04-23", "2026-02-08" + "ora": "12:00" |
| «€ 209.487,33» | 209487.33 con "valuta": "EUR" |
| «BACC321347 e BACC32241A» (gara a due lotti, un campo solo) | ["BACC321347", "BACC32241A"] |
| «OG1», «III-bis», «prevalente» in tre stringhe libere | sigla: "OG1", classifica: "III-BIS", classifica_ordine: 4, ruolo: "prevalente" |
| «Procedura aperta con il criterio dell’offerta economicamente più vantaggiosa» | "offerta_economicamente_piu_vantaggiosa" — un valore da switch |
| «420 giorni naturali e consecutivi» | {"valore": 420, "unita": "giorni"} |
| «sopralluogo obbligatorio a pena di esclusione» | sopralluogo_obbligatorio: true |
| «€ 400.000,00 oltre IVA» (una cifra dentro una frase) | null — e il campo compare in non_normalizzati col motivo: «la cifra non è scritta da sola» |
L’ultima riga è la più importante. Un’API che ti restituisce 400000 al posto di «€ 400.000,00 oltre IVA» ha indovinato, e tu non lo sai. Qui ciò che non si converte resta null ed è elencato per nome con la stringa originale e il motivo: il tuo codice può decidere cosa farne, e un revisore umano sa dove guardare. classifica_ordine è la posizione nell’ordine ufficiale delle classifiche SOA (I, II, III, III-bis, IV, IV-bis, V, VI, VII, VIII): serve a confrontare «ho la III» con «serve la IV» con un >=.
Esempio vero di normalizzati
Lo stesso bando dell’esempio pubblico del prodotto web: INPS – Direzione Provinciale di Sondrio, rifacimento dei servizi igienici, 50 pagine, letto il 22/09/2026. Le chiavi sono sempre queste, su ogni bando: null quando il documento non le dice, mai assenti.
{
"schema": "tender.normalizzati/1",
"nota": "Ricavato dai campi di `fields` con conversioni nostre, senza IA: date in ISO 8601, importi in numeri, codici in liste. Nessun valore aggiunto o dedotto: ciò che non si è potuto convertire resta null ed è elencato in `non_normalizzati` con il testo del documento.",
"identificativi": {
"cig": [
"BA3CDB07B2"
],
"cup": [
"F77H20005120005"
],
"numero_gara": "RS30-2026-00038"
},
"importi": {
"valuta": "EUR",
"importo_base_gara": 209487.33,
"oneri_sicurezza": 5312.67,
"importo_complessivo": 214800,
"costi_manodopera": 65095.82,
"garanzia_provvisoria": 4296
},
"date": {
"termine_offerte": {
"data": "2026-03-10",
"ora": "12:00",
"descrizione": "Termine per la presentazione delle offerte"
},
"termine_chiarimenti": {
"data": "2026-03-03",
"ora": "12:00",
"descrizione": "Termine per la richiesta di chiarimenti"
},
"sopralluogo": null,
"seduta_pubblica": null,
"inizio_esecuzione": null,
"altre": []
},
"categorie_soa": [
{
"sigla": "OG1",
"come_scritta": "OG1",
"classifica": "I",
"classifica_ordine": 1,
"ruolo": "prevalente",
"importo": 214800
}
],
"cpv": [
"45332400-7"
],
"criterio_aggiudicazione": "prezzo_piu_basso",
"punteggi": null,
"durata": {
"valore": 120,
"unita": "giorni",
"come_scritta": "120 giorni naturali e consecutivi"
},
"sopralluogo_obbligatorio": true,
"non_normalizzati": []
}
Il blocco normalizzati arriva accanto a fields, che resta con le parole del documento: se un valore convertito non ti torna, il testo originale è nella stessa risposta.
Risultato
Un file JSON con fields — i campi con le parole del documento: ente, oggetto, identificativi, importi, procedura, requisiti, criteri di valutazione, scadenze, categorie SOA, CPV, durata, garanzia provvisoria, sopralluogo — e accanto normalizzati, gli stessi dati convertiti per una macchina. Poi un blocco riferimenti che per ogni campo dice la pagina e, quando c’è, la sezione in cui il dato è scritto; un elenco not_found dei campi cercati e non presenti nel documento; e un blocco quality con quante pagine sono state lette, se il risultato è completo e gli avvisi. Le chiavi sono le stesse su ogni bando, anche quando il valore è null: è la condizione per importare senza riscrivere il codice a ogni documento nuovo.
I requisiti, i criteri di valutazione e le scadenze sono elenchi, e su un elenco lungo il risultato può contenerne solo una parte anche quando la lettura arriva in fondo al documento. Non è il limite di lettura descritto sopra — quello riguarda quante pagine apriamo — ma quante voci l’elaborazione riesce a restituire, e non c’è una soglia fissa da dirti in anticipo: dipende da quante voci ha l’elenco e da quanto è lunga ognuna. Quando il documento numera le voci — 1), 2), 3) — ce ne accorgiamo e te lo diciamo: il file porta un blocco elenchi_incompleti con la prima voce che manca, e dentro quality il campo complete passa a false. Quando invece il documento le elenca senza numerarle — trattini, puntini, rientri — non abbiamo modo di accorgercene, e un elenco accorciato ti arriva senza nessun avviso. In tutti e due i casi il risultato ti viene consegnato e i crediti restano spesi. Se ti serve l’elenco completo, dividi il documento in più PDF di poche pagine ciascuno, caricali come lavori separati e unisci i risultati.
Input supportati
Bandi, disciplinari e avvisi di gara in PDF fino a 15 MB con testo selezionabile. Un job elabora un documento. I PDF fatti solo di immagini — le scansioni che non sono passate da un OCR — non sono ancora supportati: l’elaborazione si ferma subito, viene segnalata come non riuscita e non ti viene addebitato nulla. I PDF protetti da password vengono rifiutati allo stesso modo: senza la password le pagine non si aprono, l’elaborazione si ferma subito e non ti viene addebitato nulla. Apri il file con la sua password, salvane una copia senza protezione e carica quella. Per saperlo prima di caricare: se il PDF si apre senza chiederti una password e riesci a selezionare una riga di testo, il documento si legge — ma la prova vale pagina per pagina. Un documento in parte digitale e in parte scansionato (una pagina fotografata, un allegato firmato a mano) viene elaborato e addebitato, perché il testo delle altre pagine c’è: in quel caso il risultato ti dice quali pagine non contenevano testo selezionabile e che il loro contenuto non è nel file, invece di dichiararsi completo. Se invece da quelle pagine non esce nessuno dei dati che questo strumento estrae — è il caso della copertina digitale generata dal gestionale sopra un documento fotocopiato — l’elaborazione si ferma, non ti viene addebitato nulla e il messaggio ti dice quante pagine sono immagini: il gesto che sblocca è passare il PDF per un OCR, non caricare un altro file. Ogni elaborazione legge al massimo le prime 80 pagine del PDF e, dentro quelle, circa 280.000 caratteri di testo. I due tetti sono tarati per mordere nello stesso punto: misurato l’8 settembre 2026 su dieci documenti di gara italiani veri, è esattamente quanto testo portano ottanta pagine. Solo su un documento molto più fitto della media — un computo metrico, un allegato a righe serrate — è il limite sui caratteri ad arrivare per primo, e la lettura si ferma prima dell’ottantesima pagina. Il risultato dichiara sempre a quale pagina la lettura si è fermata, e il messaggio ti dice da quale pagina ripartire. Se un dato obbligatorio è rimasto oltre il taglio l’elaborazione si ferma e non ti viene addebitato nulla; se invece i dati obbligatori erano tutti nelle pagine lette il risultato ti viene consegnato e i crediti restano spesi, e ciò che non abbiamo trovato è dichiarato come non presente nella parte letta, mai come assente dal documento. In entrambi i casi puoi caricare come lavoro separato un PDF con le pagine che ti servono, tenendo dentro anche le pagine iniziali del documento: questo strumento estrae i suoi dati in una lettura sola, e un PDF con le sole pagine finali verrebbe rifiutato perché i dati di apertura non ci sarebbero.
Il contratto tecnico
L’endpoint pubblico non è ancora aperto (vedi «Stato» qui sotto). Il contratto qui sotto è però quello del codice che gira già oggi per il prodotto web, non una bozza: base REST https://megaengine.it/wp-json/mega-engine/v1/, chiavi API a scope limitato conservate solo come hash, modello asincrono (crei un lavoro, leggi l’esito quando è pronto).
# 1. crea il lavoro (multipart, UN file per lavoro)
curl -X POST https://megaengine.it/wp-json/mega-engine/v1/jobs
-H "X-API-Key: $ME_API_KEY"
-F "product=tender-to-json-api"
-F "file=@disciplinare.pdf"
# → 201 {"uuid":"7b1e…","status":"queued","credits_charged":2}
# 2. leggi lo stato
curl https://megaengine.it/wp-json/mega-engine/v1/jobs/7b1e…
-H "X-API-Key: $ME_API_KEY"
# → {"status":"succeeded","output":{"summary":[…],"preview":[…]}}
# 3. scarica il JSON intero (con `fields` e `normalizzati`)
curl -L https://megaengine.it/wp-json/mega-engine/v1/jobs/7b1e…/download
-H "X-API-Key: $ME_API_KEY" -o bando.json
| Cosa | Valore |
|---|---|
| Autenticazione | X-API-Key: <chiave> oppure Authorization: Bearer <chiave>; chiavi create e revocate da /app/ |
| Creazione lavoro | POST /jobs — multipart, campi product e file |
| Stato ed esito | GET /jobs/<uuid> |
| File del risultato | GET /jobs/<uuid>/download — disponibile 14 giorni |
| Annullamento | POST /jobs/<uuid>/cancel |
| Scheda del prodotto | GET /products/tender-to-json-api |
| Errori | 401 chiave assente o revocata · 402 crediti finiti · 400 file rifiutato (estensione, peso, PDF illeggibile) · 429 troppe richieste · 5xx guasto nostro: il lavoro fallisce e i crediti tornano |
| Tetti | 50 lavori al giorno per account (≈1.500 al mese) · 15 MB per file · 80 pagine per documento |
Quanto costa
2 crediti per lavoro, cioè da 0,98 € (pacchetto da 100 crediti a 49 €) a 1,95 € (pacchetto da 4 crediti a 3,90 €). Ogni nuovo account riceve 3 crediti di benvenuto: la prima chiamata la provi senza pagare. Un lavoro = un documento: il bando e il disciplinare della stessa gara sono due chiamate (4 crediti).
Per confronto, misurato il 27/09/2026 sui listini pubblici: i quattro servizi italiani che leggono i documenti di gara con l’IA si vendono a postazione — dove il prezzo è pubblico, da 199 a 449 € al mese per utente (Tender Brain) o 450-1.900 € all’anno (InfoBandiPA) — e nessuno dei quattro espone un’API. Chi espone un’API dà i metadati ANAC/TED, non il contenuto del PDF; l’unico estrattore di documenti di gara su Apify costa 200 $ ogni 1.000 documenti ma legge solo avvisi in inglese e non segue il link al disciplinare. Un estrattore generico (Azure, Google, AWS) costa dai 10 ai 50 $ ogni 1.000 pagine ma non ha un modello per i bandi: i loro modelli pronti sono fatture, ricevute e documenti d’identità.
Altri limiti
- Un file per lavoro. Non si mandano bando e disciplinare nella stessa richiesta.
- La confidenza la dichiara il modello su sé stesso (
quality.confidence_origin), non è una verifica indipendente: un’estrazione sbagliata può dichiararsi sicura quanto una giusta. - Non è una lettura legale. Scadenze e requisiti vanno verificati sul documento ufficiale prima di partecipare. Rettifiche, proroghe e chiarimenti pubblicati dopo il documento non ci sono: non interroghiamo ANAC.
- Conservazione: il file che carichi viene cancellato appena il lavoro riesce; il file del risultato resta 14 giorni; il file di un lavoro fallito resta 2 giorni.
- Nessuno SLA. Il servizio è in beta e può avere interruzioni; la responsabilità è limitata a quanto hai speso nei 12 mesi precedenti.
- Nessuna fattura. Il gestore è una persona fisica senza partita IVA e non emette fattura: se la tua azienda ne ha bisogno, scrivici prima di comprare.
Che cosa non fa
- Non trova i bandi: legge il documento che gli mandi tu. Per scoprire le gare nuove ci sono Bandi Radar e Tender Database.
- Non legge i PDF scansionati né le fotografie: serve un PDF con il testo selezionabile.
- Un lavoro legge un documento: bando e disciplinare della stessa gara sono due chiamate.
- Non verifica il bando su ANAC: rettifiche, proroghe e chiarimenti pubblicati dopo il documento non ci sono.
- Non dice se conviene partecipare né compila la domanda: estrae i dati dichiarati dal documento.
Dati e IA
Il testo estratto dal PDF viene mandato a OpenAI per la lettura dei campi; il file originale no. Il blocco normalizzati invece non passa da nessun modello: sono conversioni deterministiche fatte qui sui campi già letti, con lo stesso lettore di date e di importi che genera il foglio Excel del prodotto web — per questo la data del JSON e quella del foglio non possono divergere. I dati non sono usati per addestrare modelli. Dettaglio completo in Trattamento dati & AI.
Stato
L’endpoint pubblico non è ancora aperto. Il motore, il tracciato dei campi e il blocco normalizzati funzionano e sono coperti da test; quello che manca è la decisione di aprire l’accesso programmatico a chi non ha un account dal pannello. Fino ad allora il contratto qui sopra è documentazione verificata sul codice, non una promessa di disponibilità: se ti serve adesso, il motore identico si usa da Bando → JSON, che consegna anche l’Excel del registro gare.
Domande frequenti
In che cosa è diverso dal prodotto web «Bando → JSON»?
Il motore di lettura è lo stesso. Cambiano il canale — qui una chiamata REST invece di un modulo da riempire — e la forma dell’uscita: il prodotto web consegna i campi con le parole del documento più un Excel pronto da incollare nel registro gare; qui ricevi in più il blocco normalizzati, pensato per finire diritto in un database senza passare da un parser tuo.
Posso fidarmi delle date e degli importi convertiti?
Ogni valore di normalizzati viene da una stringa che il documento scrive: non c’è niente aggiunto e niente dedotto. Una data si converte solo se nella frase ce n’è una sola; un importo solo se la cifra è scritta da sola. In tutti gli altri casi il valore resta null e il campo compare in non_normalizzati col testo originale e il motivo. Per la partecipazione fa fede il documento ufficiale.
Gestisce le gare a lotti?
I CIG arrivano in lista, quindi una gara a due lotti non ti nasconde il secondo codice. Non c’è ancora un blocco per lotto con oggetto e importo separati: i dati dei lotti, quando il documento li distingue, restano dentro i campi di fields.
Che cosa succede se l’estrazione fallisce?
Se il lavoro fallisce, viene annullato o scade, i crediti tornano automaticamente sul tuo saldo e la risposta dice il motivo. Attenzione a un caso: un risultato parziale (per esempio un elenco accorciato su un documento molto lungo) viene consegnato e pagato — la risposta lo dichiara negli avvisi, ma non è un fallimento e non viene rimborsato da solo. Se ti sembra sbagliato, scrivici.
Quante richieste posso fare?
50 lavori al giorno per account, circa 1.500 al mese. Se ti serve di più, scrivici prima di progettare l’integrazione.