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
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
Create a Miro Developer App and Get Credentials
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.
Store Miro Credentials in Replit Secrets
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.
Create Boards and Add Widgets with Python
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.
1import os2import requests3from typing import Optional45ACCESS_TOKEN = os.environ["MIRO_ACCESS_TOKEN"]6BASE_URL = "https://api.miro.com/v2"78HEADERS = {9 "Authorization": f"Bearer {ACCESS_TOKEN}",10 "Content-Type": "application/json",11 "Accept": "application/json"12}1314def 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_id2627 response = requests.post(f"{BASE_URL}/boards", json=payload, headers=HEADERS)28 response.raise_for_status()29 return response.json()3031def 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, blue42 """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=HEADERS53 )54 response.raise_for_status()55 return response.json()5657def add_frame(58 board_id: str,59 title: str,60 x: float = 0,61 y: float = 0,62 width: float = 800,63 height: float = 60064) -> 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=HEADERS75 )76 response.raise_for_status()77 return response.json()7879def 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=HEADERS91 )92 response.raise_for_status()93 return response.json()9495def 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_type100 response = requests.get(101 f"{BASE_URL}/boards/{board_id}/items",102 params=params,103 headers=HEADERS104 )105 response.raise_for_status()106 return response.json().get('data', [])107108# Example: Generate a retrospective board109if __name__ == "__main__":110 TEAM_ID = os.environ.get("MIRO_TEAM_ID", "")111112 board = create_board("Sprint 42 Retrospective", "Auto-generated retro board", TEAM_ID)113 board_id = board['id']114 print(f"Board created: {board['viewLink']}")115116 categories = [117 ("What Went Well", -900, 0, "green"),118 ("What Could Improve", 0, 0, "yellow"),119 ("Action Items", 900, 0, "light_pink")120 ]121122 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 frame126 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)128129 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.
Build a Board Automation Server with Node.js
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.
1const express = require('express');2const axios = require('axios');34const app = express();5app.use(express.json());67const ACCESS_TOKEN = process.env.MIRO_ACCESS_TOKEN;8const TEAM_ID = process.env.MIRO_TEAM_ID || '';9const BASE_URL = 'https://api.miro.com/v2';1011const miroHeaders = {12 'Authorization': `Bearer ${ACCESS_TOKEN}`,13 'Content-Type': 'application/json',14 'Accept': 'application/json'15};1617async 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}3031async 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}4041async 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}4950// POST /boards/generate β generate a Miro board from submitted items51app.post('/boards/generate', async (req, res) => {52 const { boardName, columns } = req.body;53 // columns: [{title: 'col1', items: ['item1', 'item2'], color: 'yellow'}, ...]5455 if (!boardName || !columns || !Array.isArray(columns)) {56 return res.status(400).json({ error: 'boardName and columns array are required' });57 }5859 try {60 const board = await createBoard(boardName);61 const boardId = board.id;62 const columnWidth = 800;6364 for (let i = 0; i < columns.length; i++) {65 const col = columns[i];66 const xPos = (i - Math.floor(columns.length / 2)) * (columnWidth + 100);6768 await addFrame(boardId, col.title, xPos, 0, columnWidth, 600);6970 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 }8081 res.json({82 success: true,83 boardId: board.id,84 boardUrl: board.viewLink85 });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});9192// GET /boards/:id/items β get all sticky notes from a board93app.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});104105app.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.
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.
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.
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.
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.
1import time23def 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 batchesBoard 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.
1# Place sticky note inside frame at (frame_x, frame_y) with width 800, height 6002# Frame extends from frame_x-400 to frame_x+400 (horizontal)3# Place note at frame center with small offset4note_x = frame_x - 150 # offset within frame5note_y = frame_y - 100 # offset within frame6add_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
Mural focuses on structured facilitated workshops with specific frameworks, making it a better choice if your use case is facilitator-led team sessions rather than general-purpose automation.
Notion provides structured databases and wikis with a powerful API, making it a better fit if your team needs organized knowledge management rather than freeform visual collaboration.
Trello uses a structured card-based kanban format with a simpler API, making it easier to integrate for straightforward task management workflows that do not require visual whiteboard layouts.
Asana provides structured project management with timelines and dependencies, making it a better choice for managing tasks and deadlines rather than visual brainstorming and retrospectives.
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.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation