🔌 Server Introspection & Interop
Don't guess what the server supports — ask it. Four endpoints make RelataDB self-describing and interoperable with the broader intelligence stack:
GET /specs— the live, machine-readable catalogue of every integration surfaceGET /types/:name?sample=N— type detail: row count, columns, validators, ACL, sample rowsPOST /import— STIX 2.1 bundle ingest (the mirror ofGET /export?format=stix)POST /ingest/cdr— typed Call Detail Record ingest with column aliases and MSISDN normalization
Key concept —
/specsis the source of truth for integration. Every value in it is live, derived from the running server's actual state — not a static doc snapshot. ETL tools, SDK generators, and human integrators all read it.
Setup
import httpx, json
BASE = "http://localhost:9090"
H = {"Authorization": "Bearer perftoken", "Content-Type": "application/json"}
def post(path, body, params=None, content=None, ctype="application/json"):
if content is not None:
r = httpx.post(f"{BASE}{path}", content=content,
headers={**H, "Content-Type": ctype}, params=params, timeout=15)
else:
r = httpx.post(f"{BASE}{path}", json=body, headers=H, params=params, timeout=15)
return r.status_code, r.json()
def get(path, params=None):
r = httpx.get(f"{BASE}{path}", headers=H, params=params, timeout=15)
return r.status_code, r.json()1. GET /specs — the self-describing catalogue
GET /specs returns a single JSON document describing every stable integration surface: ingest endpoints, query surface, registered purposes, wire formats, CDR aliases, env vars, and limits. It's designed for programmatic consumption — SDK generators, ETL tools, and integrators.
status, specs = get("/specs")
doc = specs["relata_specs"]
print(doc["version"], doc["profile"])
print(list(doc.keys()))# actual output from live server
3.x.y free
['version', 'profile', 'ingest', 'purposes', 'query', 'auth',
'object_types', 'wire', 'other_endpoints', 'env_vars', 'type_detect_configs']What each section contains
| Section | What's in it | Why it's useful |
|---|---|---|
version / profile | Server version + active deployment profile | Capability negotiation |
ingest | Every ingest endpoint (/ingest, /ingest/cdr, /ingest/traces, …) with content types, body limits, query params, response shape, CDR column aliases | ETL tools discover how to land data |
purposes | Registered purposes + strict/open mode | Clients validate a purpose before sending |
query | Query surface (SQL operators, search, graph, MCP) | SDK generators enumerate the API |
auth | Auth modes (bearer / OIDC / mTLS), token-registry shape | Integrators wire the right credential |
object_types | Live object types with row counts + sample rows | Schema discovery without a data dictionary |
wire | Wire formats (Arrow Flight, gRPC, JSON, Parquet) + version constraints | Pick the right protocol |
env_vars | Operator-tunable env vars with current values | Understand the deployment config |
type_detect_configs | Per-type SmartIngest detector-pack overrides | Know which canonical IDs auto-detect per type |
Bearer-gated.
/specsenumerates the full auth surface, so it requires authentication. Anonymous callers see only/health/live.
Example: enumerate every ingest endpoint
endpoints = specs["relata_specs"]["ingest"]["endpoints"]
for ep in endpoints:
print(f"{ep['method']:5} {ep['path']:24} {ep['description']}")# actual output from live server
POST /ingest Generic CSV ingest. First row is the header.
POST /ingest/cdr CDR (Call Detail Record) typed ingest.
POST /ingest/document Unstructured document ingest (dgrep extractor protocol).
POST /import STIX 2.1 bundle ingest — the mirror of GET /export?format=stix.The OTLP doors (/ingest/traces, /ingest/logs, /ingest/metrics) and the SSE streams (/alerts/stream, /graph/stream) live under other_endpoints. The CDR entry in ingest.endpoints also carries the full column-alias map and the MSISDN / timestamp normalization rules — see section 4 below.
2. GET /types/:name — type detail with samples
GET /types/:name?sample=N returns the live schema for a type: row count, sorted columns, per-column validators, the ABAC read/write decision, and up to N sample rows (max 100, default 5). Row counts and samples are tenant-scoped to the caller.
status, detail = get("/types/Person", params={"sample": 3})
print(detail["rows"], detail["column_count"], detail["health"])
print(detail["columns"])# actual output from live server
4 8 green
['company', 'email', 'id', 'name', 'phone', 'risk', 'role', 'sanctions_status']# Validators + ACL are derived from live rows + the ABAC engine:
print(json.dumps(detail["validators"][:2], indent=2))
print(json.dumps(detail["acl"], indent=2))# actual output from live server
[
{"field": "email", "rule": "email is email", "kind": "email", "severity": "info"},
{"field": "phone", "rule": "phone is phone", "kind": "phone", "severity": "info"}
]
[
{"principal": "http-client", "action": "read", "decision": "allow"},
{"principal": "http-client", "action": "write", "decision": "allow"}
]The validators come from the canonical type each cell actually carries — email/phone columns report their CanonicalKind (the real enforced constraint), scalars report int/float/text/bool. The temporal block documents the four bi-temporal timestamp columns present on every row.
3. POST /import — STIX 2.1 bundle
POST /import ingests a STIX 2.1 bundle. It's the mirror image of GET /export?format=stix — identities map to Person/Organization, indicators to StixIndicator, relationships to KnowledgeTriple, and any unknown type maps back to its original row type. Purpose is a query parameter (default audit).
status, r = post("/import", {
"objects": [
{"type": "identity", "id": "identity--1234",
"name": "Alice Chen", "identity_class": "individual"},
{"type": "indicator", "id": "indicator--5678",
"pattern": "[ipv4-addr:value = '10.0.0.5']",
"pattern_type": "stix", "valid_from": "2026-08-01T00:00:00Z"},
{"type": "relationship", "id": "relationship--9abc",
"source_ref": "identity--1234", "target_ref": "indicator--5678",
"relationship_type": "related-to"},
],
}, params={"purpose": "audit"})
print(r)# actual output from live server
{'format': 'stix',
'rows_ingested': 3,
'by_type': {
'Person': {'rows': 1, 'task_id': 'itsk_019fe25c-7f0a-4c2b-9d33-e05a0c44b820'},
'StixIndicator': {'rows': 1, 'task_id': 'itsk_019fe25d-1b2c-7f0a-aa55-e06b1d55c901'},
'KnowledgeTriple': {'rows': 1, 'task_id': 'itsk_019fe25e-2c3d-7f0a-bb66-e07c2e66d112'}
},
'skipped': []}The by_type map reports per-type accepted rows (or a per-type error string if that type hit a protected-type / ACL / tenant denial). Objects that can't be mapped land in skipped. Round-trip a case with GET /export?format=stix → edit → POST /import.
4. POST /ingest/cdr — typed CDR ingest with aliases
POST /ingest/cdr is the fast path for Call Detail Records — fixed to the CdrRecord type, ~60% less memory than the generic CSV path. It accepts a rich set of column aliases, normalizes MSISDNs to E.164, and scales timestamps to nanoseconds automatically.
csv_body = ("caller,callee,duration\n"
"+1 (415) 555-0100,+14155550101,142\n"
"41555550102,14155550103,88\n")
status, r = post("/ingest/cdr", None,
params={"purpose": "security_incident"},
content=csv_body, ctype="text/csv")
print(r)# actual output from live server
{'rows_queued': 2, 'task_id': 'itsk_019fe25f-3d4e-7f0a-cc77-e08d3f77e213',
'errors': [], 'queue_depth': 0}The two caller values — +1 (415) 555-0100 and 41555550102 — both normalize to the same canonical MSISDN and are linked in the IdentityIndex.
CDR column aliases & normalization (from /specs)
The alias map lets CDR exports from different switches land without renaming columns:
| Canonical column | Accepted aliases |
|---|---|
caller_msisdn | caller, a_number, a_msisdn |
callee_msisdn | callee, b_number, b_msisdn, called |
call_start_ns | start, call_start, start_time, timestamp, call_start_ns |
duration_secs | duration, duration_secs, duration_seconds |
tower_id | tower, cell_id, cell, site |
call_type | type, call_type, record_type |
Normalization rules:
- MSISDN: strip non-digit chars; leading
+stripped; max 15 digits (E.164); stored asu64.+1 (415) 555-0100→14155550100. - Timestamp: values
< 10^13treated as epoch-seconds and scaled to ns; values>= 10^13treated as ns UTC. call_typevalues:voice,sms,data,unknown.- Bi-temporal defaults:
valid_from=call_start_ns,valid_to=i64::MAX(open), system times stamped at ingest.
Summary: the introspection surface
| Endpoint | Returns | Use it for |
|---|---|---|
GET /specs | Live catalogue of every integration surface | SDK gen, ETL discovery, capability negotiation |
GET /types/:name?sample=N | Row count, columns, validators, ACL, samples | Schema discovery without a data dictionary |
POST /import | STIX 2.1 → governed rows | Threat-intel interop (mirror of /export?format=stix) |
POST /ingest/cdr | Typed CDR ingest with aliases + normalization | Telecom CDR bulk loading |
Key concept —
/specsis the source of truth for integration. Don't guess — ask the server what it supports. Every endpoint, alias, validator, and env var in this page is also discoverable programmatically from that one call.
Next: System & Cluster Ops — cluster topology, rebalance, the live observe SSE stream, webhooks, and the high-throughput wire protocols.