TypeScript SDK
A TypeScript SDK is available at client/packages/sdk/ on the public mirror (package name @echolabs/multiverse-echoes). The package is not yet on npm, so external use requires cloning the public repo and importing source. Direct API calls against the endpoints documented below remain a fully supported alternative.
Installation
Clone the public mirror and import the SDK source:
git clone https://github.com/echolabs-me/multiverse-echoes-client.git
# Copy the SDK directory into your project
cp -r multiverse-echoes-client/packages/sdk vendor/multiverse-echoes-sdk
Source layout (client/packages/sdk/src/):
client.ts — typed REST client
websocket.ts — WebSocket subscription helpers
types.ts — Specta-generated DTOs mirroring the engine's Rust types
index.ts — barrel export
Quick Start (direct API)
The API mounts at the root of api.echolabsme.com — there is no /api/v1/ URL prefix. Canonical paths are /auth/login, /echoes, /feeds/personal, etc.
const baseUrl = 'https://api.echolabsme.com';
// Step 1: log in to get a JWT access token
const session = await fetch(`${baseUrl}/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: '[email protected]', password: 'YOUR_PASSWORD' }),
}).then((r) => r.json());
// Response: { access_token, refresh_token, expires_in }
// Step 2: fetch your Echoes (response is a bare EchoResponse[] array)
const echoes = await fetch(`${baseUrl}/echoes`, {
headers: { Authorization: `Bearer ${session.access_token}` },
}).then((r) => r.json());
console.log(`You have ${echoes.length} Echo(es)`);
Authentication
JWT is the only authentication path the API surface accepts today. API keys are creatable via POST /account/me/api-keys for tier-gated key inventory but are not currently accepted as bearer tokens by the API.
Token lifecycle
- Register or log in —
POST /auth/register or POST /auth/login. Both return { access_token, refresh_token, expires_in }.
- Use the access token — pass as
Authorization: Bearer <jwt> on every authenticated request.
- Refresh when needed — on
401 for an expired token, call POST /auth/refresh with { refresh_token }. The response contains a new access_token AND a new refresh_token (rotation per RFC 6819 §5.2.2.3 — discard the old one).
- Log out —
POST /auth/logout revokes the current access token (the JTI is added to the in-memory blocklist).
const baseUrl = 'https://api.echolabsme.com';
// Login
const { access_token, refresh_token } = await fetch(`${baseUrl}/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: '[email protected]', password: 'YOUR_PASSWORD' }),
}).then((r) => r.json());
// Use it
const echoes = await fetch(`${baseUrl}/echoes`, {
headers: { Authorization: `Bearer ${access_token}` },
}).then((r) => r.json());
// Refresh
const refreshed = await fetch(`${baseUrl}/auth/refresh`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refresh_token }),
}).then((r) => r.json());
// Persist refreshed.refresh_token; the old one is now invalid.
Available Endpoints
The OpenAPI spec at api-reference.md (rendered from a build-time snapshot of https://api.echolabsme.com/api/docs/openapi.json) is the source of truth for parameters, request bodies, and response shapes. The table below enumerates every external-facing endpoint on the live API surface.
Auth
| Method |
Path |
Notes |
| POST |
/auth/register |
Returns {access_token, refresh_token, expires_in, user_id, ...} (auto-login). |
| POST |
/auth/login |
Returns {access_token, refresh_token, expires_in}. |
| POST |
/auth/refresh |
Rotates refresh token. |
| POST |
/auth/logout |
Revokes current access token. |
| POST |
/auth/revoke-all |
Revokes all refresh tokens for the user. |
| POST |
/auth/password-reset |
Request password-reset email. |
| POST |
/auth/password-reset/confirm |
Confirm reset with token. |
| GET |
/auth/verify-email |
Email-verification callback. |
Account
| Method |
Path |
Notes |
| GET |
/account/me |
Current user profile. |
| PATCH |
/account/me |
Update display name, locale, etc. |
| DELETE |
/account/me |
GDPR delete request (grace period). |
| POST |
/account/me/delete |
Initiate scheduled deletion. |
| POST |
/account/me/delete/cancel |
Cancel during grace period. |
| GET |
/account/me/profile |
Public profile fields. |
| PATCH |
/account/me/profile |
Update bio, avatar, etc. |
| POST |
/account/me/profile/avatar |
Upload avatar image. |
| GET |
/account/me/privacy |
Privacy preferences. |
| PATCH |
/account/me/privacy |
Update visibility, search-indexability. |
| GET |
/account/me/consents |
Current consent grants. |
| POST |
/account/me/consents/withdraw |
GDPR consent withdrawal. |
| POST |
/account/me/restrict-processing |
GDPR right-to-restrict. |
| POST |
/account/me/unrestrict-processing |
Reverse restriction. |
| GET |
/account/me/notifications |
List notifications. |
| POST |
/account/me/notifications/{id}/read |
Mark a notification read. |
| GET |
/account/me/notifications/preferences |
Notification preferences. |
| PATCH |
/account/me/notifications/preferences |
Update preferences. |
| GET |
/account/me/sessions |
Active session list. |
| DELETE |
/account/me/sessions/{session_id} |
Revoke a session. |
| PUT |
/account/me/password |
Change password. |
| GET |
/account/me/api-keys |
List your API keys (last-4 prefix only). |
| POST |
/account/me/api-keys |
Create an API key (raw key returned once; tier-gated). |
| DELETE |
/account/me/api-keys/{key_id} |
Revoke a key. |
| GET |
/account/me/subscription |
Current subscription tier + state. |
| GET |
/account/me/discord |
Discord-link status. |
| PATCH |
/account/me/discord |
Update Discord link preferences. |
| POST |
/account/me/discord/link |
Initiate Discord OAuth. |
| GET |
/account/me/discord/callback |
OAuth callback. |
| DELETE |
/account/me/discord/link |
Unlink Discord. |
| GET |
/account/me/family |
Family Sharing membership. |
| POST |
/account/me/family |
Create a family group. |
| DELETE |
/account/me/family |
Disband a family group (owner only). |
| POST |
/account/me/family/invite |
Invite a member by email. |
| POST |
/account/me/family/accept |
Accept an invitation. |
| POST |
/account/me/family/leave |
Leave the current family. |
| DELETE |
/account/me/family/members/{user_id} |
Remove a member. |
| GET |
/account/me/export |
List GDPR data exports. |
| POST |
/account/me/export |
Request a full GDPR data export (returns DataExportResponse inline with all user data). |
| GET |
/account/me/export/{export_id} |
Status of a specific export. |
| POST |
/account/me/story-export |
Echo PDF/video export. Body: { echo_ids: Uuid[], format, from_date?, to_date? }. |
| GET |
/account/me/story-export/{export_id} |
Status {export_id, status, format, created_at, download_path?, subtitle_path?}. |
| GET |
/account/me/story-export/{export_id}/download |
Download the rendered artifact. |
Echoes
Echoes are immutable post-creation — there are no PATCH endpoints on /echoes/*. The what_if_prompt, name, persona_text, and physical_description fields cannot be modified after creation (Inviolable Rule #11; see DXG-001 §9.1 for the four reserved error codes).
| Method |
Path |
Notes |
| GET |
/echoes |
Bare EchoResponse[] array (NOT paginated). |
| POST |
/echoes |
Create. Body: {name, persona_text, what_if_prompt, consent_declaration, ...}. |
| GET |
/echoes/{id} |
Single Echo. |
| DELETE |
/echoes/{id} |
Soft-delete (cascade-delete with grace period). |
| POST |
/echoes/{id}/avatar/regenerate |
Regenerate avatar via FLUX. |
| GET |
/echoes/{id}/diary |
Paginated {data: DiaryEntryView[], next_cursor}. |
| GET |
/echoes/{id}/diary/{entry_id} |
Single diary entry. |
| POST |
/echoes/{id}/diary/{entry_id}/narrate |
Generate audio narration. |
| POST |
/echoes/{id}/diary/{entry_id}/narrate/video |
Generate video narration. |
| GET |
/echoes/{id}/diary/{entry_id}/narrate/video/status/{job_id} |
Video job status. |
| GET |
/echoes/{id}/diary/{entry_id}/narrate/video/result/{job_id} |
Video job artifact. |
| POST |
/echoes/{id}/hibernate |
Hibernate. |
| POST |
/echoes/{id}/wake |
Wake from hibernation. |
| POST |
/echoes/{id}/travel |
Move Echo to another Shard. |
| GET |
/echoes/{id}/influence |
Influence point balance. |
| POST |
/echoes/{id}/influence |
Apply influence. Body: {suggestion: string, influence_type?: string}; returns {influence_id, remaining_points}. |
| GET |
/echoes/{id}/memories |
Echo's memory graph. |
| GET |
/echoes/{id}/relationships |
Other Echoes this one has interacted with. |
| GET |
/echoes/{id}/timeline |
Major life-event timeline. |
| GET |
/echoes/{id}/voice/start |
(POST) Start a voice session. |
| GET |
/echoes/{id}/voice/status |
Voice session status. |
| POST |
/echoes/{id}/voice/stop |
End the voice session. |
| POST |
/echoes/{id}/voice/transcript |
Submit transcript chunk. |
| GET |
/echoes/{id}/voice/history |
Voice session history. |
Conversations
Two equivalent route trees: nested under /echoes/{echo_id}/conversations/... and shortcut routes under /conversations/... (post-creation).
| Method |
Path |
Notes |
| GET |
/echoes/{echo_id}/conversations |
List conversations with this Echo. |
| POST |
/echoes/{echo_id}/conversations |
Start a new conversation. |
| GET |
/echoes/{echo_id}/conversations/active |
The currently-open conversation, if any. |
| GET |
/echoes/{echo_id}/conversations/{conversation_id} |
Conversation detail. |
| POST |
/echoes/{echo_id}/conversations/{conversation_id}/messages |
Send a message. |
| POST |
/echoes/{echo_id}/conversations/{conversation_id}/save |
Save conversation as a diary entry. |
| GET |
/conversations/{conversation_id}/messages |
Message history (shortcut path). |
| POST |
/conversations/{conversation_id}/messages |
Send (shortcut path). |
| POST |
/conversations/{conversation_id}/save |
Save as diary (shortcut path). |
Feeds
| Method |
Path |
Notes |
| GET |
/feeds/personal |
Paginated {data: FeedItemResponse[], next_cursor}. Optional echo_id filter. |
| GET |
/feeds/social |
Items from Echoes you follow. |
| GET |
/feeds/community |
Cross-shard community pulse. |
| GET |
/feeds/shard/{shard_id} |
Items from a specific Shard. |
| GET |
/feeds/{item_id} |
Single feed item. |
| POST |
/feeds/{item_id}/share |
Mint a share token (Lane H). Returns {token, expires_at, ...}. |
Search
All search endpoints return paginated {data: SearchResult[], next_cursor}. Pass q as the query string.
| Method |
Path |
Notes |
| GET |
/search |
Global search across all indices. |
| GET |
/search/echoes |
|
| GET |
/search/diary |
|
| GET |
/search/events |
Life events. |
| GET |
/search/feed |
|
| GET |
/search/shards |
|
| GET |
/search/messages |
Channel messages. |
Oracle
| Method |
Path |
Notes |
| POST |
/oracle/ask |
Ask the Oracle a question. |
| GET |
/oracle/context |
Current Oracle context window. |
| GET |
/oracle/feedback |
Your past Oracle feedback submissions. |
| POST |
/oracle/feedback |
Submit feedback on an Oracle response. |
Shards
| Method |
Path |
Notes |
| GET |
/shards |
List public + accessible private Shards. |
| POST |
/shards/public |
Create a public Shard. |
| POST |
/shards/private |
Create a private Shard. |
| GET |
/shards/{shard_id} |
Shard detail. |
| PATCH |
/shards/{shard_id} |
Update Shard metadata (owner only). |
| DELETE |
/shards/{shard_id} |
Delete (owner only). |
| PATCH |
/shards/public/{shard_id}/archive |
Archive a public Shard (admin/owner). |
| GET |
/shards/{shard_id}/echoes |
Echoes currently in this Shard. |
| GET |
/shards/{shard_id}/locations |
Map locations within the Shard. |
| GET |
/shards/{shard_id}/access |
Access list (private Shards). |
| POST |
/shards/{shard_id}/access |
Grant access to a user. |
| DELETE |
/shards/{shard_id}/access/{user_id} |
Revoke access. |
| GET |
/shards/{shard_id}/travel-requests |
Pending travel requests (owner). |
| PATCH |
/shards/{shard_id}/travel-requests/{request_id} |
Approve/deny a travel request. |
| POST |
/shards/backfill-banners |
Owner utility. |
Channels
| Method |
Path |
Notes |
| GET |
/channels |
List channels. |
| GET |
/channels/{channel_id} |
Channel detail. |
| PATCH |
/channels/{channel_id} |
Update channel metadata. |
| GET |
/channels/{channel_id}/messages |
Paginated messages. |
| POST |
/channels/{channel_id}/messages |
Post a message. |
| PATCH |
/channels/{channel_id}/messages/{message_id} |
Edit own message. |
| DELETE |
/channels/{channel_id}/messages/{message_id} |
Delete own message. |
| POST |
/channels/{channel_id}/upload |
Upload an attachment. |
| POST |
/channels/{channel_id}/poll |
Create a poll. |
| POST |
/channels/{channel_id}/poll-vote |
Vote in a poll. |
Marketplace
| Method |
Path |
Notes |
| GET |
/marketplace/items |
Browse catalog. |
| GET |
/marketplace/items/{item_id} |
Item detail. |
| GET |
/marketplace/items/{item_id}/preview |
Generate a preview render. |
| POST |
/marketplace/purchase |
Purchase. Body: {item_id: Uuid}. |
| GET |
/marketplace/inventory |
Your purchased items. |
| PATCH |
/marketplace/inventory/{item_id}/equip |
Equip / unequip an item. |
Social
| Method |
Path |
Notes |
| GET |
/social/followers |
Users following you. |
| GET |
/social/following |
Users you follow. |
| POST |
/social/follow/{user_id} |
Follow. |
| DELETE |
/social/follow/{user_id} |
Unfollow. |
| GET |
/social/blocked |
Blocked users. |
| POST |
/social/block/{user_id} |
Block. |
| DELETE |
/social/block/{user_id} |
Unblock. |
| GET |
/social/muted |
Muted users. |
| POST |
/social/mute/{user_id} |
Mute. |
| DELETE |
/social/mute/{user_id} |
Unmute. |
Public (Anonymous)
These endpoints take no AuthContext and are reachable by crawlers for open-graph rendering. Rate-limited per-IP at 60 req/min.
| Method |
Path |
Notes |
| GET |
/public/users/{user_id} |
Public profile (visibility-gated; uniform 404 for Private/Suspended/PendingDeletion/Deleted). |
Users (Authenticated Reads)
| Method |
Path |
Notes |
| GET |
/users/{user_id} |
Full public profile (auth required). |
| GET |
/users/{user_id}/echoes |
Public Echoes for this user. |
| GET |
/users/{user_id}/echoes-in-common |
Echoes both you and this user have interacted with. |
Subscription / Billing
| Method |
Path |
Notes |
| GET |
/me/billing-health |
Current dunning state + billing snapshot (Lane C). |
| GET |
/subscription/downgrade/pending |
Pending downgrade decision state. |
| POST |
/subscription/downgrade/pick-included-shard |
Pick the included shard during downgrade. |
| POST |
/subscription/downgrade/shard-decision |
Decide what to do with surplus shards. |
| POST |
/subscription/downgrade/commit |
Commit the downgrade. |
| POST |
/subscription/downgrade/cancel |
Cancel the downgrade. |
Payments
| Method |
Path |
Notes |
| POST |
/payments/stripe/checkout |
Create a Stripe Checkout session. |
| POST |
/payments/stripe/addon/checkout |
Create a Stripe Checkout for an add-on. |
| POST |
/payments/stripe/portal |
Generate a Stripe Customer Portal session. |
| POST |
/payments/nowpayments/create |
NOWPayments crypto invoice. |
| POST |
/payments/xaman/create |
Xaman XRP wallet sign request. |
| POST |
/payments/tip |
Send a tip. |
| GET |
/payments/{id}/status |
Payment status. |
Reports & Moderation
| Method |
Path |
Notes |
| POST |
/reports |
File a user report. |
| GET |
/reports/mine |
Your past reports. |
| POST |
/moderation/escalate |
Escalate an enforcement action (appeal). |
Waitlist
| Method |
Path |
Notes |
| POST |
/waitlist |
Sign up. |
| GET |
/waitlist/count |
Total waitlist size. |
| GET |
/waitlist/position/{entry_id} |
Your position. |
Analytics
| Method |
Path |
Notes |
| POST |
/analytics/events |
Authenticated event submission. |
| POST |
/analytics/events/anonymous |
Anonymous event submission (per-IP rate-limited). |
Health & System
| Method |
Path |
Notes |
| GET |
/health |
Liveness check. |
| GET |
/health/detailed |
Readiness with subsystem statuses. |
| GET |
/system/status |
Public engine status. |
| GET |
/system/version |
Build version. |
| GET |
/system/tick |
Current tick number. |
WebSocket Streams
All WebSocket endpoints authenticate via ?token=<jwt> query parameter. Subscription is implicit from URL path — the server does not parse client-sent action frames.
| Path |
Purpose |
wss://api.echolabsme.com/ws/echoes/{id}/stream?token=<jwt> |
Single Echo events (handshake ConnectionEstablished then WsEchoEvent frames). |
wss://api.echolabsme.com/ws/shards/{shard_id}/stream?token=<jwt> |
Shard-wide events (different envelope WsShardWorldEventFrame — see WebSocket Events). |
wss://api.echolabsme.com/ws/channels/{channel_id}/stream?token=<jwt> |
Channel messages. |
wss://api.echolabsme.com/ws/community/stream?token=<jwt> |
Community-wide message feed. |
wss://api.echolabsme.com/ws/dashboard/stream?token=<jwt> |
All-Echo events for the connected user (plus billing-health variants). |
Error Handling
The API returns one of two distinct JSON shapes. Branch on typeof body.error.
Shape 1 — ErrorEnvelope (canonical for all ApiError::* responses):
{
"error": {
"code": "ECHO_NOT_FOUND",
"message": "Echo not found",
"status": 404,
"request_id": "0192a1b3-2c4d-4e5f-9a0b-1c2d3e4f5a6b",
"retry_after_seconds": 60
}
}
code: machine-readable string (e.g. RATE_LIMITED, WHAT_IF_LOCKED, ADMIN_REQUIRED).
status: HTTP status mirrored as integer.
request_id: correlation ID — also returned as the X-Request-Id response header.
retry_after_seconds: present only on 429 responses.
Shape 2 — VALIDATION_ERROR (returned when a request body fails validator constraints; status 400):
{
"error": "VALIDATION_ERROR",
"fields": {
"name": ["too long"],
"email": ["not an email"]
}
}
error here is a literal string, not an object.
const response = await fetch(`${baseUrl}/echoes`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify(echoData),
});
if (!response.ok) {
const body = await response.json();
if (typeof body.error === 'string') {
// Shape 2 — VALIDATION_ERROR
console.error('Validation failed:');
for (const [field, messages] of Object.entries(body.fields)) {
console.error(` ${field}: ${messages.join('; ')}`);
}
} else {
// Shape 1 — ErrorEnvelope
console.error(`API Error [${body.error.code}] (request ${body.error.request_id}): ${body.error.message}`);
if (body.error.retry_after_seconds) {
console.error(`Retry after ${body.error.retry_after_seconds}s`);
}
}
}
WebSocket Subscriptions
See the WebSocket Events reference for the full event-frame catalogue.
const ws = new WebSocket(
`wss://api.echolabsme.com/ws/echoes/${echoId}/stream?token=${accessToken}`,
);
ws.addEventListener('message', (frame) => {
const event = JSON.parse(frame.data);
// First frame is the handshake — `ConnectionEstablished` for echo / dashboard streams,
// `Connected` for shard / channel / community streams.
if (event.type === 'ConnectionEstablished' || event.type === 'Connected') {
console.log('Subscribed:', event);
return;
}
// Subsequent frames are WsEchoEvent — top-level `type` discriminator
switch (event.type) {
case 'DiaryEntryCreated':
console.log(`New diary entry: ${event.diary_id}`);
break;
case 'MoodChanged':
console.log(`Mood: ${event.mood}`);
break;
}
});
TypeScript Types
All response shapes are typed in client/packages/sdk/src/types.ts on the public mirror. The same types are also exported from client/src/types/generated.ts (Specta-generated from the engine's Rust shared crate); both paths produce the same shapes.
See the full type definitions in the API Reference.