Connect FlutterFlow to Miro using a FlutterFlow API Call to your Cloud Function proxy, which calls the Miro REST API v2 at https://api.miro.com/v2. Unlike Lucidchart's embed-only approach, Miro has a real REST API with OAuth 2.0 — your Cloud Function handles the OAuth token exchange and storage, while FlutterFlow fetches boards and sticky-note items as native list data. You can also embed a live interactive board using Miro's embed URL in a WebView widget.
| Fact | Value |
|---|---|
| Tool | Miro |
| Category | Productivity |
| Method | FlutterFlow API Call |
| Difficulty | Intermediate |
| Time required | 75 minutes |
| Last updated | July 2026 |
Miro in FlutterFlow: A Real API, Not Just an Embed
Miro and Lucidchart are both visual collaboration tools, but they have fundamentally different integration realities for FlutterFlow developers. Lucidchart offers only static exports and a publish-embed link — there is no runtime API you would wire into a mobile app. Miro, by contrast, has a genuine REST API v2 at https://api.miro.com/v2 that lets you read board lists, fetch items (sticky notes, cards, shapes, frames), and update board content programmatically. This means you can build a real Miro-powered data dashboard in FlutterFlow — not just a WebView.
Miro uses OAuth 2.0, which is more involved to set up than a static API token but provides a proper authentication flow. The OAuth exchange produces an access token and a refresh token tied to a specific Miro user's authorization. Because access tokens expire and require refresh, and because the OAuth flow involves client secrets that must never live in client code, the entire authentication lifecycle must happen inside a backend function. A Firebase Cloud Function handles the OAuth code exchange, stores the access token, handles token refresh when the token expires, and proxies all API calls with the correct Authorization header. FlutterFlow calls the function URL — never Miro directly.
Miro's API rate limit is approximately 1,000 requests per minute per token — significantly more generous than monday.com's 60 requests per minute. This means a genuine board dashboard with frequent refresh cycles is realistic. The free Miro plan allows API access for basic board operations, though accessing private workspace boards or higher-volume use cases may require a paid plan. For teams that just want to display a board interactively without building a data integration, Miro's embed link in a WebView widget is the fastest path — no API setup required.
Integration method
Miro provides a REST API v2 at https://api.miro.com/v2 with OAuth 2.0 authentication. FlutterFlow creates an API Call Group pointing at your Cloud Function proxy URL — the Cloud Function handles the OAuth 2.0 token exchange, stores the access token securely, and proxies GET requests for boards and items to Miro. FlutterFlow then binds the board and item JSON to Custom Data Types for native list views. Alternatively, a Miro live-embed URL works in a WebView widget for an interactive, zero-API board view.
Prerequisites
- A Miro account (free plan is sufficient to start; private workspace boards may need a paid plan)
- A Miro Developer account — sign up at miro.com/app/settings/user-profile/apps
- A Firebase project for deploying the Cloud Function proxy (Blaze plan required)
- A FlutterFlow project with a screen planned for board or item display
- Familiarity with the concept of OAuth 2.0 — the step-by-step guide below covers the specifics
Step-by-step guide
Create a Miro app and configure OAuth 2.0
Log in to Miro and go to miro.com/app/settings/user-profile/apps. Click 'Create new app'. Give your app a name — something like 'FlutterFlow Integration' — and select the team or workspace it belongs to. Click Create app. In your new app's settings page, you will see your Client ID and Client Secret. Copy both values — you will add them to your Cloud Function environment variables in the next step. The Client Secret must never appear in FlutterFlow or in your app bundle. Scroll down to the 'Permissions' section and enable the scopes your app needs. For reading boards and items, enable `boards:read` and `board:item:read`. For writing content, also enable `boards:write` and `board:item:write`. Only enable the scopes you actually need — fewer scopes means a more limited risk surface if credentials are ever compromised. Scroll to the 'Redirect URI for OAuth 2.0' section. Add the redirect URI for your Cloud Function — it will look like `https://us-central1-your-project.cloudfunctions.net/miroAuth/callback`. This is the URL that Miro will redirect to after the user authorizes your app, carrying the authorization code that your function exchanges for an access token. Note the exact URL format: your Cloud Function name followed by `/callback`. Save the app settings. You now have a Client ID, Client Secret, and the OAuth 2.0 flow configured. The Client ID is safe to reference in your FlutterFlow app; the Client Secret must go only to the Cloud Function.
Pro tip: For a single-team internal app where all users share the same Miro workspace, you can simplify by using a single OAuth token generated for your own account and storing it in the Cloud Function — rather than implementing the full multi-user OAuth flow. Regenerate the token periodically as a maintenance task.
Expected result: Your Miro app is created with Client ID, Client Secret, and a redirect URI configured. You have the Client ID and Client Secret copied for the Cloud Function setup.
Deploy a Firebase Cloud Function that handles OAuth and proxies Miro API calls
Open the Firebase Console and navigate to your project's Functions section. Create a new Cloud Function named `miroProxy`. Set the trigger to HTTP. In the Runtime environment variables section, add three variables: `MIRO_CLIENT_ID` (your app's Client ID from Step 1), `MIRO_CLIENT_SECRET` (your app's Client Secret), and `MIRO_REDIRECT_URI` (your function's callback URL, e.g. `https://us-central1-your-project.cloudfunctions.net/miroProxy/callback`). The Cloud Function handles two responsibilities: (1) the OAuth authorization flow — when your FlutterFlow app initiates login, it opens the Miro authorization URL in a WebView, the user approves, and Miro redirects to your function's `/callback` path carrying a code, which the function exchanges for an access token and stores in Firestore; and (2) the API proxy — FlutterFlow calls the function's `/boards` or `/boards/{id}/items` path, the function reads the stored access token from Firestore, checks if it is expired and refreshes if needed, then forwards the request to Miro with the Authorization header. Paste the Node.js code below and deploy. The function URL (e.g. `https://us-central1-your-project.cloudfunctions.net/miroProxy`) is your FlutterFlow API Group's Base URL. Access tokens expire after a set period — Miro's OAuth provides a refresh token to get a new access token without re-authorizing. The function checks the token's expiry on each request and automatically refreshes it, so your FlutterFlow app never needs to handle token management. If you'd rather skip building the full OAuth flow, RapidDev's team implements FlutterFlow integrations like this every week — free scoping call at rapidevelopers.com/contact.
1const functions = require('firebase-functions');2const fetch = require('node-fetch');3const admin = require('firebase-admin');4admin.initializeApp();56const CLIENT_ID = process.env.MIRO_CLIENT_ID;7const CLIENT_SECRET = process.env.MIRO_CLIENT_SECRET;8const REDIRECT_URI = process.env.MIRO_REDIRECT_URI;910// Step 1: Initiate OAuth — return the Miro authorization URL11exports.miroProxy = functions.https.onRequest(async (req, res) => {12 res.set('Access-Control-Allow-Origin', '*');13 res.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');14 res.set('Access-Control-Allow-Headers', 'Content-Type');1516 if (req.method === 'OPTIONS') { res.status(204).send(''); return; }1718 const path = req.path;1920 // OAuth callback handler21 if (path === '/callback') {22 const code = req.query.code;23 const tokenRes = await fetch('https://api.miro.com/v1/oauth/token', {24 method: 'POST',25 headers: { 'Content-Type': 'application/x-www-form-urlencoded' },26 body: new URLSearchParams({27 grant_type: 'authorization_code',28 client_id: CLIENT_ID,29 client_secret: CLIENT_SECRET,30 code,31 redirect_uri: REDIRECT_URI32 })33 });34 const tokens = await tokenRes.json();35 // Store tokens in Firestore (keyed by user/team)36 await admin.firestore().collection('miro_tokens').doc('default').set({37 access_token: tokens.access_token,38 refresh_token: tokens.refresh_token,39 expires_at: Date.now() + (tokens.expires_in * 1000)40 });41 res.send('Authorization successful. Return to the app.');42 return;43 }4445 // All other paths: proxy to Miro API with stored token46 const tokenDoc = await admin.firestore().collection('miro_tokens').doc('default').get();47 let { access_token, refresh_token, expires_at } = tokenDoc.data() || {};4849 // Auto-refresh if expired50 if (!access_token || Date.now() > expires_at - 60000) {51 const refreshRes = await fetch('https://api.miro.com/v1/oauth/token', {52 method: 'POST',53 headers: { 'Content-Type': 'application/x-www-form-urlencoded' },54 body: new URLSearchParams({55 grant_type: 'refresh_token',56 client_id: CLIENT_ID,57 client_secret: CLIENT_SECRET,58 refresh_token59 })60 });61 const refreshed = await refreshRes.json();62 access_token = refreshed.access_token;63 await admin.firestore().collection('miro_tokens').doc('default').update({64 access_token,65 expires_at: Date.now() + (refreshed.expires_in * 1000)66 });67 }6869 const miroRes = await fetch(`https://api.miro.com/v2${path}${req.url.includes('?') ? req.url.substring(req.url.indexOf('?')) : ''}`, {70 method: req.method,71 headers: {72 'Authorization': `Bearer ${access_token}`,73 'Content-Type': 'application/json'74 },75 body: req.method !== 'GET' ? JSON.stringify(req.body) : undefined76 });7778 const data = await miroRes.json();79 res.status(miroRes.status).json(data);80});Pro tip: For a simpler single-user setup, skip the full OAuth flow and generate a personal access token in Miro's developer settings. Store it directly as a MIRO_ACCESS_TOKEN environment variable and use it in the Authorization header without the OAuth exchange — this works for internal tools where a single account's token is sufficient.
Expected result: The Cloud Function is deployed. Visiting the function URL + /callback after a Miro OAuth authorization stores the access token in Firestore. API proxy calls to /boards return your Miro board list.
Create a FlutterFlow API Group calling the Cloud Function
In your FlutterFlow project, click API Calls in the left navigation. Click + Add → Create API Group. Name the group 'Miro' and set the Base URL to your Cloud Function URL from Step 2 (for example, `https://us-central1-your-project.cloudfunctions.net/miroProxy`). Because the OAuth token management happens inside the Cloud Function, FlutterFlow needs no Authorization headers — the API Group has no shared headers to configure. Inside the Miro API Group, create the following API Calls: 1. 'GetBoards' — Method: GET. Endpoint: `/boards`. No variables needed for a simple board list. This returns a paginated list of boards the authorized user has access to. 2. 'GetBoardItems' — Method: GET. Endpoint: `/boards/{{ boardId }}/items`. Add a variable named `boardId` (String). Add optional query parameters: `type=sticky_note` to filter to only sticky notes, or leave blank for all item types. This returns items on a specific board. Test each Call. For GetBoards, click Test → Run and confirm the response contains a `data` array with board objects including `id`, `name`, and `modifiedAt`. For GetBoardItems, enter a board ID from the GetBoards response in the boardId test field and confirm item data returns. Now generate JSON Paths from both responses. In the API Call's Response & Test tab, click 'Generate JSON Paths from Sample' after a successful test. FlutterFlow will create named response fields for each JSON path, which you will bind to Custom Data Type fields in the next step.
1// API Call configurations:23// GetBoards:4// Method: GET5// Path: /boards6// No body, no variables78// Response JSON paths:9// Board IDs: $.data[*].id10// Board names: $.data[*].name11// Modified at: $.data[*].modifiedAt12// View links: $.data[*].viewLink1314// GetBoardItems:15// Method: GET16// Path: /boards/{{ boardId }}/items?type=sticky_note17// Variable: boardId (String)1819// Response JSON paths:20// Item IDs: $.data[*].id21// Item type: $.data[*].type22// Item content: $.data[*].data.content23// Item color: $.data[*].style.fillColorPro tip: Miro sticky notes return their text content in $.data[*].data.content as HTML (e.g. '<p>Task name</p>'). Strip the HTML tags in a FlutterFlow Custom Function before displaying in a Text widget, or handle it in your Cloud Function proxy.
Expected result: Both API Calls test successfully. GetBoards returns your Miro board list, and GetBoardItems returns items for a specific board. JSON paths are generated and ready for binding.
Bind board and item data to Custom Data Types and ListViews
In your FlutterFlow project, click the Data Types section in the left navigation. Create a Custom Data Type named 'MiroBoard' with fields: `id` (String), `name` (String), `modifiedAt` (String), and `viewLink` (String — the URL to view the board in Miro). Create a second Custom Data Type named 'MiroItem' with fields: `id` (String), `type` (String), `content` (String — the text of the sticky note), and `fillColor` (String — the background color). Navigate to your boards screen. In the Action Flow Editor, wire the screen's On Load event to call the Miro → GetBoards API Call. After the call completes, store the boards list in an App State variable of type List (using the MiroBoard Custom Data Type). Add a ListView widget to the screen and set it to generate children from the App State boards list. In the list item template, add a Card with Text widgets bound to `board.name` and `board.modifiedAt`. Add a tap action on each card: navigate to a detail screen and pass the `board.id` as a parameter. On the board detail screen, wire the On Load event to call GetBoardItems with the passed `boardId` parameter. Store the items in an App State List of MiroItem. Add a ListView that renders each sticky note as a colored card — use the `item.fillColor` value to set the card's background color conditionally (FlutterFlow's Color field supports dynamic values from data), and display `item.content` in a Text widget with HTML stripped. For the WebView embed alternative: if you don't need native list rendering and just want users to see an interactive board, skip the API integration entirely. In Miro, click Share → Embed → copy the embed link. Add a WebView widget to a screen in FlutterFlow and set the URL to the Miro embed link. Users can pan, zoom, and interact with the full board without any API setup.
1// JSON path binding for MiroBoard:2// id: $.data[*].id3// name: $.data[*].name4// modifiedAt: $.data[*].modifiedAt5// viewLink: $.data[*].viewLink67// JSON path binding for MiroItem:8// id: $.data[*].id9// type: $.data[*].type10// content: $.data[*].data.content11// fillColor: $.data[*].style.fillColor1213// Note: content may contain HTML like <p>Text</p>14// Strip tags with a Custom Function:15// String stripHtml(String html) {16// return html.replaceAll(RegExp(r'<[^>]*>'), '').trim();17// }Pro tip: Miro's viewLink field in the boards response is the direct URL to open the board in Miro's web app. You can use this as a Launch URL action on a board card so users can tap to open the full Miro board in their browser.
Expected result: Your FlutterFlow app shows a list of Miro boards on the main screen, and tapping a board shows a native list of sticky note cards with their text and background colors pulled from Miro's API.
Handle token expiry and add a manual refresh
Miro's OAuth access tokens expire after a period defined in the OAuth response's `expires_in` field (check current Miro documentation for the exact duration). The Cloud Function you deployed in Step 2 automatically handles token refresh using the stored refresh token, so FlutterFlow should not encounter 401 errors in normal operation. However, if the refresh token itself expires (which happens if the user has not authorized the app for an extended period) or if it is revoked in Miro, your Cloud Function will return a 401 and you will need to re-initiate the OAuth flow. In your FlutterFlow action flows, add an error check after each Miro API Call. If the response HTTP status is 401, show a message to the user ('Your Miro session has expired — please reconnect') and offer a 'Reconnect Miro' button. This button opens the Miro authorization URL in a WebView: `https://miro.com/oauth/authorize?response_type=code&client_id=YOUR_CLIENT_ID&redirect_uri=YOUR_REDIRECT_URI&scope=boards:read+board:item:read`. After the user authorizes, the Cloud Function's callback handler stores the new tokens in Firestore and subsequent API calls succeed again. Add a pull-to-refresh gesture on all board and item list screens. Miro's generous 1,000 requests per minute rate limit means you can safely refresh on every pull-to-refresh without hitting limits, even for active teams checking the board frequently. Add a visible 'Last updated' timestamp below the list header so users know how current the data is. For boards that update frequently, consider a periodic background refresh: use FlutterFlow's Timer action on the screen to call GetBoardItems every 30-60 seconds while the screen is open. At 1,000 req/min this is well within limits and gives users a near-real-time view of board changes.
Pro tip: Private workspace boards on Miro return 403 Forbidden if the authorized user doesn't have access. Test with a board in your own personal space first, then move to workspace boards once the integration is working.
Expected result: Token expiry is handled gracefully with a user-visible 'reconnect' flow. Pull-to-refresh works on all board and item screens. The app shows a 'last updated' timestamp so users know the data's freshness.
Common use cases
Sprint planning app showing live Miro sticky notes as task cards
A product team builds a FlutterFlow app that fetches sticky note items from their Miro sprint planning board and displays them as native cards in a grouped list. Team members can view sprint tasks and their status on mobile without opening Miro in a browser.
Build a 'Sprint Board' screen that shows all sticky notes from our Miro sprint planning board as cards grouped by column — each card should show the note text and its color.
Copy this prompt to try it in FlutterFlow
Client presentation app with an embedded live Miro board
A design agency builds a FlutterFlow app for client reviews that embeds their current Miro wireframe board in a full-screen WebView. Clients can pan and zoom the wireframes interactively, and the board always shows the latest version the agency updated in Miro.
Add a 'Design Review' screen that shows our Miro wireframe board as a live, interactive view — clients should be able to pinch to zoom and see the latest version without any app update.
Copy this prompt to try it in FlutterFlow
Team dashboard app listing all active Miro boards
A team manager builds a FlutterFlow app that pulls the list of all Miro boards from their workspace via the API, displays them as cards with board name and last modified date, and lets users tap to open any board in a WebView for interactive viewing.
Create a 'Boards' screen that lists all our Miro boards with the board name and when it was last updated — tapping a board should open it in full screen.
Copy this prompt to try it in FlutterFlow
Troubleshooting
API Call returns 401 Unauthorized after the initial setup worked
Cause: The Miro OAuth access token has expired and the Cloud Function's automatic refresh attempt failed — likely because the refresh token also expired or was revoked in Miro's app settings.
Solution: Re-initiate the OAuth authorization flow. In your FlutterFlow app, open the Miro authorization URL in a WebView, have the user re-authorize, and the Cloud Function callback will store fresh tokens in Firestore. Check the function logs in Firebase Console for the specific refresh error message.
GetBoardItems returns 403 Forbidden even with a valid access token
Cause: The authorized Miro user does not have access to the board being requested (private workspace board), or the OAuth scopes granted do not include board:item:read.
Solution: In Miro, confirm that the user who authorized the app has 'Can view' or higher access to the board. Also confirm that board:item:read scope was included when the user authorized your Miro app. If the scope was missing, the user needs to re-authorize after you add the scope in your Miro app settings.
Sticky note content shows raw HTML like '<p>Task text</p>' in the Text widget
Cause: Miro returns sticky note content as HTML-formatted text, including paragraph and formatting tags. FlutterFlow's Text widget renders this as plain text including the HTML markup.
Solution: Add a Custom Function in FlutterFlow that strips HTML tags from the content string before binding it to the Text widget. Use the regex pattern r'<[^>]*>' to remove all HTML tags. Alternatively, strip the tags in your Cloud Function proxy before returning the item data to FlutterFlow.
1// Custom Function to strip HTML (add in FlutterFlow Custom Code):2String stripHtml(String html) {3 return html.replaceAll(RegExp(r'<[^>]*>'), '').trim();4}WebView embed shows a blank screen or Miro login page instead of the board
Cause: The Miro embed URL was not generated using the 'Embed' share option, or the board's sharing settings require login to view.
Solution: In Miro, open the board, click Share, and look for the Embed option (not the standard share link). Copy the embed URL from there. Also ensure the board's share settings allow 'Anyone with the link' to view — boards set to 'Team only' or 'Invite only' will require login in the WebView.
Best practices
- Never store the Miro OAuth Client Secret in FlutterFlow — it must live only in your Cloud Function environment variables, as it enables token generation for your entire app.
- Use Miro's embed URL in a WebView for teams that only need to view boards interactively — it takes 5 minutes to set up and requires no backend, making it ideal for presentation or review use cases.
- Implement automatic token refresh in your Cloud Function so FlutterFlow never receives a 401 error during normal usage — check the token expiry before every proxied request and refresh proactively.
- Add pull-to-refresh on board and item screens rather than automatic polling by default — Miro's 1,000 req/min limit is generous, but consider user data costs and battery life on mobile.
- Strip HTML from Miro item content fields (sticky notes return HTML-formatted text) either in your Cloud Function proxy or in a FlutterFlow Custom Function before displaying in Text widgets.
- Test with a personal workspace board first before moving to team workspace boards — private workspace boards return 403 if the authorized user lacks access, and debugging permission errors is easier with a simpler test board.
- Only request the OAuth scopes your app actually needs — boards:read and board:item:read are sufficient for read-only dashboards; only add write scopes if you need to create or update board content.
- Store OAuth tokens in Firestore rather than Cloud Function environment variables so they persist across function cold starts and can be updated via the refresh flow without redeployment.
Alternatives
Lucidchart has no app-facing runtime API — it is embed-only via WebView or static image, making it simpler to add but limited to display-only use cases without live data.
Notion uses a simpler Bearer token instead of OAuth 2.0, making it easier to set up if you want structured content from a database rather than visual board items.
Monday.com's GraphQL API is better suited for task and project management workflows with structured column data, while Miro is the right choice for visual board content like sticky notes and diagrams.
Frequently asked questions
Does Miro's API require OAuth 2.0, or can I use a simpler token?
Miro's official API uses OAuth 2.0. However, for single-user internal apps, you can generate a personal access token in Miro's developer settings and use it directly as a static Bearer token in your Cloud Function — no OAuth flow required. This is the fastest approach for internal tools where one account's data powers the app. For multi-tenant apps where different users connect their own Miro accounts, you need the full OAuth 2.0 flow.
What is the difference between the Miro WebView embed and the API integration?
The WebView embed shows a live, interactive Miro board (pan, zoom, see all content in Miro's native UI) without any API setup — just paste the embed URL into a WebView widget. The API integration reads board and item data as structured JSON that you render natively in FlutterFlow widgets (lists, cards, text). Use the embed for quick board viewing; use the API when you need to display, filter, or process board data in your app's own native UI.
Can I write content back to Miro boards from FlutterFlow?
Yes. Miro's API supports creating and updating items via POST and PATCH requests. Your Cloud Function proxies these write requests the same way it proxies reads. You need to include the boards:write and board:item:write scopes when the user authorizes your Miro app. Common write operations include creating sticky notes, updating their text and color, and changing their position on the board.
Why does Miro's API show 403 for some boards?
403 errors occur when the authorized Miro user does not have the required access level for that board. Private workspace boards require 'Can view' or higher access. Team boards require board membership. The OAuth token is tied to the specific user who authorized the app — if that user doesn't have board access in Miro, the API reflects that restriction.
How does Miro compare to Lucidchart for a FlutterFlow integration?
They serve the same visual collaboration category but have opposite integration realities. Lucidchart has no app-facing runtime API — the only correct approach is to embed a static image or a live WebView. Miro has a full REST API v2 with OAuth, so you can read board data as structured JSON and display it natively in FlutterFlow. If you need real data integration (reading items, filtering content, displaying data in native widgets), Miro is the right choice. If you only need to display a diagram, Lucidchart's embed path is simpler.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation