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)

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

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. 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

  • 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, and the website dashboard use long-lived service bearer tokens configured on the server (not created with /api generate). Those keys skip consent and may call internal routes.

If you are building a third-party integration, you only need a user key from /api generate. You do not need STAGEHAND_API_KEY or MULTIMUSE_SERVICE_API_KEY.


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

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).

General (all routes except message post)

  • GET (and non-POST methods): 50 requests per minute, 200 per hour

  • POST (except messages/post below): 30 per minute, 120 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)

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

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.


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

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?name=<partial>&guild_id=<id>&user_id=<id>

Resolve a muse card by partial name (case-insensitive).

  • name (required)

  • guild_id (optional) — Restrict to the server’s default tag when set

  • user_id (optional) — Restrict to one owner

Single match: { "status": "ok", "muse": { ... } } (includes private, owner_id, tags, avatar, etc.)

Multiple matches: { "status": "multiple_matches", "matches": [ ... ] }

Inaccessible private muses are omitted; you may get 404 if nothing visible matches.

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/guilds/{guild_id}/dice_presets

Dice preset definitions pushed from StageHand (read-only for integrators).

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_name": "My Muse",

"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)

  • content (required) — Message text (Discord limits apply)

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

Success: { "status": "ok", ... } 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".


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)

  • Obsidian plugin routesscenes/register, scenes/create, scenes/query, and related legacy paths

  • 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)


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.


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?name=

  • 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