From miro-pack
Diagnoses Miro REST API v2 errors by HTTP status code with curl diagnostics and fixes for bad requests, auth failures, and permissions.
npx claudepluginhub jeremylongshore/claude-code-plugins-plus-skills --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.
Runs triage, mitigation, and postmortem for Miro REST API v2 outages and errors using bash scripts for status, rate limits, auth checks.
Debugs and resolves common MaintainX API errors like 400 Bad Request, 401 Unauthorized, and 403 Forbidden with curl diagnostics, bash tests, and concrete fixes.
Guides Next.js Cache Components and Partial Prerendering (PPR) with cacheComponents enabled. Implements 'use cache', cacheLife(), cacheTag(), revalidateTag(), static/dynamic optimization, and cache debugging.
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.