# How to Integrate Replit with Miro

- Tool: Replit
- Difficulty: Intermediate
- Time required: 30 minutes
- Last updated: March 2026

## TL;DR

To integrate Replit with Miro, register a Miro developer application to get OAuth 2.0 credentials, store them in Replit Secrets (lock icon 🔒), and call the Miro REST API from your Python or Node.js backend to create boards, add sticky notes and shapes, manage frames, and automate visual collaboration workflows. Deploy with Autoscale for on-demand board operations.

## Why Connect Replit to Miro?

Miro's REST API transforms its infinite whiteboard from a manual design surface into a programmable data visualization layer. Connecting Replit to Miro lets you auto-populate boards from external data: generate retrospective boards from sprint ticket data, visualize database schemas as entity-relationship diagrams, map customer journey stages from CRM pipeline data, or build real-time dashboards using Miro's visual primitives.

The API covers the full widget vocabulary: sticky notes, shapes, text boxes, images, connectors, frames, and embeds. This means you can generate sophisticated visual layouts — not just add a few text boxes, but create structured diagrams with connected nodes, color-coded categories, and organized frames. When combined with Replit's ability to connect to databases, CRMs, and project management tools, Miro becomes a powerful visualization endpoint for complex data.

Miro supports two authentication modes: OAuth 2.0 for apps where multiple users authorize access to their own boards, and static access tokens for personal automation scripts. For a Replit backend automating a single team's Miro workspace, a static token is simpler. For apps where each user has their own Miro account, use OAuth. Store all tokens in Replit Secrets (lock icon 🔒 in the sidebar) — Miro access tokens grant broad access to boards and team data.

## Before you start

- A Replit account with a Python or Node.js project created
- A Miro account (Developer plan or any paid plan for full API access)
- A Miro developer app created at miro.com/app/settings/user-profile/apps
- Basic familiarity with REST APIs, OAuth 2.0, and JSON request bodies
- Node.js 18+ or Python 3.10+ (both available on Replit by default)

## Step-by-step guide

### 1. Create a Miro Developer App and Get Credentials

Navigate to miro.com and log in to your account. Click your profile avatar in the top-right corner and go to 'Profile Settings'. In the left sidebar, find 'Your apps' and click it. Then click 'Create new app'.

Fill in the app details:
- App Name: 'Replit Integration' or something descriptive
- Description: what your integration will do
- Redirect URI: your Replit app's OAuth callback URL (e.g., https://your-repl.repl.co/oauth/callback) — required if using OAuth, can be a placeholder if using static tokens

In the app settings, select the required scopes. Common scopes for board automation are:
- boards:read — read board data
- boards:write — create and modify boards
- board:content:read — read items on a board
- board:content:write — create and modify items on a board

After creating the app, you have two authentication options:
1. Static access token: click 'Create token' in the app settings — this token is tied to your account and is simpler for personal automation.
2. OAuth 2.0: use the Client ID and Client Secret for multi-user flows.

Copy your static access token or the Client ID/Secret depending on your use case.

**Expected result:** You have either a Miro static access token or OAuth 2.0 Client ID and Client Secret copied and ready to add to Replit Secrets.

### 2. Store Miro Credentials in Replit Secrets

Open your Replit project and click the lock icon 🔒 in the left sidebar to open the Secrets pane. For static token authentication, add:

- Key: MIRO_ACCESS_TOKEN — Value: your static access token

For OAuth 2.0 authentication, add:
- Key: MIRO_CLIENT_ID — Value: your app's Client ID
- Key: MIRO_CLIENT_SECRET — Value: your app's Client Secret
- Key: MIRO_REDIRECT_URI — Value: your OAuth callback URL

Also add a secret for your team ID if you know it:
- Key: MIRO_TEAM_ID — Value: your Miro team ID (found in the URL when viewing your team: miro.com/app/settings/team/{teamId}/)

Click 'Add Secret' after each entry. Access them in Python with os.environ['MIRO_ACCESS_TOKEN'] and in Node.js with process.env.MIRO_ACCESS_TOKEN. Never hardcode Miro tokens in source files — a token grants full access to your boards and team data.

**Expected result:** MIRO_ACCESS_TOKEN (or OAuth credentials) appear in the Replit Secrets pane and are accessible as environment variables.

### 3. Create Boards and Add Widgets with Python

The Miro REST API v2 base URL is https://api.miro.com/v2. All requests require the Authorization: Bearer {token} header. You can create a new board, then add widgets (items) to it by POSTing to the board's items endpoint.

Miro's item types include sticky_note, shape, text, image, frame, connector, embed, and app_card. Each has its own schema but they all share common position and geometry properties. The coordinate system places (0,0) at the center of the board with x increasing to the right and y increasing downward.

The Python module below creates a board, adds a frame, populates it with sticky notes in a grid layout, and connects items with connectors. Install requests with pip install requests.

```
import os
import requests
from typing import Optional

ACCESS_TOKEN = os.environ["MIRO_ACCESS_TOKEN"]
BASE_URL = "https://api.miro.com/v2"

HEADERS = {
    "Authorization": f"Bearer {ACCESS_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json"
}

def create_board(name: str, description: str = "", team_id: str = "") -> dict:
    """Create a new Miro board."""
    payload = {
        "name": name,
        "description": description,
        "policy": {
            "permissionsPolicy": {"collaborationToolsStartAccess": "all_editors"},
            "sharingPolicy": {"access": "private"}
        }
    }
    if team_id:
        payload["teamId"] = team_id

    response = requests.post(f"{BASE_URL}/boards", json=payload, headers=HEADERS)
    response.raise_for_status()
    return response.json()

def add_sticky_note(
    board_id: str,
    content: str,
    x: float = 0,
    y: float = 0,
    color: str = "yellow"
) -> dict:
    """
    Add a sticky note to a board.
    color options: gray, light_yellow, yellow, orange, light_green, green,
                   dark_green, cyan, light_pink, pink, violet, red, light_blue, blue
    """
    payload = {
        "data": {"content": content, "shape": "square"},
        "style": {"fillColor": color},
        "position": {"x": x, "y": y, "origin": "center"},
        "geometry": {"width": 200}
    }
    response = requests.post(
        f"{BASE_URL}/boards/{board_id}/sticky_notes",
        json=payload,
        headers=HEADERS
    )
    response.raise_for_status()
    return response.json()

def add_frame(
    board_id: str,
    title: str,
    x: float = 0,
    y: float = 0,
    width: float = 800,
    height: float = 600
) -> dict:
    """Add a labeled frame to organize content on the board."""
    payload = {
        "data": {"title": title, "type": "freeform"},
        "position": {"x": x, "y": y, "origin": "center"},
        "geometry": {"width": width, "height": height}
    }
    response = requests.post(
        f"{BASE_URL}/boards/{board_id}/frames",
        json=payload,
        headers=HEADERS
    )
    response.raise_for_status()
    return response.json()

def add_text_box(board_id: str, content: str, x: float, y: float, font_size: int = 24) -> dict:
    """Add a text element to the board."""
    payload = {
        "data": {"content": content},
        "style": {"fontSize": str(font_size), "fontFamily": "opensans"},
        "position": {"x": x, "y": y, "origin": "center"},
        "geometry": {"width": 400}
    }
    response = requests.post(
        f"{BASE_URL}/boards/{board_id}/texts",
        json=payload,
        headers=HEADERS
    )
    response.raise_for_status()
    return response.json()

def get_board_items(board_id: str, item_type: str = "") -> list:
    """List all items on a board, optionally filtered by type."""
    params = {"limit": 50}
    if item_type:
        params["type"] = item_type
    response = requests.get(
        f"{BASE_URL}/boards/{board_id}/items",
        params=params,
        headers=HEADERS
    )
    response.raise_for_status()
    return response.json().get('data', [])

# Example: Generate a retrospective board
if __name__ == "__main__":
    TEAM_ID = os.environ.get("MIRO_TEAM_ID", "")

    board = create_board("Sprint 42 Retrospective", "Auto-generated retro board", TEAM_ID)
    board_id = board['id']
    print(f"Board created: {board['viewLink']}")

    categories = [
        ("What Went Well", -900, 0, "green"),
        ("What Could Improve", 0, 0, "yellow"),
        ("Action Items", 900, 0, "light_pink")
    ]

    for title, x, y, color in categories:
        frame = add_frame(board_id, title, x=x, y=y, width=750, height=500)
        print(f"Frame added: {title}")
        # Add sample sticky notes inside each frame
        for i, note_text in enumerate(["Team communication was excellent", "Deployment went smoothly"]):
            add_sticky_note(board_id, note_text, x=x - 150 + (i * 320), y=y, color=color)

    print("Retrospective board generated successfully!")
```

**Expected result:** Running the Python script creates a new Miro board with three labeled frames and sample sticky notes, printing the board view link.

### 4. Build a Board Automation Server with Node.js

An Express server provides HTTP endpoints that trigger Miro board generation based on external events. This pattern is useful for integrating Miro into a broader workflow — for example, a webhook from a project management tool triggers the creation of a planning board.

The Node.js code below creates a server with endpoints to generate a Miro board from submitted data and retrieve items from an existing board. Install dependencies with npm install express axios.

```
const express = require('express');
const axios = require('axios');

const app = express();
app.use(express.json());

const ACCESS_TOKEN = process.env.MIRO_ACCESS_TOKEN;
const TEAM_ID = process.env.MIRO_TEAM_ID || '';
const BASE_URL = 'https://api.miro.com/v2';

const miroHeaders = {
  'Authorization': `Bearer ${ACCESS_TOKEN}`,
  'Content-Type': 'application/json',
  'Accept': 'application/json'
};

async function createBoard(name, description = '') {
  const payload = {
    name,
    description,
    policy: {
      permissionsPolicy: { collaborationToolsStartAccess: 'all_editors' },
      sharingPolicy: { access: 'private' }
    }
  };
  if (TEAM_ID) payload.teamId = TEAM_ID;
  const res = await axios.post(`${BASE_URL}/boards`, payload, { headers: miroHeaders });
  return res.data;
}

async function addStickyNote(boardId, content, x, y, color = 'yellow') {
  const res = await axios.post(`${BASE_URL}/boards/${boardId}/sticky_notes`, {
    data: { content, shape: 'square' },
    style: { fillColor: color },
    position: { x, y, origin: 'center' },
    geometry: { width: 200 }
  }, { headers: miroHeaders });
  return res.data;
}

async function addFrame(boardId, title, x, y, width = 800, height = 600) {
  const res = await axios.post(`${BASE_URL}/boards/${boardId}/frames`, {
    data: { title, type: 'freeform' },
    position: { x, y, origin: 'center' },
    geometry: { width, height }
  }, { headers: miroHeaders });
  return res.data;
}

// POST /boards/generate — generate a Miro board from submitted items
app.post('/boards/generate', async (req, res) => {
  const { boardName, columns } = req.body;
  // columns: [{title: 'col1', items: ['item1', 'item2'], color: 'yellow'}, ...]

  if (!boardName || !columns || !Array.isArray(columns)) {
    return res.status(400).json({ error: 'boardName and columns array are required' });
  }

  try {
    const board = await createBoard(boardName);
    const boardId = board.id;
    const columnWidth = 800;

    for (let i = 0; i < columns.length; i++) {
      const col = columns[i];
      const xPos = (i - Math.floor(columns.length / 2)) * (columnWidth + 100);

      await addFrame(boardId, col.title, xPos, 0, columnWidth, 600);

      for (let j = 0; j < col.items.length; j++) {
        await addStickyNote(
          boardId,
          col.items[j],
          xPos - 150 + (j % 3) * 160,
          -100 + Math.floor(j / 3) * 230,
          col.color || 'yellow'
        );
      }
    }

    res.json({
      success: true,
      boardId: board.id,
      boardUrl: board.viewLink
    });
  } catch (error) {
    console.error('Miro API error:', error.response?.data || error.message);
    res.status(500).json({ error: 'Failed to generate Miro board' });
  }
});

// GET /boards/:id/items — get all sticky notes from a board
app.get('/boards/:id/items', async (req, res) => {
  try {
    const response = await axios.get(
      `${BASE_URL}/boards/${req.params.id}/items`,
      { headers: miroHeaders, params: { type: 'sticky_note', limit: 50 } }
    );
    res.json(response.data);
  } catch (error) {
    res.status(500).json({ error: 'Failed to retrieve board items' });
  }
});

app.listen(3000, '0.0.0.0', () => {
  console.log('Miro integration server running on port 3000');
});
```

**Expected result:** POST /boards/generate creates a Miro board with frames and sticky notes from the submitted column data, returning the board URL.

## Best practices

- Store MIRO_ACCESS_TOKEN and MIRO_TEAM_ID in Replit Secrets — never hardcode tokens in source files
- Use static access tokens for single-user automation and OAuth 2.0 only when multiple users need to connect their own Miro accounts
- Respect the 100 requests per 10 seconds rate limit by batching item creation and adding delays between batches for large boards
- Store the board ID and view URL in your application database after creating boards so you can link back to them and add items later
- Design your coordinate layout before writing code — sketch out frame positions and item grids on paper to avoid off-by-one positioning errors
- Use frames to organize content thematically and enable teams to navigate large boards by collapsing frames they are not working on
- Retrieve existing board items before adding new ones to avoid duplicating content on boards that are updated incrementally
- Deploy on Replit Autoscale for HTTP-triggered board generation and use Reserved VM for scheduled board refresh jobs that sync external data to Miro boards

## Use cases

### Automated Sprint Retrospective Board Generator

At the end of each sprint, a Replit script pulls tickets from a project management tool, creates a Miro board with frames for 'What went well', 'What could be improved', and 'Action items', and populates each frame with sticky notes generated from the sprint data, saving the team 30 minutes of manual board setup.

Prompt example:

```
Build a Python script that fetches completed tickets from an Asana project for the past two weeks, creates a new Miro board, adds three labeled frames for retrospective categories, and places sticky notes with ticket summaries in the appropriate frames using the Miro REST API.
```

### Real-Time Database Schema Visualization

A Replit backend connects to a PostgreSQL database, reads the table and column structure, and generates a Miro board with shapes for each table connected by lines representing foreign key relationships — giving developers an always-current visual schema diagram without using a separate diagramming tool.

Prompt example:

```
Create a Python script that queries PostgreSQL for all tables and foreign key relationships, then generates a Miro board with shapes for each table, text inside each shape listing the columns, and connectors between shapes that share foreign key relationships.
```

### CRM Pipeline Kanban Board Sync

A Replit job syncs deal stages from a CRM into a Miro board, creating frames for each pipeline stage (Prospect, Demo, Proposal, Closed) and sticky notes for each active deal, updating colors to reflect deal size and urgency. Sales managers get a visual overview without logging into the CRM.

Prompt example:

```
Write a Node.js script that retrieves active deals from a Pipedrive CRM, creates a Miro board with frame columns for each pipeline stage, and places color-coded sticky notes for each deal with the deal name and value displayed, refreshing the board daily.
```

## Troubleshooting

### API returns 401 Unauthorized

Cause: The access token is missing, incorrect, or has been revoked. Miro static tokens from app settings need to be explicitly created — they are not automatically generated when you create the app.

Solution: Go to your Miro app settings at miro.com/app/settings/user-profile/apps, select your app, and click 'Create token' if you have not already. Copy the token and update MIRO_ACCESS_TOKEN in Replit Secrets. Restart your Repl.

```
HEADERS = {
    "Authorization": f"Bearer {os.environ['MIRO_ACCESS_TOKEN']}",
    "Content-Type": "application/json"
}
```

### POST to create a sticky note or frame returns 403 Forbidden

Cause: The access token does not have the required scopes for write operations, or the board belongs to a team your token does not have access to.

Solution: In your Miro app settings, verify that board:content:write and boards:write scopes are enabled. If you are using a static token, regenerate it after updating the scopes. For team boards, ensure your Miro account has editor access to that team.

### Rate limit error: 429 Too Many Requests

Cause: The Miro API limits requests to 100 per 10 seconds per token. Generating large boards with many sticky notes can easily exceed this limit when creating items in a tight loop.

Solution: Add a delay between batches of API calls when creating many items. Process items in batches of 10-20 with a 1-second pause between batches.

```
import time

def add_sticky_notes_batch(board_id, notes, batch_size=10):
    """Add sticky notes in batches to respect rate limits."""
    for i in range(0, len(notes), batch_size):
        batch = notes[i:i + batch_size]
        for note in batch:
            add_sticky_note(board_id, note['content'], note['x'], note['y'])
        if i + batch_size < len(notes):
            time.sleep(1)  # 1 second pause between batches
```

### Board items appear but are not visually inside their expected frames

Cause: Miro frames and items are positioned independently on the infinite board. Placing an item at coordinates that overlap a frame's area does not automatically make it a child of that frame in older API versions.

Solution: In Miro API v2, items placed within a frame's coordinate bounds should visually appear inside the frame. If items appear outside frames, verify your coordinate calculations account for the frame's position and size. Items at (frameX - frameWidth/2) to (frameX + frameWidth/2) are within the frame bounds.

```
# Place sticky note inside frame at (frame_x, frame_y) with width 800, height 600
# Frame extends from frame_x-400 to frame_x+400 (horizontal)
# Place note at frame center with small offset
note_x = frame_x - 150  # offset within frame
note_y = frame_y - 100  # offset within frame
add_sticky_note(board_id, content, note_x, note_y)
```

## Frequently asked questions

### How do I connect Replit to Miro?

Create a developer app at miro.com/app/settings/user-profile/apps, generate a static access token or OAuth credentials, then add them to Replit Secrets (lock icon 🔒 in the sidebar). Use Bearer token authentication in the Authorization header when calling the Miro REST API from your Python or Node.js backend.

### Do I need OAuth 2.0 to use the Miro API from Replit?

Not for single-user automation. If your Replit app only needs to access your own Miro workspace, generate a static access token in your Miro app settings. OAuth 2.0 is only needed if your application allows multiple different users to connect their own Miro accounts.

### How do I add sticky notes to a specific frame in Miro?

Place sticky notes at coordinates within the frame's bounding box. Frames are positioned at a center point with a width and height, so calculate the bounds (centerX ± width/2, centerY ± height/2) and place sticky notes within that range. Create the frame first, note its position, then add items within those bounds.

### What is the Miro API rate limit?

The Miro API allows 100 requests per 10 seconds per access token. When generating large boards with many items, process them in batches of 10-20 and add a 1-second pause between batches to stay within the limit and avoid 429 errors.

### Can I read sticky note content from an existing Miro board?

Yes. Use GET /v2/boards/{boardId}/items with the type parameter set to 'sticky_note'. The response includes each sticky note's content, position, and style. You can also retrieve all item types by omitting the type filter.

### What deployment type should I use on Replit for Miro integrations?

Use Autoscale deployment for HTTP-triggered board generation. Use Reserved VM for scheduled jobs that periodically sync external data to Miro boards — for example, a daily script that refreshes a CRM pipeline visualization board.

---

Source: https://www.rapidevelopers.com/replit-integration/miro
© RapidDev — https://www.rapidevelopers.com/replit-integration/miro
