🔌 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 surface
  • GET /types/:name?sample=N — type detail: row count, columns, validators, ACL, sample rows
  • POST /import — STIX 2.1 bundle ingest (the mirror of GET /export?format=stix)
  • POST /ingest/cdr — typed Call Detail Record ingest with column aliases and MSISDN normalization

Key concept — /specs is 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

SectionWhat's in itWhy it's useful
version / profileServer version + active deployment profileCapability negotiation
ingestEvery ingest endpoint (/ingest, /ingest/cdr, /ingest/traces, …) with content types, body limits, query params, response shape, CDR column aliasesETL tools discover how to land data
purposesRegistered purposes + strict/open modeClients validate a purpose before sending
queryQuery surface (SQL operators, search, graph, MCP)SDK generators enumerate the API
authAuth modes (bearer / OIDC / mTLS), token-registry shapeIntegrators wire the right credential
object_typesLive object types with row counts + sample rowsSchema discovery without a data dictionary
wireWire formats (Arrow Flight, gRPC, JSON, Parquet) + version constraintsPick the right protocol
env_varsOperator-tunable env vars with current valuesUnderstand the deployment config
type_detect_configsPer-type SmartIngest detector-pack overridesKnow which canonical IDs auto-detect per type

Bearer-gated. /specs enumerates 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 columnAccepted aliases
caller_msisdncaller, a_number, a_msisdn
callee_msisdncallee, b_number, b_msisdn, called
call_start_nsstart, call_start, start_time, timestamp, call_start_ns
duration_secsduration, duration_secs, duration_seconds
tower_idtower, cell_id, cell, site
call_typetype, call_type, record_type

Normalization rules:

  • MSISDN: strip non-digit chars; leading + stripped; max 15 digits (E.164); stored as u64. +1 (415) 555-010014155550100.
  • Timestamp: values < 10^13 treated as epoch-seconds and scaled to ns; values >= 10^13 treated as ns UTC.
  • call_type values: 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

EndpointReturnsUse it for
GET /specsLive catalogue of every integration surfaceSDK gen, ETL discovery, capability negotiation
GET /types/:name?sample=NRow count, columns, validators, ACL, samplesSchema discovery without a data dictionary
POST /importSTIX 2.1 → governed rowsThreat-intel interop (mirror of /export?format=stix)
POST /ingest/cdrTyped CDR ingest with aliases + normalizationTelecom CDR bulk loading

Key concept — /specs is 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.