Contents

mumo REST API

Run structured multi-model deliberations on demand. Send a prompt; get back a session containing every model's response and the cross-model claim map. Opt in to a per-round Takeaway via the takeaway parameter.

This is the consumer reference. For agent-runtime use (Claude Code, Cursor, etc.), see the MCP server docs.


Quickstart#

  1. Get a key at mumo.chat/settings/api-keys. Keys begin with mmo_live_.
  2. Send a prompt:
curl https://mumo.chat/api/deliberation \
  -H "Authorization: Bearer mmo_live_…" \
  -H "Content-Type: application/json" \
  -d '{
    "prompt": "Should we use Postgres or MongoDB for an event store?",
    "rounds": 1
  }'

You'll get back a session object. With rounds: 1 the status is ready immediately and the round's full artifact stack is in the response. Here's a redacted example:

{
  "id": "f5731fe5-27ce-4f82-b8ce-868e72ff8bb9",
  "status": "ready",
  "mode": "remote",
  "active_models": ["claude-opus-4-6", "gpt-5.4", "grok-4-20-reasoning"],
  "rounds": [
    {
      "index": 0,
      "completion_state": "complete",
      "responses": [
        { "model": "claude-opus-4-6", "text": "...", "snippets": [...] },
        { "model": "gpt-5.4",        "text": "...", "snippets": [...] },
        { "model": "grok-4-20-reasoning", "text": "...", "snippets": [...] }
      ],
      "claim_map": {
        "claims": [
          {
            "quote": "Postgres' append-only WAL plus JSONB columns gives you...",
            "originator": "claude-opus-4-6",
            "reaction_count": 2,
            "positions": [
              { "model": "gpt-5.4",        "type": "KEEP",  "comment": "Agreed — JSONB lets you..." },
              { "model": "grok-4-20-reasoning", "type": "CHALLENGE", "comment": "But what about field-level migrations 18 months in?" }
            ]
          }
        ]
      },
    }
  ],
  "summary": null,
  "confidence_disclaimer": "Confidence scores (0–1) on claims and snippet comments are self-reported..."
}

Read the artifact stack section below for what each piece is for.


Authentication#

Authorization: Bearer mmo_live_…

Keys are minted at /settings/api-keys. Each key is hashed at rest; copy the secret on creation — it's never shown again.

Calls without a valid key return 401 Unauthorized. Registered callers can select any model in the registry — runtime success is governed by credit balance, not tier. Anonymous callers see only tier-0 models in GET /api/models.


Credit wallet#

Every billable LLM call debits a dollar-denominated wallet. Requests are admitted while the caller's effective balance is above zero; once it reaches zero or goes negative, calls are rejected pre-flight with 403 credit_exhausted — see Errors. A round whose true cost overshoots the remaining balance still completes; the overshoot is carried as a negative refill balance.

Three buckets, FIFO debit: freesubscriptionrefill. The free bucket resets monthly on the 1st (UTC) to a platform-configured amount. Subscription and refill buckets are populated through paid flows (Stripe stubs present; paid tier not yet live).

Wallet amounts are markup-included; ledger-cost amounts are not. Every figure on GET /api/credit and in a credit_exhausted body is markup-included — what the user actually paid. The two cost_usd fields on the session response (total_cost_usd and rounds[].cost_usd) are the exception: they report ledger cost, which is markup-exclusive and therefore lower than the corresponding wallet charge. Do not treat the two as interchangeable, and do not reconcile a balance delta against a cost_usd.

Where wallet state surfaces on responses:

  • Write-op errors (credit_exhausted) include the balance breakdown and reset timing needed to understand why the request was rejected.
  • GET /api/credit is the canonical wallet resource — bucket breakdown, reset timing, rollover cap, subscription status, auto-refill state, FIFO debit order.
  • GET /api/sessions/:id round objects carry cost_usd — ledger cost attributed to that round. Note this is markup-exclusive, so it is not the amount debited from the wallet and does not reconcile against a balance delta. To reconcile spend, read GET /api/credit before and after; there is no per-transaction debit array on the session response.
  • GET /api/models per-model available + unavailable_reasonavailable: false with unavailable_reason: "credit_exhausted" means the wallet is out, not that this particular model is too expensive.

Balance is not embedded on GET /api/sessions/:id top-level — that endpoint is high-volume during session polling, and wallet state on a read-heavy path creates cache-coherency and semantic-coupling problems. Use GET /api/credit when you need a fresh balance outside the write-op flow.


Session mode: remote#

POST /api/deliberation creates a remote-mode session — you drive each round, steering between them with typed snippets.

MCP note. MCP is agent-moderated only: create_deliberation starts one appendable round, then the agent calls wait_for_round, reads the responses and claim map, and decides whether to call append_round.

Historical: an autonomous (AI-moderated) mode was supported via a moderator_model parameter until 2026-05. It was retired. Requests that send moderator_model will be rejected by the request schema.

You drive the rounds#

The endpoint commits round 1 and returns a 202 ack immediately; model execution runs in the background. Poll the returned progress_url until the round is terminal, read the claim_map, then call POST /api/sessions/:id/rounds with steering snippets to add the next round. Repeat as long as you want — there's no preset cap on remote sessions.

{ "prompt": "..." }

Single-call use case: if you only want one round of three opinions and no follow-up, just don't call append_round. The session ends after round 1.


POST /api/deliberation accepts optional improvement_consent for the new session. This controls whether session data may be used for platform improvement, not billing, credit consumption, model routing, or response visibility.

  • Omit it to use the account's effective default.
  • Any account may send true or false.
  • Session-level consent is narrowing-only: sending false excludes the session even if the account default is on. Sending true never widens beyond the account's own Platform Improvement choice — if the account default is off, the session is still excluded.
  • improvement_consent is session-level. POST /api/sessions/:id/rounds rejects it rather than changing consent mid-session.

Session responses expose the resolved decision:

"improvement_consent": {
  "enabled": true,
  "reason": "free_tier_terms",
  "requested": null,
  "disclosure": null
}

Existing sessions created before this field shipped may report reason: "default_include".


Async by default#

As of 2026-04, POST /api/deliberation and POST /api/sessions/:id/rounds return a 202 Accepted ack in <500ms after the round row commits and budget is debited — they do not wait for model execution. Model work runs in the background; callers poll progress_url for terminal state.

What the ack looks like:

{
  "session_id": "abc-123",
  "round_id": "round-456",
  "round_index": 0,
  "status": "processing",
  "idempotency_key": "auto-gen-uuid-if-omitted",
  "client_request_id": null,
  "poll_after_ms": 5000,
  "progress_url": "/api/sessions/abc-123/progress",
  "progress_version": 0
}

The ack confirms that the round committed and tells the caller where to poll progress. The canonical wallet resource with bucket breakdown and reset timing lives at GET /api/credit. Write operations still perform a balance preflight and return credit_exhausted before committing if the wallet is out.

Why: a client timeout no longer produces server/client state divergence. If the ack reached you, the round committed; if it didn't, your idempotency key replay resolves the uncertainty.

?wait=true compatibility shim. REST-only. Appending ?wait=true to the create/append URL makes the endpoint block until terminal state or 290s (whichever first), preserving the pre-2026-04 response shape. This is a migration aid, deprecated (see "Deprecation timeline" below). MCP does not use ?wait=true; use the wait_for_round tool after the write-op ack.

Idempotency#

  • Scope: (account, endpoint, Idempotency-Key). Same key can be used on both create_deliberation and append_round without colliding.
  • Request fingerprint: the server hashes a canonical subset of your body (prompt + snippets[] order-sensitive + model set sorted + moderator_name + reference + improvement_consent + register + web_search), NFC-normalized. Same key + same semantic body = replay; same key + different semantic body = 409 idempotency_conflict with original_request_fingerprint in the response for caller-side diff.
  • Auto-generation: MCP always sends one (its adapter derives a stable key from tool+args). REST: if you omit the Idempotency-Key header, the server generates a UUID and echoes it in the ack's idempotency_key field. Persist the echoed key if you want retry safety across transport failures.
  • Retry during refund: if a round's refund_status='pending' at replay time, the response includes status: "processing_refund" with retry_after_ms instead of the original terminal. Once the refund credits, replays reflect the terminal failure.
  • TTL: 24h rolling window.
  • Novel error patterns: unclassified failures (classifier fell through to internal_error) do NOT auto-credit refunds. They write the failure fact and sit refund_status='pending' for admin review. Only canonical codes (see "Failure codes" below) auto-credit.

Refund lifecycle#

When a round fails catastrophically (all providers error), the ledger emits a failure fact + credits back your budget atomically. The rounds[].refund_status lifecycle you'll see on the progress endpoint:

  • none — happy path, no failure — or a failed round whose refund is still reserved (see below).
  • pending — worker emitted the failure fact (failure_event_at set); ledger is about to credit.
  • credited — refund is in; refund_credited_at stamped. Budget restored.
  • not_applicable — partial-success round (≥1 model returned output); round counted as "delivered."

SLO: p99 latency from failure_event_at to refund_credited_at is < 60s. The reservation window below sits before failure_event_at and is deliberately outside this measurement.

Refunds sit behind the retry budget. A round can be terminally failed while its refund_status is still none: as long as a model has retry budget left, the refund is reserved rather than credited, because crediting closes retries (a credited round rejects further attempts). So on a failed round, refund_status: "none" means "not decided yet," not "no refund owed" — poll until it reaches credited or not_applicable. The refund credits when the retry budget is exhausted, when a failure is classified as one a retry cannot fix, or when the reservation window expires; if a retry succeeds first, the round resolves to not_applicable.

Failure codes#

Canonical failure_code values surfaced on the progress endpoint and in refund-conflict responses:

CodeMeaning
model_provider_rate_limitProvider returned 429 / rate-limit
model_provider_outageProvider returned 5xx or was unreachable
model_provider_oomProvider returned out-of-memory
model_output_malformedResponse couldn't be parsed
model_timeoutProvider call exceeded the deadline
all_providers_failedEvery participant failed (composite) — fires refund
dependency_timeoutUpstream (e.g. Brave Search) timed out
dependency_outageUpstream dependency 5xx
dependency_malformed_responseUpstream returned unparseable data
stuck_reconciledReconciliation cron detected a stuck round and emitted synthetic failure
internal_errorUnclassified — held for admin review, does NOT auto-credit
test_forced_failureAdmin-only test hook

Per-model error codes (failed_models[].error_code and /progress models[].error_code)#

Distinct from the round-level failure_code above. Round-level codes describe why the round as a whole failed (typically all-providers composite). Per-model error_code describes why an individual model's call terminated. A round can carry partial_failure completion state with some failed_models[] entries that each have their own error_code, while the round-level failure_code stays null (no refund fires on partial success).

CodeMeaningCarries partial_text?
provider_errorProvider returned an error mid- or post-stream (after first byte). Partial text preserved.Yes
pre_stream_provider_errorProvider returned a 4xx/5xx HTTP response before the stream opened. Covers auth/malformed/pre-stream rate-limit; treat as non-transient for retry decisions.No
stream_ended_without_final_markerThe stream closed without signaling completion — either no done event before EOF, or no terminal chunk at all (no finish_reason, no usage frame). Partial text preserved.Yes
empty_completionThe provider returned a terminal completion — usage present, finish_reason normal — whose prose was empty. Typically a reasoning model that spent its budget out-of-band.No (there is no text)
internal_deadline_reachedThe 240s in-band deadline fired while the model was still producing output. Partial text preserved when bytes were already flushed.Maybe
deadline_expiredThe 1-min sweep cron found a row past its deadline_at without terminal stream_status and wrote this terminal. Out-of-band counterpart to internal_deadline_reached.No
stream_interruptedWorker restarted while the row was mid-stream.Maybe
max_retries_exceededPre-first-byte retry cap hit. Provider was unreachable long enough that no bytes ever rendered.No
provider_auth_failureProvider rejected the request for auth reasons (typically configuration error). Non-transient.No
pre_stream_failureError thrown before provider.stream() was even called (prompt build, factory, etc.). Non-transient.No
rate_limitProvider rate-limited (explicit code path; distinct from a generic 429 routed via pre_stream_provider_error).No
canceledUser-initiated abort.No

Retry/abandon classifier (used by MCP wait_for_round's recommended_client_action and a useful default for REST callers too):

  • Transient — retry-eligible: rate_limit, provider_error, internal_deadline_reached, deadline_expired, stream_interrupted, stream_ended_without_final_marker, empty_completion.
  • Abandon (or escalate): pre_stream_provider_error, provider_auth_failure, pre_stream_failure, max_retries_exceeded, canceled. These don't usually clear on retry under the same conditions.

Deprecation timeline for ?wait=true#

  • Day 0–90 from GA: full support, Deprecation: true + Sunset + Link response headers on every ?wait=true response.
  • Day 90–120: advisory window; headers remain.
  • Day 120+: returns 410 Gone with a pointer to the async-polling pattern. Enforced via WAIT_ENFORCE_410 env flag.

Round Takeaway artifacts (opt-in, per-round)#

One optional boolean opts rounds in to per-round Takeaway generation. POST /api/deliberation (the create path) and POST /api/sessions/:id/rounds (append) both accept takeaway.

FieldAccepted onDefaultNotes
takeawaycreate + appendfalseGenerate a round_takeaway artifact when this round completes — a structured per-round summary (bottom_line + items[] of { question, answer, consensus, claim_ids }). Surfaces on GET /api/sessions/:id once written.

Pricing. The Takeaway bills via the standard credit wallet but with 0 bps markup — at-cost passthrough. The per-session breakdown at /settings/sessions surfaces a dedicated line item with the bucket scope and at-cost marker so you can reconcile what was charged.

Artifacts on the session response. When a Takeaway exists, it surfaces on the session response:

  • rounds[].round_takeaway — the sole per-round summary artifact, for every source (web, x, api, mcp). Populated for any round that opted in via takeaway=true once generation has completed; null otherwise.

Legacy distill removed. The former distill request parameter is no longer accepted and does not appear in responses. Use takeaway for per-round Takeaways.


The session response#

Every GET /api/sessions/:id response carries these top-level fields:

FieldNotes
idSession UUID.
statusstreaming | processing | ready | failed. See "Status flow" below.
moderemote. REST metadata describing how the session was created. (Historical sessions may report autonomous; that mode was retired in 2026-05.)
active_modelsModel IDs participating in this session.
moderator_modelNull for all new sessions. Historical sessions may carry a value.
moderator_name, applicationOptional identity metadata.
model_metadata{ [model_id]: { display_name, provider } }.
created_at, estimated_ready_atTimestamps.
total_usageAggregated tokens_in / tokens_out across the session.
total_cost_usdGround-truth ledger cost (USD) for the entire session. Sums every billable bucket: deliberation + moderator + round summary (Takeaway) + snippet extraction + editorial + search. Markup-exclusive — distinct from wallet debits, which are markup-included. 0 for sessions with no ledger rows yet.
roundsArray of round objects (see below).
summarySession-level editorial. Null until generated for multi-round sessions.
confidence_disclaimerVerbatim advisory string. Surface alongside any displayed confidence scores.
share_urlPublic reader-deck URL (https://mumo.chat/p/{slug}) when the session has an active share, else null. Read parity for POST /api/sessions/:id/share — re-find an existing link without re-sharing.
share_statusshared (link-access only, not indexed) | published (platform-listable) | null (never shared, or share removed).

Each round in rounds[] carries: id, index, prompt, completion_state, responses, failed_models, in_progress_models, claim_map, claim_map_url, round_takeaway, cost_usd. round_takeaway is the sole per-round summary artifact for all sources; it is null unless the round opted in via takeaway=true (and generation has completed) — see Round Takeaway artifacts.

completion_state (per-round; distinct from session-level status) is 4-way:

ValueMeaning
completeEvery target model produced a final response.
partial_failureAll target models reached terminal state; at least one final AND at least one errored. Round is usable but degraded.
failedAll target models reached terminal state; every one errored, zero finals. Round produced no usable output.
in_progressAt least one target model is still queued, streaming, or expected-but-absent. Round not yet settled — keep polling /progress.

responses[] is the success-only collection: each entry has the canonical content plus two fields for downstream branching:

  • is_partial (boolean) — true when the response is a successful-but-truncated stream (the model produced output and the call reached done, but the provider signaled truncation via finish_reason). Treat the text as a partial answer; consider asking the user whether to extend.
  • finish_reason (string | null) — provider-native stop reason, surfaced as-is rather than normalized (Anthropic: end_turn / max_tokens / stop_sequence; OpenAI: stop / length / content_filter; Gemini: STOP / MAX_TOKENS / SAFETY). null when the stream did not complete naturally (error, abort, deadline).

failed_models[] is the error-attribution collection. Each entry:

FieldNotes
modelModel ID that failed.
errorFree-text error description from the row's error column. Stable but not safe to pattern-match — switch on error_code for branching.
messageHuman-readable error message.
error_codeCanonical STREAM_ERROR_CODES value (provider_error, stream_ended_without_final_marker, internal_deadline_reached, …). null on legacy rows. See "Per-model error codes" below.
partial_textOptional. Present when the failed stream emitted bytes before terminating (post-first-byte provider_error, stream_ended_without_final_marker, internal_deadline_reached). Diagnostic value; sometimes usable as a partial answer.
partial_text_lengthOptional. Character count of partial_text when present.

in_progress_models[] is the "still working" collection — present when completion_state === "in_progress". Each entry:

FieldNotes
modelTarget model ID.
statequeued (row pre-inserted, provider call not yet started), streaming (≥1 delta observed, no terminal yet), or absent (rare; backstop window or race against pre-insert).
deadline_atISO 8601 timestamp at which the sweep cron will write a terminal error if the row hasn't transitioned. null on legacy rows.

cost_usd is the per-round counterpart of session-level total_cost_usd — same ledger source, same markup-exclusive semantics. It is useful after a round completes; during an in-flight round it may be 0 or incomplete because ledger rows settle as model/finalizer calls finish. The relationship: sum(rounds[].cost_usd) ≤ total_cost_usd — session-scoped buckets (session title generation, editorial summary) appear in total_cost_usd only.

The per-round artifacts:

  • responses[].text — raw prose from each model.
  • responses[].snippets[] — model-emitted reactions (typed KEEP/CHALLENGE/etc, with verbatim quotes from peers and optional commentary).
  • claim_map.claims[] — verbatim claims that ≥2 models reacted to, with each reactor's position (type + commentary). The highest-signal artifact for understanding agreement and disagreement. A position can also be the moderator (model: "moderator"): when a later round's moderator snippet reacts to this round's content, that reaction backfills onto the prior round's claim here. A moderator reaction can also surface a claim that no two models cross-reacted on (a moderator-only claim), so don't assume every claim has ≥2 model positions.
  • claim_map_url — browser URL for this round's claim map (https://mumo.chat/cm/{round_id}). Auth-gated and owner-only: it requires signing in with the mumo account that owns the API key, then opens the claim map inside the full session view (alongside the raw responses with click-through to the inline text). Agents should surface it to the human at the end of their summary so the deliberation can be reviewed directly. Always present.

Agents driving remote-mode deliberations should use claim_map to decide whether to continue or stop, and opt in to takeaway when they want structured per-round summaries.

The session-level summary field carries the final editorial across the whole session (surface, agreed, split, open blocks plus anchor_quote and og_quote). It's only populated for completed multi-round legacy sessions.


Endpoints#

POST /api/deliberation#

Create a session. Body:

FieldTypeNotes
promptstringThe question or topic. Required.
referencestringOptional spec, doc, or design injected as shared context.
modelsstring[]2–3 model IDs. Defaults to platform selection. Call GET /api/models to enumerate.
moderator_namestringDisplay name for the steering identity (≤100 chars). Surfaces in the published transcript.
applicationstringDisplay name of your client (≤100 chars). Surfaces in the session info panel.
takeawaybooleanOpt round 0 in to a per-round Takeaway (round_takeaway). Default false. See Round Takeaway artifacts.
register"conversational" | "agent"Deprecated — ignored. The platform runs a single prompt environment for all sessions. The field is still accepted (and still participates in the idempotency fingerprint) so existing callers' retries stay stable. Omit it in new integrations.
web_searchbooleanSet false to skip web search entirely (no search gate, no source pack). Default true — the platform decides per-prompt whether to search. Disable for prompts grounded purely in supplied reference material.

Returns a session object (see Quickstart for an example shape).

Idempotency: pass Idempotency-Key: <stable-string> to make retries safe. Same key + same body returns the cached response; same key + different body returns 409 idempotency_conflict.


POST /api/sessions/:id/rounds#

Append a round to a remote-mode session. Body:

{
  "prompt": "Focus on the pricing mechanism, not positioning.",
  "snippets": [
    {
      "type": "CHALLENGE",
      "quote": "Per-seat pricing assumes teams of >10.",
      "quoted_model": "gpt-5.4",
      "comment": "Most enterprise pilots start at 3–5."
    },
    {
      "type": "KEEP",
      "quote": "Usage-based pricing aligns incentives.",
      "quoted_model": "claude-opus-4-6"
    }
  ],
  "takeaway": false
}

snippets is optional but high-signal — it's how you steer attention round-to-round.

takeaway is an optional boolean (default false) that yields a round_takeaway for this round (see Round Takeaway artifacts).

Snippet types:

  • KEEP — this point is strong; preserve it
  • EXPLORE — dig deeper here
  • CHALLENGE — push back on this claim
  • CORE — load-bearing; build on it
  • SHIFT — this reframes the question

Quotes must be verbatim from a prior round's response. quoted_model is the model ID that originated the quote.

Idempotency-Key is strongly recommended on this endpoint. Round-append duplication corrupts deliberation history.

Errors:

  • 409 session_busy — a round is currently streaming or processing. Retry after a short delay with the same Idempotency-Key.
  • 403 credit_exhausted — your wallet balance is exhausted (zero or negative). Body includes effective_balance_usd, free_usd, subscription_usd, refill_usd, per_model_minimum_usd, next_reset_at. Free-tier balance resets on the 1st of each month (UTC).

GET /api/sessions/:id#

Fetch the full state of a session — all rounds, responses, snippets, claim maps, and the editorial summary if present.

The response is fresh on every call.


POST /api/sessions/:id/share#

Share a session at a public URL — anyone with the link can view it in the read-only reader deck. No request body.

{
  "session_id": "…",
  "status": "shared",
  "slug": "your-session-title-a1b2c3",
  "share_url": "https://mumo.chat/p/your-session-title-a1b2c3",
  "markdown_url": "https://mumo.chat/p/your-session-title-a1b2c3.md",
  "brief_url": "https://mumo.chat/p/your-session-title-a1b2c3.brief.md"
}

markdown_url is the full-transcript machine-readable twin (per-round claim maps included — the review/audit surface); brief_url is the synthesis-only triage tier (~1–2k tokens, predictable cost).

Semantics:

  • Idempotent — re-calling returns the same slug and URLs.
  • Snapshot, kept current — the public page is a point-in-time snapshot; a share call made after new rounds were appended refreshes it at the same URL.
  • Link-access only — shared pages are noindex and unlisted. status stays shared unless the platform has separately elevated the artifact; a share call never changes an elevated status.
  • Registered accounts only — anonymous callers receive 403 registration_required.
  • The first share of a long session generates any missing round takeaways before freezing the snapshot (~15–30s worst case). A 500 share_failed whose message says summaries are still generating is transient — retry after a moment.

Errors: 403 registration_required, 404 not_found, 500 share_failed.


GET /api/sessions/:id/progress#

Lightweight poll endpoint for round state without fetching the full session body. Two consumers:

  1. Async REST/MCP callers — after a create_deliberation / append_round ack, poll this to learn whether the round reached terminal state (and whether the refund lifecycle moved). The terminal check is: moderation_status is "complete" or "failed", or execution_status is "complete" or "failed". Rounds created from the mumo web UI never write moderation_status (it stays null) — execution_status is their only terminal signal, so a moderation-only check polls forever on web-created rounds. Either terminal value is an end-state; stop polling. "failed" can mean an all-models error OR a catastrophic round-level failure (e.g., pre-insert / internal pipeline error) — to distinguish, read failure_code and the per-model state / error_code entries in models[]. Once terminal, fetch the full content via GET /api/sessions/:id, whose round objects carry failed_models[] for end-of-poll attribution.
  2. Real-time UIs — surface per-model state (queued / streaming / final / error / absent) and a heartbeat without re-rendering the whole transcript.

The response is intentionally compact (no joins on responses text, snippets, or claim map):

{
  "session_id": "abc-123",
  "is_ai_moderated": false,
  "auto_moderation_completed_at": null,
  "rounds": [
    {
      "id": "round-456",
      "index": 0,
      "moderation_status": "in_progress",
      "execution_status": "running",
      "refund_status": "none",
      "failure_code": null,
      "failure_event_at": null,
      "refund_credited_at": null,
      "refund_deadline_at": null,
      "progress_version": 3,
      "models": [
        {
          "model": "claude-opus-4-7",
          "state": "streaming",
          "deadline_at": "2026-05-13T03:02:30Z",
          "error_code": null,
          "expired_at_read": false,
          "provider": "anthropic",
          "inference_provider": "anthropic",
          "partial_text_length": 1840,
          "last_chunk_at": "2026-05-13T03:00:07Z",
          "since_last_chunk_ms": 3000
        }
      ]
    }
  ]
}

Round-level status fields:

FieldNotes
moderation_statusLegacy round lifecycle: pending | in_progress | complete | failed | null. Stamped and settled by API/MCP-created rounds; always null on web-created rounds.
execution_statusGo-forward round lifecycle. Non-terminal: created | running | reclaimable | cancelling. Terminal: complete | failed | cancelled. (cancelling/cancelled are defined by the schema but have no writer yet — no round currently reaches them.) Written terminal by every settle path, including web-created rounds. For web rounds, terminal here means all model responses have settled — round artifacts (claim map, takeaway) may still be generating for a short window afterward.

Per-model fields:

FieldNotes
modelModel ID.
statefinal | error | streaming | queued | absent. Same state machine as in_progress_models[].state plus terminal values.
deadline_atISO 8601 timestamp at which the row will be swept to a terminal error if it hasn't transitioned. null on legacy rows.
error_codeSTREAM_ERROR_CODES value when state === "error". null otherwise.
expired_at_readtrue when state is queued or streaming AND deadline_at is already past at read time. The sweep cron's ~60s cadence means a row can be expired up to that long before its terminal write lands; this field exposes the derived state immediately so callers can render "this model is past its deadline; result expected within a minute." Always false on terminal states.
providerModel family (anthropic | openai | google | xai | moonshot | zai | alibaba). null when the registry lookup fails.
inference_providerInference endpoint family — distinct from provider when the model routes through a third-party inference host (e.g., Kimi via Fireworks). Matches provider when no cross-provider route is active.
partial_text_lengthCharacter count of the response's accumulated text. null for queued / absent rows; 0 for streaming rows that haven't flushed yet. Use null-vs-0 to distinguish "not producing yet" from "observed zero-length partial." For terminal final / error rows, this is the final character count.
last_chunk_atISO 8601 timestamp of the most recent SDK delta. Updated on the streaming producer's ~2s text flush. null for queued, absent, or non-streaming code paths.
since_last_chunk_msRead-time computed: now - last_chunk_at. Only present when state === "streaming"; null otherwise. Combined with partial_text_length this renders as "claude: streaming, 1840 chars, last chunk 3s ago."

ETag / If-None-Match support. Every /progress response carries a weak ETag. Send it back on the next poll via If-None-Match to short-circuit no-change polls with a 304 Not Modified. The validator covers every body-derived signal — round-level state, per-model state, registry attribution, and (when any model is non-terminal) a 5-second wall-clock bucket so since_last_chunk_ms and expired_at_read can't go stale past one bucket boundary. Terminal-only payloads keep their state-based ETag stable across reads, so cache hits on completed rounds are long-lived.

cache-control: no-store on every response — clients shouldn't share-cache, but their own If-None-Match re-poll still works.


GET /api/sessions#

List your sessions.

QueryValues
moderemote (historical sessions may also report autonomous)
statusready | streaming
limit1–200 (default 7)
offsetpagination

Returns a lightweight list (no response bodies). Use GET /api/sessions/:id for full content.


GET /api/models#

List the full model catalog — every registry-active model, whatever your credentials. available reports whether you can use each one right now; nothing is filtered out. Each entry:

{
  "id": "claude-opus-4-6",
  "provider": "anthropic",
  "display_name": "Claude Opus 4.6",
  "available": true,
  "unavailable_reason": null,
  "min_user_tier": 1,
  "context_window": 200000,
  "max_output_tokens": 16384,
  "pricing": {
    "input_per_million": 15,
    "output_per_million": 75,
    "cached_input_per_million": 1.5,
    "minimum_usd": 0.05,
    "cache_write_per_million": 18.75
  },
  "sort_order": 10
}
  • availabletrue if the caller can actually use this model right now through this API. That means all three of: an mmo_live_* key, the model enabled in your preferences, and credit remaining — the same things /api/deliberation checks before it runs anything. Any other caller (no bearer, or a browser/anonymous session token) gets false on every model, since the execution endpoints reject them regardless of which model they name. false means a call that uses this model will fail.
  • unavailable_reason — why available is false, ordered by what to fix first. "api_key_required" — no mmo_live_* key was presented. "disabled_for_account" — the model is turned off in your model preferences, so creating a deliberation with it returns ineligible_models. "credit_exhausted" — the wallet is zero or negative. Absent when available is true.
  • min_user_tier — visibility tier: 0 = Core (available to everyone, including unauthenticated callers), 1 = Plus and 2 = Premium (registered). Every tier is listed for every caller — the catalog is public, and available reports whether this caller can use a given model. Credit balance is the runtime constraint for registered users.
  • pricing.minimum_usd — the model's registered floor cost, reported for reference. It is not an admission threshold: preflight admits on any effective balance above zero, whatever this value is. It still appears in the credit_exhausted body as per_model_minimum_usd.
  • pricing.cache_write_per_million — present only on models that bill prompt-cache writes: Anthropic models and the GPT-5.6 family (both at 1.25× the input rate). When absent, cache writes are not billed for that model.
  • Per-million pricing fields reflect raw provider cost (platform COGS), not markup-inclusive user-paid amounts. They're informational — for "will this work?" preflight, use available (or effective_balance_usd > 0 from GET /api/credit). For "what did that cost?" compare effective_balance_usd from GET /api/credit before and after the round; the round's cost_usd reports ledger cost, which is markup-exclusive and so smaller than the wallet debit.

GET /api/credit#

Canonical wallet resource. Returns the caller's full credit state.

{
  "effective_balance_usd": 1.42,
  "buckets": {
    "free": {
      "balance_usd": 1.42,
      "monthly_grant_usd": 1.50,
      "resets_at": "2026-05-01T00:00:00Z"
    },
    "subscription": {
      "balance_usd": 0,
      "rollover_cap_usd": 30.00,
      "subscription_status": null
    },
    "refill": {
      "balance_usd": 0,
      "auto_refill_enabled": false
    }
  },
  "per_model_minimum_usd_default": 0.05,
  "debit_order": ["free", "subscription", "refill"]
}
  • effective_balance_usd — sum across all three buckets. This is the preflight number: above zero admits, zero or negative is credit_exhausted. Markup-included — reflects what you have left to spend, not raw LLM-cost headroom.
  • buckets.free — monthly free-tier credit. monthly_grant_usd is the platform grant each cycle; resets_at is the next UTC 1st-of-month boundary when the bucket refills.
  • buckets.subscription — Stripe-granted credit (paid tier). rollover_cap_usd is the max unused balance that carries forward into a new cycle. subscription_status is one of "active" | "past_due" | "cancelled" | "expired" | null (null = no subscription).
  • buckets.refill — auto-refill top-ups. auto_refill_enabled is the user's current setting. When enabled, two additional fields appear: auto_refill_threshold_usd (trigger level) and auto_refill_amount_usd (top-up size). The fields are absent when autorefill is off.
  • per_model_minimum_usd_default — platform fallback minimum used when a model's registry row has no explicit pricing_minimum_usd. Reported for reference only; it does not gate admission. For per-model values, read pricing.minimum_usd from GET /api/models.
  • debit_order — FIFO debit sequence. Settlement drains each bucket in this order; surfaced so dashboards and reconciliation tooling don't need to read server code.

Anonymous callers receive the same shape with all balances at 0, subscription_status: null, and auto_refill_enabled: false. Anonymous usage is gated by guest-round budget elsewhere, not the credit wallet.


GET /api/defaults#

Discover platform defaults. Wallet state is not here — use GET /api/credit.

{
  "models": ["…", "…", "…"],
  "daily_budget": { "limit": 200, "used": 4, "resets_at": "…" }
}

GET /api/health#

Returns { "status": "ok", "version": "v1" }. No auth required.


Status flow#

Two distinct state machines surface in the response.

Session-level status — high-level rollup; what to switch on in REST polling loops:

streaming  →  processing  →  ready
                            ↘
                             failed
  • streaming — at least one model in the most recent round is actively responding.
  • processing — all model responses landed; post-processing (snippet extraction, claim map, optionally the round Takeaway) is in flight.
  • ready — fully complete; safe to call append_round or read final artifacts.
  • failed — terminal error.

Per-round completion_state — finer-grained signal that lives on each rounds[] entry; the canonical "is this round usable yet?" check:

in_progress  →  complete | partial_failure | failed
  • in_progress — ≥1 target model still queued / streaming / absent.
  • complete — every target model produced a final response.
  • partial_failure — every target model is terminal; ≥1 succeeded, ≥1 errored. Round is usable.
  • failed — every target model is terminal; zero succeeded. No usable output.

Note: a session can be status: "processing" (post-execution finalizer running) while its latest round is already completion_state: "complete". The session moves to ready once the post-processing pipeline finishes. For "is the round content available?" — check completion_state. For "is the session settled (including editorial/summary)?" — check status.


Errors#

All non-2xx responses return JSON:

{
  "error": "session_busy",
  "message": "Session has a round in progress — poll and retry with same Idempotency-Key",
  "retryable": true
}

Every field above is always present: error is a stable machine code, message is human-readable prose, and retryable says whether repeating the identical request could succeed. Some codes add typed extras (documented per row below); treat unknown extra keys as forward-compatible additions.

CodeHTTPRetryableWhat to do
unauthorized401noBad or missing bearer token.
malformed_json400noRequest body was not parseable JSON.
invalid_request400noBody failed schema validation. Body includes issues — the Zod flatten shape, with fieldErrors keyed by field name and formErrors for whole-body problems.
invalid_params400noA path parameter failed validation (e.g. an empty session id). Body includes issues in the same shape.
unknown_models400noOne or more model IDs in the request don't exist in the registry. Body includes unknown_models: string[] and models_requested: string[]. Call GET /api/models to enumerate valid IDs.
ineligible_models400noOne or more model IDs were disabled by the calling account. Body includes ineligible_models: string[]. Omit the disabled IDs from your request.
insufficient_active_models400noThe caller omitted models and the curated default panel couldn't produce ≥2 picks against the account's enabled set. Body includes collapsed_buckets: number[] (zero-indexed). Pass an explicit models array.
credit_exhausted403noWallet balance is exhausted (zero or negative). Body includes bucket breakdown + reset timing (see below). Top up (paid) or wait for the 1st-of-month free-tier reset.
forbidden403noFeature not available on your account.
terms_acceptance_required403noUpdated terms need acceptance before the platform can be used. Body includes a terms_acceptance_required object with doc_types_pending, current_versions, deadline, summary_urls, and acceptance_url.
not_found404noSession ID doesn't exist or isn't yours. The 403 "exists but not yours" case is deliberately collapsed into this code so a caller cannot probe for the existence of someone else's session. Also returned for any path under /api/ that matches no endpoint — that body carries docs_url, openapi_url, and mcp_url so a mistyped path points you somewhere useful.
idempotency_conflict409noSame key reused with a different body. Use a new key.
session_busy409yesAnother round is in flight. Retry with the same Idempotency-Key.
wait_sunset410noThe removed ?wait=true synchronous mode. Create the round, then poll GET /api/sessions/:id/progress.
rate_limited429yesToo many appends too quickly. Body includes retry_after_seconds.
daily_limit_reached429yesRound budget exhausted for the period. Body includes resets_at.
internal_error500yesTransient. Retry with the same Idempotency-Key.

credit_exhausted body#

{
  "error": "credit_exhausted",
  "message": "Your balance is insufficient to run this request.",
  "retryable": false,
  "effective_balance_usd": 0,
  "free_usd": 0,
  "subscription_usd": 0,
  "refill_usd": 0,
  "per_model_minimum_usd": 0.05,
  "next_reset_at": "2026-05-01T00:00:00Z"
}

Unlike session_busy / internal_error, this is not transient — retrying the same request won't resolve it. Either top up (when paid tier ships) or wait for next_reset_at. The bucket breakdown is included here because an exhausted caller needs to know which bucket is empty and when funds return. per_model_minimum_usd is reported for reference — the rejection is on the balance being at or below zero, not on that figure.

unknown_models body#

{
  "error": "unknown_models",
  "message": "One or more model IDs are not in the registry.",
  "retryable": false,
  "unknown_models": ["gpt-typo"],
  "models_requested": ["claude-opus-4-6", "gpt-typo"]
}

Preflight check. Fails before any provider call is made, so no credit is debited.


Confidence scores#

When models emit self-reported confidence (via {{C=0.8}}…{{/C}} tags in their prose, or on snippet commentary), those scores surface on responses:

  • responses[].claim_confidence: [{ claim_text, confidence_score }] — per-claim scores extracted from prose. Tags are stripped from text before return.
  • responses[].snippets[].comment_confidence: number | null
  • confidence_disclaimer: string — verbatim advisory at the top of every session response.

These are self-reported and only meaningful relative to the same model's other claims. They are not calibrated across models. If you display them, surface the disclaimer too.


Naming philosophy#

Field names you see in API requests and responses are the canonical contract — they're the names mumo guarantees to consumers. Internal type names and DB columns may differ; the serializer maps between them. We follow a contract-first principle and one-way mapping (internal → API), with the full mapping documented in docs/CONVENTIONS.md.

One convention worth knowing up front: snippet types (type field) are always UPPERCASE at the API boundary — KEEP, EXPLORE, CHALLENGE, CORE, SHIFT. They're lowercase only in internal storage.

We don't rename API fields casually. Any boundary rename comes with a deprecation period that accepts the old name as alias.