Skip to content

REST Endpoints

All endpoints require a valid X-API-Key or Authorization: Bearer <jwt> header unless otherwise noted. Admin endpoints require a separate admin JWT. See Authentication for details.

SDF Documents

MethodPathDescription
POST/sdfUpload a .sdf file
GET/sdf/:idDownload a .sdf file
GET/sdf/:id/metaRetrieve meta.json
GET/sdf/:id/dataRetrieve data.json
DELETE/sdf/:idDelete a document and its storage object
GET/sdfList documents (paginated)

Upload — POST /sdf

Upload a .sdf file. The server stores it in S3/MinIO and enqueues a background validation job.

Request:

POST /sdf HTTP/1.1
X-API-Key: sdf_k1a2b3c4d5e6f7...
Content-Type: multipart/form-data
--boundary
Content-Disposition: form-data; name="file"; filename="invoice.sdf"
Content-Type: application/octet-stream
<binary SDF data>
--boundary--

Response 201 Created:

{
"id": "3f8a1c2d-e4f5-4a6b-b7c8-9d0e1f2a3b4c",
"document_id": "a1b2c3d4-e5f6-7a8b-9c0d-e1f2a3b4c5d6",
"document_type": "invoice",
"schema_id": "invoice/v0.2",
"sdf_version": "0.1",
"is_signed": false,
"status": "pending",
"file_size_bytes": 48291,
"job_id": "validate-sdf:7f9e3a1b",
"created_at": "2026-03-15T14:30:00.000Z"
}

The status field moves from pendingvalid or invalid once the validate-sdf worker completes. Poll GET /sdf/:id or receive a webhook notification.

Download — GET /sdf/:id

Returns the raw .sdf binary (ZIP).

GET /sdf/3f8a1c2d-e4f5-4a6b-b7c8-9d0e1f2a3b4c HTTP/1.1
X-API-Key: sdf_k1a2b3c4d5e6f7...

Response 200 OK:

Content-Type: application/octet-stream
Content-Disposition: attachment; filename="invoice.sdf"
<binary>

Get meta — GET /sdf/:id/meta

Returns the parsed meta.json from inside the SDF archive.

Response 200 OK:

{
"sdf_version": "0.1",
"document_id": "a1b2c3d4-e5f6-7a8b-9c0d-e1f2a3b4c5d6",
"document_type": "invoice",
"schema_id": "invoice/v0.2",
"issuer": { "name": "Acme Supplies GmbH", "id": "DE123456789" },
"issued_at": "2026-03-15T12:00:00.000Z",
"locale": "de-DE"
}

Get data — GET /sdf/:id/data

Returns the parsed data.json from inside the SDF archive.

Response 200 OK:

{
"invoice_number": "INV-2026-001",
"issue_date": "2026-03-15",
"due_date": "2026-04-14",
"payment_terms": "NET_30",
"total": { "amount": "1250.00", "currency": "EUR" }
}

List documents — GET /sdf

Returns a paginated list of documents for the authenticated tenant.

Query parameters:

ParameterTypeDefaultDescription
pagenumber1Page number
limitnumber20Items per page (max 100)
statusstringFilter by status: pending, valid, invalid, signed
document_typestringFilter by document type

Response 200 OK:

{
"data": [
{
"id": "3f8a1c2d-...",
"document_id": "a1b2c3d4-...",
"document_type": "invoice",
"schema_id": "invoice/v0.2",
"is_signed": true,
"status": "signed",
"file_size_bytes": 48291,
"created_at": "2026-03-15T14:30:00.000Z"
}
],
"total": 142,
"page": 1,
"limit": 20
}

Signing

MethodPathDescription
POST/sign/:idSign an SDF document (async)
POST/verify/:idVerify a document’s signature

Sign — POST /sign/:id

Enqueues a sign-sdf job. The SDF is signed using the tenant’s active signing_key and re-uploaded to S3.

Response 202 Accepted:

{
"job_id": "sign-sdf:9c2e4f1a",
"status": "queued"
}

Once the job completes, the document status becomes signed and is_signed becomes true.

Verify — POST /verify/:id

Synchronously verifies the digital signature on the document.

Response 200 OK:

{
"valid": true,
"algorithm": "ECDSA-P256",
"key_id": "key-2026-03"
}

Response 200 OK (invalid signature):

{
"valid": false,
"error": "SDF_ERROR_INVALID_SIGNATURE"
}

Validation

MethodPathDescription
POST/validateSynchronous full validation

Validate — POST /validate

Validates a .sdf file without storing it. Returns the full validation report synchronously.

Request:

POST /validate HTTP/1.1
X-API-Key: sdf_k1a2b3c4d5e6f7...
Content-Type: multipart/form-data
--boundary
Content-Disposition: form-data; name="file"; filename="invoice.sdf"
...

Response 200 OK:

{
"valid": true,
"sdf_version": "0.1",
"document_type": "invoice",
"schema_id": "invoice/v0.2",
"is_signed": false,
"errors": []
}

Response 200 OK (validation failed):

{
"valid": false,
"errors": [
{
"code": "SDF_ERROR_SCHEMA_MISMATCH",
"message": "data.json does not satisfy schema.json",
"details": [
{ "path": "/total/currency", "message": "must match pattern ^[A-Z]{3}$" }
]
}
]
}

Schema Registry

MethodPathDescription
GET/schemasList registered schemas
GET/schemas/:idGet schema by ID
POST/schemasRegister a new schema version

List schemas — GET /schemas

[
{ "schema_id": "invoice", "versions": ["v0.1", "v0.2"] },
{ "schema_id": "nomination", "versions": ["v1.0"] }
]

Get schema — GET /schemas/:id

The :id path parameter uses the format {type}:{version}, e.g. invoice:v0.2.

Response 200 OK:

{
"schema_id": "invoice",
"version": "v0.2",
"schema": { "$schema": "...", "type": "object", "properties": { ... } },
"is_published": true,
"created_at": "2026-02-01T10:00:00.000Z"
}

Register schema — POST /schemas

{
"schema_id": "invoice",
"version": "v0.3",
"schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", ... }
}

Response 201 Created:

{
"id": "7d9f3a1b-...",
"schema_id": "invoice",
"version": "v0.3",
"is_published": false,
"created_at": "2026-03-21T09:00:00.000Z"
}

SAML 2.0

MethodPathDescription
GET/saml/metadataSP metadata XML for IdP configuration
GET/saml/loginInitiate SP-initiated SSO
POST/saml/acsAssertion Consumer Service callback

These endpoints do not require an API key or JWT. See your IdP documentation for SAML 2.0 SP setup. Configure the tenant’s IdP in the admin API before using these endpoints.


Admin

All admin endpoints require Authorization: Bearer <admin_jwt>. The admin JWT is signed with ADMIN_JWT_SECRET, which is separate from JWT_SECRET.

MethodPathDescription
POST/admin/tenantsCreate a new tenant
GET/admin/tenantsList all tenants
PUT/admin/tenants/:idUpdate tenant
DELETE/admin/tenants/:idDelete tenant
POST/admin/tenants/:id/keysGenerate an API key
DELETE/admin/keys/:keyIdRevoke an API key
GET/admin/auditQuery the audit log

Create tenant — POST /admin/tenants

{
"name": "Acme Corp",
"slug": "acme",
"rate_limit_rpm": 120
}

Response 201 Created:

{
"id": "c1d2e3f4-...",
"name": "Acme Corp",
"slug": "acme",
"rate_limit_rpm": 120,
"created_at": "2026-03-21T09:00:00.000Z"
}

Generate API key — POST /admin/tenants/:id/keys

{
"name": "ERP integration key",
"expires_at": "2027-03-21T00:00:00.000Z"
}

Response 201 Created:

{
"id": "k1a2b3c4-...",
"key": "sdf_k1a2b3c4d5e6f7g8h9i0j1k2l3m4n5o6p7",
"key_prefix": "sdf_k1a2",
"name": "ERP integration key",
"expires_at": "2027-03-21T00:00:00.000Z"
}

Query audit log — GET /admin/audit

ParameterTypeDescription
tenant_idstringFilter by tenant
actionstringFilter by action type
fromstringISO 8601 start timestamp
tostringISO 8601 end timestamp
pagenumberPage number (default 1)
limitnumberItems per page (default 50, max 500)

ERP Connectors

MethodPathDescription
POST/connectors/configureSave ERP connection config
GET/connectors/healthTest ERP connection
POST/connectors/matchMatch an SDF nomination in the ERP
GET/connectors/erp-status/:refGet document status from ERP
POST/connectors/push-to-erp/:idPush SDF document to ERP

Health

MethodPathDescription
GET/healthLiveness check

Response 200 OK:

{
"status": "ok",
"version": "0.1.2",
"uptime": 3728
}

Error responses

All error responses use a consistent JSON envelope:

{
"error": {
"code": "SDF_ERROR_SCHEMA_MISMATCH",
"message": "Human-readable description",
"statusCode": 422
}
}
StatusWhen
400Malformed request body or missing required fields
401Missing or invalid authentication credentials
403Valid credentials but insufficient permissions
404Document or resource not found
422SDF validation error
429Rate limit exceeded (per-tenant)
500Internal server error