KKmatch / KKOS / Developers

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:

The eleven dimensions:

communication_style
directness
openness
engagement
decision_style
uncertainty_response
reflection_style
conflict_style
trust_disclosure_pattern
emotional_expression
ai_interaction_style
One loop, not two products. conversation → model → matching → your correction → back into the model. The matching engine consumes these same eleven dimensions, which is why KKOS and relationship matching are the same pipeline rather than two unrelated features. See how the model is built.

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:

CredentialHeaderFormatWorks 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.
A model is always owned by a person. /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.

MethodPathPurpose
POST/mirror/startOpen a session, receive the greeting session token
POST/mirror/messageOne turn of conversation
POST/mirror/completeClose the session and build the 11-dim model
GET/mirror/model/{model_id}Retrieve a stored model
POST/mirror/model/{model_id}/regenerateRebuild from the stored transcript (v2+)
DELETE/mirror/model/{model_id}Delete a model, its challenges and its samples
POST/model/feedbackCorrect a single trait
POST/mirror/sttSpeech → text
POST/mirror/ttsText → speech
GET/mirror/voicesAvailable voices per provider
WS/mirror/eviFull-duplex realtime voice bridge
POST/challenge/createInvite someone to be modelled
GET/challenge/{challenge_id}Read a challenge
POST/challenge/{challenge_id}/acceptAccept a challenge
POST/mirror/compareDyadic comparison of two models
GET/meAggregated account data session token
GET/plansPublic plan catalogue
GET/POST/apikey/{status,mine,create,revoke}Self-serve key management session token

Start a session

POST /mirror/start

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

FieldTypeDefaultDescription
modestring"voice"One of video, voice, voice_only, text
consentobject{}Explicit consent flags — see the consent keys
experimentobject{}A/B assignment, e.g. {"ai_persona":"warm"}
utm, referrer, landingobject / string{}Attribution captured on first landing
ref_codestring""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. ..."
}
The greeting is written into the session before you send anything, so a client that renders reply immediately feels instant. session_id is the only identifier you need from here on.

Send a message

POST /mirror/message

Appends the person's turn and returns the assistant's next reply, plus the interaction signals detected in that utterance.

Request body

FieldTypeDescription
session_idstringRequired
messagestringThe person's utterance for this turn
input_modestringvoice or text
client_ts_msintClient clock at the moment speech started — used to measure turn latency from the speaker's side
voice_prosodyarrayPer-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

POST /mirror/complete

Closes the session and produces the eleven-dimension model. This is the expensive call: plan for a few seconds, not milliseconds.

Request body

FieldTypeDescription
session_idstringRequired
transcriptarray[{role, content}]. Omit to use the transcript the server already accumulated.
video_featuresobjectAggregated 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_seqarrayPer-sample face sequence: {t_ms, smile, blink, mouth, brow, face}
voice_prosodyarrayPer-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

GET /mirror/model/{model_id}

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.

POST /mirror/model/{model_id}/regenerate

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.

DELETE /mirror/model/{model_id}

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

POST /model/feedback

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

FieldTypeDescription
model_idstringRequired
traitstringOne of the eleven dimension keys
feedbackstringright, wrong or unsure
user_scoreintOptional — 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:

StepCallYou sendYou receive
1POST /mirror/startmode, consentsession_id, assistant greeting
2POST /mirror/sttAudio segment (optional)text
3POST /mirror/messagesession_id, messagereply, state, events
4POST /mirror/ttsthe reply textAudio URL
5POST /mirror/completesession_id, optional signalsmodel, redirect

Path B — realtime WebSocket

WS /mirror/evi?api_key=...&name=...&tone=...

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.

DirectionFramePayload
server → clientsession_settingsSent once on connect. Declares audio format {linear16, 16000 Hz, mono}.
client → serveraudio_input{"type":"audio_input","data":"<base64 PCM16>"}
server → clientaudio_outputbase64 audio in data
server → clientuser_message / assistant_messagemessage.content is a string, not an array
server → clientassistant_endEnd of the assistant's turn
server → clientprosodyScores under models.prosody.scores
On the realtime path the browser must not send audio before it has received 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

GET /mirror/voices

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", "..." ]
}
POST /mirror/tts
FieldTypeDescription
textstringRequired, max 2,000 characters
providerstringauto (default), kkos or edge. Leave it on auto so a provider outage degrades instead of failing.
voicestringVoice name for KKOS, voice id for Edge — the two namespaces are different. Empty means the provider default.
session_idstringOptional — 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

POST /mirror/stt
FieldTypeDescription
audio_b64stringRequired, base64 audio. Max 350,000 characters.
mimestringaudio/webm (default), ogg, wav, mp4 / m4a
session_idstringOptional

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.

If you send segmented audio, resend the container header. A browser 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

POST /challenge/create

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

GET /me

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" }
}
POST /account/merge

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

GET /plans

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:

curl https://kkmatch.com/health

A 200 means the host is reachable; a 401 on an /api/ path means the key was not accepted.

EndpointPurpose
GET /apikey/statusWhether the caller is signed in, whether a key exists, and the per-account ceiling
GET /apikey/mineList keys, masked. The plaintext is never returned again.
POST /apikey/createIssue a key. Body: {"name":"production"}. Returns 409 once the ceiling is reached.
POST /apikey/revokeRevoke by api_id. Immediate.
Keys are per-account, not per-model, and a revoked key stops working on the next request — there is no cache to wait out.

Quotas & limits

Limits are enforced where they are listed. Values below are the current production ones.

Request limits

LimitValueEnforced at
Audio payload per STT call350,000 base64 characters (~256 KB raw)/mirror/stt413 audio_too_large
Minimum audio length800 bytes raw/mirror/stt{"text":"", "reason":"too_short"}
Conversation history in contextlast 8 turns/mirror/message
Transcript read by the modellerfirst 2,000 characters/mirror/complete
TTS input2,000 characters/mirror/tts422
ChatTTS input500 characters/mirror/tts/chattts422
API keys per account5/apikey/create409 too_many_keys
Session token lifetime30 daysSign-in
Evidence per trait2 items (+1 uncertainty, +1 alternative)Model schema
Recommended next questions5Model 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.

KeyRequiredGrants
analysis_consentYesBehavioural analysis of the conversation
storage_consentYesStoring the model and its transcript
camera_consentMode-dependentVideo signals (facial features)
audio_consentMode-dependentMicrophone capture
research_consentNoUse of the de-identified sample for model research
share_consentNoPublishing 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:

StageProviderWhere it runsMetered
Conversation & modelling LLMDeepSeek-family model through an OpenAI-compatible gatewayUpstream, called server-sideYes — input and output tokens
Speech to textfaster-whisper (tiny.en), CPU int8On our own serverTime only, no API fee
Text to speechKKOS voice (expressive) with an Edge TTS fallbackKKOS voice is reached through a dedicated overseas egress; Edge is directTime only in the usage counter
Realtime voiceThird-party EVI, bridged through our WebSocketServer-side bridgeBy upstream session
Choosing a model. The modelling model is configured server-side by the operator — there is no per-request model override in the public API, so a given deployment returns stable, comparable results across calls. If you need a pinned LLM for compliance reasons, talk to us about a private deployment.

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

MetricDefinitionRange
scorePosition on one dimension, derived from the conversation0–100
confidenceThe model's own certainty for that dimension0–1
overall_confidenceAggregate certainty for the model as a whole0–1
versionGeneration counter. Regeneration increments it and keeps the previous entry in versions[].1, 2, …

How the model is validated

Report outputs, not inputs. Evaluation is defined in terms of what a finished, correctable model looks like. We do not publish accuracy claims against personality inventories, because the eleven dimensions are behavioural and conversational — they describe how someone talks and decides, not a clinical category. See the 11-dim model for the definitions themselves.

Errors

Standard HTTP status codes, JSON bodies throughout. The detail string is stable enough to branch on.

CodeMeaningTypical detail
200OK
400Bad requestempty_audio, invalid_base64, api_id_required
401UnauthorizedMissing or invalid API key, login_required
404Not foundsession_not_found, model_not_found, key_not_found
409Conflicttoo_many_keys
413Payload too largeaudio_too_large
422Validation errorField over its maximum length, or an invalid enum value
429Too many requestsUpstream rate limit. Retried server-side with backoff; surfaces only if all attempts fail.
500Server errorUnexpected failure on our side
503Unavailableapikey_store_unavailable, or an unconfigured payment provider
A request to an /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:

ProbeTells you
GET /healthThe API process is up
GET /mirror/tts/healthWhether the expressive voice path is reachable and configured
GET /mirror/evi/health?probe=1Whether 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

DateVersionChange
2026-09-12v1.6.1Documented 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-11v1.6.0Self-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-10v1.5.0Realtime voice bridge over WebSocket, plus TTS with an automatic fallback provider. Speech-to-text moved to a self-hosted model.
2026-09-09v1.4.5Six-field explicit consent on session creation. Sensitive-attribute compliance guard on model output.
2026-09-05v1.4.0/admin/stats, /admin/event-stats, /users, /events. Dual-person compatibility endpoint.
2026-09-04v1.0.0Initial 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.