API Documentation
Integrate Kadeon with your ERP, CRM or back-office system via REST API.
Overview
All APIs are available at https://<your-instance>, with the prefix /api/v1/. The exceptions are /webhook/mailgun and /health, which don't use the prefix.
All responses are in JSON format.
Content-Type: application/json Accept: application/json
Authentication methods
| Method | Header | Used for |
|---|---|---|
| JWT Bearer | Authorization: Bearer <token> | User sessions (dashboard/app login) |
| API Key | X-API-Key: <key> | Server-to-server integrations (document ingestion) |
Authentication
POST /api/v1/auth/login
Authenticates a user with email and password, returns a JWT.
POST /api/v1/auth/login
Content-Type: application/json
{
"email": "user@example.com",
"password": "secret"
}
// 200 OK
{
"access_token": "eyJ...",
"token_type": "bearer",
"user": {
"id": "...",
"email": "user@example.com",
"role": "tenant_admin",
"tenant_id": "..."
}
}The token must be included in the Authorization header of all subsequent requests.
GET /api/v1/auth/me
Returns the authenticated user's data.
GET /api/v1/auth/me Authorization: Bearer <token>
Machine token (M2M) tenant_admin+
Issues a scope-limited token, designed for integration with external systems (ERP).
POST /api/v1/auth/token
Authorization: Bearer <tenant_admin_jwt>
Content-Type: application/json
{
"scopes": ["records:read"],
"ttl_seconds": 3600
}
// 200 OK
{
"access_token": "eyJ...",
"token_type": "bearer",
"expires_at": "2026-07-06T15:00:00Z",
"scopes": ["records:read"],
"tenant_id": "..."
}Use this token to poll records from your ERP, without exposing user credentials.
Document ingestion
Send documents to the platform via email API or direct upload.
API key
Generate an API key from Settings, or via POST /api/v1/tenants/{id}/api-keys. Keep it safe: it is shown only once and must be included in every request in the X-API-Key header.
/api/v1/ingest/emailSubmit an email with attachments to processPOST /api/v1/ingest/email
X-API-Key: kd_live_xxxxxxxxxxxxxxxxxxxxxxxx
Content-Type: application/json
{
"message_id": "unique-id-001",
"sender": "supplier@example.com",
"subject": "Ordine #42",
"body_text": "In allegato l'ordine.",
"attachments": [
{
"filename": "ordine_42.pdf",
"content_b64": "JVBERi0xLjQ...",
"mime": "application/pdf"
}
]
}
// 202 Accepted
{
"status": "accepted",
"email_raw_id": "...",
"source": "API",
"attachments": 1
}The endpoint is idempotent on message_id: duplicate submissions are ignored.
Direct upload any role
/api/v1/ingest/uploadUpload one or more files directly (requires user session)POST /api/v1/ingest/upload
Authorization: Bearer <token>
Content-Type: multipart/form-data
files=@document.pdf
files=@image.jpg
// 202 Accepted
{
"status": "accepted",
"email_raw_id": "...",
"source": "upload",
"attachments": 2
}Record
Records are the result of AI processing: each email/attachment becomes a record with extracted data, status and metadata.
Lifecycle
NORMALIZING → NEEDS_REVIEW → APPROVED | REJECTED → EXPORTED
List records
/api/v1/recordsPaginated list with filtersGET /api/v1/records?status_filter=NEEDS_REVIEW&limit=50&offset=0
Authorization: Bearer <token>
// 200 OK
{
"items": [
{
"id": "rec_...",
"identity_code": "ORDER",
"identity_name": "Ordine di acquisto",
"status": "NEEDS_REVIEW",
"confidence": 0.94,
"email_subject": "Ordine #42",
"email_sender": "supplier@example.com",
"data": { "numero_ordine": "42", "importo": "1250.00" },
"created_at": "2026-07-06T10:00:00Z"
}
],
"total": 1,
"limit": 50,
"offset": 0
}Available query parameters
| Parameter | Description |
|---|---|
status_filter | Single status (NEEDS_REVIEW, APPROVED, REJECTED, EXPORTED) |
status_list | Multiple statuses, comma-separated |
identity | Identity code (e.g. ORDER) |
q | Free-text search on sender, subject, data |
mailbox | Mailbox UUID |
today | true → today's records only |
limit / offset | Pagination (max 200) |
sort_by / sort_dir | Field and direction (asc/desc) |
Record detail
/api/v1/records/{record_id}Full detailRecord actions
/api/v1/records/{id}/approveApprove the record/api/v1/records/{id}/rejectReject the record/api/v1/records/{id}/correctCorrect extracted data/api/v1/records/{id}/reprocessReprocess with AI/api/v1/records/{id}Delete recordData correction
POST /api/v1/records/{record_id}/correct
Authorization: Bearer <token>
Content-Type: application/json
{
"data": { "numero_ordine": "42-B", "importo": "1300.00" },
"note": "Importo corretto"
}
// 200 OK — record with status NEEDS_REVIEWDownload PDF
/api/v1/records/{id}/document.pdfGenerated PDF with extracted dataBulk operations
POST /api/v1/records/bulk
Authorization: Bearer <token>
Content-Type: application/json
{
"action": "approve",
"record_ids": ["rec_aaa", "rec_bbb"]
}
// Oppure — tutti i record che matchano filtri:
{
"action": "approve",
"all_matching": true,
"filters": { "status": "NEEDS_REVIEW", "identity": "ORDER" }
}
// 200 OK
{ "action": "approve", "affected": 12 }File export (CSV / Excel)
/api/v1/records/export-fileDownload up to 10,000 recordsGET /api/v1/records/export-file?format=xlsx&identity=ORDER&status_filter=APPROVED Authorization: Bearer <token> // Response: binary Excel file
ERP Export (M2M pull)
Recommended flow for ERP/back-office systems: issue a scope-limited token and use the export endpoint to periodically poll approved records.
Full flow
// 1. Login con credenziali tenant_admin
POST /api/v1/auth/login
→ { access_token: "<admin_jwt>" }
// 2. Emetti un token M2M con scadenza e scope limitato
POST /api/v1/auth/token
Authorization: Bearer <admin_jwt>
{ "scopes": ["records:read"], "ttl_seconds": 86400 }
→ { access_token: "<m2m_token>", expires_at: "..." }
// 3. Poll dei record approvati (usa il token M2M)
GET /api/v1/export/records?status_filter=APPROVED&since=2026-07-01T00:00:00Z
Authorization: Bearer <m2m_token>
→ { items: [...], total: 8 }Export parameters
| Parameter | Default | Description |
|---|---|---|
status_filter | APPROVED,EXPORTED | Status/statuses to export |
identity | — | Filter by identity code |
since | — | ISO-8601 — only records created after this date |
limit | 200 (max 1000) | Maximum number of records |
Response structure
{
"items": [
{
"id": "rec_...",
"identity_code": "ORDER",
"schema_version": 1,
"status": "APPROVED",
"exported": false,
"confidence": 0.97,
"data": {
"numero_ordine": "42",
"fornitore": "Acme Srl",
"importo": "1250.00",
"data_consegna": "2026-07-15"
},
"created_at": "2026-07-06T10:00:00Z"
}
],
"total": 1
}Mailgun Webhook
Configure Mailgun to push inbound emails directly to Kadeon via webhook. Authenticity is verified via HMAC-SHA256.
/webhook/mailgunMailgun inbound emailThe webhook accepts both multipart payloads (standard Mailgun) and JSON. The HMAC key is configured via the MAILGUN_SIGNING_KEY environment variable on the server.
// Mailgun inbound route URL:
https://<your-instance>/webhook/mailgun
// 202 Accepted
{
"status": "accepted",
"email_raw_id": "...",
"source": "MAILGUN",
"attachments": 2
}
// 401 Unauthorized — firma HMAC non validaUsage & quota
/api/v1/usage/meQuota and plan for the current userGET /api/v1/usage/me
Authorization: Bearer <token>
// 200 OK
{
"plan": { "name": "professional", "period": "monthly" },
"cycle": { "period": "2026-07", "start": "2026-07-01", "end": "2026-07-31", "tokens": 50000 },
"quota_remaining": 34210,
"storage_used_mb": 142,
"storage_quota_mb": 5120,
"entitlement": { "ok": true, "tokens_ok": true, "storage_ok": true }
}/api/v1/usageDetailed tenant usage (tenant_admin+)Includes daily breakdown, by AI provider and by record status. Optional parameter: ?period=2026-07
Error codes
| HTTP | Meaning | Typical cause |
|---|---|---|
| 400 | Bad request | Missing parameters or wrong format |
| 401 | Unauthenticated | Missing/expired token or invalid HMAC signature |
| 402 | Quota exceeded | Plan tokens or storage exhausted |
| 403 | Forbidden | Insufficient role for the operation |
| 404 | Not found | Resource does not exist or is out of tenant scope |
| 409 | Conflict | Email already imported (duplicate message_id), record already exported |
| 422 | Validation failed | JSON body does not match the expected schema |
| 429 | Too many requests | Rate limit exceeded (login: 10 attempts / 15 min) |
| 500 | Internal error | Unexpected server-side error |
Error format
// Errore 422 — esempio
{
"detail": [
{
"loc": ["body", "email"],
"msg": "value is not a valid email address",
"type": "value_error.email"
}
]
}
// Errore generico
{
"detail": "Record not found"
}