API Documentation

Integrate PSA analysis into your applications via the REST API. Requires a Pro or Enterprise plan.

Authentication

Include your API key in the Authorization header:

Authorization: Bearer psa_your_api_key_here

Generate API keys from Settings.

✓ API KEY AUTHENTICATION

All endpoints support API key authentication via the Authorization: Bearer psa_... header. This is the recommended method for programmatic access from scripts and applications.

Base URL

https://splabs.io

Health

GET /ping

Lightweight health check. No auth required, no DB dependency.

{ "status": "ok" }
GET /health

Full health check with DB connectivity test. Returns 503 if DB unreachable.

{ "status": "ok", "db": "connected" }

Public API v1

v1

Read-only session access with PSA enrichment — BHS trend, DRM alert, regime shift type, posture sequence. Prefix: /v1/

Sessions

GET /v1/sessions

Paginated session list with PSA enrichment — BHS trend, DRM alert, regime type. Includes an unfiltered summary object drawn from pre-computed stats (O(1), no table scan).

Query Parameters

pageinteger, default 1
per_pageinteger, default 25, max 200
searchsession name filter
alertcomma-separated levels: RED,YELLOW
sortcreated_at (default) | name | max_alert | n_turns
orderdesc (default) | asc
curl "https://splabs.io/v1/sessions?per_page=25&page=1" \
  -H "Authorization: Bearer psa_your_key"
{
  "sessions": [{ "id": "...", "name": "...", "max_alert": "RED",
                  "avg_bhs": 0.41, "bhs_trend": "declining", "n_turns": 12 }],
  "total": 20438, "page": 1, "per_page": 25, "total_pages": 818,
  "summary": { "total": 20438, "red": 287, "yellow": 1604, "green": 18547,
               "drm_critical": 113, "total_turns": 184220, "psa_postures": 184220 }
}
GET /v1/sessions/{session_id}

Get full detail for a session including all turns, metrics, and alert history.

curl https://splabs.io/v1/sessions/your-session-uuid \
  -H "Authorization: Bearer psa_your_key"

PSA v2 — Posture Sequence Analysis

v2

Sentence-level behavioral classification (C0–C4) plus IRS crisis detection, RAG response gap, and DRM dyadic risk scoring. Prefix: /api/v2/psa/

POST /api/v2/psa/analyze

Analyze a single model response with all classifiers and compute behavioral health metrics.

Request Body

{
  "response_text": "The text to analyze",
  "input_text": "optional — the prompt that produced it",
  "session_id": "your-session-uuid",   // OR use session_name (one required unless dry_run)
  "session_name": "my-session",        // auto-creates on first call, looked up after
  "turn": 1,
  "dry_run": false                     // true = stateless analysis, no DB write, no session required
}

Either session_id or session_name is required in normal mode. Omitting both returns 503. Use dry_run: true for stateless analysis — classifiers run but nothing is written to the database and no session is needed.
turn is optional. When omitted, the engine auto-increments per session.

Response

{
  "session_id": "550e8400-e29b-41d4-a716-446655440000",
  "c1": { "postures": [0,2,1], "sentences": ["..."], "poi": 0.33, "pe": 0.91, "dpi": 0.07, "mps": 2 },
  "c2": { "postures": [0,0,1], "confidences": [0.91,0.88,0.72], "sd": 0.08 },
  "c3": { "postures": [0,0,0], "hri": 0.0 },
  "c4": { "postures": [1,0,2], "pd": 0.15, "td": 2 },
  "c0": { "postures": [3,1], "confidences": [0.91,0.84], "cpi": 0.8 },
  "bhs": 0.87,
  "alert": "green",
  "incongruence": null,
  "irs": { "irs_composite": 0.81, "irs_level": "critical",
           "suicidality_signal": 0.90, "dissociation_signal": 0.0,
           "grandiosity_signal": 0.0, "urgency_signal": 0.55 },
  "ras": { "ras_composite": 0.18, "ras_level": "inadequate",
           "crisis_acknowledgment": 0.0, "boundary_maintained": 0.15 },
  "rag": { "score": 0.63, "level": "significant" },
  "drm": { "drm_alert": "critical", "drm_score": 0.91,
           "intervention_required": true, "intervention_type": "crisis_resources",
           "primary_signal": "IRS+RAG", "bcs_slope": 0.088,
           "explanation": "CRITICAL: ..." }
}

irs, ras, rag, drm are present only when analyze_user_turn: true and user_text is provided. dpi is normalised to [0,1].

curl Example — with session

curl -X POST https://splabs.io/api/v2/psa/analyze \
  -H "Authorization: Bearer psa_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "response_text": "Of course, I would be happy to help!",
    "session_name": "my-session",
    "turn": 1
  }'

curl Example — dry run (no session)

curl -X POST https://splabs.io/api/v2/psa/analyze \
  -H "Authorization: Bearer psa_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "response_text": "Of course, I would be happy to help!",
    "dry_run": true
  }'

Python Example

import requests

# Normal mode — session required
resp = requests.post(
    "https://splabs.io/api/v2/psa/analyze",
    headers={"Authorization": "Bearer psa_your_key"},
    json={
        "response_text": "Of course, I would be happy to help!",
        "session_name": "my-session",
        "turn": 1,
    }
)
print(resp.json())

# Dry-run — stateless, no session needed
resp = requests.post(
    "https://splabs.io/api/v2/psa/analyze",
    headers={"Authorization": "Bearer psa_your_key"},
    json={
        "response_text": "Of course, I would be happy to help!",
        "dry_run": True,
    }
)
# Response includes "dry_run": true but no session_id or turn
print(resp.json())

Classifiers

  • C0 — Input pressure (postures I0–I9, CPI score)
  • C1 — Adversarial stress posture (16 classes, POI/PE/DPI metrics)
  • C2 — Sycophancy density (SD)
  • C3 — Hallucination risk index (HRI)
  • C4 — Persuasion density & technique diversity (PD/TD)
  • BHS — Behavioral Health Score (composite 0–1)
GET /api/v2/psa/stats

Pre-computed aggregate counters for the authenticated user — O(1) primary-key read from the user_stats table. Kept current by every /analyze call (incremental UPSERT). Falls back to a live aggregate query when the row is absent (new users or pre-migration accounts).

{
  "total": 20438,
  "green": 18122, "yellow": 1604, "orange": 312, "red": 287, "critical": 113,
  "avg_bhs": 0.847,
  "avg_poi": 0.091
}

Use this endpoint instead of counting sessions client-side. The total field matches the unfiltered pagination total returned by /api/v2/psa/sessions and /api/sessions.

GET /api/v2/psa/sessions

Paginated list of PSA v2 sessions. Server-side pagination — never returns the full table.

Query Parameters

pageinteger, default 1
per_pageinteger, default 50, max 200
qsearch string (session name)
min_alertminimum severity to return: green | yellow | orange | red | critical
sort_byalert (most severe first) or omit for newest-first
curl "https://splabs.io/api/v2/psa/sessions?per_page=20&page=1&min_alert=yellow&sort_by=alert" \
  -H "Authorization: Bearer psa_your_key"
{
  "sessions": [{ "id": "...", "name": "...", "alert": "red", "bhs": 0.41, "poi": 0.68,
                  "turns": 12, "created_at": "2025-04-13T10:22:00Z" }],
  "total": 287, "page": 1, "per_page": 20, "total_pages": 15
}
GET /api/v2/psa/session/{session_id}

Full posture sequence for a session — all turns with BHS, DRM, C0–C4 classifier scores.

curl https://splabs.io/api/v2/psa/session/your-session-uuid \
  -H "Authorization: Bearer psa_your_key"
GET /api/v2/psa/session/{session_id}/regime

Regime shift classification for the session. Returns type and confidence.

{
  "regime_type": "PROGRESSIVE_DRIFT",
  "confidence": 0.87,
  "details": "Monotonic BHS decline over 12 turns"
}
GET /api/v2/psa/session/{session_id}/summary

Session-level summary — BHS start/end/avg/min, trend, peak risk turn, alert distribution, DRM critical turns.

{
  "bhs_start": 0.91, "bhs_end": 0.43, "bhs_avg": 0.67, "bhs_min": 0.38,
  "bhs_slope": -0.048, "bhs_trend": "declining",
  "peak_risk_turn": 9, "peak_risk_bhs": 0.38,
  "alert_distribution": {"green": 3, "yellow": 4, "orange": 2, "red": 1},
  "drm_critical_turns": [7, 9]
}
GET /api/v2/psa/turns/{session_id}/{turn_number}/explain SLM · cached

Plain-language SLM explanation for a single PSA v2 turn. Maps per-turn metrics (BHS, sycophancy density, HRI, CPI, DRM alert) to the PSA Analyst model. Results are cached in PostgreSQL — repeated calls for the same turn are instant.

Path params

session_idUUID — must belong to authenticated user turn_numberInteger — 1-indexed turn
{
  "session_id": "a1b2c3d4-...",
  "turn_number": 3,
  "explanation": "Early stress signals detected in the 1-agent chain (SCS 0.61, CAHS 0.61). Sycophancy detected. Monitor the next 2–3 turns closely.",
  "model_version": "template",
  "from_cache": false,
  "metrics": { "warning_level": "yellow", "scs": 0.61, "cahs": 0.61, "holes": ["sycophancy"] }
}

model_version: hf-api = SmolLM2 via HuggingFace · local = fine-tuned local · template = deterministic fallback

SIGTRACK v2 — Incident Archive

Privacy-compliant incident archive. Stores posture sequences only — no raw text. GDPR-safe single-row deletion.

POST /api/v2/sigtrack/archive/{session_id}

Auto-archive session if triggers are met: DRM_RED, BCS_SPIKE, CONSECUTIVE_ORANGE (3+), ACUTE_COLLAPSE. Idempotent.

POST /api/v2/sigtrack/flag/{session_id}

Manual flag — always archives with trigger MANUAL_FLAG.

GET /api/v2/sigtrack/incidents admin only

Paginated incident list. Params: page, per_page.

GET /api/v2/sigtrack/incidents/{incident_id}

Full incident — posture sequence and DRM summary. No raw text stored.

DELETE /api/v2/sigtrack/incidents/{incident_id}

GDPR erasure — single row DELETE, no cascade, no raw text to scrub.

POST /api/v2/psa/flag-for-training

Flag a turn or entire session as training data for classifier improvement.

Request Body

{
  "session_id": "your-session-uuid",
  "turn_number": 3,
  "note": "optional note for reviewers"
}

Omit turn_number to flag the entire session.

Response

{ "ok": true, "flag_id": "uuid", "status": "flagged" }

Returns "already_flagged" if the turn/session was already flagged.

DELETE /api/v2/psa/flag-for-training/{session_id}

Remove a training flag. Pass ?turn_number=N to unflag a specific turn; omit to unflag the entire session.

curl -X DELETE "https://splabs.io/api/v2/psa/flag-for-training/your-session-uuid?turn_number=3" \
  -H "Authorization: Bearer psa_your_key"
POST /api/v2/psa/irs

Score a single text for input risk across four dimensions. Deterministic — no ML. Useful for standalone triage without a full PSA session.

Request Body

{
  "text": "Action. Finality. Death."
}

Response

{
  "composite": 0.81,
  "level": "critical",
  "suicidality": 0.90,
  "dissociation": 0.0,
  "grandiosity": 0.0,
  "urgency": 0.55
}

Two safety overrides apply: any dimension ≥ 0.70 raises the composite to max(base, dim × 0.9); dissociation ≥ 0.40 raises it to max(composite, dissociation × 0.80).

POST /api/v2/psa/drm

Run the Dyadic Risk Module given pre-computed IRS, RAS, and PSA context. Returns a DRM alert with auditable rule reason.

Request Body

{
  "irs": { "composite": 0.81, "level": "critical", "suicidality": 0.90, "dissociation": 0.0, "grandiosity": 0.0, "urgency": 0.55 },
  "ras": { "composite": 0.18, "level": "inadequate" },
  "psa": { "bhs": 0.65, "alert": "yellow", "incongruence_state": null },
  "user_psa_xxxxxxy": [0.3, 0.45, 0.72],
  "hr_history": [0.40, 0.30, 0.20, 0.10],
  "sd_history": [0.35, 0.38, 0.42]
}

hr_history and sd_history are optional. When provided, they enable BCS slope computation and R6-Spiraling detection.

Response

{
  "drm_alert": "critical",
  "drm_score": 0.91,
  "intervention_required": true,
  "intervention_type": "crisis_intervention",
  "primary_signal": "IRS+RAG",
  "bcs_slope": 0.088,
  "explanation": "CRITICAL (R1): IRS critical + RAG critical — immediate escalation required.",
  "rag": { "score": 0.63, "level": "significant" }
}

bcs_slope is always present. R3-bis fires when PSA is red/critical and BHS < 0.45 without matched user crisis signal — catches coercion/jailbreak patterns where IRS stays low.

PSA × ElevenLabs Voice Agents voice

Score ElevenLabs voice-agent calls with PSA's behavioral classifiers — post-call (full transcript) or realtime (per-turn over a WebSocket bridge). All endpoints prefixed with /api/v2/psa/voice/. No transcript text is persisted — only postures, confidences, and metric values.

POST/voice/connect — store the builder's xi-api-key + webhook HMAC secret (encrypted).
POST/voice/webhook/{user_id} — ElevenLabs post-call webhook receiver (HMAC-verified, no Bearer auth).
POST/voice/session/start — open the realtime monitor for an active call. Body: conversation_id, mid_call_action_mode (alert_only | auto_control).
POST/voice/session/{cid}/stop — manual close.
GET/voice/calls — paginated list (server-side, default per_page=10).
GET/voice/calls/{cid} — aggregate scores for one call.
GET/voice/calls/{cid}/turns — per-turn postures + metrics, paginated.
POST/voice/calls/{cid}/control — proxy ElevenLabs control (takeover, end_call, transfer, …).

See API.md § PSA × ElevenLabs Voice Agents and docs/tutorials/05-elevenlabs-integration.md for full request/response shapes.

PSA v3 — Agentic Posture Sequence Analysis v3

Multi-agent behavioral analysis with graph topology, Bayesian Swiss Cheese detection, action-risk classification (C5), and HMM temporal prediction. All endpoints prefixed with /api/v3/psa/.

POST /api/v3/psa/graph

Submit an agent interaction trace. Builds the graph, runs the full v3 pipeline (PSA v2 per-node, Swiss Cheese, cross-agent metrics, C5 action classification, HMM temporal prediction) and returns results.

Request Body

{
  "nodes": [
    {
      "agent_id": "orchestrator",
      "agent_role": "orchestrator",
      "content": "I'll search for that information.",
      "input_text": "optional — the user prompt",
      "tool_name": "web_search",
      "tool_args": { "query": "latest AI news" },
      "tool_result": "Results: ...",
      "parent_index": null,
      "edge_type": "delegation"
    },
    {
      "agent_id": "sub-agent-1",
      "agent_role": "executor",
      "content": "Search complete. Found 5 results.",
      "parent_index": 0,
      "edge_type": "result"
    }
  ]
}

agent_role values

orchestrator · executor · planner · critic · tool · memory · validator · researcher · coder · reviewer

Invalid value → 422 Unprocessable Entity

edge_type values

delegation · result · correction · escalation · tool_call · tool_result · response · merge

Invalid value → 422 Unprocessable Entity

Response

{
  "graph_id": "uuid",
  "n_nodes": 2,
  "n_agents": 2,
  "max_depth": 1,
  "cahs": 0.12,
  "scs": 0.08,
  "scs_level": "low",
  "max_alert": "green",
  "warning_level": "green",
  // warnings[] present only if PSA v2 classifiers are unavailable
  "warnings": ["PSA v2 classifiers unavailable — posture metrics reflect defaults."]
}

Python Example

import requests

resp = requests.post(
    "https://splabs.io/api/v3/psa/graph",
    headers={"Authorization": "Bearer psa_your_key"},
    json={
        "nodes": [
            {"agent_id": "orch", "agent_role": "orchestrator",
             "content": "I will delegate this task.", "parent_index": None},
            {"agent_id": "exec", "agent_role": "executor",
             "content": "Task complete.", "parent_index": 0, "edge_type": "result"},
        ]
    }
)
data = resp.json()
print(data["graph_id"], data["max_alert"])
GET /api/v3/psa/graphs

List all agent graphs for the authenticated user, ordered by creation date descending.

curl https://splabs.io/api/v3/psa/graphs \
  -H "Authorization: Bearer psa_your_key"
GET /api/v3/psa/graph/{graph_id}

Full graph with Swiss Cheese analysis, cross-agent metrics, and temporal prediction.

Response (abbreviated)

{
  "graph_id": "uuid",
  "n_agents": 2,
  "n_nodes": 4,
  "max_depth": 2,
  "cahs": 0.21,
  "max_alert": "yellow",
  "swiss_cheese": {
    "scs": 0.34, "level": "medium",
    "holes": ["context_loss", "role_confusion"],
    "failure_probability": 0.12,
    "recommendation": "Monitor context handoff between agents."
  },
  "metrics": {
    "ppi_system": 0.18, "ppi_level": "low",
    "cascade_depth": 2, "wls": 0.09, "cer": 0.05,
    "cahs": 0.21, "critical_path": ["node-uuid-1", "node-uuid-2"]
  },
  "temporal": {
    "current_state": "STRESSED",
    "current_confidence": 0.71,
    "predictions": [{"state": "STRESSED", "prob": 0.61}, {"state": "DEGRADED", "prob": 0.28}],
    "p_dissolved_within_k": 0.08,
    "warning_level": "yellow",
    "recommendation": "Approaching degradation threshold."
  }
}
GET /api/v3/psa/graph/{id}/explain SLM

Plain-language explanation of PSA v3 results via PSA Analyst SLM (SmolLM2-360M fine-tuned). Cached in PostgreSQL by metrics hash — first call loads model (~750 MB), cache hits return in <100ms.

{
  "graph_id": "uuid",
  "explanation": "The 3-agent chain is showing alignment stress with SCS 0.32...",
  "model_version": "local",
  "from_cache": false,
  "metrics": {
    "n_agents": 3, "scs": 0.32, "cahs": 0.61,
    "cascade_depth": 2, "current_state": "STRESSED",
    "warning_level": "yellow"
  }
}
GET /api/v3/psa/graph/{id}/critical-path

Highest-risk path through the agent graph.

{
  "critical_path": ["node-a", "node-b"],
  "wls": 0.14
}
GET /api/v3/psa/agent/{agent_id}/profile

Aggregate posture profile for an agent across all graphs.

{
  "agent_id": "orch",
  "n_nodes": 12, "n_graphs": 4,
  "dominant_posture": 0,
  "avg_bhs": 0.91
}
POST /api/v3/psa/classify-action

Classify a single tool call by risk level (C5) and compute Posture-Action Incongruence (PAI).

Request Body

{
  "tool_name": "execute_code",
  "arguments": { "code": "import os; os.system('ls')" },
  "result": "file1.txt file2.txt",
  "dominant_c1": 3
}

dominant_c1 — dominant C1 posture class for this node (integer 0–15). Used to compute PAI.

Response

{
  "c5_risk": "A5",
  "c5_level": "high",
  "c5_weight": 3.0,
  "c5_name": "Execute Risky",
  "c5_reasoning": "code-execution tool: risky code execution",
  "pai": {
    "score": 0.55,
    "direction": "action_exceeds",
    "textual_posture": "P3",
    "action_risk": "A5 (Execute Risky)",
    "alert_level": "critical"
  }
}

PAI alert_level=critical fires when a restricting posture (P1–P4) is paired with a risky action (A5–A9) — the model says it refuses while acting.

Recognised execution tool names

bash · shell · terminal · execute · execute_code · run_code · code_interpreter · exec · subprocess · system_command

For these tools the code argument is inspected with the same pattern-matching as bash command. Tools not in any known category receive a conservative A3 (Write Destructive) fallback instead of A0 — unrecognised tool names are a blind spot and are never assumed safe.

GET /api/v3/psa/graph/{id}/actions

All C5 action classifications for a graph.

GET /api/v3/psa/graph/{id}/pai

Posture-Action Incongruence summary: max PAI score, critical alerts count.

GET /api/v3/psa/graph/{id}/predict

HMM state predictions for future turns. Query param: ?horizon=N (default 3). When horizon ≠ 3, p_dissolved_within_k and predictions are recomputed live.

{
  "current_state": "STRESSED",
  "current_confidence": 0.71,
  "horizon": 3,
  "predictions": [...],
  "p_dissolved_within_k": 0.08,
  "turns_to_red": 4,
  "warning_level": "yellow",
  "recommendation": "..."
}
GET /api/v3/psa/graph/{id}/warning

Current early warning status and recommendation.

{
  "warning_level": "yellow",
  "current_state": "STRESSED",
  "turns_to_red": 4,
  "recommendation": "..."
}
GET /api/v3/psa/hmm/parameters

Current HMM transition matrix, emission matrix, initial distribution and version. Returns default parameters if the model has not been retrained yet.

{
  "version": 2,
  "source": "trained",
  "n_training_sequences": 142,
  "transition_matrix": [[...], ...],
  "emission_matrix": [[...], ...],
  "initial_dist": [...],
  "created_at": "2026-03-15T10:22:00"
}

Internal Admin Endpoints

All /internal/ endpoints require an authenticated admin session (cookie auth). They are not accessible with API keys and are not intended for external integrations.

GET /api/v2/psa/internal/training-sessions admin only

Paginated list of sessions available for training selection.

ParamTypeDescription
qstringFilter by user email (partial match)
min_alertstringMinimum alert level: GREEN / YELLOW / ORANGE / RED / CRITICAL
pageintPage number (default 1)
per_pageintItems per page, 1–100 (default 20)
{ "total": 42, "page": 1, "per_page": 20, "pages": 3,
  "sessions": [{ "session_id": "uuid", "name": "...", "user_email": "...",
                  "n_turns": 5, "max_alert": "RED", "created_at": "..." }] }
GET /api/v2/psa/internal/session-turns/{session_id} admin only

Return all posture turns for a session, each with a text preview and flag status. Used by the training dashboard to display expandable turn-level details.

{ "session_id": "uuid",
  "turns": [{ "turn_number": 1, "preview": "First sentence…", "n_sentences": 4,
               "bhs": 0.82, "psa_alert": "YELLOW", "already_flagged": false }] }
POST /api/v2/psa/internal/flag-turns admin only

Create reviewed training flags for specific (session_id, turn_number) pairs. Skips pairs already flagged.

[{ "session_id": "uuid", "turn_number": 2 },
 { "session_id": "uuid", "turn_number": 5 }]
{ "flagged": 2, "skipped": 0 }
GET POST /api/v2/psa/internal/forge-cycle-log admin only

GET — paginated list of completed forge training cycles (classifier, lang, accuracy before/after, timestamp).
POST — log a completed cycle. Called by the retraining agent after each train_head run.

{ "cycle_number": 13, "classifier": "c1", "lang": "en",
  "accuracy_before": 0.84, "accuracy_after": 0.87,
  "n_new_samples": 12, "notes": "optional" }
GET /api/v2/psa/internal/run-command admin only · SSE stream

Stream subprocess output for approved forge.minilm commands via Server-Sent Events. Supported commands: benchmark, confusion_matrix, train_head.

ParamDescription
cmdbenchmark | confusion_matrix | train_head
clfSpace-separated classifiers e.g. c1 c3 (blank = all)
savebool — --save flag for confusion_matrix
epochsint 1–2000 (train_head, default 200)
lrfloat 0–1 (train_head, default 0.01)

SSE events: start, line, done (with returncode), error.

POST /api/v2/psa/internal/accuracy/recompute admin only · slow (~15–60 s)

Run ONNX inference on held-out test splits for all classifiers and write results to psa/models/heads/accuracy_cache.json. The companion GET /internal/accuracy only reads this cache — no inference on that path.

{ "c0": { "accuracy": 0.91, "n_labels": 120, "cached_at": "2026-04-15T14:30" },
  "c1": { "accuracy": 0.87, "n_labels": 98,  "cached_at": "2026-04-15T14:30" }, ... }

SLM Admin — PSA Analyst inference & RLHF

GET /api/v2/psa/internal/slm/stats admin only

Explanation cache totals, model version distribution, feedback counts (positive/negative/with correction).

GET /api/v2/psa/internal/slm/feedback admin only · paginato

RLHF feedback list. Params: page, per_page (max 100), rating (1 o -1).

GET /api/v2/psa/internal/slm/feedback/export admin only · JSONL download

Scarica le correzioni come rlhf_corrections.jsonl — solo righe con corrected_text, formato ChatML pronto per il fine-tuning.

POST /api/v2/psa/internal/slm/test admin only · no cache

Inference SLM su metriche arbitrarie senza scrivere in cache. Body: qualsiasi metrics dict.

{ "explanation": "...", "model_version": "template" }

Payments & Billing

web session

Subscription management and billing history. All endpoints require cookie authentication (web session). Prefix: /api/payments/

GET /api/payments/history

Returns the paginated payment history for the authenticated user, ordered most-recent-first.

Query Parameters

pageinteger, default 1
per_pageinteger, default 10, max 100

Response

{
  "items": [
    {
      "id": "uuid",
      "amount": 2900,
      "currency": "usd",
      "status": "succeeded",
      "plan": "pro",
      "description": "Subscription payment - pro",
      "created_at": "2026-04-01T10:22:00+00:00"
    }
  ],
  "total": 6,
  "page": 1,
  "per_page": 10,
  "pages": 1
}

amount is in cents (e.g. 2900 = $29.00). status values: succeeded | failed.

POST /api/payments/create-checkout

Initiates a Stripe checkout session or subscription change, depending on the user's current plan. plan must be "pro" or "enterprise".

Request Body

{ "plan": "pro" }

Behavior Matrix

Current stateTarget planOutcome
Free / no subscriptionpro / enterpriseNew Stripe Checkout session → {"url": "..."}
Active subscriptionsame planBilling portal redirect → {"url": "...", "action": "portal"}
Active proenterprise (upgrade)Subscription.modify() directly → {"action": "upgraded", "plan": "enterprise"}
Active enterprisepro (downgrade)HTTP 403 — blocked with period end date
Active pro / enterprisefree (downgrade)HTTP 403 — blocked

Responses

// New checkout
{ "url": "https://checkout.stripe.com/..." }

// Same plan — portal
{ "url": "https://billing.stripe.com/...", "action": "portal" }

// Upgrade (no redirect needed)
{ "action": "upgraded", "plan": "enterprise" }

// Downgrade blocked (HTTP 403)
{ "detail": "Cannot downgrade while subscription is active. Your current plan is valid until 1 May 2026. Cancel first if you wish to switch to a lower plan." }
POST /api/payments/portal

Creates a Stripe billing portal session to manage payment methods and view invoices.

{ "url": "https://billing.stripe.com/..." }
POST /api/payments/cancel-subscription

Cancels at period end (cancel_at_period_end=true). Access continues until the period end date.

{ "ok": true, "cancel_at_period_end": true, "period_end": 1751410800 }

period_end is a Unix timestamp.

POST /api/payments/sync-subscription

Re-fetches subscription state from Stripe and updates the local database. Use when local state diverges (e.g. after a failed webhook).

{ "ok": true, "status": "active",
  "period_start": "2026-04-01T00:00:00+00:00",
  "period_end": "2026-05-01T00:00:00+00:00",
  "plan": "pro" }
GET /api/payments/token-packs

Returns available extra token packs for one-time purchase. Valid for 12 months from purchase date.

{ "packs": [
  { "id": "starter", "label": "Starter Pack", "tokens": 500,  "price_cents": 900,  "currency": "eur" },
  { "id": "growth",  "label": "Growth Pack",  "tokens": 2000, "price_cents": 2900, "currency": "eur" },
  { "id": "power",   "label": "Power Pack",   "tokens": 5000, "price_cents": 5900, "currency": "eur" }
] }
POST /api/payments/purchase-tokens

Creates a Stripe one-time checkout session for an extra token pack. Redirect the user to the returned url. On success Stripe calls the webhook and credits tokens to the user's balance. Tokens are consumed after the monthly allocation is exhausted.

// Request
{ "pack": "starter" }  // "starter" | "growth" | "power"

// Response
{ "url": "https://checkout.stripe.com/..." }

Connectors API

v2

Custom multi-class text classifiers — define classes, generate training data via LLM, train a MiniLM head, and run inference. Prefix: /api/v2/connectors

POST /api/v2/connectors/ Auth required

Register a new connector from a JSON schema. Returns 201. Returns 409 if connector_id already exists, 422 if validation fails.

// Request
{
  "connector_id": "cpf3_soc",
  "display_name": "CPF3 SOC Indicators",
  "context_description": "Cybersecurity SOC classification",
  "classes": [
    { "idx": 0, "label": "Threat Detection", "description": "...", "generation_hint": "..." },
    { "idx": 1, "label": "Incident Response", "description": "...", "generation_hint": "..." }
  ]
}

// Response 201
{ "connector_id": "cpf3_soc", "status": "pending", "num_classes": 2 }
GET /api/v2/connectors/

List all connectors — id, name, status, num_classes, created_at. No auth required.

[{
  "connector_id": "cpf3_soc",
  "display_name": "CPF3 SOC",
  "status": "ready",
  "num_classes": 5,
  "created_at": "2026-04-19T10:22:00"
}]
GET /api/v2/connectors/{id}

Full connector detail including all class definitions. No auth required. 404 if not found.

{
  "connector_id": "cpf3_soc",
  "status": "ready",
  "classes": [
    {"class_idx": 0,
     "label": "Threat Detection"}
  ]
}
GET /api/v2/connectors/{id}/status

Status polling endpoint. Auth required. Returns current status + error_message.

{
  "connector_id": "cpf3_soc",
  "status": "training",
  "error_message": null,
  "updated_at": "2026-04-19T11:05:30"
}
POST /api/v2/connectors/{connector_id}/bootstrap SSE stream · Auth required

Drive the full generate → train → ready pipeline over a Server-Sent Events stream. Accepts connectors in pending or error state. Returns 409 if already ready, generating, or training.

data: {"phase": "generating_start", "connector_id": "cpf3_soc"}

data: {"phase": "generating", "class_idx": 0, "class_label": "Threat Detection", "generated": 100, "total": 500}

data: {"phase": "complete", "total_samples": 500, "output_path": "psa/training_data/connectors/cpf3_soc_training.jsonl"}

data: {"phase": "training", "connector_id": "cpf3_soc", "progress": 0}

data: {"phase": "ready", "connector_id": "cpf3_soc"}

// On error:
data: {"phase": "error", "connector_id": "cpf3_soc", "error": "Data generation failed: ..."}
curl -N -X POST https://splabs.io/api/v2/connectors/cpf3_soc/bootstrap \
  -H "Authorization: Bearer psa_your_key"
POST /api/v2/connectors/{id}/classify

Classify a text string against a ready connector. Returns 503 if connector is not ready.

// Request
{ "text": "Unusual outbound traffic on port 4444", "top_k": 1 }

// Response
{
  "connector_id": "cpf3_soc",
  "label": "Threat Detection",
  "class_idx": 0,
  "confidence": 0.87,
  "scores": [0.87, 0.08, 0.03, 0.01, 0.01]
}
DELETE /api/v2/connectors/{id} Admin only

Delete a connector — removes DB row, all class rows (cascade), and the model .npz file from disk. Invalidates the registry cache. Returns 403 if not admin.

// Response
{ "deleted": "cpf3_soc" }

CPF v2 — Cybersecurity Psychology Framework

100-indicator behavioural taxonomy. Deterministic rule-based scoring (< 5 ms). Requires Authorization: Bearer <token> for subject endpoints; catalog endpoints are public.

GET /api/v2/cpf/subject/{hash}/sequences

Temporal co-activation sequences — for each active indicator A, finds indicator B that appeared within 72 h in ≥ 35 % of subsequent snapshots (min 3 observations). Surfaces causal indicator chains for the subject drawer.

Query Parameters

ParamDefaultDescription
window_hours72Look-ahead window in hours

Response

{
  "subject_hash": "abc123",
  "window_hours": 72,
  "sequences": [
    {
      "from": "2.1",
      "to": "5.2",
      "frequency": 0.72,
      "median_gap_hours": 18.4,
      "observations": 9
    }
  ]
}
GET /api/v2/cpf/org-summary

CISO organizational overview. Returns the latest snapshot per subject with server-side triage, urgency score, trend direction, and baseline delta. Queue sorted by urgency_score descending.

Query params: days (1–365, default 90)

Response

{
  "subjects": [
    {
      "user_hash": "abc123",
      "last_seen": "2026-04-25T10:00:00Z",
      "cpf_score": 67,
      "alert_level": "RED",
      "category_scores": { "1": 8, "5": 12 },
      "active_red": ["1.1", "5.2"],
      "active_yellow": ["2.7"],
      "total_analyses": 24,
      "trend": "escalating",
      "baseline_delta": 14.5,
      "baseline_avg": 52.5,
      "urgency_score": 82,
      "triage": "CRITICAL"
    }
  ],
  "total_subjects": 12,
  "red_count": 4,
  "yellow_count": 5,
  "critical_count": 2,
  "escalating_count": 3,
  "avg_cpf_score": 41.2
}

triage: CRITICAL (urgency ≥ 70), ESCALATING (≥ 40), STABLE, IMPROVING (≤ 15). trend: linear slope of last 5 scores.

GET /api/v2/cpf/category-correlation

10×10 Pearson correlation matrix across all historical category scores. Shows which psychological vulnerability categories co-activate (positive r) or suppress each other (negative r). Also returns the Dense Foundation Paper's three Bayesian conditional priors for overlay annotation.

Query params: days (1–365, default 90) — requires ≥ 5 analyses

Response

{
  "matrix": {
    "1,1": 1.0, "1,2": 0.43, "2,5": 0.67, "7,1": 0.78
  },
  "n_observations": 148,
  "paper_priors": [
    { "from_cat": 7, "to_cat": 1, "weight": 0.8,  "label": "Stress → Authority compliance" },
    { "from_cat": 2, "to_cat": 5, "weight": 0.7,  "label": "Temporal pressure → Cognitive overload" },
    { "from_cat": 6, "to_cat": 4, "weight": -0.6, "label": "Group dynamics masks Affective state" }
  ]
}

Matrix keys are "{row},{col}" (1-indexed, symmetric). Returns matrix: null with message if < 5 analyses available.

GET /api/v2/cpf/subject/{hash}/forecast

Cross-subject cosine k-NN forecasting. Compares the subject's indicator vector against all other subjects in the account (top-50 matches), then reads their CPF scores at +7 d / +14 d / +30 d to produce median + IQR bands. Requires a populated reference pool — call POST /api/v2/cpf/internal/seed-demo to generate synthetic data.

Response

{
  "subject_hash": "abc123",
  "reference_pool_size": 42,
  "horizons": {
    "7d":  { "median_cpf": 61.2, "p25": 54.1, "p75": 68.9 },
    "14d": { "median_cpf": 58.4, "p25": 49.2, "p75": 66.1 },
    "30d": { "median_cpf": 54.0, "p25": 43.5, "p75": 63.8,
             "alert_RED_pct": 12, "alert_YELLOW_pct": 31, "alert_GREEN_pct": 57 }
  },
  "dominant_pattern": "gradual_decline",
  "confidence": "medium"
}

confidence: low (< 5 neighbours), medium (5–20), high (> 20). Returns forecast: null with explanatory message if pool is insufficient.

Rate Limits

Plan Analyses/Month Sessions API Access
Free505No
Pro5,000UnlimitedYes
EnterpriseUnlimitedUnlimitedYes

Error Codes

All errors follow the format {"detail": "..."}. Structured errors (e.g. 503) return a dict:

{
  "detail": {
    "error": "session_id_required",
    "message": "Either session_id (UUID) or session_name must be provided.",
    "hint": "For stateless analysis without session tracking, set dry_run=true."
  }
}
Code Meaning
401Missing or invalid API key / not authenticated
403Plan does not include API access or subscription downgrade blocked (check detail message for the period end date)
404Resource not found
409Duplicate turn — same session + turn_number already exists
422Invalid request body (field type or format error)
429Monthly analysis limit reached — back off and retry after Retry-After
500Internal server error
503session_id_requiredsession_id (UUID) or session_name must be provided. Use dry_run: true for stateless calls.