Sessions¶
A participant is pseudonymous. A session starts with the pilot's invite code and nothing about who
the person is, and the only way back to its records is the recovery credential shown once at the
start. This page is the /me/sessions routes and the token every other /me route carries.
Start a session¶
curl -s localhost:9000/me/sessions -H 'Content-Type: application/json' -d '{"invite":"pilot"}'
{
"participant": "prt-01M59GXQEBZPZHAR8CBN4P8TZR",
"recoveryCredential": "prt-01M59GXQEBZPZHAR8CBN4P8TZR~<32 random bytes, base64url>",
"session": "<token>",
"expiresAt": "2026-10-19T19:27:25.643027Z",
"notice": { "privacy": "…", "ai": "…" }
}
The invite code is one of VOICE_INVITE_CODES. It is checked at the edge and never stored; a session
started with no code, or one not among the pilot's, is refused with 403 and session.invite.invalid.
The reply is 201, and the participant's key is created in the secret store at the same time, so
every personal value this participant writes can be sealed from the first message.
The session token¶
Every /me route takes Authorization: Bearer <session>. The token is
base64url(participantId "." expiresAt) "." base64url(hmac-sha256) under VOICE_SESSION_KEY,
valid for VOICE_SESSION_HOURS, twelve by default, and verified in the endpoint with no I/O. It is
not a JWT and never reaches an OIDC issuer. A route called without one answers 401 and
session.required.
POST /me/sessions/renew with an empty body answers a fresh token and expiry for the same
participant. Starting, recovering and renewing a session each reset the participant's dormancy timer.
The recovery credential¶
The credential is the one secret that lets a participant return. It begins with the participant's id, so recovery needs no lookup, and Voice keeps only its hash. It is sent to the two session routes and nowhere else, never as a bearer header.
curl -s localhost:9000/me/sessions/recover -H 'Content-Type: application/json' \
-d '{"recoveryCredential":"prt-01M59GXQEBZPZHAR8CBN4P8TZR~…"}'
{ "participant": "prt-01M59GXQEBZPZHAR8CBN4P8TZR", "recoveryCredential": "prt-…~<a fresh one>", "session": "<token>", "expiresAt": "…" }
Presenting the credential rotates it: the reply carries a fresh one, and the old is no longer
accepted. A credential that does not match answers 401 and participant.credential.invalid,
which says nothing about whether the participant exists. A participant without their credential
starts afresh, with a new id and nothing of the old records; Voice cannot recover it for them.
The notices¶
GET /me/notice answers the privacy notice and the AI notice as Markdown, and the session reply
carries both, so an interface shows them before the first message. The privacy notice says who the
participant is to Voice, what stays private, what they can share and how, how AI is used, and what
erasure does; the AI notice is its one section on the model, shown on its own in the conversation.
After erasure¶
Once a participant's erasure has completed, every /me route for their token, and any later
recovery, answers 410 and participant.erased. Erasure.