Integrate Bolt.new with Miro by creating a Miro app at miro.com/app-install to get an access token, then embed Miro boards using the Miro Web SDK or fetch and create board items via the Miro REST API v2. Embedding a board requires only a script tag and board ID — no API key needed for public boards. The REST API enables building tools that read sticky notes, shapes, and connectors, or programmatically add items to boards.
Embedding Miro Boards and Building Board Automations in Bolt.new
Miro's integration capabilities split into two distinct approaches. The embed approach is the simplest: Miro boards can be embedded in any web page as an iframe using the board's URL with embed=1 appended, and the Miro Web SDK provides additional programmatic control over the embedded board — zooming, selecting items, and responding to user interactions. This works in Bolt's WebContainer preview without any API credentials and is ideal for use cases where you want to display a Miro board within your app's interface.
The REST API approach enables programmatic creation and management of board items. With an access token from a registered Miro app, you can fetch all items on a board (sticky notes, text, shapes, connectors, images, cards, frames), create new items, update their content and position, and delete them. This is powerful for use cases like: importing data from your app into a Miro board as sticky notes for a retrospective, generating a visual diagram from structured data, or building a custom Miro dashboard in your Bolt app that aggregates insights from multiple boards.
Miro's REST API uses OAuth 2.0 for authentication, but for single-user or team integrations where you control the Miro workspace, you can generate a static access token from your Miro app settings without implementing the full OAuth flow. This token works as a Bearer token in API requests and is the fastest path to getting the REST API working in a Bolt app.
Integration method
Bolt generates the Miro integration through two complementary paths: embedding Miro boards in your app using the Miro Web SDK (iframe-based, works immediately in the WebContainer preview), and building API routes that interact with Miro board data using the Miro REST API v2. Embedding is the simplest approach with no API key required. The REST API enables programmatic creation and reading of board items like sticky notes, shapes, and connectors.
Prerequisites
- A Miro account (free account works for testing, Starter plan for REST API in production)
- A Miro app created at miro.com/app-install (required for REST API access tokens)
- The board ID of the Miro board you want to work with (last part of the board URL: miro.com/app/board/{boardId})
- A Next.js project in Bolt.new (prompt 'Create a Next.js app' to get started)
Step-by-step guide
Create a Miro app and get your access token
Create a Miro app and get your access token
Miro REST API access requires a registered Miro app. Navigate to miro.com/app-install (or Miro's developer portal at developers.miro.com) and click 'Create new app'. Give your app a name like 'Bolt Integration' and select the team you want it to have access to. You will see your app's Client ID and Client Secret on the app configuration page. For single-user server-side integrations, you do not need to implement the full OAuth 2.0 flow. Instead, generate a static access token: in your app's settings page, scroll to the 'Access token' section and click 'Create access token'. Select the scopes your integration needs: boards:read to fetch board items, boards:write to create and update items. Copy the generated token — this is a long-lived token that authenticates your API requests. Your board ID is the alphanumeric string at the end of the board URL. When you have a Miro board open, the URL looks like: miro.com/app/board/uXjVPxxxxxxxxx=/ — the board ID is uXjVPxxxxxxxxx=. Note that board IDs include the trailing equals signs if present. For embedding boards without the REST API, no token is needed at all. The Miro embed URL format is: https://miro.com/app/board/{boardId}/?moveToViewport=&embedMode=view. Any board set to 'Anyone with the link can view' can be embedded without authentication. For private boards, the viewer needs a Miro account and appropriate board access. Store your access token in .env.local as MIRO_ACCESS_TOKEN. Board IDs are public identifiers and can be stored as NEXT_PUBLIC_MIRO_BOARD_ID if needed in client-side components, though for consistency keep all Miro configuration server-side.
Create a Next.js app with a Miro integration. Add a .env.local file with MIRO_ACCESS_TOKEN and MIRO_BOARD_ID as placeholder variables. Create a /api/miro/board-info route that fetches the board name, description, and owner from the Miro REST API v2. Use Bearer token authentication.
Paste this in Bolt.new chat
1// .env.local2MIRO_ACCESS_TOKEN=your_miro_app_access_token3MIRO_BOARD_ID=your_board_id_from_urlPro tip: Miro access tokens generated from the app settings are team-scoped — they can only access boards in the team you selected during app creation. If you need access to boards in multiple teams, create separate apps or implement the OAuth flow for user-level tokens.
Expected result: Your .env.local is configured, and calling /api/miro/board-info returns the board name, description, creation date, and owner information from the Miro API.
Embed a Miro board using the Web SDK
Embed a Miro board using the Web SDK
The simplest Miro integration is embedding a board directly in your Bolt app as an iframe. This requires no API calls and works immediately in Bolt's WebContainer preview — no deployment needed. Create a React component that renders the Miro board in a sized container. Miro's embed URL format: https://miro.com/app/live-embed/{boardId}/ for live collaborative embeds, or https://miro.com/app/board/{boardId}/?embedMode=view for view-only mode. The live embed allows full interaction — editing sticky notes, creating items, collaborating in real time — while view-only shows the board read-only. For more advanced control over the embedded board, add the Miro Web SDK. The SDK allows JavaScript communication with the embedded board: getting the current viewport, selecting items, or responding to cursor position changes. Add the SDK via script tag: https://miro.com/app/static/sdk/v2/miro.js. Note that OAuth flows and social login features fail in Miro's embedded board when viewed inside an iFrame in Bolt's WebContainer during development. The WebContainer enforces strict iframe isolation headers. This is only relevant if your Miro board requires authentication from viewers — for boards shared with 'anyone with the link', the embed works without authentication. The authentication limitation applies only in the Bolt development preview, not in production. For production use, embedded Miro boards require that the viewer either has a Miro account with board access or that the board is publicly shared. Plan your access settings accordingly before embedding.
Create a MiroBoardEmbed React component. Accept boardId (string) and mode ('live' | 'view', default 'live') as props. Render the board as an iframe using the appropriate Miro embed URL. The iframe should be 100% width and 600px tall with no border. Add a 'Open in Miro' button below the iframe that opens the board in a new tab. Handle iframe load errors by showing a 'Board unavailable' message with a direct link.
Paste this in Bolt.new chat
1// components/MiroBoardEmbed.tsx2'use client';34import { useState } from 'react';56type MiroBoardEmbedProps = {7 boardId: string;8 mode?: 'live' | 'view';9 height?: number;10};1112export function MiroBoardEmbed({ boardId, mode = 'live', height = 600 }: MiroBoardEmbedProps) {13 const [hasError, setHasError] = useState(false);1415 const embedUrl =16 mode === 'live'17 ? `https://miro.com/app/live-embed/${boardId}/`18 : `https://miro.com/app/board/${boardId}/?embedMode=view`;1920 const boardUrl = `https://miro.com/app/board/${boardId}/`;2122 if (hasError) {23 return (24 <div className="flex flex-col items-center justify-center p-8 border rounded-lg bg-gray-50">25 <p className="text-gray-600 mb-3">Board could not be loaded in embedded view.</p>26 <a27 href={boardUrl}28 target="_blank"29 rel="noopener noreferrer"30 className="text-blue-600 underline"31 >32 Open board in Miro33 </a>34 </div>35 );36 }3738 return (39 <div className="w-full">40 <iframe41 src={embedUrl}42 width="100%"43 height={height}44 frameBorder="0"45 scrolling="no"46 allowFullScreen47 onError={() => setHasError(true)}48 title="Miro Board"49 className="rounded-lg border border-gray-200"50 />51 <div className="mt-2 text-right">52 <a53 href={boardUrl}54 target="_blank"55 rel="noopener noreferrer"56 className="text-sm text-gray-500 hover:text-gray-700"57 >58 Open in Miro →59 </a>60 </div>61 </div>62 );63}Pro tip: For the best embedding experience, set the board's sharing settings to 'Anyone with the link can view' (or 'edit' for collaborative embeds). This ensures the embedded board loads without prompting viewers for Miro authentication.
Expected result: The MiroBoardEmbed component renders your Miro board as an interactive iframe in the Bolt preview. Clicking items in the board opens them, and the 'Open in Miro' link launches the full board in a new tab.
Fetch and create board items via the REST API
Fetch and create board items via the REST API
Miro's REST API v2 provides full CRUD access to board items. The main endpoints you need: GET /v2/boards/{boardId}/items returns all items on a board (sticky notes, text, shapes, frames, connectors, images, cards). You can filter by type using the type query parameter — e.g., ?type=sticky_note returns only sticky notes. Sticky note items have: id, type ('sticky_note'), data.content (the text), style.fillColor (background color), position (x, y coordinates), geometry (width, height). Shape items have: type ('shape'), data.shape (the shape type like 'rectangle', 'circle'), data.content (text inside the shape), style.fillColor, style.borderColor, and position/geometry. To create a sticky note, POST to /v2/boards/{boardId}/sticky_notes with the item data. The create request requires at minimum a data object with content. Optional: style (fillColor, textAlign), position (x, y coordinates on the board canvas — Miro uses a large coordinate space, typical values range in the thousands), and geometry (width, height in pixels). Colors are CSS color names like 'yellow', 'blue', 'green', 'orange', 'red', 'violet', 'pink', 'dark_green', 'dark_blue', 'dark_red', 'dark_violet', 'dark_yellow', 'gray', 'dark_gray'. Creating shapes follows the same pattern using POST /v2/boards/{boardId}/shapes. Frames (containers for grouping items) use POST /v2/boards/{boardId}/frames. Creating frames with a title gives you named sections on the board, useful for organizing auto-generated content. All these REST API calls are outbound HTTP requests from your Next.js API route, which work perfectly in Bolt's WebContainer without deploying.
Build a Miro board interaction page with two features. First: a /api/miro/items route that fetches all sticky notes from my board and returns their text content, color, and position. Second: a /api/miro/sticky-notes/create route that accepts text, color, and optional position and creates a sticky note on the board. Build a UI showing existing stickies and an 'Add Note' button that opens a form for text and color inputs.
Paste this in Bolt.new chat
1// app/api/miro/sticky-notes/create/route.ts2import { NextRequest, NextResponse } from 'next/server';34const MIRO_API = 'https://api.miro.com/v2';5const ACCESS_TOKEN = process.env.MIRO_ACCESS_TOKEN;6const BOARD_ID = process.env.MIRO_BOARD_ID;78const STICKY_COLORS = [9 'yellow', 'blue', 'green', 'orange', 'red', 'violet',10 'pink', 'dark_green', 'dark_blue', 'dark_red',11] as const;1213type StickyColor = typeof STICKY_COLORS[number];1415export async function POST(request: NextRequest) {16 const { content, color = 'yellow', x = 0, y = 0 } = await request.json();1718 if (!content || typeof content !== 'string' || content.trim() === '') {19 return NextResponse.json({ error: 'content is required' }, { status: 400 });20 }2122 const validColor: StickyColor = STICKY_COLORS.includes(color as StickyColor)23 ? (color as StickyColor)24 : 'yellow';2526 const body = {27 data: { content: content.trim() },28 style: { fillColor: validColor },29 position: { x, y, origin: 'center' },30 };3132 const response = await fetch(`${MIRO_API}/boards/${BOARD_ID}/sticky_notes`, {33 method: 'POST',34 headers: {35 Authorization: `Bearer ${ACCESS_TOKEN}`,36 'Content-Type': 'application/json',37 },38 body: JSON.stringify(body),39 });4041 if (!response.ok) {42 const error = await response.json();43 return NextResponse.json({ error: error.message ?? 'Miro API error' }, { status: response.status });44 }4546 const item = await response.json();47 return NextResponse.json({48 id: item.id,49 content: item.data.content,50 color: item.style.fillColor,51 position: item.position,52 });53}Pro tip: Miro's board coordinate system uses large numbers — the canvas is essentially infinite. Position new sticky notes starting from coordinates like {x: 0, y: 0} for a blank board, then offset by 250 pixels for each subsequent note to avoid overlap. Fetch existing items first to find empty space.
Expected result: The page displays existing sticky notes fetched from the Miro board as colored cards. Creating a note via the form adds it to the board, and refreshing the page shows the new sticky note in the list from the API.
Deploy and set up Miro webhooks for real-time collaboration
Deploy and set up Miro webhooks for real-time collaboration
Miro webhooks notify your app when board items change — sticky notes are added or updated, frames are moved, items are deleted. This is useful for apps that stay synchronized with an active Miro board during a workshop or design session. To deploy, click Publish in Bolt's top-right corner for Netlify, or push to GitHub and connect to Vercel. After deployment, add MIRO_ACCESS_TOKEN and MIRO_BOARD_ID to your hosting platform's environment variables. Register a Miro webhook via POST https://api.miro.com/v2/webhooks/board/{boardId} with your access token. The request body includes: callbackUrl (your deployed endpoint URL), status ('enabled' to activate immediately), and boardId. Miro webhooks fire for board_item_created, board_item_updated, and board_item_deleted events. Each webhook payload contains: event (the event type), boardId, item (the affected board item with all its properties), and createdAt (timestamp). For board_item_updated events, the item reflects its current state after the change — Miro does not include a diff of what changed, so if you need to track changes you must store the previous state. Miro does not use a signature verification system for webhooks (unlike Stripe or Asana). Verify that incoming requests are genuinely from Miro by checking the User-Agent header (should be 'miro-webhooks/1.0') and ensuring the boardId in the payload matches your expected board. During development in Bolt's WebContainer, your app has no public URL and cannot receive inbound webhook requests. This is a fundamental constraint of the WebContainer architecture — all incoming network connections are blocked in the browser-based runtime. Deploy to Netlify or Vercel to test webhooks against a real public URL.
Add a Miro webhook handler to my app at /api/miro/webhook that handles board_item_created, board_item_updated, and board_item_deleted events. Log the event type, item type, and item ID for each event. Also add a /api/miro/webhooks/register POST route that registers a webhook for my board pointing to the MIRO_WEBHOOK_URL environment variable. Add MIRO_WEBHOOK_URL to .env.local.
Paste this in Bolt.new chat
1// app/api/miro/webhook/route.ts2import { NextRequest, NextResponse } from 'next/server';34type MiroWebhookPayload = {5 event: 'board_item_created' | 'board_item_updated' | 'board_item_deleted';6 boardId: string;7 item: {8 id: string;9 type: string;10 data?: { content?: string };11 };12 createdAt: string;13};1415export async function POST(request: NextRequest) {16 // Basic validation: verify the request appears to be from Miro17 const userAgent = request.headers.get('user-agent') ?? '';18 if (!userAgent.includes('miro')) {19 return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });20 }2122 const payload: MiroWebhookPayload = await request.json();23 const { event, boardId, item } = payload;2425 console.log(`Miro webhook: ${event}`, {26 boardId,27 itemId: item.id,28 itemType: item.type,29 content: item.data?.content?.slice(0, 100),30 });3132 switch (event) {33 case 'board_item_created':34 // Handle new item — trigger sync, send notification, etc.35 break;36 case 'board_item_updated':37 // Handle item change38 break;39 case 'board_item_deleted':40 // Handle deletion41 break;42 }4344 return NextResponse.json({ received: true });45}Pro tip: Miro rate-limits webhook deliveries and may batch multiple changes together. Your webhook handler should be idempotent — processing the same event twice should produce the same result as processing it once. Use the item ID to check if you have already processed a specific change.
Expected result: After deploying and registering the webhook, your app receives real-time Miro board events in your server logs. Creating a sticky note in Miro triggers a board_item_created event within seconds.
Common use cases
Embed a Miro board inside a project page
Display a Miro planning board directly inside a Bolt project management page so team members can view and interact with the whiteboard without leaving the app. The embedded board supports full Miro functionality including sticky notes, editing, and real-time collaboration.
Add a Miro board embed to my project detail page. Create a MiroBoardEmbed React component that accepts a boardId prop. Render the board as a full-width, 600px tall iframe using the Miro embed URL. Add a fallback button that opens the board in a new tab for users who block iframes. Include a loading spinner while the iframe loads.
Copy this prompt to try it in Bolt.new
Auto-generate retrospective sticky notes from data
At the end of a sprint, automatically create sticky notes on a Miro retrospective board from sprint data — completed tasks become 'What went well' notes, blocked tickets become 'What needs improvement' notes, and ideas from comments become 'Action items'. Saves the team 20 minutes of manual note creation.
Build a /api/miro/populate-retro route that takes an array of items (each with text and category: 'went_well'|'needs_improvement'|'action_items') and creates sticky notes on a Miro board. Place 'went_well' notes in a green column on the left, 'needs_improvement' in orange in the middle, and 'action_items' in blue on the right. Space notes 200px apart vertically in each column. Use Miro REST API with my access token.
Copy this prompt to try it in Bolt.new
Extract sticky note content from a workshop board
After a workshop session where teams created dozens of sticky notes on a Miro board, automatically extract all sticky note text, group them by frame (each frame represents a team), and generate a structured summary document that can be exported or stored in your project management tool.
Build a /api/miro/extract-stickies route that fetches all sticky_note items from a Miro board ID. Return them grouped by their parent frame name. For each sticky, return the text content, color, and position. Build a UI page that displays the extracted stickies as organized cards and has an Export as Markdown button that formats all stickies into a structured document.
Copy this prompt to try it in Bolt.new
Troubleshooting
Miro board embed shows 'This content cannot be embedded' or a blank iframe
Cause: The Miro board's sharing settings do not allow embedding, or the board requires authentication that fails inside an iframe due to Bolt's WebContainer iframe security headers. Boards set to 'Team members only' cannot be embedded for external viewers.
Solution: In Miro, open the board's Share settings and set access to 'Anyone with the link can view'. This allows the board to be embedded without requiring login. For private boards that must remain restricted, use the 'Open in Miro' link approach instead of an embedded iframe.
REST API calls return 403 Forbidden even with a valid token
Cause: The access token does not have the required scopes for the operation. Reading items requires boards:read scope, creating items requires boards:write scope. Additionally, the token can only access boards within the team it was created for — cross-team access requires a different token.
Solution: In your Miro app settings, check the scopes assigned to your access token. Regenerate the token with both boards:read and boards:write scopes. Verify the board you are trying to access belongs to the same team as the app that generated the token.
Creating sticky notes places them all on top of each other in the same position
Cause: The position coordinates are not being offset between items, so all new stickies are created at the same x/y coordinate (typically 0,0) and overlap. Miro does not automatically space items.
Solution: Calculate position offsets for each new sticky note. A simple approach is to multiply the item index by a spacing value: x = index * 250 (for horizontal layout) or y = index * 200 (for vertical layout). Alternatively, fetch existing items first to find areas of the board with no items and place new content there.
1// Space sticky notes horizontally with 250px gap:2const positions = items.map((_, index) => ({3 x: index * 250,4 y: 0,5 origin: 'center',6}));Best practices
- Set board sharing to 'Anyone with the link can view' for boards you intend to embed — this avoids authentication prompts inside iframes and ensures consistent embedding behavior across different user scenarios.
- Cache board item data on the server side and use Miro webhooks for invalidation rather than fetching all items on every page load — large boards with hundreds of items take noticeable time to fetch and count against your API rate limit.
- Use frames to organize programmatically created content on a board — wrapping related sticky notes in a named frame keeps auto-generated content visually separated from manually created items and makes the board easier to navigate.
- Validate sticky note content length before calling the API — Miro truncates content silently at 5,000 characters, so long text from external sources may be cut off without an error.
- Never expose your Miro access token in client-side code — all REST API calls should go through server-side Next.js routes where the token is accessed via process.env.MIRO_ACCESS_TOKEN.
- For the webhook handler, implement idempotent processing using the item ID — store processed item IDs to avoid double-processing the same event if Miro retries a failed delivery.
- When auto-populating boards from external data (like retrospective items), add a 'Created by [AppName] on [date]' tag to the frame containing generated content so team members can distinguish auto-generated items from manually created ones.
Alternatives
Mural focuses on structured facilitation workflows and templates for design thinking, while Miro is more general-purpose with a more developer-friendly SDK for embedding and automation.
Lucidchart specializes in diagramming and flowcharts with an API for generating diagrams from data, better suited for technical documentation and process mapping than Miro's freeform collaboration focus.
Figma is design-focused with a strong REST API for reading and creating design components, better suited for product design workflows than Miro's general whiteboarding and workshop facilitation use cases.
Notion combines structured databases with document-based collaboration and a REST API for content management, better for teams that need knowledge management alongside visual planning tools like Miro.
Frequently asked questions
How do I connect Bolt.new to Miro?
Create a Miro app at miro.com/app-install and generate a static access token with boards:read and boards:write scopes. Add the token as MIRO_ACCESS_TOKEN in your .env.local file. For board embedding, no token is needed — just set the board to 'Anyone with the link can view' and render it in an iframe. For REST API operations, use Bolt chat to generate Next.js API routes calling api.miro.com/v2 with Bearer token authentication.
Can I embed a Miro board in Bolt's WebContainer preview?
Yes, with one important consideration: the board must be set to 'Anyone with the link can view' in Miro's share settings. Boards requiring Miro account authentication will show a login prompt inside the iframe due to WebContainer's cross-origin iframe restrictions. Once the board is publicly shared, the embed works perfectly in the Bolt preview without any API credentials.
Can I test Miro REST API calls in Bolt's preview before deploying?
Yes. Fetching board items, creating sticky notes, updating items, and other REST API operations are outbound HTTP requests that work perfectly in Bolt's WebContainer. You can create real sticky notes on a Miro board from your Bolt development environment. The only feature requiring deployment is Miro webhooks, which need a public URL to deliver event notifications.
What Miro items can I create via the API?
The Miro REST API v2 supports creating sticky notes, shapes (rectangles, circles, triangles, etc.), text items, frames (containers), connectors (lines between items), images (from URLs), cards, and documents. Each item type has its own endpoint under /v2/boards/{boardId}/{item-type} and accepts position, geometry, data content, and style properties.
Is Miro free to use with the API?
Miro's free plan supports basic board functionality and API access for development, but has limits on the number of boards and team members. The Starter plan ($8/user/month) removes board limits. REST API access for production use requires at least a Starter plan. The embed approach (displaying Miro boards in iframes) works on the free plan as long as the board sharing settings allow it.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation