Skip to main content

Authentication

Estuary supports trusted developer API keys and restricted end-user credentials. For shipped applications, keep the developer key on your backend and use client tokens. This page covers the existing trusted-key flow and its Socket.IO/REST authentication.

API Keys​

What They Are​

An API key is a secret token that identifies your account and grants access to the Estuary platform. Each key is scoped to a single account and can be used across all characters you own.

API keys follow the format est_ followed by a random string:

est_a1b2c3d4e5f6g7h8i9j0...

Generating Keys​

  1. Sign in to the Estuary Dashboard
  2. Navigate to Settings > API Keys
  3. Click Generate New Key
  4. Copy the key immediately -- it is only shown once
warning

Keep your API key secret. Do not commit it to source control or expose it in client-side code that end users can inspect. For native builds, load the key at runtime from a secure source rather than bundling it with the application.


Socket.IO Connection Handshake​

SDKs authenticate when connecting to the Socket.IO /sdk namespace. The authentication payload is sent as part of the namespace connection message.

Connection Flow​

ClientServerWebSocket connect(Engine.IO handshake)0 {sid, upgrades}40/sdk,{auth payload}(Socket.IO namespace connect)session_info(success)auth_error(failure)

Auth Payload​

The authentication payload is a JSON object with these fields:

FieldTypeRequiredDescription
api_keystringYesYour Estuary API key, or a session token (sst_...) issued by the Shares API
character_idstringYesUUID of the character to connect to
player_idstringYesUnique identifier for the end user
audio_sample_ratenumberYesPreferred sample rate (Hz) for TTS audio playback. Default: 24000. Use 24000 unless the platform has specific constraints (e.g., 16000 for hardware-locked devices).
enable_animationbooleanNoEXPERIMENTAL. Opt in to receive bot_animation blendshape frames for lipsync. Requires server-side feature support and audio_sample_rate: 16000. Default: false.

Example:

{
"api_key": "est_a1b2c3d4...",
"character_id": "550e8400-e29b-41d4-a716-446655440000",
"player_id": "player_abc123",
"audio_sample_rate": 24000
}

Session Info Response​

On successful authentication, the server emits a session_info event:

{
"session_id": "sid_abc123",
"conversation_id": "conv_def456",
"character_id": "550e8400-e29b-41d4-a716-446655440000",
"player_id": "player_abc123"
}
FieldDescription
session_idUnique identifier for this connection session
conversation_idPersistent conversation ID -- the same player+character pair always resumes the same conversation
character_idThe character this session is connected to
player_idThe player ID provided during authentication

Auth Errors​

If authentication fails, the server emits an auth_error event:

{
"error": "Invalid API key"
}

Common error causes:

ErrorCause
Invalid API keyThe API key is incorrect or revoked
Character not foundThe character_id does not exist or does not belong to this account
Missing required fieldA required auth field was not provided

After an auth_error, the connection is closed by the server. SDKs should not retry with the same credentials.

Session Rejection​

If a connection is rejected after authentication because a policy cap was hit, the server emits a session_rejected event and disconnects:

{
"reason": "concurrent_limit",
"cap": 5,
"share_token_id": "abc123"
}
FieldTypeDescription
reasonstringReason code. See below.
capnumberInformational -- the current limit.
share_token_idstringThe share token whose cap was hit.

reason codes​

CodeMeaning
concurrent_limitToo many active SDK sessions are open against this share token. Wait for another device to disconnect, or retry later.

General Errors​

The server emits a generic error event for unhandled server-side conditions:

{
"message": "Internal server error"
}

Connections may or may not be terminated after this event depending on severity.


REST API Authentication​

REST API requests authenticate using the X-API-Key header:

GET /api/v1/characters HTTP/1.1
Host: api.estuary-ai.com
X-API-Key: est_a1b2c3d4...
X-Estuary-Client: my-app/1.4.0

All REST endpoints require this header. Requests without a valid key receive a 401 Unauthorized response.

API Key Scopes​

A key carries scopes that decide which parts of the API it can reach. A scope is written <resource>:<action>, and write implies read for the same resource.

ScopeGrants access to
characters:read / characters:writeCharacters: read, create, update, delete, uploads, knowledge documents
conversations:read / conversations:writeConversations: conversation lists, messages, deletion
memories:read / memories:writeMemories: lists, timeline, stats, core facts, graph, search, and manual edits
generation:read / generation:writeCharacter generation: from an image, 3D model generation and status
shares:read / shares:writeShares: creating, listing and revoking share links
encounters:read / encounters:writeStarting an encounter between two characters

Rules worth knowing:

  • GET requests need :read. Anything that changes data needs :write.
  • Memory search needs only memories:read. It is a POST because the query travels in the request body, but it changes nothing, so a read-only key can call it.
  • Keys created before scopes existed hold every scope, so existing integrations are unaffected.

Creating a Scoped Key​

  1. Sign in to the Estuary Dashboard
  2. Navigate to Settings > API Keys
  3. Click Generate New Key and select the scopes the key needs
  4. Copy the key immediately, it is only shown once

:::tip Grant the narrowest set that works A key embedded in a shipped client should usually hold read scopes only. If it never writes memories, leave memories:write off, and a stolen key cannot rewrite what your characters remember. :::

Insufficient Scope​

A valid key that lacks the scope a route needs receives 403 Forbidden:

{
"detail": {
"error": "insufficient_scope",
"message": "This API key lacks the 'memories:write' scope",
"requiredScope": "memories:write"
}
}

The response also carries a WWW-Authenticate header naming the missing scope. An object that belongs to another account returns 404, not 403, so a missing resource and an inaccessible one are indistinguishable by design.

Client Identification​

Every Estuary SDK sends an X-Estuary-Client header on each REST request:

X-Estuary-Client: estuary-ts-sdk/0.9.0

The value is <name>/<version>, where the name is lowercase and the version is the package version. Estuary uses it to see which clients still call the older unversioned routes, which is what lets those routes be retired on real usage data instead of guesswork. See the Legacy migration guide.

If you call the REST API directly rather than through an SDK, the header is optional but recommended. Use your own application name and version:

curl https://api.estuary-ai.com/api/v1/characters \
-H "X-API-Key: est_your_api_key" \
-H "X-Estuary-Client: my-app/1.4.0"

The header applies to REST requests only. Socket.IO connections do not carry it, because browsers and Lens Studio cannot set headers on a WebSocket handshake. Never send it to a third-party host, such as the storage URL a character's model_url points at.


Player IDs​

The player_id is a string you choose to uniquely identify each end user of your application. It serves two purposes:

  1. Conversation persistence -- The same player_id + character_id pair always resumes the same conversation with full memory context.
  2. Memory isolation -- Each player has their own memory graph. The character remembers different things about different players.

Player IDs can be any string up to 128 characters. Common strategies:

  • Use your own user ID system (database ID, Firebase UID, etc.)
  • Generate a random UUID and persist it locally (PlayerPrefs in Unity, localStorage in web)
  • Use a device identifier for anonymous users
tip

If you change a player's ID, the character will treat them as a new person with no conversation history. Use consistent IDs to maintain memory continuity.


Next Steps​