# REST API Essentials for Miro MCP

Low-level API reference for Miro MCP tools, error handling, cost model, and real-world integration patterns.

## Tool-by-Tool API Reference

### Discovery: `board_list_items`

**Endpoint:** `POST /mcp/board/{board_id}/items/list`

**Parameters:**
```json
{
  "limit": 100,
  "cursor": "next_cursor_token",
  "item_type": "document|prototype|diagram|frame|table|sticky|shape|image",
  "parent_id": "container_id"
}
```

**Response:**
```json
{
  "items": [
    {
      "id": "item_123",
      "type": "document",
      "title": "Product Roadmap",
      "url": "https://miro.com/app/board/xyz/?select=item_123",
      "parent_id": "frame_456",
      "created_at": "2026-02-15T10:00:00Z"
    }
  ],
  "cursor": "next_cursor_token"
}
```

**Error codes:**
- `400 Bad Request` — Invalid board_id or parameters
- `401 Unauthorized` — Token expired or invalid
- `403 Forbidden` — Access denied (wrong team or no permissions)
- `404 Not Found` — Board doesn't exist
- `429 Too Many Requests` — Rate limited

---

### Discovery: `context_explore`

**Endpoint:** `POST /mcp/board/{board_id}/context/explore`

**Parameters:**
```json
{
  "include_types": ["frame", "document", "prototype", "table", "diagram"]
}
```

**Response:**
```json
{
  "frames": [
    {
      "id": "frame_123",
      "title": "Requirements",
      "url": "https://miro.com/app/board/xyz/?select=frame_123",
      "item_count": 42
    }
  ],
  "documents": [
    {
      "id": "doc_456",
      "title": "Architecture PRD",
      "url": "https://miro.com/app/board/xyz/?select=doc_456"
    }
  ],
  "prototypes": [],
  "tables": [],
  "diagrams": []
}
```

**Error codes:**
- `400 Bad Request` — Invalid parameters
- `401 Unauthorized` — Token expired
- `403 Forbidden` — Access denied

---

### Content Reading: `context_get`

**Endpoint:** `POST /mcp/board/{board_id}/items/{item_id}/context`

**Parameters:**
```json
{
  "item_type": "document|prototype|frame|image"
}
```

**Response varies by item_type:**

**Document response:**
```json
{
  "item_type": "document",
  "content": "<h1>Architecture PRD</h1><p>This document describes...</p>",
  "format": "html",
  "version": 5,
  "last_modified": "2026-02-15T14:30:00Z"
}
```

**Prototype response:**
```json
{
  "item_type": "prototype",
  "screens": [
    {
      "id": "screen_1",
      "title": "Home Screen",
      "markup": "<div class='screen'><button>Login</button></div>"
    }
  ],
  "format": "ui_markup"
}
```

**Frame response:**
```json
{
  "item_type": "frame",
  "content": "Summary of frame contents generated by AI...",
  "format": "summary",
  "child_items": 15
}
```

**⚠️ Cost:** Uses Miro AI credits

**Error codes:**
- `400 Bad Request` — Invalid item_id or type
- `401 Unauthorized` — Token expired
- `403 Forbidden` — Access denied
- `429 Too Many Requests` — Rate limited (this tool is most restrictive)

---

### Diagram: `diagram_create`

**Endpoint:** `POST /mcp/board/{board_id}/diagrams`

**Parameters:**
```json
{
  "type": "flowchart|uml_class|uml_sequence|erd",
  "dsl": "flowchart DSL text...",
  "title": "Architecture Diagram",
  "x": 1000,
  "y": 500,
  "width": 800,
  "height": 600
}
```

**Response:**
```json
{
  "id": "diagram_789",
  "type": "flowchart",
  "url": "https://miro.com/app/board/xyz/?select=diagram_789",
  "created_at": "2026-02-15T15:00:00Z"
}
```

**DSL Examples:**

Flowchart:
```
start
  -> action: "Process data"
  -> decision: "Valid?"
    -> yes -> action: "Save to DB"
    -> no -> action: "Log error"
  -> end
```

UML Class:
```
class User
  - id: String
  - email: String
  + login(password): boolean
  + logout(): void

class Board
  - id: String
  - owner: User
  + addItem(item): void
```

**Error codes:**
- `400 Bad Request` — Invalid DSL syntax
- `401 Unauthorized` — Token expired
- `403 Forbidden` — Access denied
- `422 Unprocessable Entity` — DSL syntax error with details

---

### Diagram: `diagram_get_dsl`

**Endpoint:** `GET /mcp/diagram-dsl/{diagram_type}`

**Parameters:**
```
diagram_type: flowchart|uml_class|uml_sequence|erd
```

**Response:**
```json
{
  "type": "flowchart",
  "syntax": {
    "start": "Marks beginning of flow",
    "end": "Marks end of flow",
    "action": "Represents an action/process",
    "decision": "Represents conditional branching"
  },
  "examples": [
    "start -> action: 'Do something' -> end"
  ],
  "colors": {
    "primary": "#1F77B4",
    "success": "#2CA02C",
    "error": "#D62728"
  },
  "max_nodes": 1000
}
```

**Error codes:**
- `404 Not Found` — Invalid diagram_type

---

### Documents: `doc_create`

**Endpoint:** `POST /mcp/board/{board_id}/documents`

**Parameters:**
```json
{
  "title": "Implementation Guide",
  "content": "# Header\n\nParagraph with **bold**...",
  "x": 1000,
  "y": 500
}
```

**Response:**
```json
{
  "id": "doc_123",
  "title": "Implementation Guide",
  "url": "https://miro.com/app/board/xyz/?select=doc_123",
  "created_at": "2026-02-15T15:30:00Z"
}
```

**Markdown support:**
- `# Heading 1`, `## Heading 2`, `### Heading 3`
- `**bold**`, `*italic*`
- `- bullet`, `* bullet`
- `[link text](https://...)`

**Error codes:**
- `400 Bad Request` — Invalid parameters
- `401 Unauthorized` — Token expired
- `403 Forbidden` — Access denied

---

### Documents: `doc_get`

**Endpoint:** `GET /mcp/board/{board_id}/documents/{doc_id}`

**Response:**
```json
{
  "id": "doc_123",
  "title": "Implementation Guide",
  "content": "# Header\n\nContent...",
  "version": 3,
  "last_modified": "2026-02-15T16:00:00Z",
  "created_at": "2026-02-15T10:00:00Z"
}
```

**Error codes:**
- `404 Not Found` — Document doesn't exist
- `401 Unauthorized` — Token expired
- `403 Forbidden` — Access denied

---

### Documents: `doc_update`

**Endpoint:** `PATCH /mcp/board/{board_id}/documents/{doc_id}`

**Parameters:**
```json
{
  "find": "Old text",
  "replace": "New text",
  "replace_all": true
}
```

**Response:**
```json
{
  "id": "doc_123",
  "changes_made": 3,
  "version": 4,
  "updated_at": "2026-02-15T16:10:00Z"
}
```

**Error codes:**
- `404 Not Found` — Document doesn't exist
- `400 Bad Request` — Find text not found
- `401 Unauthorized` — Token expired
- `403 Forbidden` — Access denied

---

### Tables: `table_create`

**Endpoint:** `POST /mcp/board/{board_id}/tables`

**Parameters:**
```json
{
  "title": "Task Tracking",
  "columns": [
    {
      "id": "col_1",
      "name": "Task",
      "type": "text"
    },
    {
      "id": "col_2",
      "name": "Status",
      "type": "select",
      "options": ["To Do", "In Progress", "Done"]
    }
  ],
  "x": 1000,
  "y": 500
}
```

**Response:**
```json
{
  "id": "table_123",
  "title": "Task Tracking",
  "url": "https://miro.com/app/board/xyz/?select=table_123",
  "columns": [...],
  "created_at": "2026-02-15T16:30:00Z"
}
```

**Error codes:**
- `400 Bad Request` — Invalid column definition
- `401 Unauthorized` — Token expired
- `403 Forbidden` — Access denied

---

### Tables: `table_list_rows`

**Endpoint:** `POST /mcp/board/{board_id}/tables/{table_id}/rows/list`

**Parameters:**
```json
{
  "limit": 100,
  "cursor": "cursor_token",
  "filter": {
    "column_id": "col_2",
    "value": "Done"
  }
}
```

**Response:**
```json
{
  "rows": [
    {
      "id": "row_1",
      "cells": {
        "col_1": "Implement auth",
        "col_2": "Done"
      }
    }
  ],
  "cursor": "next_cursor_token"
}
```

**Error codes:**
- `404 Not Found` — Table doesn't exist
- `401 Unauthorized` — Token expired
- `403 Forbidden` — Access denied

---

### Tables: `table_sync_rows`

**Endpoint:** `POST /mcp/board/{board_id}/tables/{table_id}/rows/sync`

**Parameters:**
```json
{
  "key_column": "col_1",
  "rows": [
    {
      "col_1": "Implement auth",
      "col_2": "In Progress"
    },
    {
      "col_1": "Write tests",
      "col_2": "To Do"
    }
  ]
}
```

**Upsert logic:**
- If row with matching `key_column` value exists → update it
- If row doesn't exist → create it
- Efficient for bulk updates

**Response:**
```json
{
  "added": 1,
  "updated": 1,
  "total": 2,
  "created_at": "2026-02-15T16:45:00Z"
}
```

**Error codes:**
- `404 Not Found` — Table doesn't exist
- `400 Bad Request` — Invalid row format
- `401 Unauthorized` — Token expired
- `403 Forbidden` — Access denied

---

### Images: `image_get_url`

**Endpoint:** `GET /mcp/board/{board_id}/images/{image_id}/url`

**Response:**
```json
{
  "url": "https://miro.cdn.com/images/...",
  "expires_in": 3600
}
```

**Error codes:**
- `404 Not Found` — Image doesn't exist
- `401 Unauthorized` — Token expired
- `403 Forbidden` — Access denied

---

### Images: `image_get_data`

**Endpoint:** `GET /mcp/board/{board_id}/images/{image_id}/data`

**Response:**
```json
{
  "data": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
  "format": "png|jpg|gif|webp",
  "size_bytes": 68
}
```

**Error codes:**
- `404 Not Found` — Image doesn't exist
- `401 Unauthorized` — Token expired
- `403 Forbidden` — Access denied

---

## Error Handling Patterns

### Handling Rate Limits (429)

```python
import time
import random

def call_with_exponential_backoff(func, max_retries=5):
  for attempt in range(max_retries):
    try:
      return func()
    except RateLimitError:
      wait_time = (2 ** attempt) + random.uniform(0, 1)
      print(f"Rate limited. Waiting {wait_time:.1f}s...")
      time.sleep(wait_time)
  raise Exception("Max retries exceeded")
```

### Handling Token Expiry (401)

```python
def call_with_token_refresh(func, refresh_func):
  try:
    return func()
  except UnauthorizedError:
    # Refresh token and retry
    new_token = refresh_func()
    return func()  # Retry with new token
```

### Handling Access Denied (403)

```python
def call_with_team_check(func, board_id):
  try:
    return func(board_id)
  except ForbiddenError as e:
    raise Exception(
      f"Access denied to board {board_id}. "
      f"Ensure board is in authenticated team. "
      f"Re-authenticate if needed."
    )
```

## Cost Model

### Free Operations
- `board_list_items` — Discovery
- `context_explore` — High-level structure
- `diagram_create` — DSL-based diagram creation
- `diagram_get_dsl` — DSL specification
- `doc_create` — Document creation
- `doc_get` — Document reading
- `doc_update` — Document editing
- `table_create` — Table creation
- `table_list_rows` — Row retrieval
- `table_sync_rows` — Bulk row updates
- `image_get_url` — Image URL
- `image_get_data` — Image binary data

### Paid Operations (Uses Miro AI Credits)
- `context_get` — Only tool using AI credits for content understanding

**Strategy:** Minimize `context_get` calls; use discovery tools (`context_explore`, `board_list_items`) to understand structure first, then selectively call `context_get` on key items only.

## Real-World Integration Examples

### Example 1: Analyze Board + Generate Docs

```python
# Step 1: Discover structure (free)
structure = context_explore(board_id)

# Step 2: Find key documents (free)
items = board_list_items(board_id, item_type="document")

# Step 3: Read one key document (paid)
prd = context_get(board_id, items[0].id, "document")

# Step 4: Generate response (free)
doc_create(board_id, title="Implementation Guide", content=generated_content)
```

### Example 2: Bulk Update Task List

```python
# Step 1: Read task table (free)
rows = table_list_rows(board_id, table_id, limit=1000)

# Step 2: Process rows
updated_rows = [update_status(row) for row in rows]

# Step 3: Bulk sync (free)
result = table_sync_rows(board_id, table_id, updated_rows)
```

### Example 3: Code Analysis → Diagrams

```python
# Step 1: Analyze codebase (local, free)
code_structure = analyze_code(repo_path)

# Step 2: Get DSL specification (free)
dsl_spec = diagram_get_dsl("uml_class")

# Step 3: Create diagram from DSL (free)
diagram = diagram_create(
  board_id,
  type="uml_class",
  dsl=generate_uml_from_code(code_structure, dsl_spec)
)
```

## Performance Tips

### Pagination for Large Boards

```python
def process_large_board(board_id, batch_size=100):
  cursor = None
  while True:
    items, cursor = board_list_items(
      board_id,
      limit=batch_size,
      cursor=cursor
    )
    process_items(items)
    if not cursor:
      break
```

### Caching to Reduce Calls

```python
class MiroCache:
  def __init__(self):
    self.cache = {}
  
  def get_document(self, board_id, doc_id):
    key = f"{board_id}:{doc_id}"
    if key not in self.cache:
      self.cache[key] = context_get(board_id, doc_id, "document")
    return self.cache[key]
```

## Next Steps

- **Tools & prompts:** See [references/mcp-prompts.md](mcp-prompts.md) for high-level tool reference
- **Best practices:** See [references/best-practices.md](best-practices.md) for workflow patterns
- **Setup:** See [references/ai-coding-tools.md](ai-coding-tools.md) for client-specific setup
