# MindTwins developer docs

Give an MCP-capable agent or a server application access to a Mind Twin from one workspace.

Documentation: https://mindtwins.com/developers
Markdown: https://mindtwins.com/developers/docs
[OpenAPI 3.1 reference](https://mindtwins.com/developers/openapi.json). Examples use illustrative values unless stated otherwise.

Documentation revision: 2026-10-08. This date versions the guide, not the API. REST paths are currently unversioned.

Use mcp.mindtwins.com/mcp for MCP clients and app.mindtwins.com/api/twin-mcp for REST requests. These are different transports for the same Mind Twin integration; keep the hostname and path together.

This guide covers custom MCP and REST integrations. Automated installer validation is not part of the integration workflow.

## Choose a path

- MCP: let an agent choose and call the right Mind Twin tool from plain language.
- REST API: call the same capabilities directly from any server language and receive structured JSON.

Both paths use the same three-action flow:

1. Upload a dated transcript.
2. Check periodically until the Mind Twin is ready.
3. Ask a question and receive its answer, Think, Feel, Say, and Act result.

Re-uploading a transcript already built returns 200. If that same content is still processing, 409 ALREADY_PROCESSING means nothing more to do. Do not upload it again.

## Before your first call

1. **Set up the workspace.** Use the MindTwins app to set up a shared team workspace and invite each person whose Mind Twin you need. Invitees receive their Mind Twin when they join. There is no public endpoint to create a Mind Twin.
2. **Get the right key.** Open MCP Twin in the app. Use the provisioned twin:chat key for your own Mind Twin. For teammates, ask your workspace administrator to enable Mind Twin Teams and provide a key with twin:upload. Keep the key on your server.
3. **Confirm who you can reach.** Call list_twins in MCP or GET /twins in REST. Use the returned display name as target, or the person's email if names collide. Listing is not an access guarantee. Check GET /me/status with the intended target before uploading.

## Prepare a transcript

- Label every speaker with their full name, matching the person's name in MindTwins. The Mind Twin identifies its person by name.
- Send one meeting per transcript. Keep speaker labels attached to what each person said.
- Use VTT, SRT, or plain text for a straightforward first upload. CSV is not supported.
- Use the real meeting date. Never guess or default to today; confirm each transcript separately.

Good: full names identify the speakers.

```vtt
WEBVTT

00:00:01.000 --> 00:00:05.000
<v Maya Chen>I want to test one region before we commit to a full rollout.</v>

00:00:05.000 --> 00:00:09.000
<v Jordan Lee>What would you need to see before expanding?</v>

00:00:09.000 --> 00:00:14.000
<v Maya Chen>Four weeks of steady adoption and a clear way to stop if it slips.</v>
```

Wrong: anonymous labels cannot identify the person.

```text
Speaker 1: I want to test one region first.
Meet participant 2: What would you need to see?
```

Recommended request budget: under 4,000,000 bytes for the complete UTF-8 JSON body. This is a conservative client budget, not an exact service maximum. Count the encoded body, including JSON escaping and metadata. Oversized requests can return a plain-text 413 FUNCTION_PAYLOAD_TOO_LARGE before the API runs. The MCP transcript schema limit does not override HTTP or downstream limits. REST has a larger application cap of 52,428,800 bytes for transcript plus segment text and 54,525,952 bytes for the raw JSON body; the public hosting limit applies first, so these are not usable upload sizes at this URL.

## Node.js prerequisite

The universal MCP install command runs through `npx`, so install Node.js first:

| Platform | Action |
| --- | --- |
| Mac | Run `brew install node` |
| Windows | Download Node.js from https://nodejs.org |
| Linux (Debian or Ubuntu) | Run `sudo apt install nodejs` |

Verify both Node.js and `npx` before continuing:

```bash
node --version
npx --version
```

## MCP quickstart

1. Open MCP Twin in the app and copy the provisioned `twin:chat` key.
2. Run the universal installer below. It detects supported clients and adds the hosted server with your Bearer header.
3. Open the client and verify the connection. Some clients load new servers only when they open. If the mind-twin tools do not appear, fully quit and reopen the client, then ask which mind-twin tools it can see.

```bash
npx -y add-mcp mcp-remote --name mind-twin \
  --args=https://mcp.mindtwins.com/mcp \
  --args=--header --args="Authorization: Bearer dev_YOUR_TWIN_CHAT_KEY" \
  --all -g -y
```

**Claude Code only (optional):**

```bash
claude mcp add --transport http mind-twin https://mcp.mindtwins.com/mcp \
  --header "Authorization: Bearer dev_YOUR_TWIN_CHAT_KEY"
```

| Setting | Value |
| --- | --- |
| Server | `https://mcp.mindtwins.com/mcp` |
| Client connector name | `mind-twin` |
| Transport | Streamable HTTP |
| Hosting | Managed for you |
| Prerequisite | Node.js for `npx` |

Supported fast-path clients include Claude Desktop, Claude Code, Cursor, and any client that supports Streamable HTTP MCP.

**After install:** Some clients load new servers only when they open. If the mind-twin tools do not appear, fully quit and reopen the client, then ask which mind-twin tools it can see.

## MCP tools

Feed, check readiness, and ask is the core loop. Use `whoami` or `list_twins` to find your identity or workspace members, then verify access with `get_status`. This table lists integration tools; automated installer checks are outside this workflow.

MCP chat does not remember earlier messages. Include the relevant conversation context in each message.

| Tool | What it does | Inputs |
| --- | --- | --- |
| `whoami` | Shows your identity and an early readiness signal. Use get_status for build readiness. | None. |
| `list_twins` | Lists workspace Mind Twins by display name. Use get_status with the intended target to verify access and build readiness. | None. |
| `chat_with_twin` | Asks a Mind Twin. Include conversation context in each message; prior messages are not remembered. | message (required, 1 to 800,000 characters), target, format (text by default or json). |
| `upload_transcript` | Feeds a Mind Twin a dated transcript. | transcript (required, up to 10,000,000 characters), date (required, real ISO-8601 meeting date), title (up to 500 characters), format (vtt, srt, text, or agent-json; omit for VTT/text passthrough), target, participants, source, duration (seconds), self_speaker. See the field reference below. |
| `get_status` | Checks whether a Mind Twin is ready. | target (optional). |
| `list_snapshots` | Lists a Mind Twin's snapshots with id, name, status, and counts. Results are paginated. | target (up to 200 characters), cursor (1 to 500 characters), limit (integer 1 to 100; default 25). |
| `snapshot_detail` | Shows one snapshot's summaries and counts. Raw psychometric records are withheld; the profile can include derived scores and confidence labels. | snapshot_id (required, 1 to 200 characters), target, detail (concise or detailed; default detailed). |
| `delete_raw` | Permanently removes one transcript from recall while keeping the snapshot counts and Mind Twin; no rebuild. Preview without confirm, then confirm with the expected_count. Irreversible. | snapshot_id (required, 1 to 200 characters), confirm (omit for preview; true to delete), expected_count (nonnegative integer, required with confirm), idempotency_key (1 to 200 characters), target. |

MCP tool results use a content array containing text. Upload and status return readable text. Chat with format: json puts a JSON string inside content[0].text; parse that string to read the answer fields. These examples show the tool result, without the outer JSON-RPC envelope. Tool failures normally set isError: true; inspect it before reading content as success.

### upload_transcript

Give a Mind Twin dated context. Learning starts in the background.

The minimal example uses five fields. The full tool accepts nine; participants, source, duration, and self_speaker are optional meeting details. Inspect tools/list if your connected server differs. Recommended request budget: under 4,000,000 bytes for the complete UTF-8 JSON body. This is a conservative client budget, not an exact service maximum. Count the encoded body, including JSON escaping and metadata. Oversized requests can return a plain-text 413 FUNCTION_PAYLOAD_TOO_LARGE before the API runs. The MCP transcript schema limit does not override HTTP or downstream limits. REST has a larger application cap of 52,428,800 bytes for transcript plus segment text and 54,525,952 bytes for the raw JSON body; the public hosting limit applies first, so these are not usable upload sizes at this URL. Use the real meeting date and ask for it when it is unknown.

Request:

```json
{
  "name": "upload_transcript",
  "arguments": {
  "date": "2026-10-07",
  "title": "Rollout planning",
  "format": "vtt",
  "transcript": "WEBVTT\n\n00:00:01.000 --> 00:00:05.000\n<v Maya Chen>I want to test one region before we commit to a full rollout.</v>\n\n00:00:05.000 --> 00:00:09.000\n<v Jordan Lee>What would you need to see before expanding?</v>\n\n00:00:09.000 --> 00:00:14.000\n<v Maya Chen>Four weeks of steady adoption and a clear way to stop if it slips.</v>",
  "target": "Maya Chen"
}
}
```

Example response:

```json
{
  "content": [
    {
      "type": "text",
      "text": "Uploaded \"Rollout planning\" to Maya Chen's team twin.\n\n---\nThe team twin is getting ready \u2014 it sharpens as more transcripts are added."
    }
  ]
}
```

### get_status

Check at the recommended polling interval. A ready result means the Mind Twin can answer.

A Mind Twin with no transcripts needs an upload; REST status returns 200 building with progress 0 and needs_upload: true. A missing Mind Twin returns 404 NOT_FOUND. Ready plus ingesting stays chattable while new material lands. Ready plus hydrating means the latest upload is not available yet. Investigate failed builds and requests that exceed your integration's waiting budget.

Request:

```json
{
  "name": "get_status",
  "arguments": {
    "target": "Maya Chen"
  }
}
```

Example response:

```json
{
  "content": [
    {
      "type": "text",
      "text": "Maya Chen's twin is still getting ready (42% ready) \u2014 this can take a while; keep polling calmly."
    }
  ]
}
```

### chat_with_twin

Ask one consequential question and request JSON when your agent needs structure.

format defaults to text. Pass json for the structured answer, think, feel, say, and act object inside content[0].text. Missing TFSA fields may be omitted. MCP chat does not remember earlier messages. Include the relevant conversation context in each message.

Request:

```json
{
  "name": "chat_with_twin",
  "arguments": {
    "message": "What would make you reject this rollout?",
    "target": "Maya Chen",
    "format": "json"
  }
}
```

Example response:

```json
{
  "content": [
    {
      "type": "text",
      "text": "{\n  \"answer\": \"I would reject a company-wide rollout without a contained pilot.\",\n  \"think\": \"The upside is clear. The implementation risk is not.\",\n  \"feel\": \"Interested, but exposed if adoption fails.\",\n  \"say\": \"Show me one region working first.\",\n  \"act\": \"Approve a 30-day pilot with a stop point.\"\n}"
    }
  ]
}
```

### Upload field reference

Limits below are validated by the MCP tool. REST names show what the MCP client sends.

| MCP field | REST field | Type and limit | Meaning |
| --- | --- | --- | --- |
| `transcript` | `transcript` | string · required · 1 to 10,000,000 characters | One meeting in VTT, SRT, plain text, or agent JSON. The character limit is the MCP schema limit; the encoded request budget still applies. |
| `date` | `date` | ISO-8601 string · required | The real meeting start time with its UTC offset, or YYYY-MM-DD when the time zone is unknown. Use the recording app's meeting time or a date supplied in the transcript or by the user. Never use file timestamps. Preserve the exact string on retries. |
| `title` | `title` | string · optional · 1 to 500 characters | The meeting's actual title, when known. |
| `format` | `format` | enum · optional | vtt, srt, text, or agent-json. Omit to pass VTT or plain text through; set srt or agent-json explicitly. There is no auto or json format value. |
| `target` | `target` | string · optional · up to 200 characters | A teammate's name or email; requires twin:upload. Whitespace is trimmed. Omit or leave blank for your own Mind Twin. |
| `participants` | `participants` | string[] · optional · up to 100 names | Each name is trimmed and limited to 200 characters. Blank names are dropped. Keep the same order on retries. |
| `source` | `source` | enum · optional | The recording app: fathom, zoom, gong, google_meet, fireflies, microsoft_teams, manual, sybill, affinity_notetaker, granola, raw_import. Omit or use manual for pasted text; use raw_import for another app or an export. Never guess. |
| `duration` | `duration` | number · optional · zero or greater | Meeting duration in seconds, not minutes or milliseconds. Helps space lines when the transcript has no timestamps. |
| `self_speaker` | `selfSpeaker` | string · optional · up to 200 characters | The exact speaker label for the Mind Twin's owner, or the teammate named by target. With VTT, text, or omitted format, also include it in participants. With SRT or agent-json, it is checked against transcript speakers and participants. A mismatch returns SELF_NOT_A_SPEAKER and saves nothing. Omit it to match by name. |

For a retry, resend the same transcript, exact date string, title, format, target, source, duration, and participants in the same order. Changing meeting details can save a second copy. Deduplication compares generated transcript content within the selected Mind Twin. A completed duplicate returns 200; queued or processing duplicates can also return 200 in workspaces with a build queue. ALREADY_PROCESSING means the same transcript is in progress: check status instead of uploading again. BUILD_IN_PROGRESS means this upload was not saved: wait for the current build, then retry. If an upload reply is lost, check status first and preserve the original payload for any retry.

## REST API quickstart

Base URL: `https://app.mindtwins.com/api/twin-mcp`

Send authenticated requests from server-side code:

```http
Authorization: Bearer <your-key>
Content-Type: application/json
```

No SDK is required. Use any HTTPS client. Never expose a `dev_` key in browser or mobile code.

### cURL

Request:

```bash
curl --connect-timeout 10 --max-time 600 https://app.mindtwins.com/api/twin-mcp/chat \
  -H "Authorization: Bearer $MINDTWINS_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "message": "What would make you reject this rollout?",
    "target": "Maya Chen"
  }'
```

Example response:

```json
{
  "answer": "I would reject a company-wide rollout without a contained pilot.",
  "think": "The upside is clear. The implementation risk is not.",
  "feel": "Interested, but exposed if adoption fails.",
  "say": "Show me one region working first.",
  "act": "Approve a 30-day pilot with a stop point."
}
```

### Node.js

Request:

```javascript
// npm install undici
import { request } from "undici";

const { statusCode, headers, body } = await request(
  "https://app.mindtwins.com/api/twin-mcp/chat",
  {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.MINDTWINS_KEY}`,
      "Content-Type": "application/json",
    },
    headersTimeout: 600_000,
    bodyTimeout: 600_000,
    signal: AbortSignal.timeout(600_000),
    body: JSON.stringify({
      message: "What would make you reject this rollout?",
      target: "Maya Chen",
    }),
  },
);

if (statusCode < 200 || statusCode >= 300) {
  await body.dump();
  throw new Error(`MindTwins request failed: ${statusCode}`);
}
if (!String(headers["content-type"]).includes("application/json")) {
  await body.dump();
  throw new Error("Expected a JSON response");
}
const mindTwin = await body.json();
```

Example response:

```json
{
  "answer": "I would reject a company-wide rollout without a contained pilot.",
  "think": "The upside is clear. The implementation risk is not.",
  "feel": "Interested, but exposed if adoption fails.",
  "say": "Show me one region working first.",
  "act": "Approve a 30-day pilot with a stop point."
}
```

### Python

Request:

```python
import os
import requests

response = requests.post(
    "https://app.mindtwins.com/api/twin-mcp/chat",
    headers={
        "Authorization": f"Bearer {os.environ['MINDTWINS_KEY']}"
    },
    json={
        "message": "What would make you reject this rollout?",
        "target": "Maya Chen",
    },
    timeout=(10, 600),
)

response.raise_for_status()
mind_twin = response.json()
```

Example response:

```json
{
  "answer": "I would reject a company-wide rollout without a contained pilot.",
  "think": "The upside is clear. The implementation risk is not.",
  "feel": "Interested, but exposed if adoption fails.",
  "say": "Show me one region working first.",
  "act": "Approve a 30-day pilot with a stop point."
}
```

## REST endpoint reference

| Intent | MCP tool | REST endpoint | Returns |
| --- | --- | --- | --- |
| Ask a Mind Twin | `chat_with_twin` | POST `/chat` | Answer + TFSA |
| Add a meeting | `upload_transcript` | POST `/upload` | Processing state |
| Check readiness | `get_status` | GET `/me/status` | Status + progress |
| Check service | Not exposed | GET `/health` | Service status |
| Identify yourself | `whoami` | GET `/me` | Identity + own Mind Twin readiness |
| Find a Mind Twin | `list_twins` | GET `/twins` | Mind Twins with name, email, ownership, ready, status |
| Browse snapshots | `list_snapshots` | GET `/snapshots` | snapshots, next_cursor, total |
| Inspect a snapshot | `snapshot_detail` | GET `/snapshots/{id}` | Snapshot summaries and counts |
| Remove raw material | `delete_raw` | DELETE `/snapshots/{id}/raw` | Dry run or confirmed deletion |

### POST /chat

REST chat accepts history: up to 20 earlier turns, each with role (user or assistant) and content. User content allows 800,000 UTF-16 code units; assistant content allows 100,000. Send the turns you want considered on each request. There is no conversation ID that restores earlier turns.

Use context for attached text: one string or up to 10 strings. Each item allows 100,000 UTF-16 code units and, after trimming and normalization, at most 51,200 UTF-8 bytes. Context is allowed on the current question and earlier user turns, never assistant turns. Extract text from PDFs or documents before sending it. The prepared current message plus context must fit 1,470,000 characters after escaping; a punctuation-heavy message can hit this limit before 800,000 input characters. The complete JSON body must also fit the hosting limit.

Requests accept Unicode text and have no language selector or published per-language guarantee. Test representative transcripts and questions in the languages your integration needs before rollout.

Example with history and attached text:

```json
{
  "message": "How should we respond?",
  "context": [
    "Jordan Lee: Can we expand the pilot next week?"
  ],
  "history": [
    {
      "role": "user",
      "content": "What matters most for this launch?"
    },
    {
      "role": "assistant",
      "content": "A clear rollback plan."
    }
  ]
}
```

Ask one question. Omit target to use your own Mind Twin. MCP also accepts a blank target; REST chat and upload reject an empty string. If a name matches more than one person, the API returns 422 AMBIGUOUS_TARGET; use their email. list_twins shows name (email) when names collide. The response is the bare answer, think, feel, say, act object; empty parts are omitted. The REST API does not accept the MCP-only format field.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `message` | string, 1 to 800,000 characters | Yes | The question to answer. |
| `target` | string | No | a teammate's display name (see list_twins) or email. Requires `twin:upload` for a teammate. |

Request:

```http
POST https://app.mindtwins.com/api/twin-mcp/chat
Authorization: Bearer $MINDTWINS_KEY
Content-Type: application/json

{
  "message": "What would make you reject this rollout?",
  "target": "Maya Chen"
}
```

Example response:

```json
{
  "answer": "I would reject a company-wide rollout without a contained pilot.",
  "think": "The upside is clear. The implementation risk is not.",
  "feel": "Interested, but exposed if adoption fails.",
  "say": "Show me one region working first.",
  "act": "Approve a 30-day pilot with a stop point."
}
```

`answer` is the complete response. `say` is the short spoken line. Empty Think, Feel, Say, or Act fields are omitted.

### POST /upload

Use the real meeting date in ISO 8601, from January 1, 2000 through 24 hours ahead of server time. Confirm the date for every transcript.

REST-only segments accepts up to 10,000 objects: text (required string), speaker (optional full name), startTime and endTime (optional nonnegative seconds). transcript and date remain required. When segments is supplied, it takes precedence over format normalization. Keep transcript and segments consistent; their combined text contributes to the size limit.

For format: agent-json, transcript is a string containing JSON, not a nested object. Serialize either an array of segments or an object with a segments array. Each segment uses text, speaker, startTime, and endTime; times are nonnegative seconds. Blank or non-string text is skipped, invalid timestamps are ignored, and only the first 10,000 usable segments are retained. Malformed JSON or no usable text returns 400 INVALID_TRANSCRIPT.

```json
{
  "date": "2026-10-07",
  "format": "agent-json",
  "transcript": "{\"segments\":[{\"speaker\":\"Maya Chen\",\"text\":\"Start with a small pilot.\",\"startTime\":0,\"endTime\":4},{\"speaker\":\"Jordan Lee\",\"text\":\"We will review the results first.\",\"startTime\":4,\"endTime\":8}]}"
}
```

Recommended request budget: under 4,000,000 bytes for the complete UTF-8 JSON body. This is a conservative client budget, not an exact service maximum. Count the encoded body, including JSON escaping and metadata. Oversized requests can return a plain-text 413 FUNCTION_PAYLOAD_TOO_LARGE before the API runs. The MCP transcript schema limit does not override HTTP or downstream limits. REST has a larger application cap of 52,428,800 bytes for transcript plus segment text and 54,525,952 bytes for the raw JSON body; the public hosting limit applies first, so these are not usable upload sizes at this URL.

Learning continues in the background. The MCP transcript limit is 10,000,000 characters at the schema level; request-body limits still apply.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `transcript` | string | Yes | VTT, SRT, plain text, or agent JSON. |
| `date` | ISO-8601 string | Yes | The real meeting date or timestamp. Invalid dates return 400 INVALID_DATE. Use the real meeting date; confirm each transcript separately. |
| `title` | string, max 500 characters | No | A human-readable meeting name. |
| `format` | `vtt`, `srt`, `text`, or `agent-json` | No | Omit for VTT/text passthrough; set srt or agent-json explicitly. |
| `target` | string | No | a teammate's display name (see list_twins) or email. Requires `twin:upload` for a teammate. |
| `participants` | string[] | No | Meeting speaker names; keep their order on retry. |
| `duration` | nonnegative number | No | Meeting duration in seconds. |
| `source` | enum | No | Recording app; values below. |
| `selfSpeaker` | string | No | Exact owner speaker label; MCP spells this self_speaker. For VTT/text, also include it in participants. |

`source` values: fathom, zoom, gong, google_meet, fireflies, microsoft_teams, manual, sybill, affinity_notetaker, granola, raw_import.

For a retry, resend the same transcript, exact date string, title, format, target, source, duration, and participants in the same order. Changing meeting details can save a second copy. Deduplication compares generated transcript content within the selected Mind Twin. A completed duplicate returns 200; queued or processing duplicates can also return 200 in workspaces with a build queue. ALREADY_PROCESSING means the same transcript is in progress: check status instead of uploading again. BUILD_IN_PROGRESS means this upload was not saved: wait for the current build, then retry. If an upload reply is lost, check status first and preserve the original payload for any retry.

Request:

```http
POST https://app.mindtwins.com/api/twin-mcp/upload
Authorization: Bearer $MINDTWINS_KEY
Content-Type: application/json

{
  "date": "2026-10-07",
  "title": "Rollout planning",
  "format": "vtt",
  "transcript": "WEBVTT\n\n00:00:01.000 --> 00:00:05.000\n<v Maya Chen>I want to test one region before we commit to a full rollout.</v>\n\n00:00:05.000 --> 00:00:09.000\n<v Jordan Lee>What would you need to see before expanding?</v>\n\n00:00:09.000 --> 00:00:14.000\n<v Maya Chen>Four weeks of steady adoption and a clear way to stop if it slips.</v>",
  "target": "Maya Chen"
}
```

Example response:

```json
{
  "success": true,
  "data": {
    "status": "processing"
  }
}
```

### GET /me/status

Check readiness. Use `?target=Maya%20Chen` for a teammate. Recommended client strategy: check status every 15 seconds, then every 30 to 60 seconds for longer builds. These intervals are guidance, not server requirements. Build time varies; use status rather than a fixed delay. No completion webhooks are available.

Read ready, building, or failed together with the optional flags. needs_upload: true means upload a transcript instead of polling an empty Mind Twin. ready + hydrating: true means it can answer from previously learned material, but the latest upload is not available yet. ready + ingesting: true means a transcript is being folded in. Treat absent flags as false; low or zero early progress alone does not mean failure.

- **needs_upload:** A Mind Twin with no transcripts returns 200 building, progress 0, and needs_upload: true. Upload a transcript to begin.
- **no Mind Twin in workspace:** REST returns 404 NOT_FOUND when your account has no Mind Twin in this workspace. Set one up in the MindTwins app.
- **building:** Learning is in progress. Check every 15 seconds, then back off for longer builds.
- **ready:** The Mind Twin is ready to answer.
- **ready + ingesting:** It stays chattable while a new transcript is folded in.
- **ready + hydrating:** It can answer from previously learned material. The latest upload is not available until its build finishes.
- **failed:** The build needs attention.

Request:

```http
GET https://app.mindtwins.com/api/twin-mcp/me/status?target=Maya%20Chen
Authorization: Bearer $MINDTWINS_KEY
```

Example response:

```json
{
  "status": "building",
  "progress": 42,
  "ingesting": false
}
```

### GET /health

Check service liveness. No key is required.

Request:

```http
GET https://app.mindtwins.com/api/twin-mcp/health
```

Example response:

```json
{
  "status": "ok",
  "service": "twin-mcp",
  "timestamp": "2026-10-08T14:35:56.581Z"
}
```

### GET /me

Identify yourself. Use GET /me/status to check build readiness; the identity readiness flag is a lighter signal. Returns 404 NO_TWIN if you have no Mind Twin.

None.

All of these endpoints require the same Bearer key.

Example response (illustrative values):

```json
{
  "twin_id": "example-twin_id",
  "mind_id": "example-mind_id",
  "workspace_id": "example-workspace_id",
  "user_id": "example-user_id",
  "name": "Maya Chen",
  "email": "maya\u0040example.com",
  "handle": "maya",
  "ready": true,
  "status": "ready"
}
```

### GET /twins

List the workspace directory with name, email, ownership (self or team), ready, and status. Listing does not guarantee target permission; use GET /me/status to check access and build readiness.

None. The response contains twins: [...].

All of these endpoints require the same Bearer key.

Example response (illustrative values):

```json
{
  "twins": [
    {
      "twin_id": "example-twin_id",
      "mind_id": "example-mind_id",
      "name": "Maya Chen",
      "email": "maya\u0040example.com",
      "handle": "maya",
      "ready": true,
      "status": "ready",
      "ownership": "self"
    }
  ]
}
```

### GET /snapshots

Browse a Mind Twin's snapshots. The response contains snapshots, next_cursor, and total.

Query: target, cursor, limit (1 to 100; default 25).

All of these endpoints require the same Bearer key.

Example response (illustrative values):

```json
{
  "snapshots": [
    {
      "snapshot_id": "example-snapshot",
      "date": "2026-10-07",
      "filename": "Rollout planning",
      "status": "completed",
      "counts": {
        "data_points": 120,
        "key_topics": 4,
        "findings": 8
      }
    }
  ],
  "next_cursor": null,
  "total": 1
}
```

### GET /snapshots/{id}

Read one snapshot's summaries and counts. Raw psychometric records are withheld; the profile can include derived scores and confidence labels.

Path: id. Query: detail (concise or detailed), target.

All of these endpoints require the same Bearer key.

Example response (illustrative values):

```json
{
  "ready": true,
  "snapshot_id": "example-snapshot",
  "date": "2026-10-07",
  "filename": "Rollout planning",
  "status": "completed",
  "counts": {
    "data_points": 120,
    "key_topics": 4,
    "findings": 8
  },
  "summary": {
    "disposition": null,
    "synthesis": [
      {
        "ticker": "SYN-01",
        "label": "Decision style",
        "value": "Prefers evidence before expanding a rollout."
      }
    ],
    "categories": [
      {
        "index": "01",
        "title": "Decision-making",
        "items": [
          {
            "subIndex": "01.1",
            "label": "Trade-offs",
            "value": "Balances speed with a clear rollback plan."
          }
        ]
      }
    ]
  },
  "profile": {
    "twin": "Maya Chen",
    "proceedings": 1,
    "findings": 8,
    "points": 120,
    "domainSummary": null,
    "coreProfile": null,
    "buckets": []
  }
}
```

### DELETE /snapshots/{id}/raw

Preview removal without confirm, then permanently remove raw material with confirm: true and the dry run's expected_count. A stale count returns 409 PRECONDITION_FAILED. The snapshot counts and Mind Twin remain; no rebuild.

Path: id. JSON body: confirm, dry_run, expected_count, idempotency_key, target. dry_run: true overrides confirm. Confirmation requires the latest expected_count. The idempotency cache lasts 10 minutes per server instance; do not rely on it across retries to a different instance. Reconcile a lost response with a fresh dry run.

All of these endpoints require the same Bearer key.

Example response (illustrative values):

```json
{
  "dry_run": true,
  "snapshot_id": "example-snapshot",
  "found": true,
  "would_delete": {
    "mind_sync_rows": 1
  },
  "expected_count": 1,
  "message": "This is a dry run. No data has been removed."
}
```

## Snapshot outputs and data controls

Findings and Mindsets are learned outputs associated with a snapshot. Browse snapshots, then inspect a snapshot by its ID. The API does not provide a combined per-person Findings or Mindsets view.

Snapshot lists use snapshot_id, date, filename, status, and counts; counts can be null while processing or when a recall entry has no linked engine snapshot. In concise detail, summary and profile are null; they can also be null for an unlinked recall entry. Pass snapshot_id unchanged to detail or deletion, including an rc: prefix if present. next_cursor is opaque; stop when it is absent or null. In detail, ready: false means the snapshot is still processing. Findings appear in summary.categories and Mindsets in profile.buckets. MCP renders readable Markdown, limited to 9 finding categories, 6 items per category, and 12 mindset attributes; use REST detail when you need the structured sections.

delete_raw removes one transcript from the Mind Twin's recall store and browse list. It does not delete the snapshot or Mind Twin, erase learned outputs, or trigger a rebuild. The public integration API has no Mind Twin deletion, snapshot deletion, or export endpoint.

## Mind Twin Teams

Add a teammate's display name (see list_twins) or email. as `target` to route every action to their team Mind Twin. Omit target to use your own Mind Twin. MCP also accepts a blank target; REST chat and upload reject an empty string. If a name matches more than one person, the API returns 422 AMBIGUOUS_TARGET; use their email. list_twins shows name (email) when names collide.

- Workspace-scoped: a target never resolves outside the key's workspace.
- Team-only: private Mind Twins cannot be reached through `target`.
- Owner-controlled: off-limits topics and private material stay withheld.
- Team trust model: A teammate target requires twin:upload, team visibility in the same workspace, and permission under any target allowlist attached to the key. The directory can show members that this key cannot target. A directory entry is not proof of access; verify the intended target with GET /me/status before uploading.

The key identifies the caller. The workspace is derived from that key and is never supplied by the request.

## Authentication and scopes

| Scope | Access |
| --- | --- |
| `twin:chat` | Every action on the key owner's Mind Twin, including snapshot browsing and delete_raw. |
| `twin:upload` | Adds teammate targeting, subject to workspace visibility and any key target allowlist. Mind Twin Teams only. |

Keys are provisioned in the app. There is no public create-key endpoint.

1. Keep keys server-side. Never ship a `dev_` key in browser or mobile code.
2. Rotate on exposure. Regenerate the key in MCP Twin; the old key stops working immediately.
3. Use the narrow path. Omit `target` when you only need the key owner's Mind Twin.

A valid key starts with `dev_` and is 41 characters in total, including `dev_`.

## Rate limits and timing

| Key environment | Requests / minute | Requests / hour | Requests / day |
| --- | --- | --- | --- |
| Development | 60 | 1,000 | 10,000 |
| Production | 300 | 10,000 | 100,000 |

Limits apply per key. A 429 response does not include a Retry-After header; read the retryAfter ISO timestamp in its JSON body. If it is absent or invalid, use bounded exponential backoff with jitter. Do not immediately retry. A 403 IP_BLOCKED response has separate Retry-After handling.

On a 429, X-RateLimit-Limit-Minute, X-RateLimit-Remaining-Minute, X-RateLimit-Limit-Hour, X-RateLimit-Remaining-Hour, X-RateLimit-Limit-Day, and X-RateLimit-Remaining-Day contain request counts. X-RateLimit-Reset-Minute is an ISO timestamp. Hour and day reset headers are not returned. Successful integration responses do not guarantee these headers. Use retryAfter in the body for the window that actually blocked the request.

Direct REST chat can take up to 9 minutes; allow 10 minutes in your HTTP client and proxy. MCP has a separate 290-second downstream request budget and a 300-second hosting limit. A longer MCP client timeout cannot extend that server budget. After a timeout, the server may still be working; avoid an immediate retry loop.

Recommended client strategy: check status every 15 seconds, then every 30 to 60 seconds for longer builds. These intervals are guidance, not server requirements. Build time varies; use status rather than a fixed delay. No completion webhooks are available.

## Errors and retries

Application errors return JSON with `error`; `code` and `message` are usually present. Check Content-Type before parsing: hosting-layer failures can return plain text.

Example 401 response (missing Authorization header):

```json
{
  "error": "Missing or invalid Authorization header",
  "message": "Provide a developer API key: Authorization: Bearer dev_xxx"
}
```

| Status | Code | Meaning | Your next move |
| --- | --- | --- | --- |
| 400 | BAD_REQUEST | Invalid snapshot ID or deletion confirmation | Copy the ID from the list and use the latest dry-run expected_count. |
| 500 | none | Authentication or rate-limit service failed | Retry with bounded backoff; contact support if it persists. |
| 502 | UPSTREAM | Snapshot browse or raw deletion failed | Back off; reconcile a deletion with a fresh dry run before retrying confirmation. |
| 400 | none ("error": "Validation error") | Missing or invalid field | Fix the request; message or details identifies the invalid field. |
| 400 | INVALID_DATE | The server rejected the upload date | Send the real meeting date. |
| 400 | INVALID_TRANSCRIPT | Empty or unrecognized transcript | Send a supported format and match format to the content. |
| 400 | NO_ORG | The Mind Twin is not fully provisioned for uploads | Contact support to complete setup. |
| 400 | SELF_NOT_A_SPEAKER | The owner label does not match a speaker | Correct self_speaker (MCP) or selfSpeaker (REST); for VTT/text, include it in participants. Nothing was saved. |
| 400 | INVALID_CURSOR | The snapshot page cursor is invalid | Restart browsing without cursor. |
| 401 | none | Missing, malformed, or revoked key | Re-copy or regenerate the key. |
| 403 | none ("Insufficient scope") or INSUFFICIENT_SCOPE | The key lacks the scope; a teammate target needs twin:upload | Omit target, or use a Mind Twin Teams key. |
| 403 | NO_CALLER | The key is not tied to a user | Check the key owner in the app. |
| 403 | IP_BLOCKED | Too many failed key attempts from your IP | Wait for the Retry-After header (seconds), then fix the key. |
| 404 | NOT_FOUND | No Mind Twin for your account in this workspace | Set one up in the MindTwins app. |
| 404 | NO_TWIN | GET /me found no Mind Twin for your account | Set one up in the MindTwins app. |
| 404 | TARGET_NOT_FOUND | target does not match a reachable team Mind Twin | Check the name or email with GET /twins. |
| 404 | MIND_NOT_FOUND | Upload target has no mind to feed yet | Set it up in the app first. |
| 409 | ALREADY_PROCESSING | Same transcript is already processing | Nothing to do. Do not upload it again. |
| 409 | BUILD_IN_PROGRESS | Mind Twin is busy with another build | Retry after it finishes. |
| 409 | ORPHANED_ARTIFACT | A previous upload did not finish building; this upload was not saved | Retry the same upload; contact support if it repeats. |
| 409 | PRECONDITION_FAILED | Raw deletion count changed since the dry run | Run the dry run again before confirming. |
| 413 | CONTENT_TOO_LARGE / FUNCTION_PAYLOAD_TOO_LARGE | Message, transcript, or encoded request body too large | Chat: up to 800,000 characters. Keep the complete upload request under the recommended 4,000,000-byte budget. Hosting-layer errors may be plain text. |
| 422 | AMBIGUOUS_TARGET | Name matches more than one person | Use their email. |
| 422 | LOW_QUALITY_CONTENT | Not enough meaningful conversation to learn from | Use a longer, substantive transcript. Aim for a few hundred words; this is guidance, not a fixed minimum. |
| 423 | TWIN_PAUSED | The owner paused the Mind Twin | Resume your Mind Twin in the app, or wait for the teammate to resume theirs. |
| 429 | none | Rate limit | Wait until the retryAfter body field (an ISO timestamp), then back off. |
| 502 / 503 / 504 | INTERNAL / UNAVAILABLE / TIMEOUT | Transient service or upstream failure | Back off for read requests. For an upload or deletion with a lost response, reconcile the outcome before retrying. |

MCP may refuse a call before REST runs: INVALID_SNAPSHOT_ID means the snapshot ID is invalid; copy it unchanged from list_snapshots. KEY_FORMAT means an invalid key shape; KEY_RECENTLY_REJECTED means the key was recently refused; KEY_CHECKS_PAUSED means new key checks are briefly paused. Fix the key or wait as directed instead of looping. OUTCOME_UNKNOWN means an upload or deletion may have completed despite a lost reply. For upload, preserve the exact payload. For deletion, run a fresh dry run without confirm to check whether the transcript remains. These are MCP-side conditions, not additional REST response codes.

## Troubleshooting

### The client sees no tools

Run the install command again, confirm the Bearer key is present, then restart the MCP client.

### Status is building with little visible progress

That is expected for a fresh Mind Twin. In MCP, a blank or zero learned-transcript count is not a live meter. Check at the recommended polling interval. A Mind Twin with nothing uploaded returns 200 building with progress 0 and needs_upload: true. Investigate failed builds and requests that exceed your integration's waiting budget.

### Status says needs_upload

A Mind Twin with nothing uploaded returns 200 building, progress 0, and needs_upload: true. Upload a transcript to start building. A 404 NOT_FOUND means this account has no Mind Twin in the workspace; set one up in the MindTwins app.

### Status says ready and ingesting

Keep chatting. The Mind Twin remains ready while it folds in the new transcript; recall of that material sharpens shortly.

### The transcript date is unknown

Ask for the real meeting date. Never guess, infer, or default to today. When uploading multiple transcripts, confirm each date separately.

### The API says the transcript already exists

Deduplication compares generated transcript content within the selected Mind Twin, not just the filename. If the content was already built, the normal response is 200. Workspaces with a build queue can also return 200 while the same content is still queued or processing. If it is still processing, 409 ALREADY_PROCESSING means nothing more to do. Do not upload it again. For 409 BUILD_IN_PROGRESS on different content, retry after the build finishes.

### The key format is rejected

A valid provisioned key starts with `dev_` and is 41 characters in total, including `dev_`. Re-copy it from MCP Twin.

## Documentation changelog

### 2026-10-08

- Added source-verified REST history, attached text, agent JSON, response examples, rate headers, and an OpenAPI reference.
- Added workspace setup, named-speaker transcript examples, and per-key rate limits.
- Aligned chat limits, corrected the live health response, and replaced the 50 MB upload claim with a conservative request budget.
- Explained stateless MCP chat, long requests, polling, snapshot outputs, and current data controls.
- Made examples readable without email decoding and linked the Markdown reference directly.
- Verified MCP upload fields, source values, tool result envelopes, readiness flags, and retry behavior against the current server source.