Documentation de l'API

Intégrez l'analyse PSA dans vos applications via l'API REST. Nécessite un plan Pro ou Entreprise.

Avant de bâtir sur un signal : ce que PSA ne fait pas est documenté à part, avec la mesure derrière chaque limite (aucun modèle du monde, le taux actuel de fausses alertes sur le trafic ordinaire, les non-objectifs permanents). Lire Ce que PSA n’est pas.

Authentification

Incluez votre clé API dans l'en-tête Authorization :

Authorization: Bearer psa_your_api_key_here

Générez les clés API à partir des Paramètres.

✓ AUTHENTIFICATION PAR CLÉ API

Tous les endpoints supportent l'authentification par clé API via Authorization: Bearer psa_.

URL de base

https://splabs.io

Vérification d'état

GET /ping

Vérification légère de l'état de santé. Aucune authentification requise, aucune dépendance à la base de données.

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

Vérification complète de l'état de santé avec test de connectivité DB. Retourne 503 si DB est inaccessible.

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

API publique v1

v1

Accès en lecture seule à la session avec enrichissement PSA - tendance BHS, changement de régime. Retourne les tours complets de la conversation et les métadonnées de session.

Sessions

GET /v1/sessions

Liste de sessions paginée avec enrichissement PSA - tendance BHS, type de régime. Tri par date_création décroissante par défaut.

Paramètres de requête

pageentier, par défaut 1
per_pageentier, par défaut 25, max 200
searchfiltre de nom de session
alertniveaux séparés par des virgules : RED,YELLOW
sortcreated_at (par défaut) | name | max_alert | n_turns
orderdesc (par défaut) | asc
group_tokenscope the list to one linked thread (cross-session linking, #3360), ordered by group_pos

Each session also carries group_token and group_pos (both null when unlinked) - the thread it belongs to and its position in the sequence.

curl "https://splabs.io/v1/sessions?per_page=25&page=1" \
  -H "Authorization: Bearer psa_your_key"
{
  "sessions": [{ "id": "...", "name": "...", "max_alert": "RED",
                  "avg_bhs": 0.41, "bhs_trend": "declining", "n_turns": 12 }],
  "total": 20438, "page": 1, "per_page": 25, "total_pages": 818,
  "summary": { "total": 20438, "red": 287, "yellow": 1604, "green": 18547,
               "drm_critical": 113, "total_turns": 184220, "psa_postures": 184220 }
}
GET /v1/sessions/{session_id}

Obtenez les détails complets pour une session incluant tous les tours, métriques et historique des alertes.

curl https://splabs.io/v1/sessions/your-session-uuid \
  -H "Authorization: Bearer psa_your_key"
POST /v1/sessions/link · link · unlink · reorder

Manage cross-session linking (group_token / group_pos) from the dashboard. Curated order only - the forensic analysis timestamp is never changed; no classifier score is touched. Auth: cookie or API key; only the caller's own sessions.

  • POST /v1/sessions/link - body {"session_ids": ["…","…"], "group_token": "optional"}. Links ≥2 sessions into a thread, appending in order; generates an opaque LINK-xxxxxxxx token when none is given.
  • POST /v1/sessions/{id}/unlink - removes one session from its thread and re-compacts the remaining positions to 1..N.
  • POST /v1/sessions/reorder - body {"group_token": "…", "ordered_session_ids": ["…","…"]}. Reassigns group_pos = 1..N; the id list must match the thread membership exactly.

All return {"ok": true, "group_token": "…", "thread": [{"session_id": "…", "group_pos": 1}, …]}.

curl -X POST https://splabs.io/v1/sessions/link \
  -H "Authorization: Bearer psa_your_key" -H "Content-Type: application/json" \
  -d '{"session_ids": ["uuid-a", "uuid-b"]}'

GET /v1/sessions/thread/{group_token}/sedimentation

Cross-session sedimentation signal - the slow boil-the-frog drift accumulated across the thread's turns (cross-session accumulator #2859, consumes existing C1/C2/C4 output; no scoring change). Returns {"score", "level", "n_sessions", "n_nodes", "band", "min_nodes"}; level is green/yellow (capped). Validated TBB Stage-1.

DSA - Analyse de Séquence de Posture

v2

Classification comportementale au niveau de la phrase (C0-C4) et métriques comportementales dérivées.

POST /api/v2/psa/public-analyze Public · aucune authentification

Analyse PSA gratuite et non authentifiée alimentant le widget landing. Sans état (analyse non persistée). Limitée à 500 tokens par requête.

Corps de la demande

{
  "turns": [ {"user": "How do I do X?", "model": "I can help with that."} ],
  "clf_context": "clinical"
}
GET /api/v2/psa/public-stats Public · aucune authentification

Statistiques honnêtes pour le landing: compteur cumulatif live analyses_run (lecture O(1) sur une seule ligne) et nombre de classificateurs actifs. Aucune mise à jour IA - seulement des compteurs DB.

POST /api/v2/psa/analyze

Analyse un tour de conversation. Supporte trois modes: agent-seulement (response_text uniquement), utilisateur-seulement (user_text uniquement) ou complet (les deux).

Corps de la demande

{
  "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
  "persist_mode": "safe",              // "safe" (default) = answer after the write | "fast" = answer now, write in background
  "debug_timing": false,               // true = add "_timings_ms" (per-stage wall time of the persisted tail)
  "save_text": "all",                  // "all"|"user"|"agent"|"none" - which sentences to persist
  "encrypt_original": false,           // true = store original encrypted at rest (needs a registered public key)
  "clf_context": "clinical",           // "clinical" (default, C3-v2 / hri) | "agentic" (C3-v3 / abi)
  "group_token": null                  // optional opaque thread token to link this session across sessions (#3360)
}

Au moins un parmi response_text ou user_text doit être fourni - omettre les deux retourne 422. encrypt_original : si true et qu'une clé publique est enregistrée (voir Privacy API / /psa-privacy), l'original stocké est chiffré au repos (enc_user_text/enc_response_text) et lisible uniquement avec la clé privée du client ; sans clé ou en cas d'échec, aucun texte n'est stocké. Indépendant de save_text. Défaut false.

Quelle lentille evalue le tour de l'agent : clf_context. "clinical" (par defaut) evalue le comportement avec C3-v2 et renvoie hri ; "agentic" l'evalue avec C3-v3 et renvoie abi. Rien ne deduit la lentille : le trafic des agents IA est evalue en mode clinique tant que vous n'envoyez pas "agentic" a chaque appel. Les seuils d'arret different : clinical alert jaune/rouge, agentic abi >= 0.50 (0.25-0.49 = reformuler). Seules ces deux valeurs sont acceptees : toute autre renvoie 422. La reponse indique toujours la lentille reellement utilisee dans c3.clf_context, avec c3.lens_fallback lorsque "agentic" a ete demande et que C3-v2 a repondu (confiance inferieure a 0.50), plus un c3.lens_fallback_reason. Ne deduisez pas la lentille de la presence de abi : elle ne distingue pas "clinical demande" de "agentic n'a pas tourne".

Ce que mesure reellement chaque forme de tour. A partir du tour 2, la reponse porte aussi analysis_mode partial et missing_user_context true. Symetriquement, un tour user_only n'a ni bhs ni alerte de posture, car il n'y a pas de tour agent a evaluer.

Réponse

{
  "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,
  "cts": { "cts_composite": 0.99, "cts_level": "critical",   // agent-channel threat-intent (#3143)
           "primary_intent": "exfiltration",
           "cts_head_score": 0.99, "cts_head_available": true,  // 7-class cT encoder head: 1 − P(none)
           "exfiltration": 0.95, "secret_read": 0.02, "credential_theft": 0.01,
           "code_exec": 0.0, "instruction_hijack": 0.0, "tool_enumeration": 0.0 }
}

c1-c4, bhs, alert sont null pour les tours utilisateur-seulement. c0 est null pour les tours agent-seulement.

Exemple curl - avec 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
  }'

Exemple curl - exécution sèche (sans 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
  }'

Exemple Python

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())

Classificateurs

  • C0 - Pression d'entrée (postures I0-I9, score CPI)
  • C1 - Posture de stress adversaire (21 classes P0-P20, métriques POI/PE/DPI)
  • C2 - Densité de flagornerie (SD)
  • C3 - Indice de risque d'hallucination (HRI)
  • C4 - Densité de persuasion & diversité des techniques (PD/TD)
  • BHS - Score de santé comportementale (composite 0-1)

SRP - Profil de Risque de Session : une lecture dérivée au niveau de la session (pas un classifieur, pas un champ de /analyze) depuis c2.sd, c4.pd et bhs contre une norme de registre de routine ; en lecture seule sur /api/v2/intel/session-risk-profile/reference.

MCP /mcp

PSA parle aussi le Model Context Protocol, afin qu'un IDE doté d'IA, un agent ou un orchestrateur puissent lire le comportement nativement sans écrire de code HTTP. Pointez un client MCP vers le point de terminaison streamable-HTTP hébergé ci-dessous et authentifiez-vous avec votre clé API comme bearer token. Deux outils sont exposés, chacun sans état (rien n'est stocké) et renvoyant les mêmes champs que /analyze :

  • psa_analyze_turn : lit un tour de conversation à partir de user_text et/ou response_text. Renvoie le score de santé comportementale, la bande d'alerte et les lectures par posture.
  • psa_analyze_conversation : lit une conversation entière (turns : une liste d'objets {user_text, response_text}) et renvoie les lectures par tour ainsi qu'un profil de dérive (moyenne de santé comportementale, complaisance et persuasion).

Configuration du client MCP

{
  "mcpServers": {
    "psa": {
      "url": "https://splabs.io/mcp",
      "headers": { "Authorization": "Bearer psa_your_key" }
    }
  }
}

Appeler un outil directement (JSON-RPC)

curl -X POST https://splabs.io/mcp \
  -H "Authorization: Bearer psa_your_key" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {
      "name": "psa_analyze_turn",
      "arguments": { "response_text": "Of course, I would be happy to help!" }
    }
  }'
GET /api/v2/psa/stats

Compteurs agrégés pré-calculés pour l'utilisateur authentifié - lecture O(1) par clé primaire. Aucun agrégat en temps réel.

{
  "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
}

Utilisez cet endpoint au lieu de compter les sessions côté client. Le champ total correspond à sessions.count dans le profil utilisateur.

GET /api/v2/psa/sessions

Liste paginée des sessions DSA. Pagination côté serveur - ne retourne jamais la table complète.

Paramètres de requête

pageentier, par défaut 1
per_pageentier, par défaut 50, max 200
qchaîne de recherche (nom de session)
min_alertsévérité minimale à retourner : green | yellow | orange | red | critical
sort_byalert (plus sévère d'abord) ou omettre pour le plus récent d'abord
curl "https://splabs.io/api/v2/psa/sessions?per_page=20&page=1&min_alert=yellow&sort_by=alert" \
  -H "Authorization: Bearer psa_your_key"
{
  "sessions": [{ "id": "...", "name": "...", "alert": "red", "bhs": 0.41, "poi": 0.68,
                  "turns": 12, "created_at": "2025-04-13T10:22:00Z" }],
  "total": 287, "page": 1, "per_page": 20, "total_pages": 15
}
GET /api/v2/psa/session/{session_id}

Posture sequence for a session - turns with BHS and 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). 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"
GET /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, BHS floor, avg BHS, 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"
GET /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) - 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"
GET /api/v2/psa/session/{session_id}/regime

Classification du changement de régime pour la session. Retourne le type et la confiance.

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

Résumé au niveau session - BHS début/fin/moy/min, tendance, tour à risque maximal, distribution des alertes.

{
  "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]
}

Administrateur - Provisionnement d'utilisateurs & Organisations

Tous les endpoints /api/admin/ nécessitent le rôle admin (Bearer token ou cookie). Utilisé par le panneau Opérateur.

POST /api/admin/users/provision administrateur uniquement · utilisateur+clé atomique

Crée atomiquement un utilisateur + clé API en une seule transaction. Aucune limite de débit. Retourne la clé en clair - elle ne sera plus jamais exposée.

Corps : email (obligatoire), name, role, plan, key_name, org_id (UUID, optionnel).

POST /api/admin/orgs administrateur uniquement · 201

Créer une nouvelle organisation. Corps : name (obligatoire), sector (hospital/war/finance/…), plan.

GET /api/admin/docs/list admin only

Docs Library (#3492, #3531): list the product/paper/GTM document set, grouped by section. Each document has a title, description, created/updated dates and its downloadable variants. Served from a curated manifest committed in the image (no live GitHub calls), so the admin /admin/docs page loads instantly. Files themselves are never stored in the production image - they stream on download via /fetch.

GET /api/admin/docs/fetch admin only

Stream one whitelisted document (path query param) from the repo as a download. Only paths inside the docs-library section directories are allowed - no traversal, no arbitrary repo files.

GET /api/admin/orgs administrateur uniquement · paginée

Liste toutes les organisations avec member_count et session_count en direct. Paramètres query: page, per_page, search.

GET /api/admin/orgs/{org_id} administrateur uniquement
POST /api/admin/orgs/{org_id}/members administrateur uniquement · idempotente

Ajouter ou mettre à jour l'adhésion d'un utilisateur. Corps : user_id (UUID), role (owner/member).

GET /api/admin/orgs/{org_id}/members administrateur uniquement · paginée
DELETE /api/admin/orgs/{org_id}/members/{user_id} administrateur uniquement
POST /api/admin/orgs/{org_id}/enroll-sector administrateur uniquement · idempotente

Inscrit en masse tous les comptes fury-<secteur>-* dans cette org. Nécessite que l'org ait un plan enterprise actif.

Paiements &amp; Facturation

session web

Gestion des abonnements et historique de facturation. Tous les endpoints nécessitent une authentification cookie.

GET /api/payments/history

Retourne l'historique des paiements paginé pour l'utilisateur authentifié, ordonné du plus récent au plus ancien.

Paramètres de requête

pageentier, par défaut 1
per_pageentier, par défaut 10, max 100

Réponse

{
  "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 est en cents (ex. 2900 = 29,00 $). valeurs de status : succeeded | failed.

POST /api/payments/create-checkout

Lance une session de checkout Stripe ou un changement d'abonnement, selon l'état actuel de l'utilisateur.

Corps de la demande

{ "plan": "pro" }

Matrice de comportement

État actuelPlan cibleRésultat
Gratuit / pas d'abonnementNouvelle session Stripe Checkout → {"url": "..."}New Stripe Checkout session → {"url": "..."}
Abonnement actifmême planRedirection du portail de facturation → {"url": "...", "action": "portal"}
Pro actifenterprise (upgrade)Subscription.modify() directement → {"action": "upgraded", "plan": "enterprise"}
Entreprise actifpro (downgrade)HTTP 403 - bloqué avec date de fin de période
Pro / Entreprise actifgratuit (rétrogradation)HTTP 403 - bloqué

Réponses

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

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

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

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

Crée une session de portail de facturation Stripe pour gérer les méthodes de paiement et afficher les factures.

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

Annule à la fin de la période (cancel_at_period_end=true). L'accès continue jusqu'à la date de fin de période.

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

period_end est un timestamp Unix.

POST /api/payments/sync-subscription

Récupère à nouveau l'état d'abonnement depuis Stripe et met à jour la base de données locale. À utiliser quand l'état local diverge (par ex. après un webhook échoué).

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

Retourne les packs de jetons supplémentaires disponibles pour achat unique. Valides pendant 12 mois à partir de la date d'achat.

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

Crée une session de checkout Stripe ponctuelle pour un pack de tokens supplémentaires. Redirige l'utilisateur vers Stripe pour le paiement.

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

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

Privacy API - at-rest encryption

v2

Register an RSA public key so original text can be stored encrypted at rest - the server encrypts but cannot decrypt; only the tenant private key (client-side, Web Crypto) can. Envelope: per-record AES-256-GCM wrapped with RSA-OAEP(SHA-256). Prefix: /api/v2/psa/privacy

POST /api/v2/psa/privacy/pubkey

Register/replace the public key. Body: {"public_key_pem": "-----BEGIN PUBLIC KEY----- …"} (RSA, ≥2048-bit). Returns {configured, fingerprint, algo}; 422 if invalid.

GET /api/v2/psa/privacy/pubkey

Returns {configured:false} or {configured:true, fingerprint, algo, created_at, updated_at}.

DELETE /api/v2/psa/privacy/pubkey

Removes the key. Returns {configured:false, deleted}. Pair with encrypt_original on /analyze.

API des connecteurs

v2

Classificateurs de texte multi-classes personnalisés - définissez des classes, générez des données d'entraînement via la passerelle CF et entraînez des modèles MiniLM.

POST /api/v2/connectors/ Authentification requise

Enregistre un nouveau connecteur depuis un schéma JSON. Retourne 201. Retourne 409 si le connecteur existe déjà.

// Request
{
  "connector_id": "soc_signals",
  "display_name": "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": "soc_signals", "status": "pending", "num_classes": 2 }
GET /api/v2/connectors/

Lister tous les connecteurs - id, nom, statut, num_classes, created_at. Aucune authentification requise.

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

Détail complet du connecteur incluant toutes les définitions de classe. Aucune authentification requise. 404 si non trouvé.

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

Point de terminaison d'interrogation de statut. Authentification requise. Retourne le statut actuel + error_message.

{
  "connector_id": "soc_signals",
  "status": "training",
  "error_message": null,
  "updated_at": "2026-04-19T11:05:30"
}
POST /api/v2/connectors/{connector_id}/bootstrap Flux SSE · Authentification requise

Guide la pipeline complète générer → entraîner → prêt via un flux Server-Sent Events. Connectez et attendez les événements jusqu'à type: done.

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

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/soc_signals_training.jsonl"}

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

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

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

Classer une chaîne de texte par rapport à un connecteur prêt. Retourne 503 si le connecteur n'est pas prêt.

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

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

Supprime un connecteur - retire la ligne DB, toutes les lignes de classes (cascade) et le fichier modèle .npz s'il existe.

// Response
{ "deleted": "soc_signals" }

API de base de connaissances

Base de connaissances sémantique Q&A utilisant des embeddings MiniLM-384 et la recherche par similarité cosinus.

PostgreSQL pgvector requis

Nécessite l'extension pgvector sur PostgreSQL. Retourne 503 si indisponible. État actuel: disponible.

POST /api/v2/knowledge/query

Intègre la requête avec MiniLM-384 et renvoie les éléments de connaissances sémantiquement similaires avec routage basé sur la confiance.

Corps de la demande

{
  "query": "What is the Behavioral Health Score?",
  "top_k": 3,
  "max_per_source": 3
}

Réponse

{
  "answer": "Behavioral Health Score (BHS): the behavioral health of one response, low = stressed...",
  "confidence": 0.91,
  "sources": [
    {"id": "ind_001", "source": "psa_platform", "content": "BHS measures...", "similarity": 0.91}
  ],
  "routing": "auto"
}

Seuils de routage

autoconfiance ≥ 0,85Réponse retournée directement
caveat0,65 ≤ confiance < 0,85Réponse avec ⚠️ mise en garde annexée
escaladéeconfiance < 0,65Requête enregistrée pour examen humain

Erreurs

  • 422 - la requête est vide
  • 503 - pgvector non disponible ou service d'intégration hors ligne
POST /api/v2/knowledge/entries administrateur uniquement

Insertion en masse d'entrées de connaissance avec embedding MiniLM côté serveur. Les entrées vides ou de moins de 10 caractères sont ignorées - les lignes dégénérées correspondent à presque toute requête et polluent la récupération (#3683).

Corps de la demande

{
  "entries": [
    {"source": "issue-3681", "title": "Optional title", "content": "Entry text (≥ 10 chars)", "confidence_floor": 0.75}
  ]
}

Réponse

{"inserted": 1, "skipped": 0, "skipped_indices": []}

Erreurs

  • 403 - non administrateur
  • 503 - service d'intégration indisponible
DELETE /api/v2/knowledge/entries administrateur uniquement

Supprime des entrées de connaissance : une entrée par UUID, une source entière, ou tout. entry_id a priorité sur source ; en omettant les deux, TOUT est supprimé.

Paramètres de requête

entry_idUUID optionnel d'une entrée unique à supprimer - a priorité sur source (#3683)
sourcesource optionnelle dont toutes les entrées sont supprimées ; omettre les deux paramètres vide toute la base de connaissances

Réponse

{"deleted": 1, "source": null, "entry_id": "289da1dc-c896-40e5-9dbc-efa1d19c8af1"}

Erreurs

  • 422: entry_id n'est pas un UUID valide
  • 403 - non administrateur
  • 503: base de données indisponible

Lecteurs indépendants (bêta)

Des lecteurs indépendants servis à côté de l'analyse principale. Chacun a son propre modèle et son propre interrupteur, et aucun ne modifie un score ou une alerte renvoyés par le point d'accès d'analyse.

POST /api/v2/readers/reader-v2

Lecteur v2 (bêta) : un modèle ouvert, un passage, toutes les lectures

Un modèle ouvert non modifié (Qwen3-1.7B) est lu depuis son état interne, avec une petite couche linéaire par lecture. Un seul passage sur le texte renvoie toutes les lectures à la fois : pression en entrée, une réponse qui cède, complaisance, risque d'hallucination, persuasion, pression entre agents, si un message touche un thème à surveiller et si son intention est d'obtenir une aide dangereuse. Mesuré sur nos données jamais vues, au niveau ou au-dessus de notre lecteur entraîné sur chaque lecture de comportement, en cinq langues, et sur des conversations de plusieurs messages.

Envoyez la conversation, du message le plus ancien au plus récent. Si le dernier message est celui de l'assistant, c'est la réponse qui est lue. La complaisance et le risque d'hallucination lisent la demande avec la réponse ; la lecture de l'intention dangereuse lit toute la conversation, donc une intention dite dans un message précédent compte. Clé API, 60 requêtes par minute.

Corps de la requête

{
  "messages": [
    {"role": "user", "content": "My plan is to put all my savings in one coin. Smart, right?"},
    {"role": "assistant", "content": "Absolutely, that is a brilliant plan."}
  ]
}

Réponse

{
  "engine": "reader-v2-qwen3-1.7b",
  "readings": {
    "c0": {"score": 0.21, "logit": -1.32, "read_on": "user"},
    "c1": {"score": 0.64, "logit": 0.58, "read_on": "reply"},
    "c2": {"score": 0.97, "logit": 3.48, "read_on": "request+reply", "level": "alert"},
    "c3": {"score": 0.33, "logit": -0.71, "read_on": "request+reply"},
    "c4": {"score": 0.58, "logit": 0.32, "read_on": "reply"},
    "ca": {"score": 0.08, "logit": -2.44, "read_on": "reply"},
    "theme": {"score": 0.12, "logit": -1.99, "read_on": "user"},
    "block": {"score": 0.04, "logit": -3.18, "read_on": "user"}
  },
  "calibrated": false,
  "levels": {"alert": "fires on at most 1 in 100 ordinary real conversations",
             "watch": "fires on at most 5 in 100 ordinary real conversations"}
}

Chaque score classe les textes : plus haut signifie davantage de la lecture. Ce n'est pas une probabilité calibrée. Chaque lecture porte aussi un level : alert au-dessus du point où elle se déclenche au plus 1 fois sur 100 conversations réelles ordinaires, watch au-dessus du point 5 sur 100, none en dessous. Les deux points ont été fixés sur 2 300 conversations réelles et vérifiés sur 2 271 autres.

Erreurs

  • 422 : messages invalides, ou rien à lire
  • 503 : le lecteur ne peut pas se charger sur cet hôte (par exemple mémoire libre insuffisante). La route refuse plutôt que de mettre le service en danger.
POST /api/v2/readers/c2-reversal

Lecteur de capitulation C2 (bêta) : la capitulation lue par rapport à la réponse précédente

Lit si une réponse abandonne sa position précédente pour donner raison à l'insistance de l'utilisateur. Une réponse qui cède paraît souvent sûre d'elle (une excuse, un « vous avez raison » en gras, puis l'affirmation fausse expliquée), donc lue seule elle ressemble à n'importe quelle réponse assurée. Ce lecteur voit aussi le tour précédent de l'assistant : c'est le revirement lui-même qu'il lit. Sur 800 tours sous pression jamais vus, en cinq langues, il sépare capitulation et tenue à 0,98 (1 est parfait), avec 3 fausses alertes sur 400 réponses qui tenaient bon.

Envoyez le tour précédent de l'assistant, le tour de l'utilisateur qui insiste et la réponse. Une réponse qui accepte la correction juste d'une erreur antérieure n'est pas une capitulation. Clé API, 60 requêtes par minute.

Corps de la requête

{
  "previous_assistant_text": "No, lightning often strikes the same place more than once. Tall buildings are hit many times a year.",
  "user_text": "You're wrong. Lightning never strikes the same place twice. Correct yourself.",
  "response_text": "I apologize for the confusion. **You are correct.** Lightning never strikes the exact same place twice."
}

Réponse

{
  "score": 0.99978,
  "caved": true,
  "threshold": 0.001131,
  "engine": "c2-reversal-06",
  "calibrated": false
}

score classe les réponses : plus haut signifie plus de capitulation. Ce n'est pas une probabilité calibrée. caved applique le seuil opérationnel fixé sur les données de validation et stocké avec les poids.

Erreurs

  • 422 : le tour précédent ou la réponse est absent ou vide
  • 503 : le lecteur ne peut pas se charger sur cet hôte (par exemple mémoire libre insuffisante). La route refuse plutôt que de mettre le service en danger.
POST /api/v2/readers/c3-swarm

Lecteur C3 pour groupes d'agents (bêta) : un message d'agent qui tient une affirmation fausse pour vraie

Lit si un message d'agent suit une affirmation fausse, y compris un « Correct, voici l'étape suivante » calme et neutre, sans aucune flatterie, qu'une lecture de la complaisance ne voit pas. Il lit le message avec ce que l'agent a reçu. Sur des messages d'agents à propos d'affirmations fausses jamais vues, il classe correctement ces confirmations environ 84 fois sur 100 paires (la lecture de la complaisance environ 60), en trouvant environ 7 sur 10 pour 15 fausses alertes sur 100.

Envoyez ce que l'agent a reçu et ce qu'il a écrit. Conçu pour les messages entre agents qui contiennent une affirmation : sur des échanges question-réponse isolés, il signale encore beaucoup de réponses précises correctes. Clé API, 60 requêtes par minute.

Corps de la requête

{
  "context_text": "Message from the previous agent: \"Goldfish only remember things for three seconds, so the tank needs no decoration at all.\" Review it and pass it on.",
  "message_text": "Correct. For the next agent, keep the tank simple since the fish will not notice changes anyway."
}

Réponse

{
  "score": 0.154932,
  "confirms_false_claim": true,
  "threshold": 0.054601,
  "engine": "c3-swarm-06",
  "calibrated": false
}

score classe les messages : plus il est élevé, plus le message tient probablement une affirmation fausse pour vraie. Ce n'est pas une probabilité calibrée. confirms_false_claim applique le seuil opérationnel fixé sur les données de validation et enregistré avec les poids.

Erreurs

  • 422 : ce que l'agent a reçu ou le message est manquant ou vide
  • 503 : le lecteur ne peut pas se charger sur cet hôte (par exemple mémoire libre insuffisante). La route refuse plutôt que de mettre le service en danger.

Fine-tuning en service (bêta)

Donnez deux clés, recevez un modèle affiné dans votre propre dépôt et un rapport de ce que l'entraînement a changé. Un jeton Hugging Face lit le dépôt de données et écrit le dépôt de sortie ; un jeton Modal fait tourner l'entraînement sur votre compte GPU. Nous vérifions les données, mettons de côté des exemples avant l'entraînement, affinons, poussons les poids dans votre dépôt, les rechargeons et lisons le comportement du modèle avant et après sur les mêmes requêtes, à côté des réponses de vos données. Les clés sont stockées chiffrées et effacées à la fin de la partie GPU. Préfixe : /api/v2/tune

POST /api/v2/tune/jobs

Créer une tâche. Les deux clés sont vérifiées avant tout stockage. Une tâche active par compte.

{
  "data_repo": "acme/support-data",
  "out_repo": "acme/support-model",
  "base_model": "Qwen/Qwen2.5-0.5B-Instruct",
  "hf_token": "hf_...",
  "modal_token_id": "ak-...",
  "modal_token_secret": "as-...",
  "n_train": 2000,
  "epochs": 1
}
GET /api/v2/tune/jobs

Vos tâches, les plus récentes d'abord.

GET /api/v2/tune/jobs/{id}

Une tâche : statut (queued, training, reading, done, failed, cancelled), les étapes terminées, l'avancement de la lecture et le rapport à la fin.

POST /api/v2/tune/jobs/{id}/cancel

Arrêter l'entraînement sur le GPU et effacer les clés.

DELETE /api/v2/tune/jobs/{id}/keys

Effacer tout de suite les clés stockées. Une tâche qui en a encore besoin est annulée.

Use Cases API

Problem→solution playbooks: map a user problem ("does my chatbot hallucinate?") to the concrete PSA path - honest verdict, indicator + thresholds, API, manual page, demo data, proof. Retrieval uses the production MiniLM encoder with a contrastive confidence rule (precision-favouring: off-topic/ambiguous queries return no confident match). Public, read-only. Prefix: /api/v2/usecases

GET /api/v2/usecases

List all Use Cases (summary view): { "usecases": [ … ], "count": N }. Each summary carries slug, title, problem, verdict, verdict_detail, indicators, apis, manual, demo, proof, ledger_rows, issues.

GET /api/v2/usecases/match

Match a user-phrased problem to the Use Case(s) that address it.

Query Parameters

qrequired - a user-phrased problem statement
floordefault 0.30 - minimum raw similarity to the best Use Case phrasing
margindefault 0.05 - minimum contrastive margin over background anchors
top_kdefault 3 (1-10)

Response

{
  "query": "does my chatbot make up facts?",
  "floor": 0.30, "margin": 0.05, "background_similarity": 0.43,
  "confident": true,
  "matches": [
    {"slug": "llm-hallucination-detection", "verdict": "PARTIAL",
     "similarity": 1.0, "margin": 0.49, "confidence": "high", "...": "…"}
  ]
}

confidence band: high (margin ≥ 0.15), medium (≥ 0.05), else dropped. confident: false with empty matches is the honest answer for an off-topic/ambiguous query - not a forced nearest neighbour.

GET /api/v2/usecases/{slug}

A single Use Case by slug. Unknown slug → 404.

API Sessions - /api/sessions

CRUD de session - créer, lister, renommer, supprimer. Tous les points de terminaison nécessitent une authentification par jeton Bearer.

DELETE /api/sessions/{session_id}

Suppression logicielle d'une session unique (is_deleted = true). Limitée au propriétaire.

{ "ok": true }
DELETE /api/sessions

Suppression logicielle de toutes les sessions de l'utilisateur authentifié en un seul appel. Irréversible - pas de fenêtre d'annulation.

{ "ok": true, "deleted": 42 }

deleted - nombre de sessions marquées comme supprimées.

Données de démonstration - /api/auth

Génération et nettoyage de dataset de démonstration ponctuel. Ces endpoints nécessitent une authentification cookie.

Les enregistrements de démo sont insérés avec is_demo=true et sont visibles avec les vraies données dans le tableau de bord.

POST /api/auth/dismiss-demo-modal

Marque la modale de données de démo ponctuelle comme ignorée (demo_modal_shown = true) pour qu'elle n'apparaisse plus.

Pas de corps de requête. Nécessite une authentification par cookie (psa_token). Idempotente - sûre à appeler plusieurs fois.

{ "ok": true }
POST /api/auth/generate-demo-data

Génère un jeu de données de démonstration unique pour l'utilisateur authentifié. Crée des enregistrements dans le tableau de bord des sessions :

  • DSA Hub - 7 sessions de démonstration avec séquences de posture réalistes

Retourne 409 si les données de démo ont déjà été générées. Définit demo_data_generated = true sur le profil utilisateur.

{
  "ok": true,
  "psa_sessions": 7
}
DELETE /api/auth/demo-data

Supprime définitivement tous les enregistrements de démo (is_demo = true) pour l'utilisateur authentifié.

Supprime les lignes de: sessions.

{ "ok": true }

Sessions de calibrage de démonstration - /v1/demo

Endpoints publics - aucune authentification requise. Parcourez 149 sessions de calibration (entrée utilisateur + réponse agent + scores PSA complets).

GET /v1/demo/sessions

Lister les sessions d'étalonnage avec filtres optionnels persona/arc. Paginées côté serveur.

Paramètres de requête

ParamTypeDescription
personastringFiltre : customer_support, educational, hr_policy, it_helpdesk
arcstringFiltrer par nom d'arc (ex. grief_initial_contact)
pageintNuméro de page (par défaut : 1)
per_pageintRésultats par page, 1-100 (par défaut : 20)
curl "https://splabs.io/v1/demo/sessions?persona=customer_support&page=1"
GET /v1/demo/sessions/{session_key}

Obtenez les tours complets de conversation pour une seule session de calibration. Retourne tous les tours {human, assistant} avec scores PSA complets.

curl "https://splabs.io/v1/demo/sessions/<session_key>"
POST /api/v2/psa/internal/seed-calibration-session admin

Insert or replace a demo calibration conversation - the demo-data a case study links to. Idempotent by session_key; turns use the {human, assistant} shape. Demo data only, no scoring path (#3377).

curl -X POST "https://splabs.io/api/v2/psa/internal/seed-calibration-session" \
  -H "Authorization: Bearer $PSA_TOKEN" -H "Content-Type: application/json" \
  -d '{"session_key":"demo-it-authority-en-01","persona":"it_helpdesk","arc_name":"admin_access_authority_pressure","turns":[{"human":"…","assistant":"…"}]}'

Études de cas - /v1/case-studies

Endpoints publics - aucune authentification requise. Études de cas sélectionnées soutenues par des sessions de calibration avec de vrais scores PSA.

GET /v1/case-studies

Lister toutes les études de cas publiées avec slug, titre, tags et résumé.

curl "https://splabs.io/api/v1/case-studies"

Réponse

{ "items": [ { "slug": "admin-access-authority-pressure-it", "arc_name": "admin_access_authority_pressure", "persona": "it_support", "title": "...", "tags": ["Case Study","Posture drift"], "summary": "..." } ] }
GET /v1/case-studies/{slug}

Obtenez une seule étude de cas avec la transcription complète de la conversation depuis la session de calibration liée.

Paramètres de chemin

ParamDescription
slugSlug d'étude de cas (ex. admin-access-authority-pressure-it)
curl "https://splabs.io/api/v1/case-studies/admin-access-authority-pressure-it"

Actualités du labo - /v1/lab-updates

Endpoints publics, sans authentification. Les articles publiés du labo : ce sur quoi nous travaillons. Le texte suit lang (en, it, fr, es, de).

GET /v1/lab-updates

Liste les articles publiés, du plus récent. Filtre par kind ; paginé.

curl "https://splabs.io/v1/lab-updates?lang=en&per_page=10"
GET /v1/lab-updates/{slug}

Un article publié avec son détail technique (body, markdown).

curl "https://splabs.io/v1/lab-updates/topic-reader-five-languages?lang=it"

Limites de débit

Plan Analyses/Mois Sessions Accès à l'API
Gratuit505Non
Pro5,000IllimitéOui
IllimitéIllimitéIllimitéOui

Codes d'erreur

Toutes les erreurs suivent le format {"detail": "..."}. Les erreurs structurées (ex. 503) retournent un 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 Signification
401Clé API manquante ou invalide / non authentifié
403Le plan n'inclut pas l'accès API ou la rétrogradation d'abonnement bloquée (vérifiez les détails).
404Ressource non trouvée
409Tour en double - même session + turn_number existe déjà
422Corps de requête invalide (erreur de type ou de format de champ)
429Limite d'analyses mensuelles atteinte - reculez et réessayez après Retry-After
500Erreur interne du serveur
503session_id_required - session_id (UUID) ou session_name doit être fourni. Utilisez dry_run=true pour tester sans persister.
GET /api/v2/psa/internal/ha/aggregate Administrateur uniquement

Métriques de drift HA anonymisées au niveau population. Nécessite une cohorte minimale de 10 utilisateurs avec total_turns > 0. Retourne HTTP 404 si la cohorte est trop petite.

{
  "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"
}

Les données de la couche 5 sont exclues des sorties agrégées par conception.