PageSpace SDK

@pagespace/sdk is the typed TypeScript client for the PageSpace API. Everything you can do in the app — create pages, edit documents, run searches, manage tasks, ask agents — you can do from your own code, with full type inference on every call.

It's the same client the pagespace CLI and the pagespace mcp server are built on. All three surfaces are generated from one operation registry, so they can't drift apart.

Install

npm install @pagespace/sdk

ESM only, with a single runtime dependency (zod). The current version is 2.1.0.

Quickstart

import { PageSpaceClient, StaticTokenProvider } from '@pagespace/sdk';

const client = new PageSpaceClient({
  baseUrl: 'https://pagespace.ai',
  auth: new StaticTokenProvider(process.env.PAGESPACE_TOKEN!),
});

const drives = await client.drives.list({});

const page = await client.pages.create({
  driveId: drives[0].id,
  title: 'Release Notes',
  type: 'DOCUMENT',
});

await client.pages.replaceLines({
  pageId: page.id,
  startLine: 1,
  endLine: 1,
  content: '# Release Notes\n\nShipped today.',
});

Authentication

The client takes an auth provider. There are two.

StaticTokenProvider — wraps a fixed mcp_ API key. This is what you want for scripts, CI jobs, and service accounts. Mint a key with pagespace keys create --drive <id> --role member --show-token, or from Settings > MCP in the app, and pass it straight in:

new StaticTokenProvider(process.env.PAGESPACE_TOKEN!)

OAuthTokenProvider — for apps that log a user in and act on their behalf. You give it an initial token pair and a refresh function; it refreshes proactively before expiry and hands you the new pair to persist.

import { OAuthTokenProvider } from '@pagespace/sdk';

const auth = new OAuthTokenProvider({
  initialTokens: { accessToken, accessExpiresAt, refreshToken, refreshExpiresAt },
  refreshAccessToken: (refreshToken) => exchangeRefreshToken(refreshToken),
  onTokensUpdated: (tokens) => saveTokens(tokens),
});

Building your own browser-based login? The SDK ships the PKCE helpers too — generateCodeVerifier and deriveCodeChallenge (async; it runs on Web Crypto so it works in a browser bundle).

An mcp_ key works for every namespace except client.tokens, which manages keys themselves and requires an OAuth token.

Resource namespaces

Every operation hangs off a namespace on the client. Inputs and outputs are schema-validated, so your editor knows the shape of both.

NamespaceWhat it does
driveslist, create, rename, updateContext, setHomePage, trash, restore
pageslist, listTrash, create, details, rename, move, trash, restore — plus content editing: read, replaceLines, insertLines, deleteLines, editCells
taskscreate, update, delete, reorder, getAssigned, createStatus, setTrigger, deleteTrigger
searchregex, glob, multiDrive
agentslist, listMultiDrive, ask, updateConfig, listModels
conversationslist, read — full transcripts of an agent's conversations
channelssend, delete
calendarlist, get, create, update, delete, rsvp, inviteAttendees, removeAttendee, setTrigger, deleteTrigger
roleslist, get, create, update, delete, setPagePermissions, setDriveWidePermissions, removePagePermissions
memberslist — who's on a drive
collaboratorslist — people you share any drive with
commandslist, create, update, delete — slash commands
workflowslist, create, update, delete
activityget — a drive's activity feed
exportpageMarkdown, sheetCsv
tokenslist, revoke — API keys (OAuth only)

Reading and writing document content lives on pages, not a separate namespace:

// Read with line numbers, or a range
const doc = await client.pages.read({ pageId, lineStart: 1, lineEnd: 50 });

// Line-addressed edits
await client.pages.insertLines({ pageId, anchor: '## Changelog', content: '- Fixed a bug', position: 'after' });
await client.pages.deleteLines({ pageId, startLine: 10, endLine: 12 });

// Sheet cells
await client.pages.editCells({ pageId, cells: [{ address: 'A1', value: 'Hello' }] });

Need an endpoint the SDK doesn't wrap? defineOperation lets you declare one with its own Zod schemas and call it through client.invoke, keeping the same typing, auth, and retry behaviour.

Error handling

Every failure is a typed subclass of PageSpaceError, each with a matching is*Error() type guard — so you can branch on what went wrong without string-matching messages.

import { isRateLimitError, isPermissionDeniedError, isValidationError } from '@pagespace/sdk';

try {
  await client.pages.create({ driveId, title: 'Notes', type: 'DOCUMENT' });
} catch (error) {
  if (isValidationError(error)) {
    // 400 — input rejected; error.details has field-level info
  } else if (isPermissionDeniedError(error)) {
    // 403 — the credential's role doesn't allow this
  } else if (isRateLimitError(error)) {
    // 429 — error.retryAfterMs is set when the server sends Retry-After
  }
  throw error;
}
ErrorWhen
ValidationError400 — the input was rejected
AuthenticationError401 — token missing, invalid, or expired
PermissionDeniedError403 — the credential's role doesn't permit it
NotFoundError404
RateLimitError429 — carries retryAfterMs
ServerError5xx
NetworkErrorthe request never left (DNS, connection refused)
TimeoutErrorthe request exceeded timeoutMs
IncompatibleServerErrorthe server's API version is too old for this SDK
ResponseValidationErrorthe server returned a shape the SDK didn't expect
HttpErrorany other status (402, 409, …)

Retries

Reads retry themselves. A failed GET — network error, timeout, 429, or 5xx — is retried with full-jitter exponential backoff: 3 retries, starting at 250 ms, capped at 5 s. A Retry-After header is honoured when the server sends one, clamped to the same ceiling.

Writes are never replayed. POST, PUT, PATCH, and DELETE fail straight through to you, so a retry can never duplicate a page or a task.

Tune it per-client:

new PageSpaceClient({
  baseUrl,
  auth,
  retryPolicy: { maxRetries: 5, maxDelayMs: 30_000 },
  timeoutMs: 15_000,
});

Server compatibility

On its first successful response, the SDK compares the server's API version against the minimum it supports. If the server is too old, it fails closed with IncompatibleServerError rather than making calls that might silently misbehave. You can bypass the check with skipVersionCheck: true, but you generally shouldn't.

Next steps

Search

Search docs, blog posts, and more.