Build with KKOS
One conversation in — a structured, correctable, reproducible model of a person out. Eleven trait dimensions, a confidence value and the evidence behind every score.
Introduction
KKOS is the modelling engine behind KKmatch. It turns a short spoken or typed conversation into a behavioural profile — not a personality quiz result, and not free-form text. Three properties shape every endpoint:
- Structured. The output is a fixed schema: eleven trait dimensions, each with a
score(0–100), aconfidence(0–1) and theevidenceit was derived from. - Correctable. The person can overturn any trait through trait feedback. Corrections are stored as strong labels next to the model's own weak labels — that pair is the training signal.
- Reproducible. The transcript is stored with the model, so
/mirror/model/{model_id}/regeneraterebuilds it from the same input andversions[]keeps every earlier snapshot.
The eleven dimensions:
All endpoints are RESTful and return JSON. Current model generation: hm-v1. Status: v1 stable
Base URL
https://kkmatch.com/api/v1
Everything is served over HTTPS. There is no separate sandbox host — use the free plan for integration testing.
Authentication
There are two kinds of credential, and they are not interchangeable:
| Credential | Header | Format | Works for |
|---|---|---|---|
| Session token | Authorization: Bearer <token> |
HMAC-signed token, 30-day TTL | Everything, including creating a KKOS session. Issued by POST /auth/login-code or POST /auth/google. |
| API key | X-API-Key |
kk_live_<32 hex> |
Server-to-server reads and the non-session endpoints. Self-served from your account page. |
/mirror/start therefore requires a user session, not merely an API key: called with X-API-Key alone it returns 401 login_required. If you are integrating on behalf of your own users, sign them in first and call the API with their Bearer token. API keys are for reading models and for the analytics endpoints.
Every non-public /api/* path requires one of the two credentials. A request with neither gets:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: ApiKey realm="KK Match API"
{
"detail": "Missing or invalid API key",
"hint": "Registered users: get a free API key at /kkos-ai.html (API Access). Then send it as X-API-Key.",
"docs": "https://kkmatch.com/docs"
}
Quick start
Three calls take you from an empty session to a finished model. Replace $TOKEN with a session token from /auth/login-code.
# 1 — open a session (the assistant greets first)
curl -X POST https://kkmatch.com/api/v1/mirror/start \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"mode":"text","consent":{"analysis_consent":true,"storage_consent":true}}'
# 2 — send the person's replies, one call per turn
curl -X POST https://kkmatch.com/api/v1/mirror/message \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"session_id":"<sid>","message":"I usually decide fast, then adjust."}'
# 3 — close the session and build the model
curl -X POST https://kkmatch.com/api/v1/mirror/complete \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"session_id":"<sid>","transcript":[{"role":"user","content":"..."}]}'
A healthy /complete response carries the whole model object plus the canonical redirect the product uses:
{
"session_id": "b6f2...",
"model_id": "8c1a...",
"redirect": "/mirror-result.html?model=8c1a...",
"model": {
"model_id": "8c1a...",
"model_version": "hm-v1",
"version": 1,
"overall_confidence": 0.6,
"traits": [
{ "name": "directness", "score": 78, "confidence": 0.7,
"evidence": ["I usually decide fast, then adjust."],
"uncertainty": [], "alternative_explanations": [] }
],
"summary": "...",
"communication_manual": "...",
"recommended_next_questions": ["..."]
}
}
If the modelling LLM cannot be parsed, the endpoint retries with a compact prompt before falling back — the response still contains all eleven traits so your client never has to handle a missing field.
Endpoint overview
Everything under /api/v1. session token marks endpoints that need a signed-in user.
| Method | Path | Purpose |
|---|---|---|
| POST | /mirror/start | Open a session, receive the greeting session token |
| POST | /mirror/message | One turn of conversation |
| POST | /mirror/complete | Close the session and build the 11-dim model |
| GET | /mirror/model/{model_id} | Retrieve a stored model |
| POST | /mirror/model/{model_id}/regenerate | Rebuild from the stored transcript (v2+) |
| DELETE | /mirror/model/{model_id} | Delete a model, its challenges and its samples |
| POST | /model/feedback | Correct a single trait |
| POST | /mirror/stt | Speech → text |
| POST | /mirror/tts | Text → speech |
| GET | /mirror/voices | Available voices per provider |
| WS | /mirror/evi | Full-duplex realtime voice bridge |
| POST | /challenge/create | Invite someone to be modelled |
| GET | /challenge/{challenge_id} | Read a challenge |
| POST | /challenge/{challenge_id}/accept | Accept a challenge |
| POST | /mirror/compare | Dyadic comparison of two models |
| GET | /me | Aggregated account data session token |
| GET | /plans | Public plan catalogue |
| GET/POST | /apikey/{status,mine,create,revoke} | Self-serve key management session token |
Start a session
Creates a session and returns the assistant's opening line. The first turn is always the assistant's — KKOS opens with an open question rather than a form.
Request body
| Field | Type | Default | Description |
|---|---|---|---|
mode | string | "voice" | One of video, voice, voice_only, text |
consent | object | {} | Explicit consent flags — see the consent keys |
experiment | object | {} | A/B assignment, e.g. {"ai_persona":"warm"} |
utm, referrer, landing | object / string | {} | Attribution captured on first landing |
ref_code | string | "" | Referral code. The creator it belongs to is resolved server-side and cannot be forged by the client. |
Response (200)
{
"session_id": "b6f2c1d0-...",
"mode": "text",
"ai_state": "greeting",
"reply": "Hi, I'm Human Mirror. I'm not here to test you — just to talk. ..."
}
reply immediately feels instant. session_id is the only identifier you need from here on.
Send a message
Appends the person's turn and returns the assistant's next reply, plus the interaction signals detected in that utterance.
Request body
| Field | Type | Description |
|---|---|---|
session_id | string | Required |
message | string | The person's utterance for this turn |
input_mode | string | voice or text |
client_ts_ms | int | Client clock at the moment speech started — used to measure turn latency from the speaker's side |
voice_prosody | array | Per-second prosody captured locally: {t_ms, energy, voiced, f0_approx, silence} |
Response (200)
{
"session_id": "b6f2c1d0-...",
"reply": "That's a distinctive way to describe it — ...",
"state": "engaged",
"events": ["question_answered", "emotional_shift"]
}
events are real-time interaction signals detected in the utterance itself (for example question_answered, emotional_shift). They are accumulated on the session and travel into the training sample for that session.
Build the model
Closes the session and produces the eleven-dimension model. This is the expensive call: plan for a few seconds, not milliseconds.
Request body
| Field | Type | Description |
|---|---|---|
session_id | string | Required |
transcript | array | [{role, content}]. Omit to use the transcript the server already accumulated. |
video_features | object | Aggregated facial signals, if you collected video: face_presence_ratio, avg_smile, smile_variance, blink_rate_per_min, avg_mouth_open, avg_brow_furrow, frames_analyzed, duration_sec |
video_signal_seq | array | Per-sample face sequence: {t_ms, smile, blink, mouth, brow, face} |
voice_prosody | array | Per-second prosody for offline training |
When video signals are present they are blended with the spoken content, with the strongest influence on emotional_expression, engagement and uncertainty_response.
Each trait object in the response can also carry uncertainty and alternative_explanations (at most one entry each) — the model states where it is unsure instead of hiding it. A compliance guard removes any language that would diagnose a medical or mental condition or infer a protected attribute.
Retrieve a model
Returns the stored model: traits, summary, communication manual, recommended next questions, version history and any corrections recorded so far. 404 model_not_found if the id does not exist.
Re-runs the modeller against the transcript stored with the model, producing a new version instead of mutating the old one. Use it to compare generations or to pick up a model upgrade.
Deletes the model together with its challenges and training samples. Deletion is cascading — there is no soft-delete to clean up later.
Correct a trait
The corrective half of the loop. When a person says “that's not me”, you record it here and the model stops being the final word.
Request body
| Field | Type | Description |
|---|---|---|
model_id | string | Required |
trait | string | One of the eleven dimension keys |
feedback | string | right, wrong or unsure |
user_score | int | Optional — the person's own 0–100 for that dimension |
The correction is written both to the feedback log and onto the trait, so the next regeneration can take it into account, and it is included in that session's training sample as a strong label.
Message protocol
Two protocols exist because the product has two conversational paths. Pick by latency need, not by feature set — both end in the same model.
Path A — turn-based REST
Simplest to integrate, works from any language, and is what the three-call quick start uses. You control turn boundaries:
| Step | Call | You send | You receive |
|---|---|---|---|
| 1 | POST /mirror/start | mode, consent | session_id, assistant greeting |
| 2 | POST /mirror/stt | Audio segment (optional) | text |
| 3 | POST /mirror/message | session_id, message | reply, state, events |
| 4 | POST /mirror/tts | the reply text | Audio URL |
| 5 | POST /mirror/complete | session_id, optional signals | model, redirect |
Path B — realtime WebSocket
Full-duplex voice. The browser never talks to the upstream voice model directly: our server holds that connection, sends session_settings first, and then relays text and binary frames in both directions.
| Direction | Frame | Payload |
|---|---|---|
| server → client | session_settings | Sent once on connect. Declares audio format {linear16, 16000 Hz, mono}. |
| client → server | audio_input | {"type":"audio_input","data":"<base64 PCM16>"} |
| server → client | audio_output | base64 audio in data |
| server → client | user_message / assistant_message | message.content is a string, not an array |
| server → client | assistant_end | End of the assistant's turn |
| server → client | prosody | Scores under models.prosody.scores |
session_settings — the upstream session is not configured yet and the frames are dropped. If the socket drops and reconnects, session_settings must be sent again.
Both paths accumulate into the same transcript, so a session that started in realtime voice can be completed through the REST endpoint and produces one model with one version history.
Voices & TTS
Lists what is actually available right now. The response reports which providers are live rather than promising a fixed catalogue.
{
"kkos_enabled": true,
"chattts_enabled": false,
"default_provider": "kkos",
"default_kkos_voice": "Colton Rivers",
"kkos": [ { "id": "...", "name": "Colton Rivers" } ],
"edge": [ { "id": "en-US-AriaNeural", "name": "Aria (US Female)" }, "..." ],
"chattts_styles": [ "warm_narrator", "professional", "casual", "..." ]
}
| Field | Type | Description |
|---|---|---|
text | string | Required, max 2,000 characters |
provider | string | auto (default), kkos or edge. Leave it on auto so a provider outage degrades instead of failing. |
voice | string | Voice name for KKOS, voice id for Edge — the two namespaces are different. Empty means the provider default. |
session_id | string | Optional — attributes the synthesis cost to that session |
Health probes: GET /mirror/tts/health and GET /mirror/evi/health?probe=1. Both report reachability and configuration state without ever returning a credential.
Speech-to-text
| Field | Type | Description |
|---|---|---|
audio_b64 | string | Required, base64 audio. Max 350,000 characters. |
mime | string | audio/webm (default), ogg, wav, mp4 / m4a |
session_id | string | Optional |
Response: {"text": "..."}. Audio shorter than 800 bytes returns {"text":"", "reason":"too_short"} rather than an error, so you can stream aggressively and discard the empty results.
MediaRecorder puts the initialisation segment (the WebM header) only in the first chunk of each recorder instance; every later chunk is a bare cluster. Sending a header-less cluster produces a container parse failure and a permanently empty transcript. Keep a copy of the first chunk and prepend it to every subsequent upload.
Share & Challenge
Creates a challenge from an existing model and returns a link another person can open to be modelled themselves.
curl -X POST https://kkmatch.com/api/v1/challenge/create \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"session_id":"<sid>","model_id":"<model_id>"}'
Then GET /challenge/{challenge_id} to read it and POST /challenge/{challenge_id}/accept when the invited person finishes their own session. Once both sides exist, POST /mirror/compare produces the dyadic view of the two models.
Account & usage
One call for the whole account centre: the chain user_id → models[] → challenges[] / matches → subscription / api_keys collapsed into a single response. Requires a session token.
{
"ok": true,
"user": { "user_id": "...", "email": "...", "is_admin": false },
"stats": { "models_count": 3, "deep_unlocked_count": 1,
"challenges_count": 2, "matches_count": 1,
"api_keys_count": 1 },
"models": [ { "model_id": "...", "version": 2, "overall_confidence": 0.6,
"deep_unlocked": true, "shared": false } ],
"challenges": [ ... ],
"matches": [ ... ],
"subscription": { "tier": "pro", "status": "active" }
}
Remaps models created before sign-in onto the account, so a person does not lose the model they built anonymously. Body: {"from_user_id":"<anonymous id>"}. Challenges follow their models automatically.
Plans & billing
Public, unauthenticated. The same response drives the pricing page, so you never have to hard-code prices.
{
"currency": "USD",
"plans": [
{ "id": "free", "name": "Free", "category": "individual",
"price_monthly_usd": 0, "price_yearly_usd": 0,
"conversations_included": 30, "overage_price_usd": null,
"seats": 1, "highlight": false, "features": ["..."] }
]
}
Paid plans are billed in USD through PayPal, monthly or yearly. Per-conversation economics — what a plan includes and what a conversation actually costs to serve — are laid out on the pricing page.
Get an API key
Keys are self-served, not email-gated. Sign in, open your account page, and issue one:
Sign in
Use an email code or Google sign-in. The same account that owns your models owns your keys.
Create the key
POST /api/v1/apikey/create (or the button in the account page) returns a key once, in the form kk_live_<32 hex>. Copy it immediately — only a SHA-256 digest is stored, so a lost key can only be revoked, never recovered.
Send it
Attach it as the X-API-Key header on every request. Verify the wiring against a public endpoint first:
A 200 means the host is reachable; a 401 on an /api/ path means the key was not accepted.
| Endpoint | Purpose |
|---|---|
GET /apikey/status | Whether the caller is signed in, whether a key exists, and the per-account ceiling |
GET /apikey/mine | List keys, masked. The plaintext is never returned again. |
POST /apikey/create | Issue a key. Body: {"name":"production"}. Returns 409 once the ceiling is reached. |
POST /apikey/revoke | Revoke by api_id. Immediate. |
Quotas & limits
Limits are enforced where they are listed. Values below are the current production ones.
Request limits
| Limit | Value | Enforced at |
|---|---|---|
| Audio payload per STT call | 350,000 base64 characters (~256 KB raw) | /mirror/stt → 413 audio_too_large |
| Minimum audio length | 800 bytes raw | /mirror/stt → {"text":"", "reason":"too_short"} |
| Conversation history in context | last 8 turns | /mirror/message |
| Transcript read by the modeller | first 2,000 characters | /mirror/complete |
| TTS input | 2,000 characters | /mirror/tts → 422 |
| ChatTTS input | 500 characters | /mirror/tts/chattts → 422 |
| API keys per account | 5 | /apikey/create → 409 too_many_keys |
| Session token lifetime | 30 days | Sign-in |
| Evidence per trait | 2 items (+1 uncertainty, +1 alternative) | Model schema |
| Recommended next questions | 5 | Model schema |
Consent keys
/mirror/start accepts six explicit consent flags: two are required, two depend on the input mode you choose, and two are optional.
| Key | Required | Grants |
|---|---|---|
analysis_consent | Yes | Behavioural analysis of the conversation |
storage_consent | Yes | Storing the model and its transcript |
camera_consent | Mode-dependent | Video signals (facial features) |
audio_consent | Mode-dependent | Microphone capture |
research_consent | No | Use of the de-identified sample for model research |
share_consent | No | Publishing a share card and issuing challenges |
See Trust & Privacy for what each choice does to the stored data.
Plan quotas
Conversation volume is a billing quota rather than a rate limit. Current monthly allowances are 30 (Free), 300 (Plus), 1,000 (Pro), 2,000 (Team, shared across 5 seats) and 10,000 (Business, shared across 20 seats). Overages are quoted per conversation on the pricing page. The live numbers always come from GET /plans.
Model providers
KKOS is a pipeline of three components. Knowing where each one runs tells you what is metered and what can fail:
| Stage | Provider | Where it runs | Metered |
|---|---|---|---|
| Conversation & modelling LLM | DeepSeek-family model through an OpenAI-compatible gateway | Upstream, called server-side | Yes — input and output tokens |
| Speech to text | faster-whisper (tiny.en), CPU int8 | On our own server | Time only, no API fee |
| Text to speech | KKOS voice (expressive) with an Edge TTS fallback | KKOS voice is reached through a dedicated overseas egress; Edge is direct | Time only in the usage counter |
| Realtime voice | Third-party EVI, bridged through our WebSocket | Server-side bridge | By upstream session |
The gateway is called with retry-on-429 backoff, up to four attempts. Reasoning models bill reasoning and content against the same token budget, which is why the modeller is given a generous output budget and instructed to keep evidence arrays short — a truncated JSON payload would otherwise fall back instead of returning a real model.
Metrics & evaluation
Numbers on our marketing pages are product illustrations, not live statistics. This section defines what we actually measure, so you can tell the difference.
What a model reports
| Metric | Definition | Range |
|---|---|---|
score | Position on one dimension, derived from the conversation | 0–100 |
confidence | The model's own certainty for that dimension | 0–1 |
overall_confidence | Aggregate certainty for the model as a whole | 0–1 |
version | Generation counter. Regeneration increments it and keeps the previous entry in versions[]. | 1, 2, … |
How the model is validated
- Schema conformance. Every response must contain all eleven dimensions with numeric score and confidence. Violations are recorded on the stored model as
schema_violationsrather than silently dropped — you can audit how often a generation was repaired. - Correction rate. The share of traits a person marks
wrongthrough trait feedback is the primary quality signal, because it is the one number the model cannot write itself. - Inter-version stability. Regenerating from the same transcript should not move scores wildly; large deltas between
versions[]are treated as a regression, not as new insight. - Strong < weak labels. Each session ships as one training sample with weak labels (model-produced traits) and, later, strong labels (user corrections). The pair is the training signal; samples live in a JSONL log with the turn timeline, prosody trace and video signal sequence attached.
Errors
Standard HTTP status codes, JSON bodies throughout. The detail string is stable enough to branch on.
| Code | Meaning | Typical detail |
|---|---|---|
200 | OK | — |
400 | Bad request | empty_audio, invalid_base64, api_id_required |
401 | Unauthorized | Missing or invalid API key, login_required |
404 | Not found | session_not_found, model_not_found, key_not_found |
409 | Conflict | too_many_keys |
413 | Payload too large | audio_too_large |
422 | Validation error | Field over its maximum length, or an invalid enum value |
429 | Too many requests | Upstream rate limit. Retried server-side with backoff; surfaces only if all attempts fail. |
500 | Server error | Unexpected failure on our side |
503 | Unavailable | apikey_store_unavailable, or an unconfigured payment provider |
/api/ path that does not exist returns 401, not 404: authentication runs before routing. If you get 401 on a URL you believe is correct, check the credential before checking the path.
Server status
Three probes tell you where a failure is:
| Probe | Tells you |
|---|---|
GET /health | The API process is up |
GET /mirror/tts/health | Whether the expressive voice path is reachable and configured |
GET /mirror/evi/health?probe=1 | Whether the realtime voice bridge can reach upstream (a passive check only reports configuration) |
There is no email-based status page yet. During an incident, degraded voice falls back to Edge TTS automatically when provider is left on auto — the modelling path itself has no fallback and will report an error instead of returning a fabricated model.
Changelog
| Date | Version | Change |
|---|---|---|
| 2026-09-12 | v1.6.1 | Documented the eleven KKOS dimensions, the two-credential model, quotas, the message protocol and the evaluation methodology. Split the KKOS surface into overview, model, trust and build pages. |
| 2026-09-11 | v1.6.0 | Self-serve API keys (/apikey/*, five per account, SHA-256 at rest). Aggregated GET /me and POST /account/merge. Sessions became login-gated. Added PayPal subscription and one-off webhooks. |
| 2026-09-10 | v1.5.0 | Realtime voice bridge over WebSocket, plus TTS with an automatic fallback provider. Speech-to-text moved to a self-hosted model. |
| 2026-09-09 | v1.4.5 | Six-field explicit consent on session creation. Sensitive-attribute compliance guard on model output. |
| 2026-09-05 | v1.4.0 | /admin/stats, /admin/event-stats, /users, /events. Dual-person compatibility endpoint. |
| 2026-09-04 | v1.0.0 | Initial public release. |
Need help?
Email support@kkmatch.com for technical questions, quota increases or enterprise contracts. For sales and private deployments, use the contact form.