From miro-pack
Diagnose and fix Miro REST API v2 errors by HTTP status code. Use when encountering Miro API errors, debugging failed requests, or troubleshooting authentication and permission issues. Trigger with phrases like "miro error", "fix miro", "miro not working", "debug miro", "miro 401", "miro 403", "miro 429".
npx claudepluginhub flight505/skill-forge --plugin miro-packThis skill is limited to using the following tools:
Quick reference for Miro REST API v2 errors organized by HTTP status code, with real error response bodies and proven fixes.
Guides Next.js Cache Components and Partial Prerendering (PPR): 'use cache' directives, cacheLife(), cacheTag(), revalidateTag() for caching, invalidation, static/dynamic optimization. Auto-activates on cacheComponents: true.
Guides building MCP servers enabling LLMs to interact with external services via tools. Covers best practices, TypeScript/Node (MCP SDK), Python (FastMCP).
Share bugs, ideas, or general feedback.
Quick reference for Miro REST API v2 errors organized by HTTP status code, with real error response bodies and proven fixes.
curl available for diagnostic requests# 1. Verify API connectivity
curl -s -o /dev/null -w "%{http_code}" https://api.miro.com/v2/boards \
-H "Authorization: Bearer $MIRO_ACCESS_TOKEN"
# 2. Check token validity
curl -s https://api.miro.com/v1/oauth-token \
-H "Authorization: Bearer $MIRO_ACCESS_TOKEN" | jq
# 3. Check Miro status page
curl -s https://status.miro.com/api/v2/status.json | jq '.status.description'
{
"status": 400,
"code": "invalidInput",
"message": "Could not resolve the value for parameter: data.content",
"context": { "fields": [{ "field": "data.content", "message": "Required" }] }
}
Common causes:
shape: 'oval' — correct is shape: 'circle')Fix: Cross-reference your request body with the REST API reference. Each item type has specific required fields.
Sticky note required fields: data.content, data.shape (square or rectangle)
Shape required fields: data.shape (see miro-sdk-patterns for valid shapes)
Connector required fields: startItem.id, endItem.id
{
"status": 401,
"code": "tokenNotProvided",
"message": "Access token is not provided"
}
{
"status": 401,
"code": "tokenExpired",
"message": "Access token has expired"
}
Common causes:
Authorization: Bearer <token> headerFix:
# Check if token is set
echo "Token length: ${#MIRO_ACCESS_TOKEN}"
# Refresh expired token
curl -X POST https://api.miro.com/v1/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=refresh_token" \
-d "client_id=$MIRO_CLIENT_ID" \
-d "client_secret=$MIRO_CLIENT_SECRET" \
-d "refresh_token=$MIRO_REFRESH_TOKEN"
{
"status": 403,
"code": "insufficientPermissions",
"message": "Required scopes: boards:write",
"context": { "requiredScopes": ["boards:write"] }
}
Common causes:
Fix:
| Endpoint Category | Required Scope |
|---|---|
| GET boards/items | boards:read |
| POST/PATCH/DELETE boards/items | boards:write |
| GET team/members | team:read |
| Organization endpoints | organizations:read |
{
"status": 404,
"code": "boardNotFound",
"message": "Board not found or access denied"
}
Common causes:
Fix:
# Verify board exists and you have access
curl -s https://api.miro.com/v2/boards/$BOARD_ID \
-H "Authorization: Bearer $MIRO_ACCESS_TOKEN" | jq '.id, .name'
# List boards to find correct ID
curl -s "https://api.miro.com/v2/boards?limit=10" \
-H "Authorization: Bearer $MIRO_ACCESS_TOKEN" | jq '.data[] | {id, name}'
{
"status": 409,
"code": "duplicateTagTitle",
"message": "A tag with this title already exists"
}
Common causes:
Fix: Fetch existing tags first and reuse their IDs instead of creating duplicates.
{
"status": 429,
"code": "rateLimitExceeded",
"message": "Rate limit exceeded"
}
Response headers:
X-RateLimit-Limit: 100000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1700000060
Retry-After: 30
Fix: Honor the Retry-After header. See miro-rate-limits for complete backoff patterns. The global limit is 100,000 credits/minute.
Common causes:
Fix:
miro-rate-limits)async function handleMiroError(response: Response, context: string): Promise<never> {
const body = await response.json().catch(() => ({}));
switch (response.status) {
case 401:
console.error(`[Miro:${context}] Token expired/invalid. Refreshing...`);
// Trigger token refresh
break;
case 403:
console.error(`[Miro:${context}] Missing scopes: ${body.context?.requiredScopes?.join(', ')}`);
break;
case 429:
const retryAfter = response.headers.get('Retry-After') ?? '60';
console.warn(`[Miro:${context}] Rate limited. Retry after ${retryAfter}s`);
break;
default:
console.error(`[Miro:${context}] ${response.status}: ${body.message ?? 'Unknown error'}`);
}
throw new Error(`Miro API ${response.status}: ${body.message ?? context}`);
}
miro-debug-bundleX-Request-Id response header)For comprehensive debugging, see miro-debug-bundle.