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/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)
/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.
/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}
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"
/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]
}
/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.
/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}
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/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}/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"
}
}
/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/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.
/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/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.
/api/v2/psa/internal/training-sessions
admin only
Paginated list of sessions available for training selection.
| Param | Type | Description |
|---|---|---|
| q | string | Filter by user email (partial match) |
| min_alert | string | Minimum alert level: GREEN / YELLOW / ORANGE / RED / CRITICAL |
| page | int | Page number (default 1) |
| per_page | int | Items 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": "..." }] }
/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 }] }
/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 }
/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" }
/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.
| Param | Description |
|---|---|
| cmd | benchmark | confusion_matrix | train_head |
| clf | Space-separated classifiers e.g. c1 c3 (blank = all) |
| save | bool — --save flag for confusion_matrix |
| epochs | int 1–2000 (train_head, default 200) |
| lr | float 0–1 (train_head, default 0.01) |
SSE events: start, line, done (with returncode), error.
/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
/api/v2/psa/internal/slm/stats
admin only
Explanation cache totals, model version distribution, feedback counts (positive/negative/with correction).
/api/v2/psa/internal/slm/feedback
admin only · paginato
RLHF feedback list. Params: page, per_page (max 100), rating (1 o -1).
/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.
/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 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" }
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)
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
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.
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. |