miro-api

Master Miro whiteboard automation using the REST API v2 and Python SDK. This skill covers board creation, widget management, frames, shapes, connectors, templates, and real-time collaboration patterns for building automated visual workflows.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "miro-api" with this command: npx skills add vamseeachanta/workspace-hub/vamseeachanta-workspace-hub-miro-api

Miro API Skill

Master Miro whiteboard automation using the REST API v2 and Python SDK. This skill covers board creation, widget management, frames, shapes, connectors, templates, and real-time collaboration patterns for building automated visual workflows.

When to Use This Skill

USE when:

  • Automating sprint retrospective board creation

  • Building visual project status dashboards

  • Creating automated architecture diagrams

  • Setting up templated workshop boards

  • Integrating Miro with CI/CD pipelines

  • Automating user story mapping workflows

  • Creating visual incident response boards

  • Building automated onboarding boards

  • Generating meeting facilitation templates

  • Syncing data from external systems to Miro

DON'T USE when:

  • Simple text-based collaboration (use Slack or Teams)

  • Document-focused workflows (use Notion or Confluence)

  • Code-focused diagramming (use Mermaid or PlantUML)

  • Real-time whiteboarding without persistence

  • Personal note-taking (use Obsidian)

Prerequisites

Miro App Setup

1. Create a Miro App at https://miro.com/app/settings/user-profile/apps

2. Choose REST API 2.0

3. Configure OAuth 2.0 scopes

Required OAuth Scopes:

- boards:read - Read board data

- boards:write - Create and modify boards

- team:read - Read team information

- identity:read - Read user identity

- microphone:read - Read board audio (optional)

- screen_recording - Screen recording (optional)

App Permissions for different use cases:

Team App: boards:read, boards:write, team:read

User App: boards:read, boards:write, identity:read

Admin App: All scopes + organization management

Get Access Token via OAuth 2.0:

1. Redirect user to: https://miro.com/oauth/authorize?response_type=code&client_id=CLIENT_ID&redirect_uri=REDIRECT_URI

2. Exchange code for token at: https://api.miro.com/v1/oauth/token

Python Environment Setup

Create virtual environment

python -m venv miro-env source miro-env/bin/activate # Linux/macOS

miro-env\Scripts\activate # Windows

Install Miro API SDK

pip install miro-api

Install additional dependencies

pip install python-dotenv requests httpx aiohttp

Create requirements.txt

cat > requirements.txt << 'EOF' miro-api>=2.0.0 python-dotenv>=1.0.0 requests>=2.31.0 httpx>=0.25.0 aiohttp>=3.9.0 Pillow>=10.0.0 EOF

Environment variables

cat > .env << 'EOF' MIRO_ACCESS_TOKEN=your-access-token MIRO_CLIENT_ID=your-client-id MIRO_CLIENT_SECRET=your-client-secret MIRO_TEAM_ID=your-team-id EOF

API Authentication

auth.py

ABOUTME: Miro API authentication utilities

ABOUTME: Handles OAuth2 token management and refresh

import os from dotenv import load_dotenv import requests from datetime import datetime, timedelta

load_dotenv()

class MiroAuth: """Miro OAuth2 authentication handler"""

BASE_URL = "https://api.miro.com"
AUTH_URL = "https://miro.com/oauth/authorize"
TOKEN_URL = "https://api.miro.com/v1/oauth/token"

def __init__(self):
    self.client_id = os.environ.get("MIRO_CLIENT_ID")
    self.client_secret = os.environ.get("MIRO_CLIENT_SECRET")
    self.access_token = os.environ.get("MIRO_ACCESS_TOKEN")
    self.refresh_token = os.environ.get("MIRO_REFRESH_TOKEN")
    self.token_expires = None

def get_authorization_url(self, redirect_uri: str, state: str = None) -> str:
    """Generate OAuth2 authorization URL"""
    params = {
        "response_type": "code",
        "client_id": self.client_id,
        "redirect_uri": redirect_uri,
    }
    if state:
        params["state"] = state

    query = "&#x26;".join(f"{k}={v}" for k, v in params.items())
    return f"{self.AUTH_URL}?{query}"

def exchange_code_for_token(self, code: str, redirect_uri: str) -> dict:
    """Exchange authorization code for access token"""
    response = requests.post(
        self.TOKEN_URL,
        data={
            "grant_type": "authorization_code",
            "client_id": self.client_id,
            "client_secret": self.client_secret,
            "code": code,
            "redirect_uri": redirect_uri,
        },
    )
    response.raise_for_status()
    token_data = response.json()

    self.access_token = token_data["access_token"]
    self.refresh_token = token_data.get("refresh_token")
    self.token_expires = datetime.now() + timedelta(
        seconds=token_data.get("expires_in", 3600)
    )

    return token_data

def refresh_access_token(self) -> dict:
    """Refresh the access token"""
    response = requests.post(
        self.TOKEN_URL,
        data={
            "grant_type": "refresh_token",
            "client_id": self.client_id,
            "client_secret": self.client_secret,
            "refresh_token": self.refresh_token,
        },
    )
    response.raise_for_status()
    token_data = response.json()

    self.access_token = token_data["access_token"]
    self.refresh_token = token_data.get("refresh_token", self.refresh_token)

    return token_data

def get_headers(self) -> dict:
    """Get authorization headers for API requests"""
    return {
        "Authorization": f"Bearer {self.access_token}",
        "Content-Type": "application/json",
    }

Core Capabilities

  1. Board Management

boards.py

ABOUTME: Miro board management operations

ABOUTME: Create, read, update, delete boards

import os from miro_api import Miro from dotenv import load_dotenv

load_dotenv()

Initialize Miro client

miro = Miro(access_token=os.environ.get("MIRO_ACCESS_TOKEN"))

def create_board(name: str, description: str = "", team_id: str = None) -> dict: """Create a new Miro board""" team_id = team_id or os.environ.get("MIRO_TEAM_ID")

board = miro.boards.create(
    name=name,
    description=description,
    team_id=team_id,
    policy={
        "permissionsPolicy": {
            "collaborationToolsStartAccess": "all_editors",
            "copyAccess": "anyone",
            "sharingAccess": "team_members_with_editing_rights",
        },
        "sharingPolicy": {
            "access": "private",
            "inviteToAccountAndBoardLinkAccess": "editor",
            "organizationAccess": "private",
            "teamAccess": "edit",
        },
    },
)

return {
    "id": board.id,
    "name": board.name,
    "view_link": board.view_link,
    "created_at": board.created_at,
}

def get_board(board_id: str) -> dict: """Get board details""" board = miro.boards.get(board_id)

return {
    "id": board.id,
    "name": board.name,
    "description": board.description,
    "view_link": board.view_link,
    "created_at": board.created_at,
    "modified_at": board.modified_at,
}

def list_boards(team_id: str = None, limit: int = 50) -> list: """List all boards in a team""" team_id = team_id or os.environ.get("MIRO_TEAM_ID")

boards = miro.boards.get_all(team_id=team_id, limit=limit)

return [
    {
        "id": board.id,
        "name": board.name,
        "view_link": board.view_link,
    }
    for board in boards
]

def update_board(board_id: str, name: str = None, description: str = None) -> dict: """Update board properties""" update_data = {} if name: update_data["name"] = name if description: update_data["description"] = description

board = miro.boards.update(board_id, **update_data)

return {"id": board.id, "name": board.name, "description": board.description}

def delete_board(board_id: str) -> bool: """Delete a board""" miro.boards.delete(board_id) return True

def copy_board(board_id: str, new_name: str, team_id: str = None) -> dict: """Copy an existing board""" team_id = team_id or os.environ.get("MIRO_TEAM_ID")

board = miro.boards.copy(board_id, name=new_name, team_id=team_id)

return {
    "id": board.id,
    "name": board.name,
    "view_link": board.view_link,
}

Usage example

if name == "main": # Create a board board = create_board( name="Sprint Retrospective - Q1 2026", description="Team retrospective for Q1 sprint", ) print(f"Created board: {board['view_link']}")

# List boards
boards = list_boards(limit=10)
for b in boards:
    print(f"- {b['name']}: {b['view_link']}")

2. Sticky Notes and Cards

sticky_notes.py

ABOUTME: Sticky note and card creation

ABOUTME: Create, position, and style sticky notes

from miro_api import Miro import os

miro = Miro(access_token=os.environ.get("MIRO_ACCESS_TOKEN"))

def create_sticky_note( board_id: str, content: str, x: float = 0, y: float = 0, color: str = "yellow", width: float = 200, ) -> dict: """Create a sticky note on a board"""

# Color mapping
colors = {
    "yellow": "yellow",
    "green": "green",
    "blue": "blue",
    "pink": "pink",
    "orange": "orange",
    "purple": "violet",
    "gray": "gray",
    "cyan": "cyan",
    "red": "red",
    "light_yellow": "light_yellow",
    "light_green": "light_green",
    "light_blue": "light_blue",
    "light_pink": "light_pink",
}

sticky = miro.sticky_notes.create(
    board_id=board_id,
    data={"content": content, "shape": "square"},
    style={"fillColor": colors.get(color, "yellow")},
    position={"x": x, "y": y, "origin": "center"},
    geometry={"width": width},
)

return {
    "id": sticky.id,
    "content": sticky.data.content,
    "position": {"x": sticky.position.x, "y": sticky.position.y},
    "color": sticky.style.fill_color,
}

def create_sticky_grid( board_id: str, items: list, start_x: float = 0, start_y: float = 0, columns: int = 4, spacing: float = 250, color: str = "yellow", ) -> list: """Create a grid of sticky notes""" created = []

for i, item in enumerate(items):
    row = i // columns
    col = i % columns

    x = start_x + (col * spacing)
    y = start_y + (row * spacing)

    sticky = create_sticky_note(
        board_id=board_id, content=item, x=x, y=y, color=color
    )
    created.append(sticky)

return created

def create_card( board_id: str, title: str, description: str = "", x: float = 0, y: float = 0, assignee_id: str = None, due_date: str = None, tags: list = None, ) -> dict: """Create a card widget"""

card_data = {"title": title, "description": description}

if assignee_id:
    card_data["assigneeId"] = assignee_id
if due_date:
    card_data["dueDate"] = due_date
if tags:
    card_data["tagIds"] = tags

card = miro.cards.create(
    board_id=board_id,
    data=card_data,
    position={"x": x, "y": y, "origin": "center"},
    geometry={"width": 320, "height": 200},
)

return {
    "id": card.id,
    "title": card.data.title,
    "position": {"x": card.position.x, "y": card.position.y},
}

def update_sticky_note( board_id: str, sticky_id: str, content: str = None, color: str = None ) -> dict: """Update a sticky note""" update_data = {}

if content:
    update_data["data"] = {"content": content}
if color:
    update_data["style"] = {"fillColor": color}

sticky = miro.sticky_notes.update(board_id, sticky_id, **update_data)

return {"id": sticky.id, "content": sticky.data.content}

def delete_sticky_note(board_id: str, sticky_id: str) -> bool: """Delete a sticky note""" miro.sticky_notes.delete(board_id, sticky_id) return True

Retrospective board example

def create_retro_board(board_id: str) -> dict: """Create a retrospective board layout"""

# Create category headers
categories = [
    {"title": "What went well", "color": "green", "x": 0},
    {"title": "What to improve", "color": "pink", "x": 500},
    {"title": "Action items", "color": "blue", "x": 1000},
]

created_stickies = {}

for cat in categories:
    # Create header sticky
    header = create_sticky_note(
        board_id=board_id,
        content=f"&#x3C;strong>{cat['title']}&#x3C;/strong>",
        x=cat["x"],
        y=-200,
        color=cat["color"],
        width=400,
    )
    created_stickies[cat["title"]] = [header]

    # Create placeholder stickies
    for i in range(3):
        placeholder = create_sticky_note(
            board_id=board_id,
            content="Add your thoughts here...",
            x=cat["x"],
            y=i * 150,
            color=cat["color"],
        )
        created_stickies[cat["title"]].append(placeholder)

return created_stickies

if name == "main": board_id = "YOUR_BOARD_ID"

# Create grid of stickies
items = ["Task 1", "Task 2", "Task 3", "Task 4", "Task 5", "Task 6"]
stickies = create_sticky_grid(
    board_id=board_id, items=items, columns=3, color="blue"
)
print(f"Created {len(stickies)} sticky notes")

3. Shapes and Drawing

shapes.py

ABOUTME: Shape creation and manipulation

ABOUTME: Rectangles, circles, lines, and custom shapes

from miro_api import Miro import os

miro = Miro(access_token=os.environ.get("MIRO_ACCESS_TOKEN"))

def create_shape( board_id: str, shape_type: str, content: str = "", x: float = 0, y: float = 0, width: float = 200, height: float = 100, fill_color: str = "#ffffff", border_color: str = "#000000", border_width: int = 2, ) -> dict: """Create a shape on the board

Shape types: rectangle, circle, triangle, rhombus, parallelogram,
             trapezoid, pentagon, hexagon, octagon, wedge_round_rectangle_callout,
             round_rectangle, star, flow_chart_process, flow_chart_decision,
             flow_chart_terminator, flow_chart_data, flow_chart_document
"""

shape = miro.shapes.create(
    board_id=board_id,
    data={"content": content, "shape": shape_type},
    style={
        "fillColor": fill_color,
        "borderColor": border_color,
        "borderWidth": str(border_width),
        "borderStyle": "normal",
        "fontFamily": "arial",
        "fontSize": "14",
        "textAlign": "center",
        "textAlignVertical": "middle",
    },
    position={"x": x, "y": y, "origin": "center"},
    geometry={"width": width, "height": height},
)

return {
    "id": shape.id,
    "type": shape.data.shape,
    "position": {"x": shape.position.x, "y": shape.position.y},
}

def create_rectangle( board_id: str, content: str = "", x: float = 0, y: float = 0, width: float = 200, height: float = 100, fill_color: str = "#ffffff", ) -> dict: """Create a rectangle""" return create_shape( board_id=board_id, shape_type="rectangle", content=content, x=x, y=y, width=width, height=height, fill_color=fill_color, )

def create_circle( board_id: str, content: str = "", x: float = 0, y: float = 0, diameter: float = 100, fill_color: str = "#ffffff", ) -> dict: """Create a circle""" return create_shape( board_id=board_id, shape_type="circle", content=content, x=x, y=y, width=diameter, height=diameter, fill_color=fill_color, )

def create_flowchart_shape( board_id: str, shape_type: str, content: str = "", x: float = 0, y: float = 0, width: float = 150, height: float = 80, ) -> dict: """Create a flowchart shape

Types: flow_chart_process, flow_chart_decision, flow_chart_terminator,
       flow_chart_data, flow_chart_document, flow_chart_predefined_process,
       flow_chart_manual_input, flow_chart_display, flow_chart_preparation
"""

colors = {
    "flow_chart_process": "#e3f2fd",
    "flow_chart_decision": "#fff3e0",
    "flow_chart_terminator": "#f3e5f5",
    "flow_chart_data": "#e8f5e9",
    "flow_chart_document": "#fce4ec",
}

return create_shape(
    board_id=board_id,
    shape_type=shape_type,
    content=content,
    x=x,
    y=y,
    width=width,
    height=height,
    fill_color=colors.get(shape_type, "#ffffff"),
)

def create_flowchart(board_id: str, steps: list, start_x: float = 0, start_y: float = 0) -> list: """Create a flowchart from a list of steps

Each step: {"type": "process|decision|terminator", "content": "text"}
"""
created = []
current_y = start_y

for step in steps:
    shape_type = f"flow_chart_{step.get('type', 'process')}"
    height = 100 if step.get("type") == "decision" else 80

    shape = create_flowchart_shape(
        board_id=board_id,
        shape_type=shape_type,
        content=step["content"],
        x=start_x,
        y=current_y,
        height=height,
    )
    created.append(shape)

    current_y += height + 80  # Add spacing for connectors

return created

def update_shape( board_id: str, shape_id: str, content: str = None, fill_color: str = None, x: float = None, y: float = None, ) -> dict: """Update a shape""" update_data = {}

if content is not None:
    update_data["data"] = {"content": content}
if fill_color:
    update_data["style"] = {"fillColor": fill_color}
if x is not None or y is not None:
    position = {}
    if x is not None:
        position["x"] = x
    if y is not None:
        position["y"] = y
    update_data["position"] = position

shape = miro.shapes.update(board_id, shape_id, **update_data)
return {"id": shape.id}

if name == "main": board_id = "YOUR_BOARD_ID"

# Create a simple flowchart
steps = [
    {"type": "terminator", "content": "Start"},
    {"type": "process", "content": "Initialize system"},
    {"type": "decision", "content": "Valid input?"},
    {"type": "process", "content": "Process data"},
    {"type": "terminator", "content": "End"},
]

flowchart = create_flowchart(board_id, steps)
print(f"Created flowchart with {len(flowchart)} shapes")

4. Connectors and Lines

connectors.py

ABOUTME: Connector and line creation

ABOUTME: Connect shapes, create arrows, and diagram flows

from miro_api import Miro import os

miro = Miro(access_token=os.environ.get("MIRO_ACCESS_TOKEN"))

def create_connector( board_id: str, start_item_id: str, end_item_id: str, start_position: str = "right", end_position: str = "left", stroke_color: str = "#000000", stroke_width: int = 2, stroke_style: str = "normal", start_cap: str = "none", end_cap: str = "stealth", caption: str = None, ) -> dict: """Create a connector between two items

Positions: top, right, bottom, left, auto
Caps: none, stealth, rounded_stealth, diamond, diamond_filled, oval, oval_filled,
      arrow, triangle, triangle_filled, erd_one, erd_many, erd_one_or_many, erd_only_one,
      erd_zero_or_one, erd_zero_or_many
Stroke styles: normal, dashed, dotted
"""

connector_data = {
    "startItem": {"id": start_item_id, "position": {"x": start_position}},
    "endItem": {"id": end_item_id, "position": {"x": end_position}},
}

connector_style = {
    "strokeColor": stroke_color,
    "strokeWidth": str(stroke_width),
    "strokeStyle": stroke_style,
    "startStrokeCap": start_cap,
    "endStrokeCap": end_cap,
}

if caption:
    connector_data["captions"] = [{"content": caption, "position": "50%"}]

connector = miro.connectors.create(
    board_id=board_id,
    data=connector_data,
    style=connector_style,
)

return {
    "id": connector.id,
    "start_item": connector.start_item.id,
    "end_item": connector.end_item.id,
}

def create_line( board_id: str, start_x: float, start_y: float, end_x: float, end_y: float, stroke_color: str = "#000000", stroke_width: int = 2, end_cap: str = "none", ) -> dict: """Create a standalone line"""

# For lines without connected items, we use absolute coordinates
connector = miro.connectors.create(
    board_id=board_id,
    data={
        "startItem": {"position": {"x": start_x, "y": start_y}},
        "endItem": {"position": {"x": end_x, "y": end_y}},
    },
    style={
        "strokeColor": stroke_color,
        "strokeWidth": str(stroke_width),
        "startStrokeCap": "none",
        "endStrokeCap": end_cap,
    },
)

return {"id": connector.id}

def connect_flowchart_shapes(board_id: str, shape_ids: list, labels: list = None) -> list: """Connect a list of shapes in sequence""" created = []

for i in range(len(shape_ids) - 1):
    label = labels[i] if labels and i &#x3C; len(labels) else None

    connector = create_connector(
        board_id=board_id,
        start_item_id=shape_ids[i],
        end_item_id=shape_ids[i + 1],
        start_position="bottom",
        end_position="top",
        end_cap="stealth",
        caption=label,
    )
    created.append(connector)

return created

def create_decision_branches( board_id: str, decision_shape_id: str, yes_shape_id: str, no_shape_id: str, ) -> list: """Create Yes/No branches from a decision diamond"""

yes_connector = create_connector(
    board_id=board_id,
    start_item_id=decision_shape_id,
    end_item_id=yes_shape_id,
    start_position="bottom",
    end_position="top",
    end_cap="stealth",
    caption="Yes",
    stroke_color="#4caf50",
)

no_connector = create_connector(
    board_id=board_id,
    start_item_id=decision_shape_id,
    end_item_id=no_shape_id,
    start_position="right",
    end_position="left",
    end_cap="stealth",
    caption="No",
    stroke_color="#f44336",
)

return [yes_connector, no_connector]

def create_erd_relationship( board_id: str, entity1_id: str, entity2_id: str, cardinality_start: str = "erd_one", cardinality_end: str = "erd_many", label: str = None, ) -> dict: """Create an ERD relationship line

Cardinalities: erd_one, erd_many, erd_one_or_many, erd_only_one,
               erd_zero_or_one, erd_zero_or_many
"""

return create_connector(
    board_id=board_id,
    start_item_id=entity1_id,
    end_item_id=entity2_id,
    start_cap=cardinality_start,
    end_cap=cardinality_end,
    caption=label,
)

def update_connector( board_id: str, connector_id: str, stroke_color: str = None, caption: str = None, ) -> dict: """Update a connector""" update_data = {}

if stroke_color:
    update_data["style"] = {"strokeColor": stroke_color}
if caption:
    update_data["captions"] = [{"content": caption, "position": "50%"}]

connector = miro.connectors.update(board_id, connector_id, **update_data)
return {"id": connector.id}

def delete_connector(board_id: str, connector_id: str) -> bool: """Delete a connector""" miro.connectors.delete(board_id, connector_id) return True

if name == "main": board_id = "YOUR_BOARD_ID"

# Example: Connect shapes
shape_ids = ["shape1_id", "shape2_id", "shape3_id"]
connectors = connect_flowchart_shapes(board_id, shape_ids)
print(f"Created {len(connectors)} connectors")

5. Frames and Organization

frames.py

ABOUTME: Frame creation for board organization

ABOUTME: Group items, create sections, and manage layout

from miro_api import Miro import os from typing import List, Optional

miro = Miro(access_token=os.environ.get("MIRO_ACCESS_TOKEN"))

def create_frame( board_id: str, title: str, x: float = 0, y: float = 0, width: float = 800, height: float = 600, fill_color: str = "#f5f5f5", ) -> dict: """Create a frame to organize board content"""

frame = miro.frames.create(
    board_id=board_id,
    data={"title": title, "format": "custom"},
    style={"fillColor": fill_color},
    position={"x": x, "y": y, "origin": "center"},
    geometry={"width": width, "height": height},
)

return {
    "id": frame.id,
    "title": frame.data.title,
    "position": {"x": frame.position.x, "y": frame.position.y},
    "geometry": {"width": frame.geometry.width, "height": frame.geometry.height},
}

def create_frame_grid( board_id: str, titles: list, columns: int = 3, frame_width: float = 600, frame_height: float = 400, spacing: float = 50, start_x: float = 0, start_y: float = 0, ) -> list: """Create a grid of frames""" created = []

for i, title in enumerate(titles):
    row = i // columns
    col = i % columns

    x = start_x + col * (frame_width + spacing)
    y = start_y + row * (frame_height + spacing)

    frame = create_frame(
        board_id=board_id,
        title=title,
        x=x,
        y=y,
        width=frame_width,
        height=frame_height,
    )
    created.append(frame)

return created

def create_kanban_board( board_id: str, columns: list = None, frame_width: float = 400, frame_height: float = 800, ) -> list: """Create a Kanban-style board with columns"""

if columns is None:
    columns = ["To Do", "In Progress", "Review", "Done"]

colors = {
    "To Do": "#f5f5f5",
    "In Progress": "#fff3e0",
    "Review": "#e8f5e9",
    "Done": "#e3f2fd",
}

frames = []
start_x = 0

for i, column in enumerate(columns):
    frame = create_frame(
        board_id=board_id,
        title=column,
        x=start_x + i * (frame_width + 50),
        y=0,
        width=frame_width,
        height=frame_height,
        fill_color=colors.get(column, "#f5f5f5"),
    )
    frames.append(frame)

return frames

def create_workshop_layout( board_id: str, sections: list, section_width: float = 1000, section_height: float = 600 ) -> dict: """Create a workshop board layout

sections: list of {"title": "name", "description": "desc", "color": "#hex"}
"""
created = {"frames": [], "headers": []}

for i, section in enumerate(sections):
    # Create frame for section
    frame = create_frame(
        board_id=board_id,
        title=section["title"],
        x=0,
        y=i * (section_height + 100),
        width=section_width,
        height=section_height,
        fill_color=section.get("color", "#f5f5f5"),
    )
    created["frames"].append(frame)

return created

def update_frame( board_id: str, frame_id: str, title: str = None, width: float = None, height: float = None, ) -> dict: """Update a frame""" update_data = {}

if title:
    update_data["data"] = {"title": title}
if width or height:
    geometry = {}
    if width:
        geometry["width"] = width
    if height:
        geometry["height"] = height
    update_data["geometry"] = geometry

frame = miro.frames.update(board_id, frame_id, **update_data)
return {"id": frame.id, "title": frame.data.title}

def get_items_in_frame(board_id: str, frame_id: str) -> list: """Get all items contained within a frame""" frame = miro.frames.get(board_id, frame_id)

# Get child items
children = miro.frames.get_children(board_id, frame_id)

return [
    {"id": child.id, "type": child.type}
    for child in children
]

def add_items_to_frame(board_id: str, frame_id: str, item_ids: list) -> bool: """Add items to a frame by updating their parent""" for item_id in item_ids: # Items inside a frame are managed by their position # They need to be within the frame's boundaries pass return True

def delete_frame(board_id: str, frame_id: str) -> bool: """Delete a frame""" miro.frames.delete(board_id, frame_id) return True

if name == "main": board_id = "YOUR_BOARD_ID"

# Create Kanban board
kanban = create_kanban_board(board_id)
print(f"Created Kanban with {len(kanban)} columns")

# Create workshop layout
sections = [
    {"title": "Introduction", "color": "#e3f2fd"},
    {"title": "Brainstorming", "color": "#fff3e0"},
    {"title": "Voting", "color": "#e8f5e9"},
    {"title": "Action Items", "color": "#f3e5f5"},
]
workshop = create_workshop_layout(board_id, sections)
print(f"Created workshop with {len(workshop['frames'])} sections")

6. Text and Images

text_images.py

ABOUTME: Text elements and image handling

ABOUTME: Create text boxes, embed images, and manage media

from miro_api import Miro import os import requests from io import BytesIO

miro = Miro(access_token=os.environ.get("MIRO_ACCESS_TOKEN"))

def create_text( board_id: str, content: str, x: float = 0, y: float = 0, width: float = 200, font_size: int = 14, font_family: str = "arial", text_align: str = "left", color: str = "#000000", ) -> dict: """Create a text element"""

text = miro.texts.create(
    board_id=board_id,
    data={"content": content},
    style={
        "color": color,
        "fillOpacity": "1.0",
        "fontFamily": font_family,
        "fontSize": str(font_size),
        "textAlign": text_align,
    },
    position={"x": x, "y": y, "origin": "center"},
    geometry={"width": width},
)

return {
    "id": text.id,
    "content": text.data.content,
    "position": {"x": text.position.x, "y": text.position.y},
}

def create_heading( board_id: str, content: str, x: float = 0, y: float = 0, level: int = 1, ) -> dict: """Create a heading text element"""

font_sizes = {1: 36, 2: 28, 3: 22, 4: 18}
font_size = font_sizes.get(level, 14)

return create_text(
    board_id=board_id,
    content=f"&#x3C;strong>{content}&#x3C;/strong>",
    x=x,
    y=y,
    width=500,
    font_size=font_size,
)

def create_bullet_list( board_id: str, items: list, x: float = 0, y: float = 0, ) -> dict: """Create a bulleted list"""

content = "&#x3C;ul>" + "".join(f"&#x3C;li>{item}&#x3C;/li>" for item in items) + "&#x3C;/ul>"

return create_text(
    board_id=board_id,
    content=content,
    x=x,
    y=y,
    width=400,
)

def upload_image_from_url( board_id: str, image_url: str, x: float = 0, y: float = 0, width: float = None, title: str = None, ) -> dict: """Create an image from a URL"""

image_data = {"url": image_url}
if title:
    image_data["title"] = title

geometry = {}
if width:
    geometry["width"] = width

image = miro.images.create(
    board_id=board_id,
    data=image_data,
    position={"x": x, "y": y, "origin": "center"},
    geometry=geometry if geometry else None,
)

return {
    "id": image.id,
    "position": {"x": image.position.x, "y": image.position.y},
}

def upload_image_from_file( board_id: str, file_path: str, x: float = 0, y: float = 0, width: float = None, ) -> dict: """Upload an image from a local file"""

with open(file_path, "rb") as f:
    image_data = f.read()

# Use the image upload endpoint
headers = {
    "Authorization": f"Bearer {os.environ.get('MIRO_ACCESS_TOKEN')}",
}

files = {"resource": (os.path.basename(file_path), image_data)}
data = {"position": f'{{"x": {x}, "y": {y}, "origin": "center"}}'}

if width:
    data["geometry"] = f'{{"width": {width}}}'

response = requests.post(
    f"https://api.miro.com/v2/boards/{board_id}/images",
    headers=headers,
    files=files,
    data=data,
)
response.raise_for_status()

return response.json()

def create_embed( board_id: str, url: str, x: float = 0, y: float = 0, width: float = 400, height: float = 300, mode: str = "modal", ) -> dict: """Create an embedded content (web page, video, etc.)

Modes: inline, modal
"""

embed = miro.embeds.create(
    board_id=board_id,
    data={"url": url, "mode": mode},
    position={"x": x, "y": y, "origin": "center"},
    geometry={"width": width, "height": height},
)

return {
    "id": embed.id,
    "url": embed.data.url,
    "position": {"x": embed.position.x, "y": embed.position.y},
}

def create_document_section( board_id: str, title: str, description: str, items: list, x: float = 0, y: float = 0, ) -> dict: """Create a document-like section with title, description, and bullet points"""

created = {}

# Create title
heading = create_heading(board_id, title, x=x, y=y, level=2)
created["title"] = heading

# Create description
desc = create_text(board_id, description, x=x, y=y + 60, width=500)
created["description"] = desc

# Create bullet list
bullets = create_bullet_list(board_id, items, x=x, y=y + 150)
created["items"] = bullets

return created

if name == "main": board_id = "YOUR_BOARD_ID"

# Create a document section
section = create_document_section(
    board_id=board_id,
    title="Project Overview",
    description="This project aims to improve team collaboration through automated Miro workflows.",
    items=[
        "Automated board creation",
        "Template-based layouts",
        "Integration with CI/CD",
    ],
)
print(f"Created section with {len(section)} elements")

Integration Examples

GitHub Actions Integration

.github/workflows/miro-sync.yml

name: Sync to Miro

on: issues: types: [opened, labeled] pull_request: types: [opened, closed, merged]

jobs: sync-miro: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4

  - name: Set up Python
    uses: actions/setup-python@v5
    with:
      python-version: '3.12'

  - name: Install dependencies
    run: |
      pip install miro-api requests

  - name: Create issue card in Miro
    if: github.event_name == 'issues'
    env:
      MIRO_ACCESS_TOKEN: ${{ secrets.MIRO_ACCESS_TOKEN }}
      MIRO_BOARD_ID: ${{ secrets.MIRO_BOARD_ID }}
    run: |
      python &#x3C;&#x3C; 'EOF'
      import os
      from miro_api import Miro

      miro = Miro(access_token=os.environ["MIRO_ACCESS_TOKEN"])
      board_id = os.environ["MIRO_BOARD_ID"]

      # Create sticky note for new issue
      issue_title = "${{ github.event.issue.title }}"
      issue_number = "${{ github.event.issue.number }}"
      issue_url = "${{ github.event.issue.html_url }}"

      miro.sticky_notes.create(
          board_id=board_id,
          data={
              "content": f"#{issue_number}: {issue_title}\n\n{issue_url}"
          },
          style={"fillColor": "yellow"},
          position={"x": 0, "y": 0, "origin": "center"},
      )
      print(f"Created sticky note for issue #{issue_number}")
      EOF

  - name: Update PR status in Miro
    if: github.event_name == 'pull_request'
    env:
      MIRO_ACCESS_TOKEN: ${{ secrets.MIRO_ACCESS_TOKEN }}
      MIRO_BOARD_ID: ${{ secrets.MIRO_BOARD_ID }}
    run: |
      python &#x3C;&#x3C; 'EOF'
      import os
      from miro_api import Miro

      miro = Miro(access_token=os.environ["MIRO_ACCESS_TOKEN"])
      board_id = os.environ["MIRO_BOARD_ID"]

      pr_title = "${{ github.event.pull_request.title }}"
      pr_state = "${{ github.event.action }}"

      colors = {
          "opened": "light_blue",
          "closed": "red",
          "merged": "green",
      }

      miro.sticky_notes.create(
          board_id=board_id,
          data={"content": f"PR: {pr_title}\nStatus: {pr_state}"},
          style={"fillColor": colors.get(pr_state, "gray")},
          position={"x": 500, "y": 0, "origin": "center"},
      )
      EOF

Sprint Retrospective Automation

retro_automation.py

ABOUTME: Automated sprint retrospective board creation

ABOUTME: Creates templated retro board with categories

from miro_api import Miro import os from datetime import datetime

miro = Miro(access_token=os.environ.get("MIRO_ACCESS_TOKEN"))

def create_retrospective_board( sprint_number: int, team_name: str, team_id: str = None, ) -> dict: """Create a complete retrospective board for a sprint"""

team_id = team_id or os.environ.get("MIRO_TEAM_ID")

# Create board
board_name = f"Sprint {sprint_number} Retrospective - {team_name}"
board = miro.boards.create(
    name=board_name,
    description=f"Retrospective for Sprint {sprint_number}",
    team_id=team_id,
)
board_id = board.id

# Create frames for categories
categories = [
    {"title": "What Went Well", "color": "#c8e6c9", "x": 0},
    {"title": "What Could Be Improved", "color": "#ffcdd2", "x": 700},
    {"title": "Action Items", "color": "#bbdefb", "x": 1400},
]

frames = []
for cat in categories:
    frame = miro.frames.create(
        board_id=board_id,
        data={"title": cat["title"], "format": "custom"},
        style={"fillColor": cat["color"]},
        position={"x": cat["x"], "y": 0, "origin": "center"},
        geometry={"width": 600, "height": 800},
    )
    frames.append({"id": frame.id, "title": cat["title"]})

    # Add placeholder stickies
    for i in range(3):
        miro.sticky_notes.create(
            board_id=board_id,
            data={"content": "Add your thoughts here..."},
            style={"fillColor": cat["color"]},
            position={
                "x": cat["x"],
                "y": -200 + (i * 150),
                "origin": "center",
            },
        )

# Create header
miro.texts.create(
    board_id=board_id,
    data={
        "content": f"&#x3C;strong>Sprint {sprint_number} Retrospective&#x3C;/strong>&#x3C;br>{datetime.now().strftime('%B %d, %Y')}"
    },
    style={"fontSize": "36", "textAlign": "center"},
    position={"x": 700, "y": -500, "origin": "center"},
    geometry={"width": 800},
)

# Add voting instructions
miro.texts.create(
    board_id=board_id,
    data={
        "content": "Instructions:&#x3C;br>1. Add sticky notes to each category&#x3C;br>2. Vote on items using dots&#x3C;br>3. Discuss top voted items&#x3C;br>4. Create action items"
    },
    style={"fontSize": "14"},
    position={"x": -400, "y": 0, "origin": "center"},
    geometry={"width": 300},
)

return {
    "board_id": board_id,
    "view_link": board.view_link,
    "frames": frames,
}

def create_sprint_planning_board( sprint_number: int, team_name: str, stories: list, ) -> dict: """Create a sprint planning board with user stories"""

board = miro.boards.create(
    name=f"Sprint {sprint_number} Planning - {team_name}",
    description=f"Planning board for Sprint {sprint_number}",
    team_id=os.environ.get("MIRO_TEAM_ID"),
)
board_id = board.id

# Create Kanban columns
columns = ["Backlog", "To Do", "In Progress", "Review", "Done"]
col_width = 350
col_height = 1000

for i, col in enumerate(columns):
    miro.frames.create(
        board_id=board_id,
        data={"title": col, "format": "custom"},
        style={"fillColor": "#f5f5f5"},
        position={"x": i * (col_width + 30), "y": 0, "origin": "center"},
        geometry={"width": col_width, "height": col_height},
    )

# Add stories to backlog
for j, story in enumerate(stories):
    miro.cards.create(
        board_id=board_id,
        data={
            "title": story.get("title", "User Story"),
            "description": story.get("description", ""),
        },
        position={"x": 0, "y": -300 + (j * 150), "origin": "center"},
        geometry={"width": 300, "height": 120},
    )

return {"board_id": board_id, "view_link": board.view_link}

if name == "main": # Create retrospective board retro = create_retrospective_board(sprint_number=15, team_name="Platform Team") print(f"Retro board: {retro['view_link']}")

# Create planning board
stories = [
    {"title": "As a user, I want to login with SSO", "description": "Implement SSO authentication"},
    {"title": "As a user, I want dark mode", "description": "Add dark mode support"},
]
planning = create_sprint_planning_board(sprint_number=16, team_name="Platform Team", stories=stories)
print(f"Planning board: {planning['view_link']}")

Best Practices

  1. Rate Limiting

Rate limit handling

import time from functools import wraps

def rate_limit_handler(max_retries=3, base_delay=1): """Decorator for handling Miro rate limits""" def decorator(func): @wraps(func) def wrapper(*args, **kwargs): for attempt in range(max_retries): try: return func(*args, **kwargs) except Exception as e: if "429" in str(e) or "rate" in str(e).lower(): delay = base_delay * (2 ** attempt) print(f"Rate limited, waiting {delay}s...") time.sleep(delay) else: raise raise Exception("Max retries exceeded") return wrapper return decorator

@rate_limit_handler(max_retries=3) def safe_create_sticky(board_id, content, x, y): return miro.sticky_notes.create( board_id=board_id, data={"content": content}, position={"x": x, "y": y, "origin": "center"}, )

  1. Batch Operations

Batch creation for better performance

def batch_create_stickies(board_id: str, items: list, batch_size: int = 10): """Create stickies in batches to avoid rate limits""" created = []

for i in range(0, len(items), batch_size):
    batch = items[i:i + batch_size]

    for item in batch:
        sticky = miro.sticky_notes.create(
            board_id=board_id,
            data={"content": item["content"]},
            style={"fillColor": item.get("color", "yellow")},
            position={"x": item["x"], "y": item["y"], "origin": "center"},
        )
        created.append(sticky)

    # Small delay between batches
    if i + batch_size &#x3C; len(items):
        time.sleep(0.5)

return created

3. Error Handling

Comprehensive error handling

from miro_api.exceptions import MiroApiException

def safe_api_call(func, *args, **kwargs): """Wrapper for safe API calls""" try: return func(*args, **kwargs) except MiroApiException as e: if e.status_code == 404: print(f"Resource not found: {e}") return None elif e.status_code == 401: print("Authentication failed - check token") raise elif e.status_code == 403: print("Permission denied - check scopes") raise elif e.status_code == 429: print("Rate limited - implement backoff") time.sleep(60) return func(*args, **kwargs) else: raise

  1. Position Calculations

Helper functions for positioning

def calculate_grid_position(index: int, columns: int, spacing: float = 250): """Calculate x, y for grid layout""" row = index // columns col = index % columns return { "x": col * spacing, "y": row * spacing, }

def calculate_circle_position(index: int, total: int, radius: float = 300, center_x: float = 0, center_y: float = 0): """Calculate x, y for circular layout""" import math angle = (2 * math.pi * index) / total return { "x": center_x + radius * math.cos(angle), "y": center_y + radius * math.sin(angle), }

Troubleshooting

Common Issues

Issue: 401 Unauthorized

Verify token is valid

import requests

def verify_token(token: str) -> bool: response = requests.get( "https://api.miro.com/v1/users/me", headers={"Authorization": f"Bearer {token}"} ) return response.status_code == 200

Check token scopes

def get_token_scopes(token: str) -> list: response = requests.get( "https://api.miro.com/v1/oauth-token", headers={"Authorization": f"Bearer {token}"} ) return response.json().get("scopes", [])

Issue: Items not appearing on board

Check board permissions

def check_board_access(board_id: str) -> dict: board = miro.boards.get(board_id) return { "id": board.id, "permissions": board.policy.permissions_policy, "sharing": board.policy.sharing_policy, }

Issue: Rate limiting

Implement exponential backoff

import time

def with_backoff(func, max_retries=5): for i in range(max_retries): try: return func() except Exception as e: if "429" in str(e): wait = (2 ** i) + (random.random() * 0.1) time.sleep(wait) else: raise raise Exception("Max retries exceeded")

Debug Commands

Test API authentication

curl -X GET "https://api.miro.com/v1/users/me"
-H "Authorization: Bearer $MIRO_ACCESS_TOKEN"

List boards

curl -X GET "https://api.miro.com/v2/boards?team_id=$MIRO_TEAM_ID"
-H "Authorization: Bearer $MIRO_ACCESS_TOKEN"

Get board details

curl -X GET "https://api.miro.com/v2/boards/$BOARD_ID"
-H "Authorization: Bearer $MIRO_ACCESS_TOKEN"

Create sticky note

curl -X POST "https://api.miro.com/v2/boards/$BOARD_ID/sticky_notes"
-H "Authorization: Bearer $MIRO_ACCESS_TOKEN"
-H "Content-Type: application/json"
-d '{"data": {"content": "Test note"}, "position": {"x": 0, "y": 0}}'

Version History

Version Date Changes

1.0.0 2026-01-17 Initial release with comprehensive Miro API v2 patterns

Resources

  • Miro REST API Documentation

  • Miro Python SDK

  • Miro Developer Portal

  • OAuth 2.0 Guide

  • Webhooks Documentation

  • Rate Limits

This skill provides production-ready patterns for Miro whiteboard automation, enabling powerful visual collaboration workflows and team productivity tools.

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

cli-productivity

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

python-scientific-computing

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

python-pptx

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

vscode-extensions

No summary provided by upstream source.

Repository SourceNeeds Review