Skip to main content
RapidDev - Software Development Agency
replit-integrationsStandard API Integration

How to Integrate Replit with Miro

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.

What you'll learn

  • How to create a Miro developer app and obtain OAuth 2.0 credentials or a static access token
  • How to create Miro boards and add sticky notes, shapes, and frames programmatically
  • How to build automation workflows that populate Miro boards from external data sources
  • How to read widget data from Miro boards and sync it back to your application
  • How to handle Miro API authentication for both single-user and multi-user scenarios
Book a free consultation
4.9Clutch rating ⭐
600+Happy partners
17+Countries served
190+Team members
Intermediate14 min read30 minutesProductivityMarch 2026RapidDev Engineering Team
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.

Integration method

Standard API Integration

You connect Replit to Miro by creating a developer app in the Miro developer portal to obtain OAuth 2.0 credentials, storing them in Replit Secrets, and calling the Miro REST API from your server-side Python or Node.js code. The API supports creating and updating boards, adding widgets (sticky notes, shapes, text, images, connectors), managing frames, and working with team members. Miro supports both OAuth 2.0 for multi-user apps and static access tokens for single-user automation.

Prerequisites

  • 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.

Pro tip: For single-user automation (your Replit app only accesses your own Miro workspace), use the static access token β€” it is far simpler than implementing the full OAuth flow. Use OAuth 2.0 only if you need multiple users to connect their own Miro accounts to your app.

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.

Pro tip: Static access tokens generated in Miro app settings do not expire unless you revoke them. For long-running automation scripts this is convenient, but it also means you should regenerate and rotate the token periodically as a security best practice.

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.

miro_boards.py
1import os
2import requests
3from typing import Optional
4
5ACCESS_TOKEN = os.environ["MIRO_ACCESS_TOKEN"]
6BASE_URL = "https://api.miro.com/v2"
7
8HEADERS = {
9 "Authorization": f"Bearer {ACCESS_TOKEN}",
10 "Content-Type": "application/json",
11 "Accept": "application/json"
12}
13
14def create_board(name: str, description: str = "", team_id: str = "") -> dict:
15 """Create a new Miro board."""
16 payload = {
17 "name": name,
18 "description": description,
19 "policy": {
20 "permissionsPolicy": {"collaborationToolsStartAccess": "all_editors"},
21 "sharingPolicy": {"access": "private"}
22 }
23 }
24 if team_id:
25 payload["teamId"] = team_id
26
27 response = requests.post(f"{BASE_URL}/boards", json=payload, headers=HEADERS)
28 response.raise_for_status()
29 return response.json()
30
31def add_sticky_note(
32 board_id: str,
33 content: str,
34 x: float = 0,
35 y: float = 0,
36 color: str = "yellow"
37) -> dict:
38 """
39 Add a sticky note to a board.
40 color options: gray, light_yellow, yellow, orange, light_green, green,
41 dark_green, cyan, light_pink, pink, violet, red, light_blue, blue
42 """
43 payload = {
44 "data": {"content": content, "shape": "square"},
45 "style": {"fillColor": color},
46 "position": {"x": x, "y": y, "origin": "center"},
47 "geometry": {"width": 200}
48 }
49 response = requests.post(
50 f"{BASE_URL}/boards/{board_id}/sticky_notes",
51 json=payload,
52 headers=HEADERS
53 )
54 response.raise_for_status()
55 return response.json()
56
57def add_frame(
58 board_id: str,
59 title: str,
60 x: float = 0,
61 y: float = 0,
62 width: float = 800,
63 height: float = 600
64) -> dict:
65 """Add a labeled frame to organize content on the board."""
66 payload = {
67 "data": {"title": title, "type": "freeform"},
68 "position": {"x": x, "y": y, "origin": "center"},
69 "geometry": {"width": width, "height": height}
70 }
71 response = requests.post(
72 f"{BASE_URL}/boards/{board_id}/frames",
73 json=payload,
74 headers=HEADERS
75 )
76 response.raise_for_status()
77 return response.json()
78
79def add_text_box(board_id: str, content: str, x: float, y: float, font_size: int = 24) -> dict:
80 """Add a text element to the board."""
81 payload = {
82 "data": {"content": content},
83 "style": {"fontSize": str(font_size), "fontFamily": "opensans"},
84 "position": {"x": x, "y": y, "origin": "center"},
85 "geometry": {"width": 400}
86 }
87 response = requests.post(
88 f"{BASE_URL}/boards/{board_id}/texts",
89 json=payload,
90 headers=HEADERS
91 )
92 response.raise_for_status()
93 return response.json()
94
95def get_board_items(board_id: str, item_type: str = "") -> list:
96 """List all items on a board, optionally filtered by type."""
97 params = {"limit": 50}
98 if item_type:
99 params["type"] = item_type
100 response = requests.get(
101 f"{BASE_URL}/boards/{board_id}/items",
102 params=params,
103 headers=HEADERS
104 )
105 response.raise_for_status()
106 return response.json().get('data', [])
107
108# Example: Generate a retrospective board
109if __name__ == "__main__":
110 TEAM_ID = os.environ.get("MIRO_TEAM_ID", "")
111
112 board = create_board("Sprint 42 Retrospective", "Auto-generated retro board", TEAM_ID)
113 board_id = board['id']
114 print(f"Board created: {board['viewLink']}")
115
116 categories = [
117 ("What Went Well", -900, 0, "green"),
118 ("What Could Improve", 0, 0, "yellow"),
119 ("Action Items", 900, 0, "light_pink")
120 ]
121
122 for title, x, y, color in categories:
123 frame = add_frame(board_id, title, x=x, y=y, width=750, height=500)
124 print(f"Frame added: {title}")
125 # Add sample sticky notes inside each frame
126 for i, note_text in enumerate(["Team communication was excellent", "Deployment went smoothly"]):
127 add_sticky_note(board_id, note_text, x=x - 150 + (i * 320), y=y, color=color)
128
129 print("Retrospective board generated successfully!")

Pro tip: Miro's coordinate system has (0,0) at the board center. Place frames and widgets relative to each other using offset calculations. A frame at position (0,0) with width 800 extends from -400 to +400 on the x-axis. Sticky notes placed inside a frame's bounds are visually inside it but must be explicitly attached to frames via the parent relationship in some API versions.

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.

server.js
1const express = require('express');
2const axios = require('axios');
3
4const app = express();
5app.use(express.json());
6
7const ACCESS_TOKEN = process.env.MIRO_ACCESS_TOKEN;
8const TEAM_ID = process.env.MIRO_TEAM_ID || '';
9const BASE_URL = 'https://api.miro.com/v2';
10
11const miroHeaders = {
12 'Authorization': `Bearer ${ACCESS_TOKEN}`,
13 'Content-Type': 'application/json',
14 'Accept': 'application/json'
15};
16
17async function createBoard(name, description = '') {
18 const payload = {
19 name,
20 description,
21 policy: {
22 permissionsPolicy: { collaborationToolsStartAccess: 'all_editors' },
23 sharingPolicy: { access: 'private' }
24 }
25 };
26 if (TEAM_ID) payload.teamId = TEAM_ID;
27 const res = await axios.post(`${BASE_URL}/boards`, payload, { headers: miroHeaders });
28 return res.data;
29}
30
31async function addStickyNote(boardId, content, x, y, color = 'yellow') {
32 const res = await axios.post(`${BASE_URL}/boards/${boardId}/sticky_notes`, {
33 data: { content, shape: 'square' },
34 style: { fillColor: color },
35 position: { x, y, origin: 'center' },
36 geometry: { width: 200 }
37 }, { headers: miroHeaders });
38 return res.data;
39}
40
41async function addFrame(boardId, title, x, y, width = 800, height = 600) {
42 const res = await axios.post(`${BASE_URL}/boards/${boardId}/frames`, {
43 data: { title, type: 'freeform' },
44 position: { x, y, origin: 'center' },
45 geometry: { width, height }
46 }, { headers: miroHeaders });
47 return res.data;
48}
49
50// POST /boards/generate β€” generate a Miro board from submitted items
51app.post('/boards/generate', async (req, res) => {
52 const { boardName, columns } = req.body;
53 // columns: [{title: 'col1', items: ['item1', 'item2'], color: 'yellow'}, ...]
54
55 if (!boardName || !columns || !Array.isArray(columns)) {
56 return res.status(400).json({ error: 'boardName and columns array are required' });
57 }
58
59 try {
60 const board = await createBoard(boardName);
61 const boardId = board.id;
62 const columnWidth = 800;
63
64 for (let i = 0; i < columns.length; i++) {
65 const col = columns[i];
66 const xPos = (i - Math.floor(columns.length / 2)) * (columnWidth + 100);
67
68 await addFrame(boardId, col.title, xPos, 0, columnWidth, 600);
69
70 for (let j = 0; j < col.items.length; j++) {
71 await addStickyNote(
72 boardId,
73 col.items[j],
74 xPos - 150 + (j % 3) * 160,
75 -100 + Math.floor(j / 3) * 230,
76 col.color || 'yellow'
77 );
78 }
79 }
80
81 res.json({
82 success: true,
83 boardId: board.id,
84 boardUrl: board.viewLink
85 });
86 } catch (error) {
87 console.error('Miro API error:', error.response?.data || error.message);
88 res.status(500).json({ error: 'Failed to generate Miro board' });
89 }
90});
91
92// GET /boards/:id/items β€” get all sticky notes from a board
93app.get('/boards/:id/items', async (req, res) => {
94 try {
95 const response = await axios.get(
96 `${BASE_URL}/boards/${req.params.id}/items`,
97 { headers: miroHeaders, params: { type: 'sticky_note', limit: 50 } }
98 );
99 res.json(response.data);
100 } catch (error) {
101 res.status(500).json({ error: 'Failed to retrieve board items' });
102 }
103});
104
105app.listen(3000, '0.0.0.0', () => {
106 console.log('Miro integration server running on port 3000');
107});

Pro tip: The Miro API rate limit is 100 requests per 10 seconds per token. For boards with many items, batch your widget creation calls and add a short delay between batches to avoid hitting the rate limit.

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

Common 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.

Replit Prompt

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.

Copy this prompt to try it in Replit

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.

Replit Prompt

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.

Copy this prompt to try it in Replit

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.

Replit Prompt

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.

Copy this prompt to try it in Replit

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.

typescript
1HEADERS = {
2 "Authorization": f"Bearer {os.environ['MIRO_ACCESS_TOKEN']}",
3 "Content-Type": "application/json"
4}

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.

typescript
1import time
2
3def add_sticky_notes_batch(board_id, notes, batch_size=10):
4 """Add sticky notes in batches to respect rate limits."""
5 for i in range(0, len(notes), batch_size):
6 batch = notes[i:i + batch_size]
7 for note in batch:
8 add_sticky_note(board_id, note['content'], note['x'], note['y'])
9 if i + batch_size < len(notes):
10 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.

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

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

Alternatives

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.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation β€” no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.