PolderAPI

Docs API

Overview

What the Docs API is, how an integration authenticates, what it can see and how errors, pagination and limits work.

Base URLhttps://docs.polder.work

The Docs API lets another program read and write a workspace in Polder Docs: search, pages, blocks, databases, people, comments and skills. It follows the shape of Notion's API on purpose. The objects, list pagination and error codes match closely, so a client written for Notion mostly needs a new base URL and a new key. This page says where Polder differs.

  • Base URL: https://docs.polder.work/api/v1
  • Format: JSON in, JSON out. Send Content-Type: application/json with every request that has a body.
  • Versioning: the version is part of the path.

Create an integration

An integration belongs to one workspace. Owners and admins create them in Docs under Settings → Workspace → Connections → New integration. Each integration has:

  • a name, shown in page history ("Ann via Release script");
  • a set of capabilities;
  • a secret that starts with ptn_. Docs shows it once, when the integration is created or its secret is replaced. Only a SHA-256 hash is stored.

The secret is the "API key". Send it as a Bearer token:

curl https://docs.polder.work/api/v1/users/me \
  -H "Authorization: Bearer $POLDER_DOCS_KEY"

An integration acts on behalf of the admin who created it. Its edits use that person's access to each page, and page history records them as "Ann via Release script". Comments and created_by name that admin. If that person leaves the workspace, the integration stops working (401 unauthorized) and an admin has to create a new one.

Revoking access

  • New secret (Settings → Connections) issues a fresh secret. The old one stops working right away.
  • Remove deletes the integration together with all its page connections.

Note: These keys only work on docs.polder.work. The Backlog and Chat APIs have their own tokens.

What an integration can see

A new integration can see nothing. As in Notion, someone has to connect a page to it: open the page's ••• menu → Connections → Integrations, choose the integration and click Connect. That gives the integration access to the page and every page, block and database under it. Sub-pages show the connection as "Connected on "; to remove it, disconnect it on that parent page. Connecting an integration needs full access to the page.

The integration can also only do what its admin may do. A page that is read-only for that person refuses writes with 403 restricted_resource.

A page the integration cannot reach returns 404 object_not_found, whether it exists or not. This is deliberate: ids of private pages cannot be probed.

Skills and the workspace's people do not belong to a page, so /skills and /users need only their capability.

Capabilities

Capability Allows
read_content Search; read pages, blocks, databases and data sources; query databases
insert_content Create pages and databases; append blocks
update_content Update and delete pages, blocks and databases (including moving them to the trash and back)
read_comments List comments
insert_comments Post comments and replies
read_skills List the workspace's skills
read_user_info List and retrieve the workspace's people, including their email addresses

GET /api/v1/users/me works without any capability. A request that needs a capability the integration lacks gets 403 restricted_resource.

Pagination

Endpoints that return lists use cursors:

  • Request: page_size (1 to 100, default 100) and start_cursor. On GET endpoints they are query parameters; on POST endpoints they go in the body.
  • Response:
{
  "object": "list",
  "results": [],
  "next_cursor": "01k…",
  "has_more": true,
  "type": "block",
  "block": {}
}

Pass next_cursor as start_cursor to get the next page. When has_more is false, next_cursor is null. A cursor is the id of the first item of the next page, so it stays valid when earlier items change. If that item itself is gone, the request fails with validation_error and you start again without a cursor.

Errors

Every error under /api/v1 has the same shape:

{
  "object": "error",
  "status": 404,
  "code": "object_not_found",
  "message": "Could not find page with ID: 01k…. Make sure the relevant pages are shared with your integration.",
  "request_id": "6c0d1f3e-…"
}
Status code When
400 invalid_json The body is not valid JSON
400 validation_error The body or query does not fit the endpoint: wrong type, unknown property, bad cursor, a locked page
400 invalid_request_url No such endpoint, or not with that method
401 unauthorized Missing, unknown or revoked secret; the integration's creator left the workspace
403 restricted_resource Missing capability, or not allowed on this page
404 object_not_found Does not exist, or is not shared with the integration
409 conflict_error The data changed meanwhile; try again
429 rate_limited Too many requests; see Retry-After
500 internal_server_error Our fault

Any path under /api/v1 that is not an endpoint answers 400 invalid_request_url ("Invalid request URL."), after the key has been checked. That includes a path that exists with another HTTP method: there is no 405.

Rate limit

Each integration may make 180 requests per minute (three per second on average, like Notion) and at most 10 per second. Above that the API answers 429 rate_limited with a Retry-After header in seconds. Wait that long, then retry. Self-hosted installs can change the limits with API_RATE_PER_MINUTE and API_RATE_BURST_PER_SECOND.

Objects

Ids are lower-case ULIDs (26 characters). People have numeric ids, sent as strings. Times are ISO 8601 in UTC (2026-10-02T09:30:00.000Z).

Rich text follows Notion:

{
  "type": "text",
  "text": { "content": "Polder", "link": { "url": "https://example.com" } },
  "annotations": { "bold": true, "italic": false, "strikethrough": false, "underline": false, "code": false, "color": "default" },
  "plain_text": "Polder",
  "href": "https://example.com"
}

When you send rich text, the short form {"text": {"content": "Hi"}} is enough. Each page, block and database is described on its own page.

Differences from Notion

  • Acting person. An integration is not a separate bot user: it acts as the admin who created it, and history shows " via ". Comments and created_by name that admin.
  • Formulas and rollups are calculated in the browser, not on the server. Their values come back as null, and filtering on them returns 400 validation_error.
  • Dates keep a day or a minute. time_zone is always null.
  • Option lists are only added to through the API, never replaced.
  • Comments are plain text.
  • Extra in Polder: /skills, heading_4 blocks, the place property and kind on each data source.
  • Not available: public integrations (OAuth), file uploads, filter_properties, and webhooks for integrations. Per-page webhooks exist in the app (page ••• → Connections).