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
-
In Discord DM with MultiMuse, run
/api generateand store the key securely (shown once). -
Call
GET /api/v1/auth/meto confirm which Discord user owns the key. -
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— Onlythread_idin 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_KEYon StageHand) -
BACKDROP_API_KEY— BackDrop (MULTIMUSE_API_KEYinbackdrop/.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:
-
The API key owner owns the muse, or
-
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/postbelow): 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) — Default2 -
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 whenscene_pathis 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 ifmuse_idis omitted -
guild_id(optional) — When searching by name, restrict to the server’s default tag (skipped whencontext=backdrop,map, ortracker) -
user_id(optional) — Narrow search to one owner; recommended withmuse_idwhen multiple accounts could collide -
context(optional) — Set tobackdrop,map, ortrackerto 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_public—truewhen the owner published a public connections cloud -
connections_viewer_slug— Short slug for the viewer URL when published (otherwisenull)
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 asactor_idon the control plane) -
map_mode(optional) —contained(default) orextended -
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_idorfiller_name— Link target (oredge_idto 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_nameormuse_id(one required) —muse_idalone 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) — Iftrue, returns 202 and delivers in the background -
wrappers_preapplied(optional) — Iftrue, skip automatic header/footer wrapper composition (for clients that already merged wrappers intocontent)
Behavior notes:
-
Muse header/footer wrappers are applied automatically unless
wrappers_preappliedis 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
contentare 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 routes —
scenes/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-Keyor service bearer) -
POST /api/v1/proxies/invalidate-cache— MultiMuse / dashboard service keys only (user keys receive 403) -
POST /api/v1/connections/reciprocal-notifyandPOST /api/v1/connections/reciprocal-response-notify— Control plane / internal only (reciprocal connection DMs)
Integration patterns
Your own account
-
/api generatein Discord DM -
GET /api/v1/auth/me -
Read/write with your user ID — no consent DM
Server bot or shared tool
-
Tool holder generates an API key
-
When acting on another user’s tracking or posts, that user gets a consent DM
-
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
-
API Usage Policy — Commercial use, paid front-ends, and enforcement
-
Terms of Service — General service terms (TeaHouse Media)
Quick reference
-
Who am I? —
GET /api/v1/auth/me -
Tracks in a server —
GET /api/v1/guilds/{guild_id}/threads/tracked -
Tracks for a user —
GET /api/v1/threads/tracked?user_id= -
Start/update tracking —
POST /api/v1/threads/track -
List muses —
GET /api/v1/muses/list?user_id= -
Muse card —
GET /api/v1/muses/card?muse_id=or?name= -
Connections cloud link —
GET /api/v1/muses/{muse_id}/connections-public -
Connections builder cloud —
GET /api/v1/muses/{muse_id}/connections?user_id= -
Published connections graph —
GET /api/v1/muses/{muse_id}/connections/cloud -
Connection targets —
GET /api/v1/connections/targets?user_id= -
Upsert connection —
POST /api/v1/connections/upsert -
Delete connection —
POST /api/v1/connections/delete -
Reciprocal inbox —
GET /api/v1/connections/reciprocal-inbox?user_id= -
Story Line link —
GET /api/v1/muses/{muse_id}/storyline-public -
Published Story Line —
GET /api/v1/muses/{muse_id}/storyline/public -
Read muse archive —
GET /api/v1/muses/{muse_id}/archive?user_id= -
Append to archive —
POST /api/v1/archive/threads/add -
Guild muse names —
GET /api/v1/guilds/{guild_id}/muses?user_id= -
Post as muse —
POST /api/v1/messages/post -
End scene / stop tracking —
POST /api/v1/scenes/end -
Revoke a tool —
/api consent revokein Discord DM