Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.manticscore.com/llms.txt

Use this file to discover all available pages before exploring further.

The ManticScore WebSocket endpoint lets you subscribe to multiple entity channels over a single connection. Rather than opening a separate HTTP streaming request for each job, you connect once and subscribe to any mix of research runs, build graphs, Forge executions, chat sessions, and more.

Connecting

GET wss://api.manticscore.com/ws?token=<session_token>
Pass your session token as a query parameter. You can also pass a Clerk JWT — the server auto-mints a session token for the lifetime of the connection.
ManticScore allows one WebSocket connection per user. Opening a new connection evicts the previous one with close code 4003. Make sure your client handles reconnection gracefully rather than opening multiple connections.
After a successful upgrade, the server immediately sends a connected frame:
{"v":1, "event":"connected", "data":{"user_id":"usr_abc", "server_time":"2026-04-23T12:00:00Z"}}

Catchup frame

If you have any in-flight or recently completed work, the server sends a catchup frame right after connected. This lets you know what to subscribe to without polling the REST API first.
{
  "v": 1,
  "event": "catchup",
  "data": {
    "in_flight": [
      {
        "entity_id": "uuid",
        "channel": "research",
        "status": "running",
        "stage": "analyze",
        "last_event_seq": 8,
        "project_id": "uuid"
      }
    ],
    "completed": [
      {
        "entity_id": "uuid",
        "channel": "build",
        "project_id": "uuid",
        "title": "Auth layer"
      }
    ]
  }
}
Use in_flight[].last_event_seq as your cursor when subscribing, so you don’t re-receive events you already have.

Client messages

Send JSON text frames to control your subscriptions:
Subscribe to a channel for a specific entity. The server replays events after cursor, sends a subscribed confirmation, then streams live events.
{"action": "subscribe", "entity_id": "<uuid>", "channel": "research", "cursor": 0}
Pass the seq of the last event you received as cursor to resume without gaps.
Stop receiving events for an entity. The server auto-unsubscribes when it forwards a done event, but you can also unsubscribe manually.
{"action": "unsubscribe", "entity_id": "<uuid>"}
Send a client-side keepalive. The server replies with a pong frame.
{"action": "ping"}

Server messages

EventWhenKey data
connectedOn connectuser_id, server_time
catchupAfter connected, when work is in flightin_flight, completed arrays
subscribedAfter a successful subscribeentity_id, channel, replayed count
pingServer heartbeat every 30s{}
pongReply to client ping{}
auth_expiredBefore a 4001 close{}
<entity event>Live event for a subscribed entityseq, entity_id, channel, event-specific data
Entity events carry the same payloads as their HTTP NDJSON counterparts. See NDJSON streaming protocol for event shapes.

Auto-progression events

Several entity events are emitted by background hooks rather than direct user action. They follow the same envelope as any other entity event but are worth calling out so clients know to render them:
EventChannelWhen
project.seededprojectAfter POST /projects finishes, with suggested competitors and features drawn from similar prior research. May arrive with empty arrays when no similar prior data exists. See POST /projects.
card.forge_readychatWhen a build node expansion produces leaves that are low-risk, small-effort, and high-confidence. Posted into the project’s most recent chat session with a Ship it action that pre-fills start_implementation. Capped at 3 cards per expansion. See POST /build-nodes/{id}/expand.
revision_updatedbriefWhen a Forge PR is appended to the project’s progress section. The brief itself may have been auto-bootstrapped from the latest research if one did not already exist. See Pull requests.
Clients must ignore unknown event types so older builds remain forward-compatible — additional auto-progression events may be added without a version bump.

Available channels

ChannelWhat it tracksTerminal statuses
researchResearch jobscompleted, failed
buildBuild graphsready, failed
briefBriefsready, failed
forgeForge coding runscompleted, failed, cancelled, rejected
feature_researchFeature deep researchcompleted, failed
chatChat sessionscompleted, failed
signal_crawlCompetitive monitor crawlscompleted, failed
company_enrichCompany enrichmentenriched, failed
implementImplementation sessionscompleted, failed
learningBackground learning jobscompleted, failed
projectProject-scoped fanout — research, build, brief, Forge, chat, and agent events for a single projectnone (long-lived)
The project channel does not have terminal statuses — it stays open as long as the project exists and fans in events from every per-domain channel that carries a project_id. Subscribe with entity_id set to the project UUID. Use GET /projects/:id/home to bootstrap the screen, then pass the returned ws.last_event_seq as cursor to resume without gaps. Per-domain channels remain available for clients that only need a single domain’s events.

Connection lifecycle

  • Idle timeout: 90 seconds. Keep the connection alive by sending ping frames or subscribing to active channels.
  • Server heartbeat: the server sends a ping frame every 30 seconds.
  • Auth revalidation: your session is re-checked every 5 minutes. If it has expired, you receive an auth_expired frame followed by close code 4001. Re-authenticate and reconnect.

Close codes

CodeMeaning
1000Normal close or idle timeout
1001Server shutdown
4001Auth expired — re-authenticate and reconnect
4002Missing or invalid token
4003Replaced by a newer connection from the same user

JavaScript example

This example connects, handles the catchup frame, subscribes to a research job, and processes incoming events:
class ManticScoreWebSocket {
  constructor(token) {
    this.token = token;
    this.ws = null;
  }

  connect() {
    const url = `wss://api.manticscore.com/ws?token=${this.token}`;
    this.ws = new WebSocket(url);

    this.ws.addEventListener("message", (e) => {
      const frame = JSON.parse(e.data);
      this.handleFrame(frame);
    });

    this.ws.addEventListener("close", (e) => {
      console.log(`Closed: ${e.code} ${e.reason}`);
      if (e.code === 4001) {
        // Re-authenticate, then reconnect
      } else if (e.code !== 1000) {
        setTimeout(() => this.connect(), 2000); // backoff reconnect
      }
    });
  }

  handleFrame(frame) {
    switch (frame.event) {
      case "connected":
        console.log("Connected as", frame.data.user_id);
        break;

      case "catchup":
        // Subscribe to any in-flight jobs
        for (const job of frame.data.in_flight) {
          this.subscribe(job.entity_id, job.channel, job.last_event_seq);
        }
        break;

      case "subscribed":
        console.log(
          `Subscribed to ${frame.data.channel}/${frame.data.entity_id}`,
          `(replayed ${frame.data.replayed} events)`
        );
        break;

      case "stage":
        console.log(`Stage: ${frame.data.name}${frame.data.status}`);
        break;

      case "result":
        console.log("Result received:", frame.data);
        break;

      case "done":
        // The server auto-unsubscribes after done
        console.log("Job complete for", frame.data.entity_id);
        break;

      default:
        // Forward-compatible: ignore unknown event types
        break;
    }
  }

  subscribe(entityId, channel, cursor = 0) {
    this.ws.send(
      JSON.stringify({ action: "subscribe", entity_id: entityId, channel, cursor })
    );
  }

  unsubscribe(entityId) {
    this.ws.send(JSON.stringify({ action: "unsubscribe", entity_id: entityId }));
  }

  ping() {
    this.ws.send(JSON.stringify({ action: "ping" }));
  }
}

// Usage
const socket = new ManticScoreWebSocket("your-session-token");
socket.connect();

// Subscribe to a specific research job from scratch
socket.subscribe("your-job-uuid", "research", 0);
If you use the WebSocket for long sessions, set up a client-side ping interval (every 60s or so) to prevent the 90-second idle timeout from closing your connection.