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. Response includes service version, uptime, and when DB is reachable, job counters.
curl -s https://api.factum.pyragogy.org/statusPOST /v1/uploads
Section titled “POST /v1/uploads”Upload a FatturaPA XML or invoice PDF. Returns ingestion metadata; 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 field is file. Filename comes from multipart metadata, not a separate field.
Response — DocumentResponse
Section titled “Response — DocumentResponse”| Field | Type | Description |
|---|---|---|
job_id |
string (UUID) | Job ID for polling /v1/uploads/{job_id} |
doc_type |
string | Detected type (e.g. fattura_pa_xml, invoice_pdf, generic) |
content_hash |
string | SHA-256 of content (whitespace-collapsed for XML) |
size_bytes |
integer | File size in bytes |
mime_type |
string | Detected MIME type |
deduplicated |
boolean | true if document was already processed (cache hit) |
status |
enum | done | requires_vision | queued | failed | expired |
message |
string | null | Contextual message (e.g. validation error) |
For PDFs, status is requires_vision and the message directs to /v1/parse for semantic extraction.
Upload Errors
Section titled “Upload Errors”| Status | Meaning |
|---|---|
400 |
Empty file |
401 |
Missing or invalid X-API-Key |
413 |
Body/file exceeds 10 MiB limit (max_upload_bytes) |
415 |
Unsupported document/content type (not XML/PDF) |
422 |
FatturaPA XML detected but deterministic parsing failed, PDF without text layer (< 80 chars), or PDF > 50 pages |
500 |
Unexpected server/storage/database error |
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 non-existent job returns 404 as RFC 7807 application/problem+json.
The response is DocumentResponse; the parsed DocumentEnvelope is not included in this response model.
POST /v1/parse
Section titled “POST /v1/parse”Process an uploaded job or direct text. Backend schema is ParseRequest.
Auth: X-API-Key (header).
Important: job_id and text are XOR — exactly one must be present.
| Field | Type | Required | Description |
|---|---|---|---|
job_id |
UUID | null | XOR with text |
Job ID from /v1/uploads |
text |
string | null | XOR with job_id |
Direct text. Zero-retention: never on disk/DB |
doc_type |
"auto" | "fattura" | "f24" | "generico" |
No (default: "auto") |
Document type hint |
async_mode |
boolean | No (default: false) |
true → 202 Accepted. Not valid with text |
callback_url |
HTTPS URL | null | No | Webhook for async. Only with async_mode=true |
Validation constraints (422):
text< 80 characters → rejected (no text layer)text> 200,000 characters → rejected (RIZZO_PII_MAX_TEXT_CHARS)async_mode=true+text→ 422 (text is stateless, RAM-only)callback_urlwithoutasync_mode=true→ 422- Both or neither
job_idandtext→ 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":"Invoice no. 2026/001. Supplier: ACME S.r.l. Total: € 1,220.00.","doc_type":"auto","async_mode":false}'Python · httpx
import httpx
payload = { "text": "Invoice no. 2026/001. Supplier: ACME S.r.l. Total: € 1,220.00.", "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
// Replace with your actual API keyconst 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: "Invoice no. 2026/001. Supplier: ACME S.r.l. Total: € 1,220.00.", 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":"Invoice no. 2026/001. Supplier: ACME S.r.l. Total: € 1,220.00.","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' => 'Invoice no. 2026/001. Supplier: ACME S.r.l. Total: € 1,220.00.', '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”| Field | Type | Description |
|---|---|---|
job_id |
string | Job UUID |
status |
"done" | "failed" | "queued" |
Processing status |
document_type |
string | Detected document type (e.g. "invoice") |
provider_used |
string | LLM provider ("deepseek-v3", "gpt-4o-mini", "") |
prompt_version |
string | Prompt template version |
confidence |
float | null | Confidence score 0.0–1.0 (1.0 = deterministic) |
cost_usd |
float | LLM cost in USD (0.0 for deterministic) |
tokens |
integer | LLM tokens consumed |
fallbacks |
string[] | Activated LLM fallbacks |
content_hash |
string | Canonical SHA-256 of the document |
result |
DocumentEnvelope | null |
Structured content v2 (null on error) |
error |
string | null | Error message if status: failed |
result is null on error. Direct text is stateless: no ParseJob is created.
Parse Errors
Section titled “Parse Errors”| Status | Meaning |
|---|---|
401 |
Missing or invalid X-API-Key |
404 |
Referenced job_id not found |
422 |
Invalid source (XOR violation), text limits (< 80 or > 200K chars), extraction rejected |
500 |
Unexpected parsing/provider/privacy error |
503 |
Rizzo sidecar unreachable; fail-closed |
Important Contractual Distinction
Section titled “Important Contractual Distinction”DocumentResponse and ParseResponse are not interchangeable. The former describes ingestion state; the latter carries the structured DocumentEnvelope v2 when parsing produces a result.
POST /v1/webhooks/lemonsqueezy
Section titled “POST /v1/webhooks/lemonsqueezy”Webhook for Lemon Squeezy self-service license management.
This endpoint does not require an API key; HMAC-SHA256 signature in the
X-Signature header serves as implicit authentication.
Does not generate requestBody in OpenAPI (uses Starlette/FastAPI request: Request
to read raw body, required for HMAC verification).
Supported events
Section titled “Supported events”| Event | Action |
|---|---|
subscription_created |
Creates API key (active status) |
order_created |
Creates API key (active status) |
subscription_updated |
Updates tier and rate limit |
subscription_cancelled |
Revokes keys (revoked status) |
subscription_expired |
Revokes keys (revoked status) |
subscription_resumed |
Reactivates keys (active status) |
Rate limits per tier
Section titled “Rate limits per tier”| Tier | Limit |
|---|---|
starter |
60 requests/minute |
pro |
300 requests/minute |
custom |
1000 requests/minute |
Example webhook (local test)
Section titled “Example webhook (local test)”# Simulate a subscription_created eventLEMON_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"Webhook errors
Section titled “Webhook errors”| Status | Meaning |
|---|---|
200 |
Event processed (even if ignored) |
400 |
Invalid JSON payload |
401 |
Missing or invalid HMAC signature |
503 |
LEMONSQUEEZY_WEBHOOK_SECRET not configured |
Response
Section titled “Response”{ "status": "created", "subscription_id": "sub_xxxxxxx", "api_key": "factum_xxxxxxxxxxxxxxxxxxxx"}