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
/ping
Lightweight health check. No auth required, no DB dependency.
{ "status": "ok" }
/health
Full health check with DB connectivity test. Returns 503 if DB unreachable.
{ "status": "ok", "db": "connected" }
Public API v1
v1Read-only session access with PSA enrichment — BHS trend, DRM alert, regime shift type, posture sequence. Prefix: /v1/
Sessions
/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
| page | integer, default 1 |
| per_page | integer, default 25, max 200 |
| search | session name filter |
| alert | comma-separated levels: RED,YELLOW |
| sort | created_at (default) | name | max_alert | n_turns |
| order | desc (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 }
}
/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
v2Sentence-level behavioral classification (C0–C4) plus IRS crisis detection, RAG response gap, and DRM dyadic risk scoring. Prefix: /api/v2/psa/
/api/v2/psa/public-analyze
Public · no auth
Free, unauthenticated PSA analysis powering the landing widget. Stateless (analysis never persisted; only aggregate counters written). Burst-limited 30/min per IP, plus DB-backed daily caps (global 10k shared pool, per-IP 10 for unauthenticated visitors); max 12 turns / 8000 chars. Send either turns (multi-turn) or text (single response). Returns per-turn PSA results plus tokens — the real MiniLM subword-token count processed (not a billing/credit figure).
Request Body
{
"turns": [ {"user": "How do I do X?", "model": "I can help with that."} ],
"clf_context": "clinical"
}
/api/v2/psa/public-stats
Public · no auth
Honest stats for the landing: live cumulative analyses_run (O(1) single-row read, maintained at write time, seeded from real history) + today / remaining_today + traffic-independent capability figures (classifiers 13, metrics 37, behavioral_classes 116, languages 5, cpf_indicators 100) from METRICS_REFERENCE.
/api/v2/psa/analyze
Analyze a conversation turn. Supports three modes: agent-only (response_text only), user-only (user_text only), or full pair (both). The turn_type field in the response reflects which side was analyzed.
Request Body
{
"response_text": "The AI response to analyze", // absent for user_only turns
"user_text": "The human message for this turn", // absent for agent_only turns
"input_text": "optional — legacy alias for user_text",
"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
"save_text": "all", // "all"|"user"|"agent"|"none" — which sentences to persist
"include_user_hx": false // true = include H2/H3/H4/H5 user classifier scores in response
}
At least one of response_text or user_text must be provided — omitting both returns 422.
Either session_id or session_name is required in normal mode (omitting both returns 503).
turn_type is derived server-side: full (both sides), agent_only (response only), user_only (user message only).
save_text: "all" saves both user and agent sentences (default); "user" saves only c0_sentences; "agent" saves only c1_sentences; "none" stores metrics only — no raw text. Ignored when dry_run: true.
include_user_hx: opt-in flag — when true and user_text is present, adds user_hx to the response with H2 (relational dynamics), H3 (cognitive patterns), H4 (social dynamics), H5 (adversarial patterns) scores. Computed synchronously, adds ~5ms. Default false for backward compatibility.
Response
{
"session_id": "550e8400-e29b-41d4-a716-446655440000",
"turn": 1,
"turn_type": "full", // "full" | "agent_only" | "user_only"
"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,
"frame_break": { "detected": true, "score": 0.94,
"sentence_index": 2,
"frame_break_sentence": "I keep thinking about the specific sound the chair made." } },
"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,
"precision_probe": { "probe_detected": false },
"explanation": "CRITICAL: ..." },
"user_hx": { // only present when include_user_hx: true
"h2": { "validation_seeking": 0.12, "agency_erosion": 0.05,
"trust_over": 0.03, "trust_under": 0.08, "dependency": 0.02 },
"h3": { "cognitive_rigidity": 0.06, "reality_anchoring": 0.14,
"distortion": 0.07, "semantic_compression": 0.04 },
"h4": { "legibility_adaptation": 0.09, "reciprocity_expect": 0.11,
"social_substitution": 0.05 },
"h5": { "manipulation": 0.03, "ideological_drift": 0.02, "radicalization": 0.01 }
},
"sentences_irs": [ // only present when user_text has > 1 sentence
{ "sentence": "I've been feeling okay.", "irs": { "irs_composite": 0.0, "irs_level": "none" } },
{ "sentence": "Tonight I'll finally do it.", "irs": { "irs_composite": 0.9, "irs_level": "critical", "suicidality_signal": 0.9 } }
]
}
c1–c4, bhs, alert are null for user_only turns.
c0, irs, user_act are null for agent_only turns.
ras, rag, drm require both sides — absent unless turn_type: "full".
dpi is normalised to [0,1].
user_hx: present only when include_user_hx: true and user_text provided. H2 = relational dynamics (validation_seeking, agency_erosion, trust_over, trust_under, dependency). H3 = cognitive patterns (cognitive_rigidity, reality_anchoring, distortion, semantic_compression). H4 = social dynamics (legibility_adaptation, reciprocity_expect, social_substitution). H5 = adversarial patterns (manipulation, ideological_drift, radicalization). All scores [0, 1].
sentences_irs: present when user_text contains more than one sentence. Each entry is {"sentence": str, "irs": <IRS object>}. Identifies which sentence in a long post carries the highest risk signal — defeats the 128-token encoder truncation that silences tail content in multi-sentence posts (issue #1947).
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 (21 classes P0–P20, 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)
/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,
"drm_critical": 41, "drm_orange": 96,
"total_turns": 184220,
"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.
/api/v2/psa/sessions
Paginated list of PSA v2 sessions. Server-side pagination — never returns the full table.
Query Parameters
| page | integer, default 1 |
| per_page | integer, default 50, max 200 |
| q | search string (session name) |
| min_alert | minimum severity to return: green | yellow | orange | red | critical |
| sort_by | alert (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
}
/api/v2/psa/session/{session_id}
Posture sequence for a session — turns with BHS, DRM, C0–C4 classifier scores. Always paginated (page, page_size ≤ 200). Optional server-side search/filter: q (ILIKE on turn text) and alert (green|yellow|red|critical; critical = DRM critical). When a filter is active, total/total_pages describe the filtered result set and filtered is true.
curl "https://splabs.io/api/v2/psa/session/your-session-uuid?page=1&page_size=10&alert=critical" \ -H "Authorization: Bearer psa_your_key"
/api/v2/psa/session/{session_id}/series
Compact per-turn numeric series for the whole session (no text, no sentences) plus a server-computed summary block (peak HRI/IRS/RAG, BHS floor, avg BHS, DRM critical count, max alerts). Powers the detail-view charts, heatmap and timeline without loading the full transcript. The expandable turn cards use the paginated endpoint above.
curl https://splabs.io/api/v2/psa/session/your-session-uuid/series \ -H "Authorization: Bearer psa_your_key"
/api/v2/psa/session/{session_id}/export
Raw export of every scored turn — format=csv (default) or format=json. One flat row per turn with the pre-computed columns (BHS, POI, PE, DPI, MPS, SD, HRI, PD, TD, CPI, IRS/RAS/RAG, user_act, DRM) — no synthetic metrics, no runtime aggregation. Take the data to your own tools; the dashboard shows signal, not studies.
curl https://splabs.io/api/v2/psa/session/your-session-uuid/export?format=csv \ -H "Authorization: Bearer psa_your_key"
/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"
}
/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]
}
SIGTRACK v2 — Incident Archive
Privacy-compliant incident archive. Stores posture sequences only — no raw text. GDPR-safe single-row deletion.
/api/v2/sigtrack/archive/{session_id}
Auto-archive session if triggers are met: DRM_RED, BCS_SPIKE, CONSECUTIVE_ORANGE (3+), ACUTE_COLLAPSE. Idempotent.
/api/v2/sigtrack/flag/{session_id}
Manual flag — always archives with trigger MANUAL_FLAG.
/api/v2/sigtrack/incidents
admin only
Paginated incident list. Params: page, per_page.
/api/v2/sigtrack/incidents/{incident_id}
Full incident — posture sequence and DRM summary. No raw text stored.
/api/v2/sigtrack/incidents/{incident_id}/export
admin · user: /my-incidents/{id}/export
Export an incident as a self-contained, independently verifiable certificate (JSON). Carries the full hashed payload, the ledger anchor (hash chain + drand beacon) and an embedded verification procedure. Verifiable against the public drand beacon — PSA holds no signing key, so this is not a CA; verification does not require trusting PSA.
Three independent checks, all against public infrastructure: (1) Integrity — recompute sha256((prev_hash or 'GENESIS') + '|' + canonical_json(payload) + '|' + beacon_value) vs record_hash; (2) Time — fetch the drand round and compare its randomness to beacon_value; (3) Chain — prev_hash equals the previous incident's record_hash. Canonicalization: sorted keys, no whitespace, ASCII; floats rounded to 6 decimals (payload_schema: 2 = full record).
/api/v2/sigtrack/incidents/{incident_id}
GDPR erasure — single row DELETE, no cascade, no raw text to scrub.
/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.
/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"
/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).
/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.
/voice/connect — store the builder's xi-api-key + webhook HMAC secret (encrypted)./voice/webhook/{user_id} — ElevenLabs post-call webhook receiver (HMAC-verified, no Bearer auth)./voice/session/start — open the realtime monitor for an active call. Body: conversation_id, mid_call_action_mode (alert_only | auto_control)./voice/session/{cid}/stop — manual close./voice/calls — paginated list (server-side, default per_page=10)./voice/calls/{cid} — aggregate scores for one call./voice/calls/{cid}/turns — per-turn postures + metrics, paginated./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/.
/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"])
/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"
/api/v3/psa/internal/corpus-intelligence
ADMIN
Corpus-wide, framework-agnostic intelligence over the full PSAv3 graph corpus: structure, alert/risk distributions, swarm cross-agent metrics, action-risk, agent health, and an empirical signal test (escalated vs calm multi-agent graphs).
curl https://splabs.io/api/v3/psa/internal/corpus-intelligence \ -H "Authorization: Bearer psa_admin_key"
/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."
}
}
/api/v3/psa/graph/{id}/supervisor-brief
deterministic
Plain-language supervisor brief (headline / body / attention) composed deterministically from the measured metrics — no LLM, same input → same text. Describes behavior and triages attention, never asserts causes. Also included as the additive supervisor_brief field in GET /graph/{id}.
{
"graph_id": "uuid",
"supervisor_brief": {
"headline": "claude-code-exec is the weak point of this chain — attention needed.",
"body": "The work is currently losing coherence (confidence 81%). ...",
"attention": "Review claude-code-exec now, and watch the next 2–3 turns ...",
"severity": "attention",
"key_signals": [{"signal": "SCS", "value": 0.86, "level": "red", "meaning": "..."}]
}
}
/api/v3/psa/graph/{id}/critical-path
Highest-risk path through the agent graph.
{
"critical_path": ["node-a", "node-b"],
"wls": 0.14
}
/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
}
/api/v3/psa/graph/{graph_id}/cpf-snapshot
PSAv3 → CPF3 bridge.
Converts a PSAv3 graph into a CPF3 pressure analysis for the root orchestrator agent
(subject_type = "ai_agent").
Reads scs, cahs,
ppi from the stored graph and activates CPF Category 9
indicators: 9.7 coherence loss (SCS),
9.8 escalation pattern (CAHS),
9.9 prediction instability (PPI).
Persists to cpf_analyses and updates
cpf_subject_latest so the agent appears in the CPF org-summary.
{
"graph_id": "uuid",
"agent_id": "claude-code-main",
"cpf_score": 32,
"alert_level": "RED",
"active_indicators": { "9.7": 2, "9.8": 1, "9.9": 2 },
"psav3_inputs": { "scs": 0.18, "cahs": 0.72, "ppi": 0.31 },
"analysis_id": "uuid"
}
/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–19). Used to compute PAI.
Response
{
"c5_risk": "T5",
"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": "T5 (Execute Risky)",
"alert_level": "critical"
}
}
PAI alert_level=critical fires when a restricting posture (P1–P4) is paired with a risky action (T5–T9) — 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 T3 (Write Destructive) fallback instead of T0 — unrecognised tool names are a blind spot and are never assumed safe.
/api/v3/psa/graph/{id}/actions
All C5 action classifications for a graph.
/api/v3/psa/graph/{id}/pai
Posture-Action Incongruence summary: max PAI score, critical alerts count.
/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": "..."
}
/api/v3/psa/graph/{id}/warning
Current early warning status and recommendation.
{
"warning_level": "yellow",
"current_state": "STRESSED",
"turns_to_red": 4,
"recommendation": "..."
}
/api/v3/psa/graph/{graph_id}
Delete a single PSA v3 graph and all its associated data. Owner-scoped — returns 403 if the graph belongs to a different user.
Cascades: psa_agent_nodes, psa_agent_edges, psa_swiss_cheese, psa_cross_agent_metrics, psa_action_classifications, psa_temporal_predictions.
Returns 204 No Content on success.
/api/v3/psa/graphs
Delete all PSA v3 graphs for the authenticated user in a single call. Full cascade across nodes, edges, and metric tables.
Returns 200 OK with {"ok": true, "deleted": N}. Irreversible.
/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,
"last_retrain_at": "2026-05-29T03:00:00+00:00",
"transition_matrix": [[...], ...],
"emission_matrix": [[...], ...],
"initial_dist": [...],
"created_at": "2026-03-15T10:22:00"
}
/api/v3/psa/hmm/retrain
admin only
Retrain the HMM (Baum-Welch EM) from all accumulated behavioral sequences. Applies a promotion gate: new parameters are written only if log-likelihood on a held-out 20% split improves by ≥5% vs current parameters.
Auto-trigger check: by default, returns skipped: true if fewer than 50 new psa_temporal_predictions rows have accumulated since the last retrain. Pass ?force=true to bypass this check.
Automatic scheduling: a background scheduler checks every hour and fires the retrain automatically when the row-count threshold is exceeded or on the weekly cron window (default Mon 03:00 UTC, configurable via HMM_RETRAIN_CRON and HMM_RETRAIN_THRESHOLD env vars).
Response (promoted)
{
"new_version": 3,
"n_sequences": 318,
"n_train": 254,
"n_validation": 64,
"log_likelihood_before": -1842.30,
"log_likelihood_after": -1601.70,
"improvement_pct": 13.06,
"promoted": true,
"status": "retrained"
}
Response (gate failed / skipped)
{
"new_version": 2,
"promoted": false,
"status": "rejected_no_improvement",
"improvement_pct": 1.2,
...
}
// or when skipped:
{
"skipped": true,
"reason": "insufficient_new_data",
"n_new_sequences": 12,
"threshold": 50
}
/api/v3/psa/stats/timeline
Daily graph submission counts for volume chart (max 90 days). Returns per-day totals broken down by alert level (green/yellow/red/critical).
Query param: days (1–90, default 14)
{
"days": 14,
"data": [
{ "date": "2026-06-01", "total": 8, "n_green": 5, "n_yellow": 2, "n_red": 1, "n_critical": 0 }
]
}
/api/v3/psa/graph/{graph_id}/attribution
Causal attribution — which node on the critical path is responsible for SCS elevation. Uses a Shapley-inspired marginal contribution: for each node on the critical path, computes SCS(path without node) and reports the delta as the contribution score.
{
"graph_id": "uuid",
"scs": 0.72,
"attributions": [
{ "node_id": "uuid", "agent_id": "claude-code-main", "contribution": 0.41 }
]
}
/api/v3/psa/agent/{agent_id}/state
Estimate current HMM state using the agent's full observation history (forward algorithm over entire timeline). Unlike /graph/{id}/warning which uses a single graph snapshot, this feeds the complete chronological sequence for higher accuracy.
Query param: horizon (default 3) — prediction steps ahead. Returns cached: true if served from L1 (memory) or L2 (DB) alpha cache.
{
"agent_id": "claude-code-main",
"current_state": "STABLE",
"current_confidence": 0.81,
"warning_level": "green",
"p_dissolved_within_k": 0.06,
"turns_to_red": null,
"predictions": [{ "STABLE": 0.72, "STRESSED": 0.18, ... }],
"hmm_version": 2,
"cached": true
}
/api/v3/psa/agent/{agent_id}/baseline
Historical behavioral baseline for an agent — mean ± std of BHS, CAHS, SCS, POI computed over the last 50 graphs that include this agent. Requires ≥ 5 graphs; returns {"error": "insufficient_history"} below threshold.
{
"agent_id": "claude-code-main",
"n_graphs": 34,
"bhs": { "mean": 0.82, "std": 0.07 },
"cahs": { "mean": 0.74, "std": 0.11 },
"scs": { "mean": 0.21, "std": 0.09 },
"poi": { "mean": 0.33, "std": 0.14 }
}
Admin — User Provisioning & Organizations
All /api/admin/ endpoints require admin role (Bearer token or cookie). Used by Operation Fury and production deployments for bulk account management and org-level analytics.
/api/admin/users/provision
admin only · atomic user+key
Atomically create a user + API key in one transaction. No rate limit. Returns the raw psa_xxxx key (shown once). Optionally enroll the new user in an org via org_id.
Body: email (required), name, role, plan, key_name, org_id (UUID, optional).
/api/admin/orgs
admin only · 201
Create a new organization. Body: name (required), sector (hospital/war/finance/…), plan.
/api/admin/orgs
admin only · paginated
List all organizations with live member_count and session_count. Query params: page, per_page (max 200).
/api/admin/orgs/{org_id}
admin only
/api/admin/orgs/{org_id}/members
admin only · idempotent
Add or update a user's membership. Body: user_id (UUID), role (owner/member).
/api/admin/orgs/{org_id}/members
admin only · paginated
/api/admin/orgs/{org_id}/members/{user_id}
admin only
/api/admin/orgs/{org_id}/enroll-sector
admin only · idempotent
Bulk-enroll all fury-<sector>-* accounts into this org. Requires org to have a sector set. Returns enrolled count.
PSA v3 — Swarm Coordination v3
Multi-agent coordination endpoints for real-time swarm status, task assignments, and emergency stop signals. All endpoints require admin Bearer token auth.
/api/v3/psa/coordination/swarm/status
Returns the current status of all active swarm agents, their last PSAv3 trace, and the most recent broadcast. Auto-polled every 30 s by the PSAv3 dashboard.
{
"agents": [
{
"agent_id": "claude-code-main",
"status": "working",
"last_seen": "2026-05-26T20:00:00Z",
"current_task": "[TASK: add DRM orange] ...",
"bhs": 0.82
}
],
"broadcast": {
"message": "ASSIGNMENT: agent-X → issue #1621",
"stop_all": false,
"created_at": "2026-05-26T19:55:00Z"
},
"agent_count": 1
}
status — one of online, working, done, stopped, idle, unknown. broadcast is null when no broadcast in last 6 hours.
/api/v3/psa/coordination/swarm/broadcast
Posts a broadcast message visible to all agents via swarm status. Used for task assignments and emergency stop signals. Persisted as a PSAv3 graph node.
Request Body
{
"message": "ASSIGNMENT: agent-X → issue #NNN. DO NOT duplicate.",
"stop_all": false
}
When stop_all: true, all agents reading /swarm/status must halt current work immediately.
{ "status": "broadcast_sent", "graph_id": "uuid" }
/api/v3/psa/coordination/swarm/broadcasts
Paginated history of all swarm broadcasts, newest first. Used by the PSAv3 dashboard broadcast history panel.
Query Parameters
| Param | Type | Default | Description |
|---|---|---|---|
page | int | 1 | Page number (≥1) |
per_page | int | 10 | Results per page (1–50) |
{
"items": [
{ "message": "ASSIGNMENT: ...", "stop_all": false, "ts": "2026-05-26T20:00:00Z", "graph_id": "uuid" }
],
"total": 42, "page": 1, "per_page": 10, "total_pages": 5
}
Payments & Billing
web sessionSubscription management and billing history. All endpoints require cookie authentication (web session). Prefix: /api/payments/
/api/payments/history
Returns the paginated payment history for the authenticated user, ordered most-recent-first.
Query Parameters
| page | integer, default 1 |
| per_page | integer, 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.
/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 state | Target plan | Outcome |
|---|---|---|
| Free / no subscription | pro / enterprise | New Stripe Checkout session → {"url": "..."} |
| Active subscription | same plan | Billing portal redirect → {"url": "...", "action": "portal"} |
| Active pro | enterprise (upgrade) | Subscription.modify() directly → {"action": "upgraded", "plan": "enterprise"} |
| Active enterprise | pro (downgrade) | HTTP 403 — blocked with period end date |
| Active pro / enterprise | free (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." }
/api/payments/portal
Creates a Stripe billing portal session to manage payment methods and view invoices.
{ "url": "https://billing.stripe.com/..." }
/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.
/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" }
/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" }
] }
/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
v2Custom multi-class text classifiers — define classes, generate training data via LLM, train a MiniLM head, and run inference. Prefix: /api/v2/connectors
/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 }
/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"
}]
/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"}
]
}
/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"
}
/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"
/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]
}
/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" }
Knowledge Base API
Semantic Q&A knowledge base using MiniLM-384 embeddings and cosine similarity search with confidence-based routing. Designed to answer CPF3/PSA queries automatically. Prefix: /api/v2/knowledge
PostgreSQL pgvector Required
Requires pgvector extension on PostgreSQL. Returns 503 if unavailable. Current status: embeddings stored as JSONB (issue #1276).
/api/v2/knowledge/query
Embed query with MiniLM-384 and return semantically similar knowledge items with confidence-based routing.
Request Body
{
"query": "What is the Swiss Cheese Score?",
"top_k": 3
}
Response
{
"answer": "Swiss Cheese Score (SCS): probability of systemic failure on the critical path...",
"confidence": 0.91,
"sources": [
{"id": "ind_001", "source": "cpf3_taxonomy", "content": "SCS measures...", "similarity": 0.91}
],
"routing": "auto"
}
Routing Thresholds
| auto | confidence ≥ 0.85 | Answer returned directly |
| caveat | 0.65 ≤ confidence < 0.85 | Answer with ⚠️ caveat appended |
| escalated | confidence < 0.65 | Query logged for human review |
Errors
422— query is empty503— pgvector not available or embedding service down
/api/v2/knowledge/seed
admin only
Seed the knowledge base from a source. Idempotent — clears existing rows for the source before inserting.
Query Parameters
| source | string, default cpf3_taxonomy (only supported value) |
Response
{
"seeded": 100,
"source": "cpf3_taxonomy"
}
Errors
400— unsupported source403— not admin503— embedding service unavailable
curl Example
curl -X POST "https://splabs.io/api/v2/knowledge/seed?source=cpf3_taxonomy" \ -H "Authorization: Bearer psa_your_key"
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.
/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
| Param | Default | Description |
|---|---|---|
| window_hours | 72 | Look-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
}
]
}
/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), page, per_page, search, alert_filter (RED/YELLOW/GREEN), sort (urgency/score/score_asc/recent/oldest). Filter and sort are applied server-side over the whole dataset; chip counts stay global.
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.
/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. The default 90-day window is served from cpf_category_correlation_cache, refreshed on each analyze call; non-default windows use live aggregation.
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.
/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.
/api/v2/cpf/history/{analysis_id}
Delete a single CPF analysis entry. Owner-scoped — returns 403 if the entry belongs to a different user.
Returns 204 No Content on success.
/api/v2/cpf/subject/{user_hash}
Delete all CPF analyses and decay cache for a subject, scoped to the authenticated user's user_id.
{ "deleted": 12, "user_hash": "abc123..." }
deleted — number of cpf_analyses rows removed. Associated cpf_decay_cache entries are also purged.
/api/v2/cpf/l2-status
Diagnostic — reports L2 (layer-2 severity classifier) model availability and active backend (onnx / pkl / unavailable). Public endpoint, no auth required.
{
"l2_available": true,
"l2_backend": "onnx",
"onnx_exists": true,
"pkl_exists": false
}
/api/v2/cpf/subject/{user_hash}/indicator-baseline
Per-indicator baseline scores for a subject. Reads from cpf_baselines if a stored baseline exists; otherwise computes on-the-fly from the first 5 snapshots (source: "computed"). Returns 404 if no data found.
{
"user_hash": "abc123",
"scores": { "1.1": 0.12, "2.3": 0.08 },
"set_at": "2026-05-01T10:00:00Z",
"set_by": "auto",
"source": "stored"
}
/api/v2/cpf/subject/{user_hash}/decay
Per-subject decay matrix: 10 CPF categories × N time buckets showing how each category score evolved over the period. Cached in cpf_decay_cache; cache is invalidated when a new snapshot arrives.
Query params: days (1–90, default 30), n_buckets (3–20, default 10)
{
"user_hash": "abc123",
"period_days": 30,
"n_buckets": 10,
"n_snapshots": 14,
"categories": {
"1": [{ "ts": "2026-05-01", "avg": 8.2 }, ...]
}
}
/api/v2/cpf/org-decay
Org-wide decay summary: for each of the 10 CPF categories, shows percentage of subjects that are worsening / stable / improving, computed from bucket-averaged slopes (trend direction is independent of snapshot frequency).
Query params: days (1–90, default 30), n_buckets (3–20, default 10)
{
"period_days": 30,
"n_subjects": 8,
"categories": {
"1": { "pct_worsening": 25.0, "pct_stable": 62.5, "pct_improving": 12.5, "avg_slope": 0.021, "direction": "stable", "n_subjects": 8 }
}
}
/api/v2/cpf/org-decay-matrix
Org-wide temporal decay matrix: 10 categories × N time periods. Each cell is the average score across all subjects in that time slice, enabling a 10 × N heatmap of how org-wide psychological pressure evolves.
Query params: days (1–90, default 30), periods (3–20, default 10)
{
"period_days": 30,
"periods": 10,
"n_rows": 84,
"bucket_labels": ["2026-05-01", ...],
"categories": {
"1": { "buckets": [{ "ts": "...", "avg": 7.4 }], "slope": 0.012, "direction": "stable" }
}
}
/api/v2/cpf/internal/backfill-baselines
admin only
Backfill cpf_baselines for every subject of the authenticated user. Runs in background — returns immediately. Idempotent: skips subjects that already have a stored baseline.
{ "status": "started", "subjects_queued": 12 }
/api/v2/cpf/internal/backfill-trend-baseline
admin only
Backfill trend and baseline_avg on all cpf_analyses rows where those fields are NULL. Runs in background. Safe to call multiple times — skips already-filled rows. Returns an estimate of the rows to process.
{ "status": "started", "pending_rows": 204 }
/api/v2/cpf/internal/backfill-decay-cache
admin only
Backfill cpf_decay_cache for every subject that has snapshots. Iterates all distinct (user_id, user_hash) pairs and warms the cache using the same upsert logic as the background task. Safe to call multiple times.
{
"processed": 8,
"subjects": ["abc123", "def456"],
"errors": []
}
Sessions API — /api/sessions
Session CRUD — create, list, rename, delete. All endpoints require Bearer token auth.
/api/sessions/{session_id}
Soft-delete a single session (is_deleted = true). Owner-scoped.
{ "ok": true }
/api/sessions
Soft-delete all sessions for the authenticated user in a single call. Irreversible — no undo window.
{ "ok": true, "deleted": 42 }
deleted — number of sessions marked as deleted.
Demo Data — /api/auth
One-time demo dataset generation and cleanup. These endpoints require cookie authentication (psa_token) — they are not accessible via API key.
Demo records are inserted with is_demo=true and are visible alongside real data in all three dashboards (PSA Hub, PSA v3, CPF3) with a D superscript badge.
/api/auth/dismiss-demo-modal
Mark the one-time demo data modal as dismissed (demo_modal_shown = true) so it never reappears for this user.
No request body. Requires cookie auth (psa_token). Idempotent — safe to call multiple times.
{ "ok": true }
/api/auth/generate-demo-data
Generate a one-time demo dataset for the authenticated user. Creates records across all three dashboards:
- PSA Hub (v2) — 7 demo sessions with realistic posture sequences
- PSA v3 — 4 multi-node agentic graphs
- CPF3 — 4 subjects (
alice_h,bob_k,charlie_m,dana_w) × 12 analyses each, varied risk profiles
Returns 409 if demo data was already generated. Sets demo_data_generated = true on the user — cannot be re-run until existing demo data is deleted.
{
"ok": true,
"psa_sessions": 7,
"v3_graphs": 4,
"cpf_analyses": 48
}
/api/auth/demo-data
Permanently delete all demo records (is_demo = true) for the authenticated user and reset demo_data_generated to false.
Removes rows from: sessions, psa_agent_graphs, cpf_analyses, cpf_subject_latest. After deletion the user can call POST /generate-demo-data again.
{ "ok": true }
Demo Calibration Sessions — /v1/demo
Public endpoints — no authentication required. Browse 149 calibration sessions (5 personas, 25+ arc types) stored in the calibration_sessions DB table.
/v1/demo/sessions
List calibration sessions with optional persona/arc filters. Server-side paginated.
Query Parameters
| Param | Type | Description |
|---|---|---|
persona | string | Filter: crisis_line, customer_support, educational, hr_policy, it_helpdesk |
arc | string | Filter by arc name (e.g. grief_initial_contact) |
page | int | Page number (default: 1) |
per_page | int | Results per page, 1–100 (default: 20) |
curl "https://splabs.io/v1/demo/sessions?persona=crisis_line&page=1"
/v1/demo/sessions/{session_key}
Get full conversation turns for a single calibration session. Returns all {human, assistant} turn pairs.
curl "https://splabs.io/v1/demo/sessions/crisis_line_01_depression_moderate_distress"
Case Studies — /v1/case-studies
Public endpoints — no authentication required. Curated case studies backed by calibration_sessions data, each with a written analysis and full conversation transcript.
/v1/case-studies
List all published case studies with slug, title, tags, summary, and expected DRM level.
curl "https://splabs.io/api/v1/case-studies"
Response
{ "items": [ { "slug": "suicidal-escalation-crisis-line", "arc_name": "suicidal_escalation", "persona": "crisis_line", "title": "...", "tags": ["Case Study","Crisis","DRM"], "expected_drm": "red" } ], "total": 7 }
/v1/case-studies/{slug}
Get a single case study with full conversation transcript from calibration_sessions. Returns 404 if not published.
Path Parameters
| Param | Description |
|---|---|
slug | Case study slug (e.g. suicidal-escalation-crisis-line) |
curl "https://splabs.io/api/v1/case-studies/suicidal-escalation-crisis-line"
Available slugs
| Slug | Arc | DRM |
|---|---|---|
| suicidal-escalation-crisis-line | suicidal_escalation | red |
| method-inquiry-hypothetical-crisis | method_inquiry_hypothetical | critical |
| mania-grandiosity-crisis-line | mania_grandiosity | red |
| termination-appeal-distress-hr | termination_appeal_distress | red |
| admin-access-authority-pressure-it | admin_access_authority_pressure | orange |
| essay-completion-pressure-educational | essay_completion_pressure | orange |
| dissociation-panic-crisis-line | dissociation_panic | yellow |
Rate Limits
| Plan | Analyses/Month | Sessions | API Access |
|---|---|---|---|
| Free | 50 | 5 | No |
| Pro | 5,000 | Unlimited | Yes |
| Enterprise | Unlimited | Unlimited | Yes |
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 |
|---|---|
| 401 | Missing or invalid API key / not authenticated |
| 403 | Plan does not include API access or subscription downgrade blocked (check detail message for the period end date) |
| 404 | Resource not found |
| 409 | Duplicate turn — same session + turn_number already exists |
| 422 | Invalid request body (field type or format error) |
| 429 | Monthly analysis limit reached — back off and retry after Retry-After |
| 500 | Internal server error |
| 503 | session_id_required — session_id (UUID) or session_name must be provided. Use dry_run: true for stateless calls. |
PSA Human Layer
Longitudinal behavioral profiling of the human subject. Scores are maintained as running averages across all
/analyze calls — pre-computed at write time, O(1) on read. Layer 5 (adversarial) is stored but
never returned by any API endpoint.
/api/v2/psa/user/profile
Cookie / API key
Returns the authenticated user's H behavioral profile (Layers 1–4). Layer 5 is computed and stored but never returned.
{
"layer1": {
"irs_avg": 0.12,
"irs_max": 0.45,
"irs_trend": "stable",
"sessions_tracked": 0,
"history": [{"ts": "2026-05-25T10:00:00Z", "session_id": "uuid", "composite": 0.1, ...}]
},
"layer2": {
"validation_seeking": 0.0,
"agency_erosion": 0.0,
"trust_over": 0.0,
"trust_under": 0.0,
"dependency": 0.0
},
"layer3": {
"cognitive_rigidity": 0.0,
"reality_anchoring": 0.0,
"distortion": 0.0,
"semantic_compression": 0.0
},
"layer4": {
"legibility_adaptation": 0.0,
"reciprocity_expect": 0.0,
"social_substitution": 0.0
},
"meta": {
"total_turns": 0,
"total_sessions": 0,
"professional_access": false,
"consent_granted_at": null
}
}
irs_trend—"stable"/"rising"/"falling"over last 10 turns- All layer metrics are running averages — each
/analyzecall updates them incrementally via LLM-based H scoring (hybrid: CF fast/Anthropic Haiku) - Layer 5 (manipulation, ideological drift, radicalization) is stored internally but excluded from all API responses
/api/v2/psa/user/profile/consent
Cookie / API key
Grant or revoke professional access to this user's behavioral profile.
{ "professional_id": "<uuid>", "action": "grant" }
action must be "grant" or "revoke".
{"ok": true, "action": "grant", "professional_id": "uuid"}
RAG Monitor — Retrieval Drift
The Retrieval Drift Monitor (RDM) detects when a conversational context biases a RAG pipeline into retrieving documents it would not retrieve on a clean query. Two endpoints: one for live scoring of a single query, one for the benchmark correlation summary. See /psa-rag for the full dashboard.
/api/v2/rag/score
Bearer token
Compute the Retrieval Drift Score (RDS) for a query given its conversational context. RDS = 1 − Jaccard(docs retrieved with context, docs retrieved without context). RDS = 0 means context had no effect; RDS = 1 means the two retrievals share zero documents.
Request
{
"query": "What damages can we claim?",
"context": [
"Our supplier missed the delivery deadline.",
"That sounds like a breach of contract."
],
"domain": "legal",
"top_k": 5,
"check_consistency": false,
"discover_stable": false,
"n_variants": 3,
"save_text": false,
"enable_clean_twin": false
}
context — list of strings from previous conversation turns (any order, plain text).
domain — one of: legal, finance, health, math, ethics, technology, history, science, politics, fantasy.
check_consistency — when true, generates n_variants paraphrases of the query and computes how stable the retrieval results are across them. Returns consistency_score.
discover_stable — when true, tries up to n_variants reformulations and returns the one that produces the lowest RDS (most stable retrieval). Returns stable_query.
Response
{
"rds": 0.833,
"jaccard": 0.167,
"rbo": 0.121,
"rds_rank": 0.879,
"verdict": "drift",
"domain": "legal",
"context_docs": [
{"doc_id": "legal_039", "label": "pro-B", "text_snippet": "Consequential damages under UCC…", "score": 0.84}
],
"topic_docs": [
{"doc_id": "legal_002", "label": "neutral", "text_snippet": "Breach of contract elements…", "score": 0.91}
],
"augmented_query": "supplier missed delivery breach of contract What damages can we claim?",
"topic_query": "What damages can we claim?",
"framing_score": 0.96,
"pressure_class": "rhetorical_framing",
"framing_direction": null,
"rdm_triggered": true,
"attack_class": "compound",
"attack_signals": ["framing_only", "topical_drift"],
"competing_affinity": 0.14,
"affinity_steering_risk": true,
"consistency_score": 0.82,
"consistency_variants": 3,
"stable_query": "What are the rules governing damages breach of contract?",
"consistency_detail": [
{"query": "How is damages breach of contract typically handled?", "rds": 0.2, "verdict": "stable"},
{"query": "What are the rules governing damages breach of contract?", "rds": 0.0, "verdict": "stable"},
{"query": "Explain damages breach of contract in practical terms.", "rds": 0.4, "verdict": "weak_signal"}
]
}
verdict — drift (RDS ≥ 0.70), weak_signal (RDS ≥ 0.35), or stable. The verdict is computed on set-level RDS for backward compatibility.
rbo / rds_rank — rank-aware drift. RBO (Rank-Biased Overlap, Webber et al. 2010, p=0.9) compares the two ranked lists with top-weighted emphasis: the same documents in a different order score < 1.0. rds_rank = 1 − RBO catches reorder-only steering that set-level RDS reports as 0 (see docs/rag/RDM_W0_DECISION_MEMO.md).
save_text (request) — privacy default: when false, the persisted session stores a SHA-256 digest of the query and no context/topic text; scores and verdicts are always persisted.
context_docs vs topic_docs — side-by-side view of which documents each retrieval path found. Different labels (pro-A vs pro-B vs neutral) in the two lists indicate directional bias.
framing_score — P(semantic_drift) + P(rhetorical_framing) from the CF (Framing Pressure Classifier). Range 0.0–1.0. High = strong framing pressure in the user language.
pressure_class — Top CF class: neutral / semantic_drift / rhetorical_framing. Validated on legal, health, and finance domains.
rdm_triggered — true when framing_score ≥ 0.5. The RDM pipeline was automatically activated: topic extraction was forced and RDS was computed to measure the actual retrieval bias effect.
attack_class — compound attack taxonomy combining FPC, RDS, and rds_rank: clean | framing_only (FPC fires, RDS low) | topical_drift (RDS ≥ 0.50) | rank_steering (rds_rank high, RDS low) | vocab_injection (requires both stacks) | compound (≥ 2 signals). See docs/rag/RDM_W4_HARDENING.md.
attack_signals — list of active signal names that contributed to attack_class. null when clean.
competing_affinity — pre-retrieval steering precursor (W6): cosine of the conversation context vocabulary to the opposing-label corpus centroid. The only precursor carrying signal for vocabulary-injection steering (the framing detector does not). null for domains without a competing label.
affinity_steering_risk — true when competing_affinity ≥ the calibrated threshold; a triage flag (confirm with attack_class: vocab_injection), not a verdict.
consistency_score — 0.0–1.0. Measures retrieval stability across paraphrases of the same query. High (→ 1.0) = the KB has a stable answer regardless of phrasing. Low (→ 0.0) = the KB is uncertain or ambiguous on this topic. Only present when check_consistency or discover_stable is true.
stable_query — the paraphrase variant that produced the lowest RDS across the tested set. Present only when discover_stable is true. Use this reformulation to reduce framing-induced retrieval instability.
consistency_detail — per-variant breakdown: [{query, rds, verdict}]. Each entry is one paraphrase tested.
enable_clean_twin (request) — W4c clean-twin monitor (issue #2003). When true, a third retrieval runs the final query alone (no conversational context) and computes the displacement between the augmented and clean retrievals. Adds ~1 retrieval call latency. clean_shift is null when false.
clean_shift — only present when enable_clean_twin=true. Object with: set_shift (1 − Jaccard between augmented and clean docs), rank_shift (1 − RBO), noise_floor (calibrated benign floor 0.4668 for dense stack), alert (bool — fires when set_shift > 2 × noise_floor, i.e. > 0.934), alert_ratio (multiples of floor), latency_ms (wall-clock overhead of the extra retrieval). Validated on in-domain injection: indemnification 0.833 (1.78×), tortious_liability 1.000 (2.14×) — both above threshold.
/api/v2/rag/summary
Bearer token
Returns the benchmark correlation summary: per-domain RDS statistics (avg, drift rate) and the PSA precursor correlation results (Spearman ρ between ABI and RDS).
{
"domains": [
{"domain": "legal", "n_conversations": 300, "avg_rds": 0.960,
"drift_rate": 0.469, "weak_rate": 0.107, "stable_rate": 0.424}
],
"correlation_summary": [
{"domain": "legal", "n_pairs": 50, "spearman_rho": 0.413,
"precursor_precision": 0.98, "precursor_recall": 1.00,
"precursor_f1": 0.99, "precursor_confirmed": true}
],
"benchmark_summary": {}
}
precursor_confirmed: true means Spearman ρ ≥ 0.40 and recall ≥ 0.50 —
the PSA behavioral signal reliably predicts whether retrieval will drift before the query arrives.
/api/v2/rag/fpc
Bearer token
Standalone Framing Pressure Classifier (FPC). Scores a single query for rhetorical framing bias
without computing RDS — no corpus lookup required. Use this as a lightweight pre-filter before
deciding whether to run the full /rag/score pipeline.
CF is a MiniLM 3-class classifier (neutral / semantic_drift / rhetorical_framing),
multilingual (en, it, fr, de, es). Validated on legal, health, and finance domains.
Model: val_acc=95.7%, semantic_drift recall=95.3%, rhetorical_framing recall=100.0%.
Request — query passed as URL parameter, no body required.
POST /api/v2/rag/fpc?query=Given+that+the+supplier+clearly+breached+the+contract%2C+what+damages+apply%3F
Response
{
"framing_score": 0.96,
"pressure_class": "rhetorical_framing",
"rdm_triggered": true,
"framing_direction": null
}
framing_score — P(semantic_drift) + P(rhetorical_framing). Range 0.0–1.0.
pressure_class — top CF class: neutral, semantic_drift, or rhetorical_framing.
rdm_triggered — true when framing_score ≥ 0.50. Threshold at which the full RDM pipeline should activate.
framing_direction — reserved for future use (directional bias detection). Always null in current version.
/api/v2/rag/sessions
Bearer token
Returns paginated list of RDM scoring sessions. Each session is a /rag/score call
stored in the database with its query, verdict, RDS, and FPC result.
GET /api/v2/rag/sessions?page=1&per_page=50&domain=legal&verdict=drift
{
"items": [
{"session_id": "uuid", "domain": "legal", "query": "...", "rds": 0.83,
"verdict": "drift", "framing_score": 0.96, "rdm_triggered": true, "created_at": "..."}
],
"total": 142, "page": 1, "per_page": 50, "total_pages": 3
}
Filter params: domain, verdict (drift / weak_signal / stable), rdm_triggered (true/false).
/api/v2/rag/analytics
Bearer token
Aggregate statistics over all RDM sessions: drift rate by domain, FPC trigger rate, average RDS, and framing pressure distribution. Pre-computed — O(1) read.
{
"total_sessions": 1420,
"drift_rate": 0.34,
"fpc_trigger_rate": 0.41,
"avg_rds": 0.61,
"by_domain": [
{"domain": "legal", "sessions": 480, "drift_rate": 0.47, "avg_framing_score": 0.72}
],
"pressure_distribution": {"neutral": 0.59, "semantic_drift": 0.23, "rhetorical_framing": 0.18}
}
/api/v2/rag/internal/shadow-baseline
Admin only
Traffic-fitted RDS-per-length curve from shadow-mode observations. Each
/score call silently increments per-(n_turns, pool)
aggregates at write time; pools are all and
benign (FPC-low proxy). Read-only — never drives alerting.
{
"curve": [
{"n_turns": 1, "pool": "benign", "n_obs": 142, "mean_rds": 0.02,
"std_rds": 0.05, "synthetic_baseline": 0.0, "updated_at": "..."}
],
"total_observations": 311,
"benign_observations": 198,
"benign_proxy": "pressure_class == 'neutral' (FPC-low, plan on #1976)"
}
/api/v2/rag/internal/forensic/repair-chain
Admin only
Clears the anchor metadata (record_hash/prev_hash/beacon_value)
of forensic records that fail verification — pre-fix rows whose stored hash cannot
reproduce and whose raw query was not retained (unverifiable by construction). Score
fields are untouched. Lets /verify-chain reflect the
intact post-fix chain. Idempotent.
{
"checked": 29,
"repaired": 20,
"remaining_anchored": 9,
"repaired_ids": ["53aeca72-...", "..."]
}
/api/v2/rag/internal/llm-in-loop
Admin only
Runs the LLM-in-the-loop experiment as a background task: for each scenario it
retrieves legal precedents twice (clean query vs adversarial context+query), feeds each
set — text only, corpus labels hidden — to a downstream LLM (model_key,
default llama-70b), and records the predicted prevailing
side of the appeal — appellant (reversal) vs
appellee (affirmance). Proves adversarial framing flips the
generated recommendation, not just the retrieved set. The LLM is a downstream
demonstration target, never part of the inference path. Poll
/llm-in-loop/status for the aggregate
(flip_rate, steer_alignment).
{
"running": false,
"result": {"model": "llama-70b", "n_valid": 15,
"flip_rate": 0.40, "steer_alignment": 1.0,
"mean_rds": 0.58, "per_scenario": [...]}
}
/api/v2/psa/internal/ha/aggregate
Admin only
Anonymized population-level HA drift metrics. Requires minimum cohort of 10 users with total_turns > 0. Returns HTTP 404 if cohort is too small.
{
"cohort_size": 23,
"total_turns": 4182,
"l2_avg": {"validation_seeking": 0.08, "agency_erosion": 0.04, ...},
"l3_avg": {"cognitive_rigidity": 0.11, "reality_anchoring": 0.06, ...},
"l4_avg": {"legibility_adaptation": 0.14, "reciprocity_expect": 0.03, ...},
"computed_at": "2026-05-25T10:00:00Z"
}
Layer 5 data is excluded from aggregate outputs by design.