Endpoints
GET /health
Section titled “GET /health”Public liveness probe.
curl -s https://api.factum.pyragogy.org/health{"status":"ok","service":"factum-parse"}GET /status
Section titled “GET /status”Public operational status. The response includes service version, uptime and, when the DB is reachable, job counters and aggregate cost information.
curl -s https://api.factum.pyragogy.org/statusPOST /v1/uploads
Section titled “POST /v1/uploads”Upload one FatturaPA XML or invoice PDF. The endpoint returns ingestion metadata; it does
not return the parsed envelope in DocumentResponse.
Auth: X-API-Key (header).
Request
Section titled “Request”curl -X POST 'https://api.factum.pyragogy.org/v1/uploads' \ -H 'X-API-Key: YOUR_API_KEY' \ -F 'file=@./invoice.xml;type=application/xml'The only multipart form field is file. The filename is supplied as multipart metadata,
not as a separate filename field.
Response — DocumentResponse
Section titled “Response — 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) |
For PDF ingestion, status is currently requires_vision and the response message points
to /v1/parse for semantic extraction.
{ "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}Upload errors
Section titled “Upload errors”| Status | Meaning |
|---|---|
400 |
Empty file |
401 |
Missing/invalid X-API-Key |
413 |
Body/file exceeds 10 MiB limit (max_upload_bytes) |
415 |
Unsupported document/content type (non-XML/PDF) |
422 |
FatturaPA XML detected but deterministic parsing fails, PDF without text layer (< 80 chars), or PDF > 50 pages |
500 |
Unexpected server/storage/database failure |
Upload errors use RFC 7807 application/problem+json.
GET /v1/uploads/{job_id}
Section titled “GET /v1/uploads/{job_id}”Poll an upload job.
curl -s 'https://api.factum.pyragogy.org/v1/uploads/JOB_ID' \ -H 'X-API-Key: YOUR_API_KEY'A missing job returns 404 as RFC 7807 application/problem+json.
The response remains DocumentResponse; the parsed DocumentEnvelope is not embedded in
this response model.
POST /v1/parse
Section titled “POST /v1/parse”Parse either an uploaded job or direct text. The backend schema is 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)
Direct text — synchronous
Section titled “Direct text — synchronous”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. Cedente: ACME S.r.l. Totale documento: 1220,00 EUR","doc_type":"auto","async_mode":false}'Python · httpx
import httpx
payload = { "text": "Fattura n. 2026/001. Cedente: ACME S.r.l. Totale documento: 1220,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
const response = await fetch("https://api.factum.pyragogy.org/v1/parse", { method: "POST", headers: { "X-API-Key": process.env.FACTUM_API_KEY!, "Content-Type": "application/json", }, body: JSON.stringify({ text: "Fattura n. 2026/001. Cedente: ACME S.r.l. Totale documento: 1220,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. Totale documento: 1220,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$ch = curl_init('https://api.factum.pyragogy.org/v1/parse');curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_HTTPHEADER => [ 'X-API-Key: ' . getenv('FACTUM_API_KEY'), 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => json_encode([ 'text' => 'Fattura n. 2026/001. Totale documento: 1220,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 — synchronous
Section titled “Job — synchronous”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 — asynchronous
Section titled “Job — asynchronous”{ "job_id": "3f2b5e6a-0000-4000-8000-000000000004", "doc_type": "fattura", "async_mode": true, "callback_url": "https://example.com/factum/callback"}Response — ParseResponse
Section titled “Response — 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": { "cost_eur": 0.0, "confidence": 1.0, "provider": "fast-path", "prompt_version": "fast-path-v1" }, "payload": { "kind": "invoice", "dati_trasmissione": {}, "cedente_prestatore": {}, "cessionario_committente": {}, "corpi": [] } }, "error": null}result is null on error. Direct text is stateless: it does not create a ParseJob.
Parse errors
Section titled “Parse errors”| Status | Meaning |
|---|---|
401 |
Missing/invalid X-API-Key |
404 |
Referenced job_id does not exist |
422 |
Invalid source (XOR violation), text bounds (< 80 or > 200K chars), extraction rejection |
500 |
Unexpected parsing/provider/privacy-sidecar error |
503 |
Required Rizzo privacy sidecar unavailable; fail-closed |
Important contract distinction
Section titled “Important contract distinction”DocumentResponse and ParseResponse are not interchangeable. The former describes
ingestion state; the latter carries the structured DocumentEnvelope v2 when parsing
produces a result.