Authentication
Unison auth in one page: email-OTP provisioning, API key scopes (brain:read, brain:write, brain:admin, brain:act-as), key lifecycle, headless machine auth, and actor delegation.
Every request carries Authorization: Bearer <token> where the token is an API key (usk_...). Only the provisioning endpoints are unauthenticated. The server is the only security boundary - clients never pre-check scopes.
Getting a key
Interactive (email OTP, no browser): unison auth login - enter your email; a new account gets a key immediately (unverified, usage-capped) plus an OTP that lifts the caps; an existing account gets a recovery OTP that mints a new key.
Raw HTTP:
| Endpoint | Body | Returns |
|---|---|---|
POST /v1/auth/provision | { email } | { apiKey, workspaceId, status, emailSent } - 409 email_registered if the email exists |
POST /v1/auth/verify | { email, code } | first time: verifies; repeat: mints a new key |
POST /v1/auth/request-key | { email } | recovery OTP (response never leaks whether the email is registered) |
CI / machines: export UNISON_TOKEN=usk_... - no interaction. Mint dedicated keys with unison auth keys create.
Scopes
| Scope | Unlocks |
|---|---|
brain:read | all GETs, key management, invitations |
brain:write | document write/delete/tag/share, entity upsert, fact record/correct/invalidate, link |
brain:admin | dedup review (merge/unmerge), job retry |
brain:act-as | actor delegation (below); only workspace owners/admins can mint keys carrying it |
A token lacking the required scope gets 403 forbidden. New keys may only request a subset of the minting caller's scopes.
Key management (brain:read)
GET /v1/auth/keys- list (never returns hashes)POST /v1/auth/keys- body{ name?, scopes?, workspaceId? }; token returned onceDELETE /v1/auth/keys/:id- revokeGET /v1/auth/whoami- identity, workspace, scopes, andactedAswhen delegation is active
Acting on behalf of end users
One service key serves many end users without separate accounts - the pattern mem0/Zep users will recognize:
const { token } = await client.keys.create({
name: "my-service",
scopes: ["brain:read", "brain:write", "brain:act-as"],
});
const u1 = serviceClient.withActor("user-001");
await u1.write({ path: "/private/notes/chat.md", bodyMd: "user said …" });
const u2 = serviceClient.withActor("user-002");
await u2.search("what did I say?"); // isolated from user-001Under the hood this sends X-Unison-Actor: <externalId> on each request; shadow users are auto-created and each actor gets an isolated /private/ namespace. CLI: --actor <id> or UNISON_ACTOR=<id>.
Invitations and workspaces
POST /v1/auth/invitations ({ email, role? }, roles admin|member|viewer) invites a teammate into your workspace; GET /v1/auth/workspaces lists your memberships. Provisioning with an invited email joins the inviting workspace automatically.