@notionhq/client
>=1.0.0postconditions43functions22last verified2026-06-24coverage score100%Postconditions: what we check
- query · api-errorerrorWhenAny API or network failure: authentication error (401 unauthorized), permission error (403 restricted_resource), database not found (404 object_not_found), rate limit (429 rate_limited), server error (500 internal_server_error), service unavailable (503), client timeout (RequestTimeoutError), or connection failure.Throws
APIResponseError with error.code (APIErrorCode enum) for API failures; RequestTimeoutError for client-side timeouts (default 60s); UnknownHTTPResponseError for unexpected HTTP responses. Common: ObjectNotFound when database_id is deleted/unshared, Unauthorized when NOTION_TOKEN is invalid/expired. SDK auto-retries rate_limited (429) but throws after maxRetries exhausted.Required handlingCaller MUST wrap notion.databases.query() in try-catch. API and network errors are thrown after retry exhaustion. Unhandled rejections crash Next.js API routes, Express handlers, and serverless functions. Minimum handling: try { const response = await notion.databases.query({ database_id: '...' }); } catch (error) { if (isNotionClientError(error)) { console.error('Notion API error:', error.code, error.message); } throw error; } For integrations that must handle "not found" gracefully: } catch (error) { if (APIResponseError.isAPIResponseError(error) && error.code === APIErrorCode.ObjectNotFound) { return null; // Database deleted or not shared } throw error; }costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible - create · api-errorerrorWhenAny API or network failure: permission error (403), parent not found (404), validation error (400 — missing required properties or invalid property values), rate limit (429), server error (500/503), or connection failure.Throws
APIResponseError with error.code for API failures; RequestTimeoutError for timeouts; ValidationError (400) is common when required database properties are missing or property values don't match database schema.Required handlingCaller MUST wrap notion.pages.create() in try-catch. Silent failure means the entry is never created with no visible error in the application. Minimum handling: try { const page = await notion.pages.create({ parent: { ... }, properties: { ... } }); } catch (error) { if (isNotionClientError(error)) { console.error('Failed to create Notion page:', error.code); } throw error; }costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible - update · api-errorerrorWhenAny API or network failure: page not found (404 — deleted or unshared), permission error (403), validation error (400 — invalid property values), rate limit (429), server error (500/503 — not auto-retried for PATCH), or connection failure.Throws
APIResponseError with error.code for API failures; RequestTimeoutError for timeouts. Note: PATCH is not idempotent — 500/503 errors are NOT auto-retried by the SDK. The caller receives the error after the first failure.Required handlingCaller MUST wrap notion.pages.update() in try-catch. PATCH failures are not auto-retried — the SDK only auto-retries GET/DELETE on 500/503. Minimum handling: try { await notion.pages.update({ page_id: '...', properties: { ... } }); } catch (error) { console.error('Failed to update Notion page:', error); throw error; }costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible - append · api-errorerrorWhenAny API or network failure: block/page not found (404), permission error (403), validation error (400 — invalid block structure), rate limit (429), server error (500/503), or connection failure.Throws
APIResponseError with error.code for API failures; RequestTimeoutError for timeouts. ValidationError (400) is common when block objects have invalid structure. PATCH is not auto-retried on 500/503.Required handlingCaller MUST wrap notion.blocks.children.append() in try-catch. Minimum handling: try { await notion.blocks.children.append({ block_id: '...', children: [...] }); } catch (error) { console.error('Failed to append Notion blocks:', error); throw error; }costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible - search · api-errorerrorWhenAny API or network failure: permission error (403), rate limit (429), server error (500/503), or connection failure.Throws
APIResponseError with error.code for API failures; RequestTimeoutError for timeouts.Required handlingCaller MUST wrap notion.search() in try-catch. Minimum handling: try { const results = await notion.search({ query: '...' }); } catch (error) { console.error('Notion search failed:', error); throw error; }costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible - retrieve · databases-retrieve-object-not-founderrorWhendatabases.retrieve() called when database_id refers to a database that has been deleted, trashed, or was never shared with the integration. Notion databases are frequently shared/unshared by workspace members — code that cached a database_id may encounter this error on subsequent calls.Throws
APIResponseError with code ObjectNotFound (APIErrorCode.ObjectNotFound, HTTP 404). SDK auto-retries rate_limited (429) but NOT object_not_found — this error is thrown immediately.Required handlingCaller MUST wrap notion.databases.retrieve() in try-catch. Handle ObjectNotFound to avoid crashing when databases are deleted or unshared: try { const db = await notion.databases.retrieve({ database_id: id }); } catch (error) { if (APIResponseError.isAPIResponseError(error) && error.code === APIErrorCode.ObjectNotFound) { return null; // Database deleted or not shared with integration } throw error; }costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible - retrieve · databases-retrieve-restricted-resourceerrorWhendatabases.retrieve() called when the integration lacks read content capabilities, or when the database exists but has not been explicitly shared with the integration token. This is the most common error for new integrations.Throws
APIResponseError with code RestrictedResource (APIErrorCode.RestrictedResource, HTTP 403).Required handlingCaller MUST wrap notion.databases.retrieve() in try-catch. A 403 on retrieve() is typically a setup error — the integration must be added to the database via the Notion UI. Log clearly to help developers diagnose.costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible - create · databases-create-parent-not-founderrorWhendatabases.create() called with a parent.page_id that does not exist or the integration does not have access to it. The database can only be created under pages the integration can access.Throws
APIResponseError with code ObjectNotFound (APIErrorCode.ObjectNotFound, HTTP 404).Required handlingCaller MUST wrap notion.databases.create() in try-catch. Database provisioning workflows fail if the parent page is deleted or the integration lacks access: try { const db = await notion.databases.create({ parent: { page_id }, ... }); } catch (error) { if (isNotionClientError(error)) { console.error('Failed to create Notion database:', error.code); } throw error; }costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible - create · databases-create-missing-capabilitieserrorWhendatabases.create() called when the integration lacks "insert content" capabilities. This is a capability setting in the integration configuration, distinct from page-level permissions. Integration appears functional (auth works) but cannot create.Throws
APIResponseError with code RestrictedResource (APIErrorCode.RestrictedResource, HTTP 403).Required handlingCaller MUST wrap notion.databases.create() in try-catch. Log clearly — a 403 on create() usually indicates a missing capability in the integration configuration, not a data error.costmediumin prodimmediate exceptionusers seeservice unavailablevisibilitysilent - create · databases-create-validation-errorerrorWhendatabases.create() called with malformed request body — invalid property type names, invalid parent specification, title/description arrays exceeding 100 items, or parent is not a page or wiki database.Throws
APIResponseError with code ValidationError (APIErrorCode.ValidationError, HTTP 400).Required handlingCaller MUST wrap notion.databases.create() in try-catch. Validate property schema and parent type before calling to reduce 400 errors at runtime.costlowin prodimmediate exceptionusers seeservice unavailablevisibilityvisible - update · databases-update-schema-too-largeerrorWhendatabases.update() called on a database whose schema exceeds 50KB after the update is applied. The error message identifies the largest property by name, ID, and byte size. Databases with many properties or long property names hit this limit when adding new columns.Throws
APIResponseError with code ValidationError (APIErrorCode.ValidationError, HTTP 400) with a message identifying the oversized property.Required handlingCaller MUST wrap notion.databases.update() in try-catch. Monitor schema size when programmatically adding properties. On ValidationError, read the error message to identify the largest property and consider archiving unused properties: try { await notion.databases.update({ database_id, properties: newProps }); } catch (error) { if (APIResponseError.isAPIResponseError(error) && error.code === APIErrorCode.ValidationError) { console.error('Schema too large — reduce properties:', error.message); } throw error; }costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible - update · databases-update-not-retried-on-server-errorwarningWhendatabases.update() (PATCH) receives 500 internal_server_error or 503 service_unavailable. PATCH is NOT idempotent so the SDK does NOT auto-retry it. The caller receives the error after a single attempt. The database schema update may be partially applied.Throws
APIResponseError with code InternalServerError (500) or ServiceUnavailable (503). Unlike GET/DELETE, these are NOT auto-retried for PATCH requests.Required handlingCaller MUST wrap notion.databases.update() in try-catch. On 500/503 errors, re-read the database to determine if the update was applied before retrying: try { await notion.databases.update({ database_id, properties: updates }); } catch (error) { if (APIResponseError.isAPIResponseError(error)) { // Re-read to check if partial update was applied const current = await notion.databases.retrieve({ database_id }); // Compare current.properties to expected state } throw error; }costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible - retrieve · pages-retrieve-object-not-founderrorWhenpages.retrieve() called when page_id refers to a permanently deleted page or a page the integration cannot access. Note: trashed (soft-deleted) pages return 200 with in_trash: true — callers that don't check in_trash may process stale/archived content.Throws
APIResponseError with code ObjectNotFound (APIErrorCode.ObjectNotFound, HTTP 404) for permanently deleted pages or inaccessible pages.Required handlingCaller MUST wrap notion.pages.retrieve() in try-catch. Also check page.in_trash in the response to detect soft-deleted pages: try { const page = await notion.pages.retrieve({ page_id: id }); if (page.object === 'page' && 'in_trash' in page && page.in_trash) { return null; // Page is trashed } return page; } catch (error) { if (APIResponseError.isAPIResponseError(error) && error.code === APIErrorCode.ObjectNotFound) { return null; // Page permanently deleted or inaccessible } throw error; }costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible - retrieve · pages-retrieve-missing-capabilitieserrorWhenpages.retrieve() called when the integration lacks read content capabilities, or when the page exists but was not shared with the integration token.Throws
APIResponseError with code RestrictedResource (APIErrorCode.RestrictedResource, HTTP 403).Required handlingCaller MUST wrap notion.pages.retrieve() in try-catch.costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible - create · comments-create-missing-capabilitieserrorWhencomments.create() called when the integration lacks "insert comment" capabilities. This is a capability flag in the integration settings, not a page-level permission. Callers receive 403 even when the integration has full read/write content access.Throws
APIResponseError with code RestrictedResource (APIErrorCode.RestrictedResource, HTTP 403). Distinct from a page permission 403 — this means the integration was not granted comment capabilities during setup.Required handlingCaller MUST wrap notion.comments.create() in try-catch. A 403 on comments.create() typically indicates a missing "insert comment" capability in the integration settings, not a page access error: try { await notion.comments.create({ parent: { page_id }, rich_text: [...] }); } catch (error) { if (APIResponseError.isAPIResponseError(error) && error.code === APIErrorCode.RestrictedResource) { console.error('Integration lacks insert comment capability'); } throw error; }costlowin prodimmediate exceptionusers seeservice unavailablevisibilitysilent - create · comments-create-parent-not-founderrorWhencomments.create() called when the referenced parent page, block, or discussion_id no longer exists or is not accessible to the integration. Async workflows that post comments after a delay are vulnerable to this race condition.Throws
APIResponseError with code ObjectNotFound (APIErrorCode.ObjectNotFound, HTTP 404).Required handlingCaller MUST wrap notion.comments.create() in try-catch. In async/queued workflows, check that the parent page still exists before posting the comment.costlowin prodimmediate exceptionusers seeservice unavailablevisibilitysilent - create · comments-create-validation-errorerrorWhencomments.create() called with invalid body: missing both rich_text and markdown, providing both simultaneously, rich_text array exceeds 100 items, attachments array exceeds 3 items, or markdown contains unsupported block-level elements.Throws
APIResponseError with code ValidationError (APIErrorCode.ValidationError, HTTP 400).Required handlingCaller MUST wrap notion.comments.create() in try-catch. Validate that exactly one of rich_text/markdown is provided and that array sizes are within limits.costlowin prodimmediate exceptionusers seeservice unavailablevisibilityvisible - create · file-uploads-create-id-not-usedwarningWhenfileUploads.create() succeeds but the caller does not use the returned id in a subsequent fileUploads.send() call to upload file data, followed by fileUploads.complete() to finalize. The upload stays in "pending" state.Throws
No error thrown from create() itself, but downstream callers fail silently — the file is never accessible in Notion and storage quota is consumed.Required handlingThe returned id from fileUploads.create() MUST be passed to fileUploads.send() then fileUploads.complete(). All three steps must be wrapped in try-catch: try { const upload = await notion.fileUploads.create({ filename, content_type }); await notion.fileUploads.send({ file_upload_id: upload.id, file: { data, filename } }); await notion.fileUploads.complete({ file_upload_id: upload.id }); } catch (error) { console.error('File upload failed:', error); throw error; }costlowin prodsilent failureusers seelost datavisibilitysilent - create · file-uploads-create-filename-too-longerrorWhenfileUploads.create() called with a filename string exceeding 900 bytes. Filenames with non-ASCII characters (CJK, emoji, accented chars) can hit this limit with fewer visible characters than expected due to multi-byte UTF-8 encoding.Throws
APIResponseError with code ValidationError (APIErrorCode.ValidationError, HTTP 400).Required handlingCaller MUST wrap notion.fileUploads.create() in try-catch. Truncate or sanitize user-provided filenames to stay within the 900-byte limit before calling.costlowin prodimmediate exceptionusers seeservice unavailablevisibilityvisible - send · file-uploads-send-wrong-stateerrorWhenfileUploads.send() called with a file_upload_id that refers to an already-completed or expired upload, or when parts are sent out of order. File uploads cannot receive new parts after being completed.Throws
APIResponseError with code ConflictError (409 conflict_error) or ObjectNotFound (404 object_not_found) depending on the specific invalid state.Required handlingCaller MUST wrap notion.fileUploads.send() in try-catch. In retry logic, never call send() after complete() has already succeeded. Track upload state explicitly: try { await notion.fileUploads.send({ file_upload_id: id, file: { data, filename } }); } catch (error) { console.error('File send failed — check upload state before retrying:', error); throw error; }costlowin prodimmediate exceptionusers seelost datavisibilitysilent - send · file-uploads-send-no-try-catcherrorWhenfileUploads.send() called without try-catch when uploading binary data via multipart/form-data. Large files approaching the 60s timeout will throw RequestTimeoutError. Any network interruption throws UnknownHTTPResponseError. Missing try-catch leaves the upload in broken intermediate state with no cleanup.Throws
APIResponseError for API errors; RequestTimeoutError for large files near 60s timeout; UnknownHTTPResponseError for unexpected HTTP responses.Required handlingCaller MUST wrap notion.fileUploads.send() in try-catch. On failure after create() has succeeded, the entire flow must restart from create(): try { await notion.fileUploads.send({ file_upload_id: id, file: { data, filename } }); } catch (error) { // The upload is now in an indeterminate state. Must restart from create(). console.error('File send failed — restart upload flow:', error); throw error; }costlowin prodimmediate exceptionusers seelost datavisibilitysilent - complete · file-uploads-complete-no-try-catcherrorWhenfileUploads.complete() called without try-catch. Can throw APIResponseError (including conflict_error if the upload was already completed or parts are missing), or RequestTimeoutError. Missing try-catch leaves the upload permanently incomplete after file data was already sent.Throws
APIResponseError with code ConflictError (409) if upload already completed or parts are missing; RequestTimeoutError if the completion request times out.Required handlingCaller MUST wrap notion.fileUploads.complete() in try-catch. Unlike send(), a failure on complete() means the file data was already fully transmitted but the upload is not finalized. The entire flow (create + send + complete) must restart: try { const completed = await notion.fileUploads.complete({ file_upload_id: id }); return completed.id; // Now can be referenced in Notion blocks } catch (error) { console.error('Upload finalization failed — restart full flow:', error); throw error; }costlowin prodimmediate exceptionusers seelost datavisibilitysilent - token · oauth-token-invalid-granterrorWhenoauth.token() called when the authorization code has expired, was already used, or does not match the redirect_uri. Authorization codes are single-use and expire quickly. Any network error or redirect during the OAuth callback leaves the code unusable.Throws
APIResponseError with code InvalidRequest (APIErrorCode.InvalidRequest, HTTP 400) or an OAuth error body with "invalid_grant" per RFC 6749.Required handlingCaller MUST wrap notion.oauth.token() in try-catch. OAuth callback handlers that don't catch errors return 500 to users who cannot retry. On invalid_grant, redirect the user back to the OAuth authorization URL: try { const token = await notion.oauth.token({ grant_type: 'authorization_code', code, ...creds }); await saveAccessToken(token.access_token); } catch (error) { // Redirect user to restart OAuth flow — the code cannot be reused console.error('OAuth token exchange failed:', error); throw error; }costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible - token · oauth-token-invalid-clienterrorWhenoauth.token() called when client_id or client_secret are wrong, missing, or have been rotated. The token endpoint uses HTTP Basic auth with client credentials — misconfigured env vars (wrong secret, extra whitespace, old rotated secret) cause all OAuth flows to fail.Throws
APIResponseError with code Unauthorized (APIErrorCode.Unauthorized, HTTP 401).Required handlingCaller MUST wrap notion.oauth.token() in try-catch. A 401 on oauth.token() is a configuration error — check NOTION_CLIENT_ID and NOTION_CLIENT_SECRET env vars: try { const token = await notion.oauth.token({ ..., client_id: env.CLIENT_ID, client_secret: env.CLIENT_SECRET }); } catch (error) { if (APIResponseError.isAPIResponseError(error) && error.code === APIErrorCode.Unauthorized) { console.error('Invalid Notion OAuth client credentials — check env vars'); } throw error; }costhighin prodimmediate exceptionusers seeservice unavailablevisibilityvisible - retrieve · blocks-retrieve-object-not-founderrorWhenblocks.retrieve() called when block_id refers to a block that has been deleted or is not accessible to the integration. Deleted and access-restricted blocks both return 404, making them indistinguishable at the API level.Throws
APIResponseError with code ObjectNotFound (APIErrorCode.ObjectNotFound, HTTP 404).Required handlingCaller MUST wrap notion.blocks.retrieve() in try-catch. When traversing page block trees by cached IDs, handle 404 gracefully: try { const block = await notion.blocks.retrieve({ block_id: id }); return block; } catch (error) { if (APIResponseError.isAPIResponseError(error) && error.code === APIErrorCode.ObjectNotFound) { return null; // Block deleted or inaccessible } throw error; }costlowin prodimmediate exceptionusers seeservice unavailablevisibilityvisible - retrieve · blocks-retrieve-missing-capabilitieserrorWhenblocks.retrieve() called when the integration lacks read content capabilities.Throws
APIResponseError with code RestrictedResource (APIErrorCode.RestrictedResource, HTTP 403).Required handlingCaller MUST wrap notion.blocks.retrieve() in try-catch.costlowin prodimmediate exceptionusers seeservice unavailablevisibilityvisible - delete · blocks-delete-object-not-founderrorWhenblocks.delete() called when block_id does not exist, the integration cannot access it, or the block was already deleted (trashed blocks also return 404 on delete). Cleanup jobs that retry deletions must treat 404 as a success condition.Throws
APIResponseError with code ObjectNotFound (APIErrorCode.ObjectNotFound, HTTP 404).Required handlingCaller MUST wrap notion.blocks.delete() in try-catch. In cleanup flows, treat ObjectNotFound as success (already deleted): try { await notion.blocks.delete({ block_id: id }); } catch (error) { if (APIResponseError.isAPIResponseError(error) && error.code === APIErrorCode.ObjectNotFound) { return; // Already deleted — treat as success } throw error; }costlowin prodimmediate exceptionusers seeservice unavailablevisibilityvisible - delete · blocks-delete-missing-capabilitieserrorWhenblocks.delete() called when the integration lacks update content capabilities. Read-only integrations cannot delete blocks even when they can read them.Throws
APIResponseError with code RestrictedResource (APIErrorCode.RestrictedResource, HTTP 403).Required handlingCaller MUST wrap notion.blocks.delete() in try-catch. A 403 on delete() indicates missing update content capability in the integration settings.costlowin prodimmediate exceptionusers seeservice unavailablevisibilityvisible - query · data-sources-query-object-not-founderrorWhendataSources.query() called when data_source_id refers to a data source that has been deleted, archived with its parent database, or was never shared with the integration. Indistinguishable at the API level from a never-existed ID. Common in workflows that cached data_source_id from earlier sessions.Throws
APIResponseError with code ObjectNotFound (APIErrorCode.ObjectNotFound, HTTP 404). SDK auto-retries rate_limited (429) but NOT object_not_found — error thrown immediately to the caller after the first POST attempt.Required handlingCaller MUST wrap notion.dataSources.query() in try-catch. Handle ObjectNotFound to return empty results rather than crash when data sources are deleted/unshared: try { const result = await notion.dataSources.query({ data_source_id: id }); return result.results; } catch (error) { if (APIResponseError.isAPIResponseError(error) && error.code === APIErrorCode.ObjectNotFound) { return []; // Data source deleted or not shared with integration } throw error; }costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible - query · data-sources-query-restricted-resourceerrorWhendataSources.query() called when the integration lacks "read content" capabilities on the parent database, OR when the data source exists but has not been explicitly shared with the integration token via the Notion connection settings. For new v5 integrations that previously worked against a database in v1.x, the data source under the database may require separate sharing.Throws
APIResponseError with code RestrictedResource (APIErrorCode.RestrictedResource, HTTP 403).Required handlingCaller MUST wrap notion.dataSources.query() in try-catch. A 403 on dataSources.query() typically indicates the integration needs to be re-shared at the data-source level — v5 connection sharing is finer-grained than v1's database-level sharing.costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible - query · data-sources-query-database-connection-unavailablewarningWhendataSources.query() receives 503 service_unavailable specifically with the database_connection_unavailable error code (a sub-variant of 503 documented for data source queries during "backend datastore timeouts"). Notion documentation recommends narrowing filters and adding exponential backoff. SDK auto-retries 503 for GET/DELETE but dataSources.query is POST — NOT auto-retried.Throws
APIResponseError with code ServiceUnavailable (HTTP 503), specifically database_connection_unavailable for query-time backend datastore timeouts.Required handlingCaller MUST wrap notion.dataSources.query() in try-catch. POST is not auto-retried on 503 — explicit retry with exponential backoff and narrowed filter is required: try { const result = await notion.dataSources.query({ data_source_id, filter }); } catch (error) { if (APIResponseError.isAPIResponseError(error) && error.code === APIErrorCode.ServiceUnavailable) { // Narrow filter and retry with backoff console.error('Data source query timed out — narrow filter and retry'); } throw error; }costmediumin prodimmediate exceptionusers seedegraded performancevisibilityvisible - create · data-sources-create-parent-not-founderrorWhendataSources.create() called with parent.database_id referencing a database that does not exist, was deleted/trashed, or is not accessible to the integration. Data sources require a parent database — they cannot be created at the workspace or page level.Throws
APIResponseError with code ObjectNotFound (APIErrorCode.ObjectNotFound, HTTP 404).Required handlingCaller MUST wrap notion.dataSources.create() in try-catch. Database provisioning flows must handle the parent-database-missing case explicitly — silent failure here leaves a half-provisioned workspace with the database but no schema: try { const ds = await notion.dataSources.create({ parent: { database_id }, properties }); } catch (error) { if (isNotionClientError(error)) { console.error('Failed to create data source:', error.code); } throw error; }costmediumin prodimmediate exceptionusers seeservice unavailablevisibilityvisible - create · data-sources-create-validation-errorerrorWhendataSources.create() called with invalid property schema — invalid property type names, select/multi_select options exceeding 100 items, missing required title property, or property names exceeding API limits.Throws
APIResponseError with code ValidationError (APIErrorCode.ValidationError, HTTP 400).Required handlingCaller MUST wrap notion.dataSources.create() in try-catch. Validate property schema structure and option counts before calling to fail fast at the application layer rather than after a network round-trip.costlowin prodimmediate exceptionusers seeservice unavailablevisibilityvisible - move · pages-move-target-not-founderrorWhenpages.move() called with a parent.page_id or parent.data_source_id that does not exist or is not accessible to the integration. Move operations are common in archival/reorganization workflows where the target parent may have been deleted between when the move was queued and when it executes.Throws
APIResponseError with code ObjectNotFound (APIErrorCode.ObjectNotFound, HTTP 404) for missing target parent. May also throw 404 if the page being moved no longer exists.Required handlingCaller MUST wrap notion.pages.move() in try-catch. Page-organization workflows MUST treat 404 as a recoverable state — the page may need to be re-fetched, or the target parent re-validated, before retrying: try { await notion.pages.move({ page_id, parent: { page_id: targetId } }); } catch (error) { if (APIResponseError.isAPIResponseError(error) && error.code === APIErrorCode.ObjectNotFound) { console.error('Move target or source no longer exists:', error.message); } throw error; }costmediumin prodimmediate exceptionusers seelost datavisibilitysilent - move · pages-move-restricted-resourceerrorWhenpages.move() called when the integration lacks "update content" capabilities on the source page OR "insert content" capabilities on the target parent. Both permissions are required — a 403 here is more ambiguous than on other endpoints because either side of the move can fail.Throws
APIResponseError with code RestrictedResource (APIErrorCode.RestrictedResource, HTTP 403).Required handlingCaller MUST wrap notion.pages.move() in try-catch. A 403 on move() requires verifying BOTH the source and target are shared with the integration — the error message does not always distinguish which side failed.costmediumin prodimmediate exceptionusers seelost datavisibilitysilent - update · blocks-update-object-not-founderrorWhenblocks.update() called when block_id refers to a block that has been deleted, is in the trash, or the integration cannot access. Per Notion docs, blocks in trash return 404 on update — distinct from soft-deleted-but-readable pages. Background jobs that update blocks by cached IDs are vulnerable when users delete content.Throws
APIResponseError with code ObjectNotFound (APIErrorCode.ObjectNotFound, HTTP 404).Required handlingCaller MUST wrap notion.blocks.update() in try-catch. In sync flows that update blocks by stored IDs, handle ObjectNotFound to avoid crashing on user-deleted content: try { await notion.blocks.update({ block_id, paragraph: { rich_text: [...] } }); } catch (error) { if (APIResponseError.isAPIResponseError(error) && error.code === APIErrorCode.ObjectNotFound) { return null; // Block deleted or trashed — skip this update } throw error; }costmediumin prodimmediate exceptionusers seelost datavisibilitysilent - update · blocks-update-not-retried-on-server-errorwarningWhenblocks.update() (PATCH) receives 500 internal_server_error or 503 service_unavailable. PATCH is NOT auto-retried by the SDK — only GET and DELETE are retried on 500/503. The block update may be partially applied on the server before the error response, leaving the block in an inconsistent state from the application's perspective.Throws
APIResponseError with code InternalServerError (HTTP 500) or ServiceUnavailable (HTTP 503). Unlike GET/DELETE, these are NOT auto-retried for PATCH requests after the SDK's default maxRetries.Required handlingCaller MUST wrap notion.blocks.update() in try-catch. On 500/503, re-read the block with blocks.retrieve() before retrying to detect whether the update was partially applied: try { await notion.blocks.update({ block_id, ...updates }); } catch (error) { if (APIResponseError.isAPIResponseError(error) && (error.code === APIErrorCode.InternalServerError || error.code === APIErrorCode.ServiceUnavailable)) { const current = await notion.blocks.retrieve({ block_id }); // Compare current to expected to detect partial apply } throw error; }costmediumin prodimmediate exceptionusers seelost datavisibilitysilent - update · blocks-update-child-page-or-databaseerrorWhenblocks.update() called on a block whose type is "child_page" or "child_database". Per Notion documentation: "Child pages and databases cannot be updated via this endpoint; use the Update page or Update database endpoints instead." Callers that fetch a block list and update each block uniformly will hit this error on child page/db blocks.Throws
APIResponseError with code ValidationError (APIErrorCode.ValidationError, HTTP 400) with a message indicating the block type cannot be updated via this endpoint.Required handlingCaller MUST wrap notion.blocks.update() in try-catch. When iterating block children for updates, branch on block type to route child_page → pages.update() and child_database → databases.update(): try { if (block.type === 'child_page') { await notion.pages.update({ page_id: block.id, ...pageUpdates }); } else if (block.type === 'child_database') { await notion.databases.update({ database_id: block.id, ...dbUpdates }); } else { await notion.blocks.update({ block_id: block.id, ...blockUpdates }); } } catch (error) { console.error('Block update failed:', error); throw error; }costlowin prodimmediate exceptionusers seelost datavisibilityvisible - update · comments-update-not-authorerrorWhencomments.update() called on a comment that was NOT created by this integration's connection. Notion enforces author-only updates and returns 404 (object_not_found) rather than 403 (restricted_resource) — making the error indistinguishable from a genuinely missing comment. Multi-integration workflows that update comments posted by other connections will hit this.Throws
APIResponseError with code ObjectNotFound (APIErrorCode.ObjectNotFound, HTTP 404). Note: 404 is used even when the comment exists but was created by another connection — the API conflates "not found" with "not your comment" to avoid leaking existence.Required handlingCaller MUST wrap notion.comments.update() in try-catch. Track which connection created each comment locally (e.g. in your DB) to avoid attempting updates on comments the integration cannot modify: try { await notion.comments.update({ comment_id, rich_text: [...] }); } catch (error) { if (APIResponseError.isAPIResponseError(error) && error.code === APIErrorCode.ObjectNotFound) { console.error('Comment not found or not authored by this connection'); return; } throw error; }costlowin prodimmediate exceptionusers seelost datavisibilitysilent - update · comments-update-validation-errorerrorWhencomments.update() called with invalid body: missing both rich_text and markdown, providing both simultaneously (exactly one is required), rich_text array exceeding 100 items, or markdown containing unsupported block-level elements (only inline formatting is allowed in comments).Throws
APIResponseError with code ValidationError (APIErrorCode.ValidationError, HTTP 400).Required handlingCaller MUST wrap notion.comments.update() in try-catch. Validate exactly-one-of rich_text/markdown and array sizes before calling.costlowin prodimmediate exceptionusers seelost datavisibilityvisible - update · comments-update-missing-capabilitieserrorWhencomments.update() called when the integration lacks "insert comment" capabilities (Notion uses the same capability for comment write operations including update). This is a capability flag in the integration settings — distinct from page/data-source read/write permissions.Throws
APIResponseError with code RestrictedResource (APIErrorCode.RestrictedResource, HTTP 403).Required handlingCaller MUST wrap notion.comments.update() in try-catch. A 403 here means the integration was set up without comment-write capabilities — surface this clearly to the developer rather than treating it as a transient error.costlowin prodimmediate exceptionusers seelost datavisibilitysilent - delete · comments-delete-not-authorerrorWhencomments.delete() called on a comment that was NOT created by this integration's connection. Notion enforces author-only deletion. Returns 404 — indistinguishable from a genuinely missing comment. Cleanup workflows that retry deletions across comments from multiple authors will receive 404 on the comments they don't own.Throws
APIResponseError with code ObjectNotFound (APIErrorCode.ObjectNotFound, HTTP 404).Required handlingCaller MUST wrap notion.comments.delete() in try-catch. In cleanup workflows, treat 404 as success (already-deleted or not-our-comment — both terminal states): try { await notion.comments.delete({ comment_id }); } catch (error) { if (APIResponseError.isAPIResponseError(error) && error.code === APIErrorCode.ObjectNotFound) { return; // Already deleted or not our comment — treat as success } throw error; }costlowin prodimmediate exceptionusers seelost datavisibilitysilent - delete · comments-delete-missing-capabilitieserrorWhencomments.delete() called when the integration lacks "insert comment" capabilities (Notion's comment-write capability covers both create/update/delete). Read-only integrations cannot delete comments even ones they previously created during a prior capability-grant window.Throws
APIResponseError with code RestrictedResource (APIErrorCode.RestrictedResource, HTTP 403).Required handlingCaller MUST wrap notion.comments.delete() in try-catch. A 403 on delete() typically indicates the integration's comment capability was revoked since the comment was created — log clearly for diagnostic purposes.costlowin prodimmediate exceptionusers seelost datavisibilitysilent
Sources
Every postcondition cites at least one of these. Grouped by source type; numbered to match the footnotes above.
- [1]developers.notion.com/reference/errorsErrors
- [3]developers.notion.com/reference/post-database-queryPost Database Query
- [4]developers.notion.com/reference/post-pagePost Page
- [5]developers.notion.com/reference/patch-pagePatch Page
- [6]developers.notion.com/reference/patch-block-childrenPatch Block Children
- [7]developers.notion.com/reference/post-searchPost Search
- [8]developers.notion.com/reference/retrieve-a-databaseRetrieve A Database
- [9]developers.notion.com/reference/create-a-databaseCreate A Database
- [10]developers.notion.com/reference/update-a-databaseUpdate A Database
- [11]developers.notion.com/reference/retrieve-a-pageRetrieve A Page
- [12]developers.notion.com/reference/create-a-commentCreate A Comment
- [13]developers.notion.com/reference/create-a-file-uploadCreate A File Upload
- [14]developers.notion.com/reference/create-a-tokenCreate A Token
- [15]developers.notion.com/reference/retrieve-a-blockRetrieve A Block
- [16]developers.notion.com/reference/delete-a-blockDelete A Block
- [17]developers.notion.com/reference/query-a-data-sourceQuery A Data Source
- [18]developers.notion.com/reference/create-a-data-sourceCreate A Data Source
- [19]developers.notion.com/reference/update-a-blockUpdate A Block
- [20]developers.notion.com/reference/update-a-commentUpdate A Comment
- [21]developers.notion.com/reference/delete-a-commentDelete A Comment
- [2]github.com/makenotion/notion-sdk-jsmakenotion/notion-sdk-js
Research notes
Curator notes from SOURCES.md captured when the profile was written so you can verify the reasoning, not just the rules.
Sources — @notionhq/client
Package: @notionhq/client
Version: 5.12.0 (as of 2026-03-13)
Evidence quality: stub
Primary Sources
Official Error Reference
URL: https://developers.notion.com/reference/errors
What it documents: Complete list of Notion API error codes, HTTP status codes, and recommended handling patterns.
Key claim supported: All API methods throw APIResponseError with one of the defined error codes on failure.
Official SDK README
URL: https://github.com/makenotion/notion-sdk-js
What it documents: Error handling examples using isNotionClientError(), APIErrorCode, and ClientErrorCode.
Key claim supported: SDK throws on API failures (not swallows). The README includes explicit try-catch examples.
Error Types Source
URL: https://github.com/makenotion/notion-sdk-js/blob/main/src/errors.ts
What it documents: Full error hierarchy — APIResponseError, RequestTimeoutError, UnknownHTTPResponseError, InvalidPathParameterError, plus APIErrorCode and ClientErrorCode enums.
Key claim supported: Error types and codes used in required_handling sections.
API Reference — databases.query
URL: https://developers.notion.com/reference/post-database-query What it documents: Parameters, response structure, and error conditions for database queries.
API Reference — pages.create
URL: https://developers.notion.com/reference/post-page What it documents: Parameters, response, and error conditions for page creation.
API Reference — pages.update
URL: https://developers.notion.com/reference/patch-page What it documents: Parameters, response, and error conditions for page updates.
API Reference — blocks.children.append
URL: https://developers.notion.com/reference/patch-block-children What it documents: Parameters, response, and error conditions for appending block children.
API Reference — search
URL: https://developers.notion.com/reference/post-search What it documents: Parameters, response, and error conditions for search.
Retry Behavior Source (CLAUDE.md in SDK)
URL: https://github.com/makenotion/notion-sdk-js/blob/main/CLAUDE.md
What it documents: Retry logic — rate_limited (429) retried for all methods; internal_server_error (500) and service_unavailable (503) retried for idempotent methods (GET, DELETE) only. POST/PATCH (create/update/append) are NOT auto-retried on 500/503.
Real-World Evidence
No high-star repos with confirmed TPs found in initial search. Evidence quality: stub.
Planned for upgrade: search for production SaaS apps using @notionhq/client in their package.json dependencies and scan for unguarded API calls.
Evidence Quality Upgrade Path
To upgrade from stub to partial:
- Find a TypeScript repo with 100+ stars that uses
@notionhq/client - Scan with verify-cli
- Confirm at least one TRUE_POSITIVE (real database.query / pages.create without try-catch)
- Add the repo URL here under "Real-World Evidence"
- Update
evidence_qualityin contract.yaml topartial
To upgrade to confirmed:
- Confirm TP in a repo with >1k GitHub stars
- Update
evidence_qualitytoconfirmed