Miro Migration Deep Dive
Overview
Comprehensive guide for migrating Miro boards between teams and organizations, updating
from REST API v1 to v2, and re-platforming from competing whiteboard tools (Lucidchart,
FigJam). Covers board content export with cursor pagination, bulk import with rate-limit
aware queuing, widget API changes between v1 and v2, and the new app framework patterns.
Typical migration scope: dozens to thousands of boards with connectors, tags, and members.
Migration Assessment
async function assessMigration(teamId: string) {
const boards = await miroFetch(`/v2/boards?team_id=${teamId}&limit=50`);
let totalItems = 0;
for (const board of boards.data) {
const items = await miroFetch(`/v2/boards/${board.id}/items?limit=1`);
totalItems += items.total ?? 0;
}
console.log(`Team ${teamId}: ${boards.data.length} boards, ~${totalItems} items`);
console.log('API version: v2 (v1 deprecated 2024-01)');
console.log('Widget types to migrate: sticky_note, shape, card, text, frame, image, connector');
return { boardCount: boards.data.length, totalItems };
}
Step-by-Step Migration
Phase 1: Prepare — Export Source Boards
Export every item on a board to a structured JSON file with cursor-paginated reads:
interface BoardExport {
exportedAt: string;
board: { id: string; name: string; description: string; owner: { id: string; name: string } };
items: any[]; connectors: any[]; tags: any[]; members: any[];
}
async function exportBoard(boardId: string): Promise<BoardExport> {
const board = await miroFetch(`/v2/boards/${boardId}`);
const items = await paginateAll(`/v2/boards/${boardId}/items`);
const connectors = await paginateAll(`/v2/boards/${boardId}/connectors`);
const tags = await miroFetch(`/v2/boards/${boardId}/tags`);
const members = await ();
{
: ().(),
: { : board., : board., : board. ?? ,
: { : board.?., : board.?. } },
: items.( ({ : i., : i., : i., : i.,
: i., : i., : i.?. })),
connectors, : tags. ?? [], : members. ?? [],
};
}
(): <[]> {
: [] = [];
: | ;
{
params = ({ : });
(cursor) params.(, cursor);
page = ();
all.(...page.);
cursor = page.;
} (cursor);
all;
}
Phase 2: Migrate — Import to Target Board
Recreate exported items on a new board with rate-limit aware queuing (frames first,
then other items, then connectors, then tags):
import PQueue from 'p-queue';
async function importToBoard(targetBoardId: string, exportData: BoardExport): Promise<{
created: number; failed: number; idMap: Map<string, string>;
}> {
const queue = new PQueue({ concurrency: 3, interval: 1000, intervalCap: 8 });
const idMap = new Map<string, string>();
let created = 0, failed = 0;
const endpointMap: Record<string, string> = {
sticky_note: 'sticky_notes', shape: 'shapes', card: 'cards', text: 'texts',
frame: 'frames', image: 'images', document: , : ,
};
sorted = [...exportData.].(
(a. === ? : ) - (b. === ? : ));
( item sorted) {
queue.( () => {
{
ep = endpointMap[item.];
(!ep) ();
newItem = (, , {
: item., : item., : item., : item.,
});
idMap.(item., newItem.);
created++;
} { failed++; }
});
}
queue.();
( conn exportData.) {
startId = idMap.(conn.?.), endId = idMap.(conn.?.);
(!startId || !endId) ;
queue.( () => {
(, , {
: { : startId }, : { : endId },
: conn., : conn.,
}).( { failed++; });
created++;
});
}
queue.();
{ created, failed, idMap };
}
Phase 3: Validate — Compare Source and Target
async function validateMigration(sourceBoardId: string, targetBoardId: string) {
const srcItems = await paginateAll(`/v2/boards/${sourceBoardId}/items`);
const tgtItems = await paginateAll(`/v2/boards/${targetBoardId}/items`);
const srcConn = await paginateAll(`/v2/boards/${sourceBoardId}/connectors`);
const tgtConn = await paginateAll(`/v2/boards/${targetBoardId}/connectors`);
const checks = [
{ name: 'Item count', pass: tgtItems.length >= srcItems.length * 0.95,
detail: `${tgtItems.length}/${srcItems.length}` },
{ name: 'Connectors', pass: tgtConn.length >= srcConn.length * 0.9,
detail: `${tgtConn.length}/${srcConn.length}` },
];
console.log(checks.map( ).());
checks.( c.);
}
Rollback Plan
curl -X DELETE "https://api.miro.com/v2/boards/${TARGET_BOARD_ID}" \
-H "Authorization: Bearer $MIRO_TOKEN"
cat imported-ids.txt | while read id; do
curl -X DELETE "https://api.miro.com/v2/boards/${TARGET_BOARD_ID}/items/${id}" \
-H "Authorization: Bearer $MIRO_TOKEN"
done
echo "Rollback complete — source board unchanged"
Migration Checklist
Error Handling
| Issue | Cause | Fix |
|---|
429 Too Many Requests | Rate limit exceeded | Reduce PQueue concurrency to 2 |
| Connector creation fails | Referenced item missing | Verify idMap has both start/end IDs |
| Image items 404 | External URL expired | Re-upload image or use placeholder |
| Position overlap on target | No offset applied | Pass offsetX/offsetY to import |
| Tag 409 Conflict | Duplicate tag title | Catch 409, query existing tag by title |
Resources
Next Steps
For starting a new Miro integration from scratch, see miro-install-auth. For
board sharing and collaboration workflows, see miro-core-workflow-b.