MultiMuse API

Public reference for building tools against MultiMuse (bots, dashboards, desktop apps, and browser backends).

Owner's Note: If you are creating tools around MultiMuse please let us know in the support Discord! We would love to know what people are building.

Example of something built with MultiMuse API : https://github.com/Tea0S/multimuse-tracker

MultiMuse HTTP API (v1.1)

Last updated: July 7, 2026

Public reference for building tools against MultiMuse (bots, dashboards, desktop apps, and browser backends).

Usage policy: Creating or using an API key means you agree to the MultiMuse API Usage Policy. You may not charge for access to API features or ship a paid front-end that replaces free MultiMuse functionality.

Base URL: https://api.multimuse.app

Every path below is under /api/v1 and requires Authorization: Bearer <your_api_key> unless noted.


Getting started

  1. In Discord DM with MultiMuse, run /api generate and store the key securely (shown once).

  2. Call GET /api/v1/auth/me to confirm which Discord user owns the key.

  3. Use that user’s ID in query/body fields when reading or writing on their behalf.

Do not embed API keys in public front-end code. Browser apps should proxy requests through your own backend.


Authentication


Authorization: Bearer <your_api_key>

Discord commands for keys

  • /api generate [name] — Create a new key (shown once)

  • /api list — List your keys

  • /api revoke <key_id> — Deactivate a key

  • /api delete <key_id> — Permanently delete a key

  • /api consent list — Keys you allowed to act on your account

  • /api consent revoke <key_id> — Remove that permission

Who owns this key?


GET /api/v1/auth/me


{

"status": "ok",

"user_id": "123456789012345678"

}

CORS: Cross-origin browser calls are allowed (Access-Control-Allow-Origin: *). CLI and server tools are unaffected.


Authorization

Reads

Any valid API key may read public data when you pass a target Discord user ID (for example ?user_id=). That supports shared server tools and dashboards.

Private muses are never returned unless the key owner owns the muse or has an accepted share. See Private muses.

Writes (mutations)

Writes use a user_id in the JSON body (muse owner or account being updated).

  • Key owner is the target user_id — Allowed immediately.

  • Key owner is someone else — Target user must approve in Discord (unless a first-party service key; see below).

  • Allow always grant for this key — Allowed until revoked.

  • Allow once grant — One successful mutation, then consent is required again.

When consent is required, the API returns 403 with:


{

"status": "error",

"code": "consent_required",

"message": "Consent required: the target user must approve this API key in Discord.",

"request_id": "abc123...",

"target_user_id": "987654321098765432",

"key_id": "your_key_id"

}

The target user gets a MultiMuse DM with Allow once, Allow always, or Deny. The DM includes a short description of the specific request (for example which thread and muse). Each key has separate consent; grants do not carry over between keys.

Endpoints that require mutation consent (when key owner ≠ body user_id):

  • POST /api/v1/threads/track

  • POST /api/v1/messages/post

  • POST /api/v1/connections/upsert

  • POST /api/v1/connections/delete

  • POST /api/v1/connections/settings

  • POST /api/v1/connections/reciprocal/respond

  • POST /api/v1/connections/reciprocal/cancel

  • POST /api/v1/connections/reciprocal/revoke

  • POST /api/v1/archive/threads/add

  • Legacy Obsidian-oriented scene routes (not listed in this public doc)

Endpoints that do not use mutation consent (any valid key):

  • POST /api/v1/scenes/end — Only thread_id in the body; ends tracking for everyone on that thread

First-party service keys (not for integrators)

MultiMuse, StageHand, BackDrop, and the website dashboard use long-lived service bearer tokens configured on the server (not created with /api generate). Those keys skip consent, skip global rate limits, and may call internal routes.

On the MultiMuse server, set any of these to the caller’s bearer token (they may all share one value):

  • MULTIMUSE_SERVICE_API_KEY — Dashboard / control plane (LEGACY_PUBLIC_API_KEY)

  • STAGEHAND_API_KEY — StageHand (MULTIMUSE_API_KEY on StageHand)

  • BACKDROP_API_KEY — BackDrop (MULTIMUSE_API_KEY in backdrop/.env)

If you are building a third-party integration, you only need a user key from /api generate.


Private muses

A muse marked private in MultiMuse is hidden from API reads unless:

  1. The API key owner owns the muse, or

  2. The key owner has an accepted share for that muse.

Applies to:

  • GET /api/v1/muses/list

  • GET /api/v1/muses/card

  • GET /api/v1/guilds/{guild_id}/muses

  • GET /api/v1/muses/wrappers/resolve

  • GET /api/v1/muses/{muse_id}/connections-public — returns whether a public connections cloud exists; does not expose private muse data when the muse is hidden

  • GET /api/v1/muses/{muse_id}/storyline-public — same for published Story Line viewer links

  • GET /api/v1/muses/{muse_id}/storyline/public — published timeline only; hidden when the muse is private to the caller

Querying ?user_id= for another member returns only their public muses plus muses they shared with you. Private muses never appear in lists or cards for other owners.


Rate limits

Exceeded limits return 429 with an error message.

Limits apply per API key (after authentication). First-party service keys (StageHand, BackDrop, dashboard) are not counted against these buckets.

General (user-generated keys; all routes except message post)

  • GET (and non-POST methods): 80 requests per minute, 600 per hour

  • POST (except messages/post below): 40 per minute, 240 per hour

Override via env: API_RATE_LIMIT_PER_MINUTE, API_RATE_LIMIT_PER_HOUR, API_RATE_LIMIT_POST_PER_MINUTE, API_RATE_LIMIT_POST_PER_HOUR.

Message post only

POST /api/v1/messages/post uses a separate bucket per user_id in the body:

  • 10 posts per minute

  • 50 posts per hour

Calls that hit Discord are also subject to Discord’s own rate limits.


Endpoints

GET /api/v1/auth/me

Returns the Discord user ID that owns the bearer key. See Authentication.


Tracked threads (read)

Thread list endpoints merge Discord-side tracking (thread_tracking) with Obsidian vault links (obsidian_scene_registry) when present, so one thread may show muse names, user IDs, and vault paths from both sources.

GET /api/v1/guilds/{guild_id}/threads/tracked

Active thread tracks in one server.

Response: { "guild_id": "...", "threads": [ ... ] }

Each thread object may include:

  • thread_id — Discord thread ID (string)

  • guild_id — Server ID (string)

  • thread_name — Resolved from Discord when possible

  • last_reply_at — ISO timestamp

  • participants — Expected participant count

  • muse_names — Muses on this thread

  • user_ids — Discord user IDs with active rows

  • tracked_muses — Array of { "muse_name", "user_id", "muse_id" } per active track (when available)

  • scene_paths — Obsidian vault paths linked to the thread (when present)

GET /api/v1/threads/tracked?user_id=<discord_user_id>

Active tracks for one user across servers.

Response: { "threads": [ ... ] }

Each row includes thread_id, guild_id, muse_name, participants, thread_name, last_reply_at, and optionally:

  • muse_names — when multiple muses share a thread

  • scene_path / scene_paths — linked Obsidian scene path(s), when present


POST /api/v1/threads/track

Create or update Discord-side thread tracking.

Mutation consent applies when the key owner ≠ body user_id.

Body (JSON):


{

"thread_id": 1234567890123456789,

"user_id": 987654321098765432,

"muse_name": "My Muse",

"guild_id": 111111111111111111,

"participants": 2

}

  • thread_id (required) — Discord thread ID

  • user_id (required) — Muse owner / tracker user

  • muse_name (required) — Display name of the muse

  • guild_id (optional) — Inferred from the thread if omitted

  • participants (optional) — Default 2

  • scene_path (optional) — Obsidian vault path; when set, also updates the Obsidian scene registry (plugin integrations only — omit for Discord-only trackers)

  • characters (optional) — Character list for Obsidian registry when scene_path is set (defaults to [muse_name])

Success: { "status": "ok" }


Muses (read)

GET /api/v1/muses/list?user_id=<id>

List muses for a user (own + shared). Comma-separated user_ids=123,456 is supported. Respects private muses.

Response: { "muses": [ { "muse_id", "name", "trigger", "tags", "owner_id", "is_shared" }, ... ] }

GET /api/v1/muses/card?muse_id=<uuid>&name=<partial>&guild_id=<id>&user_id=<id>&context=<backdrop|map|tracker>

Resolve a muse card by muse_id (preferred for tracker / Backdrop integrations) or by partial name (case-insensitive).

Query parameters:

  • muse_id (optional) — Stable muse UUID; preferred when you already know the muse

  • name (optional) — Partial name match; required if muse_id is omitted

  • guild_id (optional) — When searching by name, restrict to the server’s default tag (skipped when context=backdrop, map, or tracker)

  • user_id (optional) — Narrow search to one owner; recommended with muse_id when multiple accounts could collide

  • context (optional) — Set to backdrop, map, or tracker to skip guild default-tag filtering (for external map/tracker tools)

Single match: { "status": "ok", "muse": { ... } }

The muse object includes card fields (name, trigger, quote, avatar, banner, color, notes, birthday, tags, private, created, messages, owner_id, muse_id) plus:

  • connections_cloud_publictrue when the owner published a public connections cloud

  • connections_viewer_slug — Short slug for the viewer URL when published (otherwise null)

Multiple matches: { "status": "multiple_matches", "matches": [ { "name", "trigger", "tags", "owner_id", "muse_id" }, ... ], "count": N }

Private muse blocked: { "status": "error", "code": "muse_private", "message": "..." } with 403 when a muse matches but is hidden from the caller.

Inaccessible private muses are omitted from lists; you may get 404 if nothing visible matches. Name lookups also try normalized matching (emoji/decoration stripped) when a substring search finds nothing.

GET /api/v1/guilds/{guild_id}/muses?user_id=<id>

Muse names available to a user in a guild (respects server default tag). Accepts user_ids=123,456. Respects private muses.

Response: { "muses": [ { "name", "owner_id" }, ... ], "guild_id": "..." }

GET /api/v1/guilds/{guild_id}/members

Member list for mention helpers. Bots excluded.

Response: { "members": [ { "id", "username", "display_name" }, ... ] }

GET /api/v1/muses/{muse_id}/connections-public

Public metadata for a muse’s connections cloud (relationship map published from MultiMuse Studio). Use this to link map/tracker/card UIs to the viewer without loading full card data.

Path:

  • muse_id (required) — Stable muse UUID

Response:


{

"status": "ok",

"muse_id": "550e8400-e29b-41d4-a716-446655440000",

"connections_cloud_public": true,

"connections_viewer_slug": "lyra-connections"

}

When connections_cloud_public is true, open the viewer at:

  • https://multimuse.app/connections/s/{connections_viewer_slug} when a slug is set, or

  • https://multimuse.app/connections/{muse_id} otherwise

This endpoint does not return connection graph data — only publish status and viewer routing. For the full published graph, use GET /api/v1/muses/{muse_id}/connections/cloud below.

GET /api/v1/muses/{muse_id}/connections?user_id=<discord_user_id>

Full builder connection cloud for a muse you own (same payload as MultiMuse Studio). Account-scoped: user_id must match the API key owner. Includes unpublished edges — use GET .../connections/cloud for published-only graphs.

Query parameters:

  • user_id (required) — Muse owner’s Discord user ID (acts as actor_id on the control plane)

  • map_mode (optional) — contained (default) or extended

  • map_depth (optional) — Depth for extended maps (integer)

Response: { "status": "ok", ... } with the connection cloud object (center, edges, settings, etc.).

Writes to the graph require the control plane to confirm the actor owns the muse (sharees cannot edit connections).

GET /api/v1/muses/{muse_id}/connections/cloud

Full published connection cloud graph. No user_id required when the owner has published the cloud (cloud_public). Privacy redaction is enforced server-side (same as the website viewer).

Query parameters:

  • user_id (optional) — Viewer context for extended-map visibility when logged in as that user

  • map_mode / map_depth — Same as builder endpoint

Returns 404 when the cloud is not published or the muse is hidden.

GET /api/v1/connections/targets?user_id=<discord_user_id>

Search muses you can link when building a connection map. Account-scoped: user_id must match the API key owner.

Query parameters: q, public_only (1), tags, owner_id

GET /api/v1/connections/reciprocal-inbox?user_id=<discord_user_id>

Pending and active reciprocal connection / trust requests for the account. Account-scoped: user_id must match the API key owner.

GET /api/v1/connections/kind-palette?user_id=<discord_user_id>

Custom connection type colors for the account. Account-scoped: user_id must match the API key owner.

POST /api/v1/connections/upsert

Create or update a connection edge on a muse you own.

Body (JSON):

  • user_id (required) — Muse owner’s Discord user ID

  • owner_muse_id (required) — Muse UUID

  • target_muse_id or filler_name — Link target (or edge_id to update an existing filler edge)

  • label, kind (friend | family | rival | romantic | other), kind_custom_label, kind_color

  • mirror_reverse, reverse_label, request_reciprocal, reciprocal_scope (connection | global)

Requires mutation consent when the key owner is not user_id.

POST /api/v1/connections/delete

Body: user_id, owner_muse_id, and edge_id or target_muse_id.

POST /api/v1/connections/settings

Update cloud publish settings and layout for a muse.

Body: user_id, muse_id, and any of cloud_public, viewer_slug, node_layout, expand_in_full_map.

POST /api/v1/connections/reciprocal/respond

Body: user_id, request_id, accept (boolean), optional scope (connection | global).

POST /api/v1/connections/reciprocal/cancel · POST /api/v1/connections/reciprocal/revoke

Body: user_id, request_id, optional scope.

Story Line (read-only)

Story Line on the public API is read-only, mirroring the lightweight connections-public pattern. Editing timelines stays in Muse Studio / the dashboard.

GET /api/v1/muses/{muse_id}/storyline-public

Metadata for linking to a published Story Line viewer (does not return entry data). Respects private muses.

Response:


{

"status": "ok",

"muse_id": "550e8400-e29b-41d4-a716-446655440000",

"storyline_public": true,

"storyline_public_view": "feed",

"storyline_launch_path": "/storyline/550e8400-e29b-41d4-a716-446655440000"

}

When storyline_public is true, open the viewer at https://multimuse.app{storyline_launch_path} (append ?view=graph when storyline_public_view is graph).

GET /api/v1/muses/{muse_id}/storyline/public

Full published Story Line payload (feed or graph, same as the website viewer). Optional user_id for viewer context. Returns 404 when Story Line is not public or the muse is hidden.

Builder CRUD, settings, archive backfill, StageHand sync, and Discord posting are not exposed on api.multimuse.app — use Muse Studio or request a future API expansion if you have a concrete integrator need.

Muse scene archive

The living archive is the ordered list of Discord threads archived for a muse (used by /track archive and Story Line backfill). Integrators can read the archive and append threads; reorder, edit, delete, and display-banner changes stay in Muse Studio.

GET /api/v1/muses/{muse_id}/archive?user_id=<discord_user_id>

Archive threads plus display embed settings for a muse you own. Account-scoped: user_id must match the API key owner.

Response:


{

"status": "ok",

"muse_id": "550e8400-e29b-41d4-a716-446655440000",

"threads": [

{

"thread_id": "1234567890123456789",

"thread_title": "Scene title",

"location": "City square",

"guild_id": "111111111111111111",

"parent_id": null,

"added_at": "2026-07-01T12:00:00+00:00",

"sort_order": 1

}

],

"display": {

"banner_url": null,

"use_muse_avatar": false,

"embed_color": null,

"embed_description": null

}

}

thread_id, guild_id, and parent_id are strings (Discord snowflakes) to avoid JSON number precision loss.

POST /api/v1/archive/threads/add

Append a Discord thread to a muse's living archive. When Story Line beta is enabled for the account, the control plane may also sync a Story Line entry from the archive row.

Body (JSON):

  • user_id (required) — Muse owner's Discord user ID

  • muse_id (required) — Muse UUID

  • thread_id (required) — Discord thread snowflake (string or number)

  • thread_title, location, guild_id, parent_id (optional)

Requires mutation consent when the key owner is not user_id.

GET /api/v1/guilds/{guild_id}/dice_presets

Dice preset definitions pushed from StageHand (read-only for integrators). Used by MultiMuse’s /dice personal add “from preset” flow and available to server tools.

Response:


{

"guild_id": "111111111111111111",

"presets": {

"preset_name": { "name": "...", "buttons": [ ... ] }

},

"results_channel_id": "222222222222222222"

}

results_channel_id is included when StageHand configured a dice results channel for that server.

GET /api/v1/muses/wrappers/resolve?thread_id=&user_id=&muse_name=

Resolve header/footer wrapper text for posting as a muse in a thread. Requires muse_id or muse_name. Respects private muses.

Response: { "header": "...", "footer": "...", "muse_id": "..." }


POST /api/v1/messages/post

Post a message as a muse via webhook.

Mutation consent applies when the key owner ≠ body user_id.

Body (JSON):


{

"thread_id": 1234567890123456789,

"user_id": 987654321098765432,

"muse_id": "550e8400-e29b-41d4-a716-446655440000",

"content": "Hello from the API!"

}

  • thread_id (required) — Target thread or channel

  • user_id (required) — Muse owner (or sharee with access)

  • muse_name or muse_id (one required) — muse_id alone is enough; the server resolves the display name

  • content (required) — Message text (up to 100,000 characters; long posts are split into multiple Discord messages automatically)

  • fast (optional) — If true, returns 202 and delivers in the background

  • wrappers_preapplied (optional) — If true, skip automatic header/footer wrapper composition (for clients that already merged wrappers into content)

Behavior notes:

  • Muse header/footer wrappers are applied automatically unless wrappers_preapplied is set; long content is split across multiple Discord messages with wrappers accounted for on the first/last chunk

  • Per-channel muse aliases (display names set in Discord) are respected for webhook username and avatar

  • Custom emoji shortcodes in content are resolved when the muse owner has emoji cache enabled

  • Role mentions (<@&role_id>) notify normally

  • Channels with muse-feed embed mode may send content as an embed instead of plain text (first image URL in content may become the embed image)

Success: { "status": "ok", "messages": [ { "message_id", "content" }, ... ], "thread_id", "muse_name" } or { "status": "accepted", ... } with 202 when fast is set.

See Rate limits for post-specific caps.


POST /api/v1/scenes/end

End scene tracking for a Discord thread (deactivates registry rows and removes thread tracking). Open to any integrator with a valid API key. No mutation consent and no user_id in the body.

Body (JSON):


{

"thread_id": 1234567890123456789

}

  • thread_id (required)

Success: { "status": "ok" }

Third parties may build similar scene workflows; consent rules for other scene routes do not apply to this endpoint.


Error responses

Most errors:


{

"status": "error",

"message": "Human-readable explanation"

}

HTTP status codes:

  • 400 — Invalid or missing parameters

  • 401 — Missing or invalid API key

  • 403 — Forbidden (consent required, muse not accessible, service-only route, etc.)

  • 404 — Resource not found

  • 429 — Rate limit exceeded

  • 500 — Server error

Consent denials include "code": "consent_required". Private muse lookups may return "code": "muse_private" with 403.


Not part of this public API

The following exist on the server but are not for third-party integrators:

  • GET /api/v1/health — Operations only (unauthenticated)

  • GET /api/v1/stats/internal — Localhost-only runtime metrics for the bot host (unauthenticated at the edge, 403 off-box)

  • Obsidian plugin routesscenes/register, scenes/check, scenes/states, scenes/create, scenes/linked, scenes/query, scenes/update-*, and related legacy paths

  • POST /api/v1/storyline/discord-post — Service only (control plane / dashboard Discord CV2 posts)

  • POST /api/v1/muses/sync — Internal proxy name sync

  • POST /api/v1/guilds/{guild_id}/dice_presets — StageHand push only (X-StageHand-Key or service bearer)

  • POST /api/v1/proxies/invalidate-cache — MultiMuse / dashboard service keys only (user keys receive 403)

  • POST /api/v1/connections/reciprocal-notify and POST /api/v1/connections/reciprocal-response-notify — Control plane / internal only (reciprocal connection DMs)


Integration patterns

Your own account

  1. /api generate in Discord DM

  2. GET /api/v1/auth/me

  3. Read/write with your user ID — no consent DM

Server bot or shared tool

  1. Tool holder generates an API key

  2. When acting on another user’s tracking or posts, that user gets a consent DM

  3. After Allow always, retries work until /api consent revoke

Desktop or CLI

Store the key locally; send Authorization: Bearer ... on each request.

Browser app

Proxy API calls through your backend; never ship keys to the client.


Related documents


Quick reference

  • Who am I?GET /api/v1/auth/me

  • Tracks in a serverGET /api/v1/guilds/{guild_id}/threads/tracked

  • Tracks for a userGET /api/v1/threads/tracked?user_id=

  • Start/update trackingPOST /api/v1/threads/track

  • List musesGET /api/v1/muses/list?user_id=

  • Muse cardGET /api/v1/muses/card?muse_id= or ?name=

  • Connections cloud linkGET /api/v1/muses/{muse_id}/connections-public

  • Connections builder cloudGET /api/v1/muses/{muse_id}/connections?user_id=

  • Published connections graphGET /api/v1/muses/{muse_id}/connections/cloud

  • Connection targetsGET /api/v1/connections/targets?user_id=

  • Upsert connectionPOST /api/v1/connections/upsert

  • Delete connectionPOST /api/v1/connections/delete

  • Reciprocal inboxGET /api/v1/connections/reciprocal-inbox?user_id=

  • Story Line linkGET /api/v1/muses/{muse_id}/storyline-public

  • Published Story LineGET /api/v1/muses/{muse_id}/storyline/public

  • Read muse archiveGET /api/v1/muses/{muse_id}/archive?user_id=

  • Append to archivePOST /api/v1/archive/threads/add

  • Guild muse namesGET /api/v1/guilds/{guild_id}/muses?user_id=

  • Post as musePOST /api/v1/messages/post

  • End scene / stop trackingPOST /api/v1/scenes/end

  • Revoke a tool/api consent revoke in Discord DM