Endpoint
GET /health
Sezione intitolata “GET /health”Sonda di liveness pubblica.
curl -s https://api.factum.pyragogy.org/health{"status":"ok"}GET /status
Sezione intitolata “GET /status”Stato operativo pubblico. La risposta include versione del servizio, uptime e, quando il DB è raggiungibile, contatori dei job e informazioni aggregate sui costi.
curl -s https://api.factum.pyragogy.org/statusPOST /v1/uploads
Sezione intitolata “POST /v1/uploads”Carica un FatturaPA XML o una fattura PDF. L’endpoint restituisce metadata di
ingestion; non restituisce l’envelope parsato in DocumentResponse.
Auth: X-API-Key (header).
Richiesta
Sezione intitolata “Richiesta”curl -X POST 'https://api.factum.pyragogy.org/v1/uploads' \ -H 'X-API-Key: YOUR_API_KEY' \ -F 'file=@./invoice.xml;type=application/xml'L’unico campo multipart è file. Il nome file è fornito come metadata multipart,
non come campo separato.
Risposta — DocumentResponse
Sezione intitolata “Risposta — DocumentResponse”| Campo | Tipo | Descrizione |
|---|---|---|
job_id |
string (UUID) | ID del job per polling /v1/uploads/{job_id} |
doc_type |
string | Tipo rilevato (es. fattura_pa_xml, invoice_pdf, generic) |
content_hash |
string | SHA-256 del contenuto (whitespace-collapsed per XML) |
size_bytes |
integer | Dimensione file in byte |
mime_type |
string | MIME type rilevato |
deduplicated |
boolean | true se il documento era già processato (cache hit) |
status |
enum | done | requires_vision | queued | failed | expired |
message |
string | null | Messaggio contestuale (es. errore di validazione) |
Per PDF, status è attualmente requires_vision e il messaggio rimanda a
/v1/parse per l’estrazione semantica.
{ "job_id": "3f2b5e6a-0000-4000-8000-000000000004", "doc_type": "fattura_pa_xml", "content_hash": "a1b2c3d4e5f6...", "size_bytes": 2841, "mime_type": "text/xml", "deduplicated": false, "status": "done", "message": null}Errori upload
Sezione intitolata “Errori upload”| Status | Significato |
|---|---|
400 |
File vuoto |
401 |
X-API-Key mancante o non valida |
413 |
Body/file supera il limite di 10 MiB (max_upload_bytes) |
415 |
Tipo documento/content non supportato (non XML/PDF) |
422 |
XML FatturaPA rilevato ma parsing deterministico fallito, PDF senza layer di testo (< 80 char), o PDF > 50 pagine |
500 |
Errore imprevisto server/storage/database |
Gli errori upload usano RFC 7807 application/problem+json.
GET /v1/uploads/{job_id}
Sezione intitolata “GET /v1/uploads/{job_id}”Polling di un job upload.
curl -s 'https://api.factum.pyragogy.org/v1/uploads/JOB_ID' \ -H 'X-API-Key: YOUR_API_KEY'Un job inesistente restituisce 404 come RFC 7807 application/problem+json.
La risposta rimane DocumentResponse; il DocumentEnvelope parsato non è
incluso in questo modello di risposta.
POST /v1/parse
Sezione intitolata “POST /v1/parse”Elabora un job caricato o un testo diretto. Lo schema backend è ParseRequest.
Auth: X-API-Key (header).
Importante: job_id e text sono XOR — esattamente uno dei due deve essere presente.
| Campo | Tipo | Obbligatorio | Descrizione |
|---|---|---|---|
job_id |
UUID | null | XOR con text |
Job ID da /v1/uploads |
text |
string | null | XOR con job_id |
Testo diretto. Zero-retention: mai su disco/DB |
doc_type |
"auto" | "fattura" | "f24" | "generico" |
No (default: "auto") |
Hint di tipo documento |
async_mode |
boolean | No (default: false) |
true → 202 Accepted. Non valido con text |
callback_url |
URL HTTPS | null | No | Webhook per async. Solo con async_mode=true |
Vincoli di validazione (422):
text< 80 caratteri → rifiutato (layer di testo assente)text> 200.000 caratteri → rifiutato (RIZZO_PII_MAX_TEXT_CHARS)async_mode=true+text→ 422 (testo è stateless, solo in RAM)callback_urlsenzaasync_mode=true→ 422job_idetextentrambi presenti o entrambi assenti → 422 (XOR)
Testo diretto — sincrono
Sezione intitolata “Testo diretto — sincrono”curl -X POST 'https://api.factum.pyragogy.org/v1/parse' \ -H 'X-API-Key: YOUR_API_KEY' \ -H 'Content-Type: application/json' \ -d '{"text":"Fattura n. 2026/001 del 08/09/2026. Cedente: ACME S.r.l. (P.IVA IT01234567890), Via Roma 1, Milano. Cessionario: Beta S.p.A., Via Verdi 15, Torino. Totale documento: 1.220,00 EUR. Imponibile: 1.000,00 EUR. IVA 22%: 220,00 EUR.","doc_type":"auto","async_mode":false}'Python · httpx
import httpx
payload = { "text": "Fattura n. 2026/001 del 08/09/2026. Cedente: ACME S.r.l. (P.IVA IT01234567890), Via Roma 1, Milano. Cessionario: Beta S.p.A., Via Verdi 15, Torino. Totale documento: 1.220,00 EUR. Imponibile: 1.000,00 EUR. IVA 22%: 220,00 EUR.", "doc_type": "auto", "async_mode": False,}
response = httpx.post( "https://api.factum.pyragogy.org/v1/parse", headers={"X-API-Key": "YOUR_API_KEY"}, json=payload,)response.raise_for_status()print(response.json())TypeScript · fetch
// Sostituisci con la tua chiave APIconst API_KEY = "YOUR_API_KEY";
const response = await fetch("https://api.factum.pyragogy.org/v1/parse", { method: "POST", headers: { "X-API-Key": API_KEY, "Content-Type": "application/json", }, body: JSON.stringify({ text: "Fattura n. 2026/001 del 08/09/2026. Cedente: ACME S.r.l. (P.IVA IT01234567890), Via Roma 1, Milano. Cessionario: Beta S.p.A., Via Verdi 15, Torino. Totale documento: 1.220,00 EUR. Imponibile: 1.000,00 EUR. IVA 22%: 220,00 EUR.", doc_type: "auto", async_mode: false, }),});
if (!response.ok) throw new Error(`${response.status}: ${await response.text()}`);const result = await response.json();Go · net/http
package main
import ( "bytes" "fmt" "net/http")
func main() { body := []byte(`{"text":"Fattura n. 2026/001 del 08/09/2026. Cedente: ACME S.r.l. (P.IVA IT01234567890), Via Roma 1, Milano. Cessionario: Beta S.p.A., Via Verdi 15, Torino. Totale documento: 1.220,00 EUR. Imponibile: 1.000,00 EUR. IVA 22%: 220,00 EUR.","doc_type":"auto","async_mode":false}`) req, _ := http.NewRequest("POST", "https://api.factum.pyragogy.org/v1/parse", bytes.NewReader(body)) req.Header.Set("X-API-Key", "YOUR_API_KEY") req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer res.Body.Close() fmt.Println(res.Status)}PHP · cURL
<?php$apiKey = 'YOUR_API_KEY'; // Sostituisci con la tua chiave$ch = curl_init('https://api.factum.pyragogy.org/v1/parse');curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_HTTPHEADER => [ 'X-API-Key: ' . $apiKey, 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => json_encode([ 'text' => 'Fattura n. 2026/001 del 08/09/2026. Cedente: ACME S.r.l. (P.IVA IT01234567890), Via Roma 1, Milano. Cessionario: Beta S.p.A., Via Verdi 15, Torino. Totale documento: 1.220,00 EUR. Imponibile: 1.000,00 EUR. IVA 22%: 220,00 EUR.', 'doc_type' => 'auto', 'async_mode' => false, ]), CURLOPT_RETURNTRANSFER => true,]);$response = curl_exec($ch);if ($response === false) throw new RuntimeException(curl_error($ch));curl_close($ch);echo $response;Job — sincrono
Sezione intitolata “Job — sincrono”curl -X POST 'https://api.factum.pyragogy.org/v1/parse' \ -H 'X-API-Key: YOUR_API_KEY' \ -H 'Content-Type: application/json' \ -d '{"job_id":"3f2b5e6a-0000-4000-8000-000000000004","doc_type":"auto","async_mode":false}'Job — asincrono
Sezione intitolata “Job — asincrono”{ "job_id": "3f2b5e6a-0000-4000-8000-000000000004", "doc_type": "fattura", "async_mode": true, "callback_url": "https://example.com/factum/callback"}Risposta — ParseResponse
Sezione intitolata “Risposta — ParseResponse”| Campo | Tipo | Descrizione |
|---|---|---|
job_id |
string | UUID del job |
status |
"done" | "failed" | "queued" |
Stato elaborazione |
document_type |
string | Tipo documento rilevato (es. "fattura") |
provider_used |
string | Provider LLM ("deepseek-v3", "gpt-4o-mini", "") |
prompt_version |
string | Versione del prompt template |
confidence |
float | null | Confidence score 0.0–1.0 (1.0 = deterministico) |
cost_usd |
float | Costo LLM in USD (0.0 per deterministico) |
tokens |
integer | Token LLM consumati |
fallbacks |
string[] | Fallback LLM attivati |
content_hash |
string | SHA-256 canonico del documento |
result |
DocumentEnvelope | null |
Contenuto strutturato v2 (null su errore) |
error |
string | null | Messaggio errore se status: failed |
{ "job_id": "3f2b5e6a-0000-4000-8000-000000000004", "status": "done", "document_type": "invoice", "provider_used": "fast-path", "prompt_version": "fast-path-v1", "confidence": 1.0, "cost_usd": 0.0, "tokens": 0, "fallbacks": [], "content_hash": "a1b2c3d4...", "result": { "schema_version": "2.0", "document_type": "invoice", "meta": { "confidence": 1.0, "provider": "fast-path", "prompt_version": "fast-path-v1" }, "payload": { "kind": "invoice", "dati_trasmissione": {}, "cedente_prestatore": {}, "cessionario_committente": {}, "corpi": [] } }, "error": null}result è null in caso di errore. Il testo diretto è stateless: non crea un
ParseJob.
Errori parse
Sezione intitolata “Errori parse”| Status | Significato |
|---|---|
401 |
X-API-Key mancante o non valida |
404 |
job_id referenziato inesistente |
422 |
Sorgente non valida (violazione XOR), limiti text (< 80 o > 200K char), estrazione rifiutata |
500 |
Errore imprevisto parsing/provider/sidecar privacy |
503 |
Sidecar Rizzo non raggiungibile; fail-closed |
Distinzione contrattuale importante
Sezione intitolata “Distinzione contrattuale importante”DocumentResponse e ParseResponse non sono intercambiabili. La prima
descrive lo stato di ingestion; la seconda trasporta il DocumentEnvelope v2
strutturato quando il parsing produce un risultato.
POST /v1/webhooks/lemonsqueezy
Sezione intitolata “POST /v1/webhooks/lemonsqueezy”Webhook per la gestione self-service delle licenze Lemon Squeezy.
L’endpoint non richiede autenticazione API key; la firma HMAC-SHA256
nel header X-Signature funge da autenticazione implicita.
Non genera documentazione requestBody in OpenAPI (usa request: Request
di Starlette/FastAPI per leggere il body raw, necessario per la verifica HMAC).
Eventi supportati
Sezione intitolata “Eventi supportati”| Evento | Azione |
|---|---|
subscription_created |
Crea chiave API (status: active) |
order_created |
Crea chiave API (status: active) |
subscription_updated |
Aggiorna tier e rate limit |
subscription_cancelled |
Revoca chiavi (status: revoked) |
subscription_expired |
Revoca chiavi (status: revoked) |
subscription_resumed |
Riattiva chiavi (status: active) |
Rate limit per tier
Sezione intitolata “Rate limit per tier”| Tier | Limite |
|---|---|
starter |
60 richieste/minuto |
pro |
300 richieste/minuto |
custom |
1000 richieste/minuto |
Esempio webhook (test locale)
Sezione intitolata “Esempio webhook (test locale)”# Simula un evento subscription_createdLEMON_SECRET="your_webhook_secret"PAYLOAD='{"meta":{"event_name":"subscription_created"},"data":{"attributes":{"user_email":"user@example.com","variant_name":"starter","first_subscription":{"license_key":{"key":"ls_xxxxxxxxx"}}}}}'SIGNATURE=$(echo -n "$PAYLOAD" | openssl dgst -sha256 -hmac "$LEMON_SECRET" | awk '{print $2}')curl -X POST 'https://api.factum.pyragogy.org/v1/webhooks/lemonsqueezy' \ -H 'Content-Type: application/json' \ -H "X-Signature: $SIGNATURE" \ -d "$PAYLOAD"Errori webhook
Sezione intitolata “Errori webhook”| Status | Significato |
|---|---|
200 |
Evento processato (anche se ignorato) |
400 |
Payload JSON non valido |
401 |
Firma HMAC mancante o non valida |
503 |
LEMONSQUEEZY_WEBHOOK_SECRET non configurato |
Risposta
Sezione intitolata “Risposta”{ "status": "created", "subscription_id": "sub_xxxxxxx", "api_key": "factum_xxxxxxxxxxxxxxxxxxxx"}