Claude-code-plugins-plus-skills notion-common-errors
git clone https://github.com/jeremylongshore/claude-code-plugins-plus-skills
T=$(mktemp -d) && git clone --depth=1 https://github.com/jeremylongshore/claude-code-plugins-plus-skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/plugins/saas-packs/notion-pack/skills/notion-common-errors" ~/.claude/skills/jeremylongshore-claude-code-plugins-plus-skills-notion-common-errors && rm -rf "$T"
plugins/saas-packs/notion-pack/skills/notion-common-errors/SKILL.mdNotion Common Errors
Overview
Quick reference for all Notion API error codes with exact HTTP statuses, error bodies, and fixes. The API returns errors as JSON with three fields:
{ "object": "error", "status": 400, "code": "validation_error", "message": "Title is not a property that exists." }
All requests require
Authorization: Bearer <token> and Notion-Version: 2022-06-28 headers.
Prerequisites
installed (@notionhq/client
)npm install @notionhq/client
environment variable set (internal integration token starting withNOTION_TOKEN
orntn_
)secret_- Target pages/databases shared with the integration via the Connections menu
Instructions
Step 1: Identify the Error
Run the diagnostic script below or check your application logs. Match the HTTP status and
code field to the sections that follow.
Step 2: Match Error Code and Apply Fix
401 — unauthorized
unauthorized{"object": "error", "status": 401, "code": "unauthorized", "message": "API token is invalid."}
Cause: Token is missing, malformed, expired, or revoked.
Fix:
# Verify token is set echo ${NOTION_TOKEN:+SET} # Test directly curl -s https://api.notion.com/v1/users/me \ -H "Authorization: Bearer ${NOTION_TOKEN}" \ -H "Notion-Version: 2022-06-28" | jq .
If the response shows your integration bot user, the token is valid. Otherwise regenerate at notion.so/my-integrations. Tokens starting with
secret_ are legacy format — new integrations use ntn_ prefix.
403 — restricted_resource
restricted_resource{"object": "error", "status": 403, "code": "restricted_resource", "message": "Insufficient permissions for this resource."}
Cause: The integration exists and the page is shared, but the integration lacks the required capability (read content, update content, insert content, read comments).
Fix: Go to notion.so/my-integrations, select your integration, and enable the needed capabilities under "Capabilities." Common missing capability: "Read comments" when querying comments, or "Insert content" when creating pages.
404 — object_not_found
object_not_found{"object": "error", "status": 404, "code": "object_not_found", "message": "Could not find page with ID: abc123..."}
Cause: The page, database, or block either does not exist or has not been shared with your integration. This is the single most common Notion API error.
Fix:
- Open the target page in Notion
- Click the
menu at top right... - Select Connections and add your integration
- Parent pages must also be shared — sharing only a child page is not enough
// Defensive retrieval pattern try { const page = await notion.pages.retrieve({ page_id: pageId }); } catch (error) { if (isNotionClientError(error) && error.code === APIErrorCode.ObjectNotFound) { console.error('Page not shared with integration. Add via Connections menu.'); } }
Page ID gotcha: Notion URLs use 32-character hex IDs without dashes (
https://notion.so/Page-abc123def456...). The API accepts both dashed (abc123de-f456-...) and undashed formats. If you're extracting IDs from URLs, strip the page title prefix and use the last 32 characters.
400 — validation_error
validation_error{"object": "error", "status": 400, "code": "validation_error", "message": "..."}
Message varies. This is the broadest error category. Common sub-cases:
| Message Pattern | Cause | Fix |
|---|---|---|
| Wrong property name | Use exact name from database schema (case-sensitive) |
| Rich text passed as string | Wrap in |
| Missing parent in page create | Include |
| Wrong property type for filter | Match filter type to property type (see below) |
| Property renamed in Notion UI | Retrieve schema with to get current names |
Filter type mismatches — the most common validation error:
// WRONG: Status is a status property, not text { property: 'Status', text: { equals: 'Done' } } // RIGHT: Use the matching filter type { property: 'Status', status: { equals: 'Done' } } // WRONG: Passing plain string for title { Name: { title: 'My Page' } } // RIGHT: Title requires rich text array { Name: { title: [{ text: { content: 'My Page' } }] } }
Debug tip: Always retrieve the database schema first to avoid property name/type errors:
const db = await notion.databases.retrieve({ database_id: dbId }); console.log(Object.entries(db.properties).map(([name, prop]) => `${name}: ${prop.type}`)); // Output: "Name: title", "Status: status", "Tags: multi_select", ...
429 — rate_limited
rate_limited{"object": "error", "status": 429, "code": "rate_limited", "message": "Rate limited"}
Cause: Exceeded Notion's average rate limit of 3 requests per second per integration.
Fix:
import { Client, isNotionClientError, APIErrorCode } from '@notionhq/client'; async function withRetry<T>(fn: () => Promise<T>, maxRetries = 3): Promise<T> { for (let attempt = 0; attempt <= maxRetries; attempt++) { try { return await fn(); } catch (error) { if (isNotionClientError(error) && error.code === APIErrorCode.RateLimited) { const wait = Math.pow(2, attempt) * 1000; // exponential backoff console.log(`Rate limited. Waiting ${wait}ms (attempt ${attempt + 1}/${maxRetries})...`); await new Promise(r => setTimeout(r, wait)); continue; } throw error; } } throw new Error('Max retries exceeded'); }
The
@notionhq/client SDK has built-in retry with exponential backoff. If you hit rate limits frequently, batch operations and add delays between sequential calls. For bulk operations, see notion-rate-limits.
409 — conflict_error
conflict_error{"object": "error", "status": 409, "code": "conflict_error", "message": "Transaction has an existing lock on the object."}
Cause: Concurrent modifications to the same page, block, or database. Common in parallel scripts or multi-user workflows.
Fix: Retry the operation. The SDK handles this automatically. If writing your own retry logic, a simple retry after 1-2 seconds resolves most conflicts. Avoid parallelizing writes to the same page.
500 — internal_server_error
internal_server_error{"object": "error", "status": 500, "code": "internal_server_error", "message": "Internal Server Error"}
Cause: Bug or transient failure on Notion's servers.
Fix: Retry with exponential backoff. If persistent (>5 minutes), check status.notion.so for ongoing incidents. Consider filing a bug report at developers.notion.com with the request ID from the response headers (
x-request-id).
502/503 — service_unavailable
service_unavailable{"object": "error", "status": 503, "code": "service_unavailable", "message": "Notion is unavailable. Try again later."}
Cause: Notion's servers are down or under maintenance.
Fix:
# Check Notion status curl -s https://status.notion.so/api/v2/status.json | jq '.status.description'
Wait and retry. Monitor status.notion.so for incident updates.
Step 3: Common Non-HTTP Gotchas
// "body failed validation: body.children should be an array" // → Block children must always be an array, even for a single child. // Rich text structure — the #1 source of frustration // WRONG: "Hello" // RIGHT: [{ type: "text", text: { content: "Hello" } }] // Rich text is ALWAYS an array of rich text objects. // Block type mismatch when appending children // → Each block type has its own structure. A paragraph block needs: // { type: "paragraph", paragraph: { rich_text: [{ text: { content: "..." } }] } } // Timeout errors (default 60s) // → Increase via Client constructor: // new Client({ auth: token, timeoutMs: 120_000 }) // Pagination: missing results // → Always check has_more and pass start_cursor for next page. // Notion returns max 100 items per request.
Output
- Identified error cause from HTTP status and
fieldcode - Applied targeted fix from the matching section
- Verified resolution with test API call
Error Handling
| Code | HTTP | Error Name | Retryable | Recommended Action |
|---|---|---|---|---|
| 401 | Authentication failure | No | Regenerate token at notion.so/my-integrations |
| 403 | Missing capability | No | Enable capability in integration settings |
| 404 | Not shared / not found | No | Share page with integration via Connections menu |
| 400 | Malformed request | No | Fix request body — retrieve schema first |
| 429 | Rate limit exceeded | Yes | Respect header, use exponential backoff |
| 409 | Concurrent modification | Yes | Retry after 1-2s, serialize writes to same object |
| 500 | Notion server error | Yes | Retry with backoff, check status.notion.so |
| 502/503 | Notion down | Yes | Wait and retry, check status.notion.so |
| 504 | Request timeout | Yes | Retry, reduce query complexity or page size |
Examples
Full SDK Error Handler
import { Client, isNotionClientError, APIErrorCode, ClientErrorCode } from '@notionhq/client'; const notion = new Client({ auth: process.env.NOTION_TOKEN }); try { const page = await notion.pages.retrieve({ page_id: pageId }); } catch (error) { if (isNotionClientError(error)) { switch (error.code) { case APIErrorCode.ObjectNotFound: console.error('Page not found or not shared with integration'); break; case APIErrorCode.Unauthorized: console.error('Invalid API token'); break; case APIErrorCode.RestrictedResource: console.error('Integration lacks required capability'); break; case APIErrorCode.RateLimited: console.error('Rate limited — retry with backoff'); break; case APIErrorCode.ValidationError: console.error(`Validation error: ${error.message}`); break; case ClientErrorCode.RequestTimeout: console.error('Request timed out'); break; default: console.error(`Notion error: ${error.code} — ${error.message}`); } } else { throw error; // Non-Notion error } }
Quick Diagnostic Script
# 1. Check Notion status curl -s https://status.notion.so/api/v2/status.json | jq '.status.description' # 2. Verify token curl -s https://api.notion.com/v1/users/me \ -H "Authorization: Bearer ${NOTION_TOKEN}" \ -H "Notion-Version: 2022-06-28" | jq '{id, type, name}' # 3. Test database access (replace DB_ID) curl -s "https://api.notion.com/v1/databases/${DB_ID}" \ -H "Authorization: Bearer ${NOTION_TOKEN}" \ -H "Notion-Version: 2022-06-28" | jq '{id, title: .title[0].plain_text}'
Resources
- Notion API Error Codes
- Request Limits & Rate Limiting
- Notion Status Page
- API Introduction
- Working with Databases
- @notionhq/client npm
Next Steps
For comprehensive debugging workflows, see
notion-debug-bundle. For rate limit strategies at scale, see notion-rate-limits.