# How to Integrate Retool with Miro

- Tool: Retool
- Difficulty: Intermediate
- Time required: 20 minutes
- Last updated: April 2026

## TL;DR

Connect Retool to Miro by creating a REST API Resource pointing to Miro's REST API (api.miro.com/v2), authenticated with an OAuth 2.0 access token or a static app token. Pull structured data from Miro boards — sticky notes, text elements, shapes, and card items — into Retool tables for retrospective analysis, sprint planning aggregation, and turning collaborative whiteboard content into actionable structured data.

## Why Connect Retool to Miro?

Product and engineering teams use Miro for collaborative sessions — sprint retrospectives, roadmap planning, user journey mapping, and brainstorming — but Miro's whiteboard format makes it difficult to extract insights and track action items after the session ends. Content lives in visual form on the board, but follow-up tasks, decisions, and patterns need to be in structured formats (databases, project management tools, spreadsheets) to be actionable. Connecting Miro to Retool bridges this gap.

Common use cases include a retrospective aggregator that reads sticky notes from a Miro retro board, categorizes them by column (went well, needs improvement, action items), and populates a Jira or Asana board with action items. Roadmap planning panels pull card items from Miro planning boards into Retool tables where engineering managers can sort, filter, and assign priority scores. Research teams extract affinity map clusters from Miro boards into structured analysis tables, identifying patterns in user research sessions that span dozens of boards.

Miro's REST API v2 provides access to boards, board items (sticky notes, text, shapes, frames, cards, images), team data, and tags. The API returns items with their content, geometry (position and size), and style attributes (color, font), enabling sophisticated extraction and categorization workflows that treat Miro board content as structured data.

## Before you start

- A Retool account (Cloud or self-hosted) with permission to create Resources
- A Miro account with access to at least one Miro team and the boards you want to query — the API access level is limited by your Miro plan (Free, Starter, Business)
- A Miro developer app created at miro.com/app/settings/user-profile/apps — from the app settings, you can obtain a static app token (for development and single-team use) or configure OAuth 2.0 (for multi-user or production use)
- The Board ID for the Miro boards you want to query — visible in the URL when you open a Miro board: miro.com/app/board/{BOARD_ID}/
- Basic familiarity with Miro's item types: sticky_note, text, shape, frame, card, image — the API returns different fields for each type

## Step-by-step guide

### 1. Create a Miro app and obtain an access token

Miro's REST API requires creating a developer app in the Miro dashboard to obtain credentials. Navigate to miro.com/app/settings/user-profile/apps and click Create new app. Enter an app name (e.g., 'Retool Integration'), select the team this app will have access to, and click Create app.

In the app settings, you will see two authentication options:

Option 1 — Static App Token (simplest for development): In the app settings page, scroll to the 'App token' section and copy the token. This is a long-lived token that grants access to boards in the team the app is installed on. It's the fastest option for getting started but is less secure than OAuth 2.0 because it doesn't expire automatically.

Option 2 — OAuth 2.0 (recommended for production): Configure the OAuth 2.0 redirect URL in the app settings (e.g., https://retool.com as a placeholder for initial setup). Miro will provide a client_id and client_secret. Implement the authorization code flow to get an access_token and refresh_token. The access_token expires after 1 hour, so production setups need token refresh handling.

For initial development, use the static App Token — it works immediately without going through the OAuth flow. Note your app token; you will use it as the Bearer token in the Retool resource configuration.

Also find the Board IDs for boards you want to query: open a Miro board and copy the ID from the URL (the part after /board/ and before any query parameters, which looks like uXjVN8Xxxxxxxx=). Store these in a Retool Configuration Variable or as selectable constants in your app.

**Expected result:** You have a Miro app created and either a static app token or OAuth access token ready. You have copied the Board ID from at least one Miro board to use in test queries.

### 2. Create the Miro REST API Resource in Retool

Navigate to the Resources tab in Retool and click Add Resource. Select REST API from the resource type list. Configure the resource:

- Name: 'Miro API'
- Base URL: https://api.miro.com/v2

For authentication, select Bearer Token from the Auth dropdown and enter your Miro app token (or OAuth access token).

Add a default header for all requests:
- Key: Accept
- Value: application/json

Click Save Changes.

Verify the resource by creating a test query:
- Method: GET
- Path: /boards
- URL Parameters: limit: 10

Run the query. A successful response returns a list of Miro boards accessible to the token, including each board's id, name, description, and team_id. If you see a 401 error, the token is incorrect or has insufficient permissions. If the boards array is empty, the token's associated team has no boards or the app has not been installed on any team.

Note the format of board IDs in the response — they contain URL-unsafe characters (= signs) that need to be URL-encoded as %3D when used in URL path segments. When using a board ID in a path (e.g., /boards/{board_id}/items), ensure the ID is properly encoded.

**Expected result:** The Miro REST API Resource appears in the Resources list. A test GET to /boards returns a list of accessible Miro boards with their IDs and names, confirming the token is working correctly.

### 3. Query board items and extract sticky note content

The Miro API's /boards/{board_id}/items endpoint returns all items on a board, with optional filtering by item type. For retrospective and brainstorming boards, sticky notes are the primary content type.

Create a new query using the Miro API resource:
- Method: GET
- Path: /boards/{{ encodeURIComponent(boardIdInput.value) }}/items
- URL Parameters:
  - type: sticky_note (filter to only sticky notes)
  - limit: 50
  - cursor: {{ pageState.cursor || '' }} (for pagination)

The response returns an items array where each sticky_note object has: id, type, data (containing content as HTML string), style (fillColor, textColor, fontSize), geometry (x, y, width, height relative positions), and createdAt/modifiedAt timestamps.

Note that Miro's item content is returned as an HTML string (e.g., '<p>Action item: Fix login bug</p>'). Your transformer needs to strip HTML tags to extract the plain text.

Write a JavaScript transformer that strips HTML from content, extracts the plain text, formats the position data (x, y coordinates normalized to the board), and returns an array of cleaned sticky note objects. This cleaned array is what you bind to the Retool Table.

```
// JavaScript transformer for Miro sticky note items
const items = data?.data || [];

// Helper to strip HTML tags from Miro item content
function stripHtml(html) {
  if (!html) return '';
  return html
    .replace(/<br\s*\/?>/gi, '\n')
    .replace(/<\/p>/gi, '\n')
    .replace(/<[^>]+>/g, '')
    .replace(/&amp;/g, '&')
    .replace(/&lt;/g, '<')
    .replace(/&gt;/g, '>')
    .replace(/&quot;/g, '"')
    .replace(/&#39;/g, "'")
    .trim();
}

return items.map(item => ({
  id: item.id,
  type: item.type,
  content: stripHtml(item.data?.content || ''),
  color: item.style?.fillColor || '#fff9b1', // default Miro yellow
  x_position: Math.round(item.position?.x || 0),
  y_position: Math.round(item.position?.y || 0),
  width: Math.round(item.geometry?.width || 200),
  height: Math.round(item.geometry?.height || 200),
  created_at: item.createdAt
    ? new Date(item.createdAt).toLocaleDateString()
    : '',
  modified_at: item.modifiedAt
    ? new Date(item.modifiedAt).toLocaleDateString()
    : ''
}));
```

**Expected result:** A Table displays all sticky notes from the selected Miro board with their plain-text content, color, and position coordinates. The HTML is stripped cleanly from the content field. Sticky notes appear as rows with readable text content.

### 4. Categorize sticky notes by position to rebuild retro columns

For retrospective boards where columns are implicit (defined by position rather than explicit frames), use the x-coordinate of each sticky note to determine which column it belongs to. Miro coordinates use a center-origin system where items to the left of center have negative x values and items to the right have positive x values.

Create a second transformer (or extend the first) that analyzes the position of sticky notes and assigns category labels. You need to know the approximate x-coordinate boundaries of each column — open the Miro board, select items in each column, and note their x-coordinates from the selection panel or URL parameters.

Alternatively, query for frame items (type: frame) first to get the named frames on the board, then determine which sticky notes fall within each frame's bounding box based on coordinates. Frames in Miro represent sections, swimlanes, or columns — they have x, y, width, and height in the geometry object.

Write a JavaScript query (no resource required) that combines the frame positions from the frames query and the sticky note positions from the items query, assigns each sticky note to the frame it falls within, and outputs the categorized list. Bind this combined output to the Retool Table with a category column showing the frame name (column label).

Add a Chart component showing a count of sticky notes per category. This gives a quick visual summary of retro sentiment — how many items in each column — before drilling into the full list.

```
// JavaScript query to join sticky notes with frame categories
// Assumes framesQuery and stickyNotesQuery are both resolved

const frames = framesQuery.data || [];
const stickyNotes = stickyNotesQuery.data || [];

// Each frame has: id, content (name), x_position, y_position, width, height
// Determine if a sticky note's center falls within a frame's bounding box
function getCategory(note, frames) {
  const noteCenterX = note.x_position;
  const noteCenterY = note.y_position;

  for (const frame of frames) {
    const frameLeft = frame.x_position - frame.width / 2;
    const frameRight = frame.x_position + frame.width / 2;
    const frameTop = frame.y_position - frame.height / 2;
    const frameBottom = frame.y_position + frame.height / 2;

    if (noteCenterX >= frameLeft && noteCenterX <= frameRight &&
        noteCenterY >= frameTop && noteCenterY <= frameBottom) {
      return frame.content || 'Unnamed Section';
    }
  }
  return 'Uncategorized';
}

return stickyNotes.map(note => ({
  ...note,
  category: getCategory(note, frames)
}));
```

**Expected result:** Sticky notes in the Table now have a 'category' column showing which frame (retro column) they belong to. The chart shows the count per category. Action items from the retro can be identified by category and sorted for follow-up.

### 5. Query all item types and build a board content summary

For a more comprehensive board analysis tool, query all item types simultaneously and build a content summary that categorizes the board by item type, total count, and content statistics. This is useful for boards used in user research synthesis or competitive analysis where teams mix sticky notes, text blocks, images, and cards.

Create queries for each relevant item type using the Miro API resource:
- One query for sticky_note items
- One query for text items (type: text)
- One query for card items (type: card) — cards have structured data with title, description, and optional assignee

Set all three queries to run in parallel when the board ID is entered. Use a JavaScript query that combines the results: count items by type, extract text content from each type, and build a summary object.

Display the summary as a set of Stat components (total sticky notes, total text items, total cards) and a comprehensive Table showing all extractable text content across item types. For card items specifically, extract the title and description separately since Miro cards have structured fields unlike the HTML-content sticky notes.

This combined view gives teams a searchable archive of all whiteboard content from a specific board — useful for finding specific keywords or themes across mixed-format boards.

```
// JavaScript transformer for Miro card items (different structure from sticky notes)
const cards = data?.data || [];

function stripHtml(html) {
  if (!html) return '';
  return html.replace(/<[^>]+>/g, '').replace(/&amp;/g, '&').trim();
}

return cards.map(card => ({
  id: card.id,
  type: 'card',
  title: stripHtml(card.data?.title || ''),
  description: stripHtml(card.data?.description || ''),
  content: [stripHtml(card.data?.title || ''), stripHtml(card.data?.description || '')]
    .filter(Boolean).join(' — '),
  assignee_id: card.data?.assigneeId || null,
  due_date: card.data?.dueDate
    ? new Date(card.data.dueDate).toLocaleDateString()
    : '',
  tags: (card.tagIds || []).join(', '),
  x_position: Math.round(card.position?.x || 0),
  y_position: Math.round(card.position?.y || 0)
}));
```

**Expected result:** A board summary panel shows Stat components for total sticky notes, text items, and cards. A unified Table displays all item content with a type column. The search input filters across all content types to find specific keywords across the entire board.

## Best practices

- Store the Miro app token or OAuth credentials in Retool Configuration Variables marked as secret — never hardcode them in query bodies or resource headers.
- Always URL-encode Miro board IDs using encodeURIComponent() before using them in URL paths — board IDs contain '=' characters that break URL parsing if not encoded.
- Strip HTML from Miro item content fields before binding to Retool Table components — all Miro item content is returned as HTML strings, not plain text.
- Use the type parameter in the /items endpoint to filter by specific item types rather than fetching all item types and filtering client-side — this reduces response size and improves query performance on large boards.
- Implement cursor-based pagination for large boards — boards with hundreds of items will require multiple paginated requests to retrieve complete data.
- Cache Miro board data for 10-30 minutes using Retool's query caching feature for retrospective analysis boards — the data doesn't change after the session ends, so caching prevents redundant API calls.
- Inspect the raw Miro API response in Retool's State panel before writing transformers — item structure varies significantly between types (sticky_note, card, text, shape), and assuming a uniform structure will cause transformer errors.

## Use cases

### Build a retrospective action item extractor

Create a Retool panel that connects to a Miro retrospective board, reads all sticky notes, categorizes them by their column (based on x-position or frame membership), and displays them in a structured Table with category (went well, improve, action items), content text, and assignee (if noted). Teams use this after each sprint retro to quickly extract and triage action items into their project management tool.

Prompt example:

```
Build a retro aggregator that accepts a Miro board ID input. Query the Miro API for all sticky_note items on that board. Use the sticky note's x-position to infer which retro column it belongs to (divide the board horizontally into thirds: left=Went Well, center=Needs Improvement, right=Action Items). Display a Table grouped by category with the sticky note content. Add a 'Copy to Clipboard' button that formats the action items as a bullet list.
```

### Create a planning board data aggregator for roadmap management

Build a Retool dashboard that reads card items from Miro roadmap boards and converts them into a structured project list. Each Miro card becomes a row in a Retool Table with title, description, assigned team, and quarter/milestone (derived from which swimlane frame the card is in). Product managers use this to quickly generate structured roadmap exports from their visual Miro planning sessions.

Prompt example:

```
Create a roadmap extractor for a Miro planning board. Query the Miro API for all card items and frame items on a specified board. For each card, determine which frame (swimlane or quarter) it belongs to based on parent-child relationships or positional overlap. Display a Table with card title, description, frame name (as the quarter or swimlane label), and card tag colors as priority indicators.
```

### Build a multi-board content search and analysis panel

Build a Retool panel that searches across multiple Miro boards in a team for specific content — finding all sticky notes mentioning a particular theme, keyword, or tag across all boards from the last quarter. Research teams and strategists use this to identify cross-session patterns in user research boards, competitive analysis sessions, or ideation workshops.

Prompt example:

```
Create a cross-board content search panel with a team input and a keyword search. Query the Miro API for all boards in the team, then for each board fetch all sticky_note and text items. Filter items whose content contains the search keyword. Display matching items in a Table with board name, item content, and date created. Group by board name using a select component to drill into specific boards.
```

## Troubleshooting

### GET /boards/{board_id}/items returns 404 Not Found even though the board exists in Miro

Cause: The board ID contains '=' characters that must be URL-encoded as '%3D' in the URL path. An unencoded '=' in the path causes the URL to be malformed, resulting in a 404 from the API.

Solution: URL-encode the board ID before using it in the path. In Retool's query path field, use: /boards/{{ encodeURIComponent(boardIdInput.value) }}/items instead of /boards/{{ boardIdInput.value }}/items. Alternatively, manually replace '=' characters with '%3D' in hardcoded board IDs.

```
// URL-encode board ID in Retool query path
// Correct:
/boards/{{ encodeURIComponent(boardIdInput.value) }}/items
// Incorrect:
/boards/{{ boardIdInput.value }}/items
```

### Sticky note content appears as raw HTML string in the Retool Table (e.g., '<p>Action item</p>')

Cause: Miro's API returns item content as HTML, not plain text. The transformer is not stripping the HTML tags before binding to the Table component.

Solution: Apply the stripHtml helper function in your transformer before returning the content field. The function should handle '<p>' tags (converting to newlines), '<br>' tags, HTML entities (&amp;, &lt;, etc.), and all other HTML elements.

```
function stripHtml(html) {
  if (!html) return '';
  return html
    .replace(/<br\s*\/?>/gi, '\n')
    .replace(/<\/p>/gi, '\n')
    .replace(/<[^>]+>/g, '')
    .replace(/&amp;/g, '&')
    .replace(/&lt;/g, '<')
    .replace(/&gt;/g, '>')
    .trim();
}
```

### Items query only returns the first 50 items even though the board has hundreds of sticky notes

Cause: The Miro API paginates results with a default limit of 50 items per page. The response includes a cursor value for fetching the next page, but additional pages are not automatically fetched.

Solution: Check the response for a cursor field in the top-level response object. If it exists, it means more items are available. Add a state variable in Retool to store the current cursor, and add a 'Load More' button that triggers the items query again with the cursor URL parameter set to the stored cursor value. Accumulate results across pages in a state variable rather than replacing them.

### OAuth access token expires after 1 hour causing all queries to fail with 401 errors

Cause: Miro's OAuth access tokens expire after 60 minutes. The static app token does not expire but OAuth tokens do, requiring automatic refresh using the refresh_token.

Solution: Implement token refresh using Retool's Custom Auth feature on the resource, or use the static app token instead of OAuth for internal dashboards. If OAuth is required, store the refresh_token in a Retool Configuration Variable and set up a Retool Workflow that runs every 50 minutes to exchange the refresh token for a new access token, updating the MIRO_ACCESS_TOKEN configuration variable automatically.

## Frequently asked questions

### What is the difference between a Miro app token and an OAuth 2.0 access token for Retool integration?

A Miro app token is a static, long-lived credential associated with a specific Miro app and team. It grants access to boards within that team and is the simplest authentication method for internal Retool dashboards. An OAuth 2.0 access token is a short-lived (1-hour) token obtained through a user authorization flow, which allows the integration to act on behalf of a specific user with their permissions. For internal team dashboards where you control the Miro boards and don't need user-level permission scoping, the static app token is the simpler and recommended choice.

### Can Retool create or modify Miro board content through the API?

Yes — Miro's REST API includes POST and PATCH endpoints for creating and updating board items. You can build Retool forms that create sticky notes, cards, or text items on Miro boards by sending POST requests to /boards/{board_id}/sticky_notes or /boards/{board_id}/cards. This enables two-way workflows: pull planning items from Miro for analysis in Retool, then push back approved action items or decisions to a designated Miro board section.

### How do I handle Miro boards with hundreds of sticky notes in Retool?

Use Miro's cursor-based pagination — the /items endpoint returns a maximum of 50 items per request along with a cursor value for the next page. In Retool, implement pagination by storing the cursor in a state variable and adding a 'Load More' button that fetches the next page. Accumulate results across pages in a state variable array using JavaScript: state.setValue([...currentItems, ...newItems]). For very large boards (500+ items), consider using a Retool Workflow to pre-fetch and cache all items in a database rather than fetching live on each dashboard load.

### Why are sticky note positions important for data extraction, and how are Miro coordinates structured?

Miro's whiteboard uses an infinite canvas with a center-origin coordinate system — the center of the canvas is (0,0), with x increasing rightward and y increasing downward. Sticky note positions determine their column or section on template boards (like retro templates where columns are defined by x-coordinate ranges). The position values in the API response are large floating-point numbers representing pixel positions on the infinite canvas. To use positions for categorization, inspect actual item coordinates by running a query and checking the position.x and position.y values in Retool's State panel.

### Does Miro's API support real-time data, or is there always a delay?

The Miro REST API reflects the current saved state of boards and does not provide real-time streaming. There is typically no significant delay — the API returns the current board state at the time of the request. However, for boards where multiple users are actively editing in real-time, the API may not reflect in-progress changes until they are committed. For post-session analysis (pulling data after a retro or planning session is complete), this is not a concern. Miro does not currently offer a webhook API for real-time board change notifications.

---

Source: https://www.rapidevelopers.com/retool-integrations/miro
© RapidDev — https://www.rapidevelopers.com/retool-integrations/miro
