JSON API

Quick start

Create an API key from your account settings, then publish a document:

# Publish a new document (POST creates, fails if it exists)
curl -X POST https://pdrive.io/api/acme/docs/readme.md \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"content": "# Hello\n\nThis is my first document."}'
# Create or update (PUT creates if new, adds a version if it exists)
curl -X PUT https://pdrive.io/api/acme/docs/readme.md \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"content": "# Hello\n\nUpdated content.", "message": "Fix typo"}'

Authentication

All API requests require a Bearer token in the Authorization header:

Authorization: Bearer YOUR_API_KEY

API keys are scoped to an account. You can create and revoke them from Settings > API keys in the web UI, or manage them via the API itself (/api/:account/-/api-keys).

Keys have a scope of read or read write. Read-only keys cannot make POST, PUT, PATCH, or DELETE requests.

Versioning and stability

The pdrive API does not use URL versioning. The current API surface lives at /api/... and the changelog tells you what's changed.

Stability commitment

Additive changes ship without notice. New fields, new endpoints, and new optional parameters can appear at any time. Write your client to ignore unknown fields rather than fail on them.

Breaking changes are announced in the changelog at least four weeks before they ship. During the deprecation window, affected endpoints return Deprecation and Sunset response headers pointing to the changelog entry. We will not silently remove a field or change a response shape.

Why no /v1/ prefix

URL versioning commits to a specific strategy before we know what kind of breaking changes we will need. Most modern SaaS APIs (Stripe, Linear, Plaid, Resend) have moved away from URL versioning toward header-based or no versioning combined with a clear stability policy. We follow that pattern.

If we ever need to ship coordinated breaking changes that cannot be handled by per-field deprecation, we will add explicit versioning at that time and announce it in the changelog with a long migration window.

What this means for you

  • Write your client against the current API. Pin nothing.
  • Subscribe to the changelog. Deprecations are announced there.
  • Check response headers. Deprecation and Sunset tell you when something is going away.
  • Be tolerant of additive changes. New fields will appear in responses over time.

The .json convention

Append .json to any pdrive URL to get its JSON representation:

# HTML version
https://pdrive.io/acme/docs/readme.md
# JSON version
https://pdrive.io/acme/docs/readme.md.json

URL conventions

All API endpoints follow the pattern /api/:account_namespace/:project_slug/:filename. The :account_namespace is the account's unique namespace (e.g. acme), :project_slug is the project's slug, and :filename is the document filename including extension.

Action endpoints use the /-/ namespace to avoid collision with wildcards (e.g. /api/acme/-/projects).

ID format

IDs are prefixed, checksummed strings of 32-35 characters, format <3-6 char type>_<24-char body><4-char checksum>. Examples:

  • doc_V1StGXR8Z5jdHi6BmyT0xq3k9fK2 for a document
  • user_abc123def456ghi789jkl012XyZq for a user
  • audit_9fK2p8QnR4mL3tX6vN7wY8z1aBcD for an audit event

The prefix tells you the resource type at a glance. The 4-char trailing checksum catches transcription errors before a DB round-trip: a malformed ID returns 422 invalid_id instead of 404 not found.

Treat IDs as opaque strings in your client. The type prefix is stable and can be used for routing or type-guarding, but the body and checksum are implementation details.

Date fields use ISO 8601 UTC format.

Pagination

All list endpoints use cursor-based pagination with a standard response envelope:

{
"items": [...],
"next_cursor": "eyJpIj...",
"has_more": true
}

Pass ?cursor=<opaque> to fetch the next page and ?limit=N to control page size (default 50, max 200). When has_more is false, next_cursor is null and there are no more results. Treat cursors as opaque strings. Do not parse, store long-term, or construct them manually.

This envelope applies to projects, documents, members, API keys, versions, notifications, deleted documents, and audit log results.

User search (/api/users/search) is the one exception. It returns {"items": [...]} with a fixed limit and no cursor.

Endpoints

Identity

GET /api/me Current user info
GET /api/me/accounts Accounts the user belongs to

Accounts

GET /api/:account Account details
DELETE /api/:account/-/settings Soft-delete account (owner)
POST /api/:account/-/restore Restore account (owner)

The account response includes project and member counts and a urls map with links to related resources. Plan-gated links (api_keys, audit_log) only appear when the account's plan enables those features.

Projects

GET /api/:account/:project Project details
GET /api/:account/-/projects List projects (?status=archived|all)
POST /api/:account/-/projects Create a project (manager+)

The project detail response includes document and member counts and a urls map. The list endpoint returns the standard {items, next_cursor, has_more} envelope.

Create a project:

curl -X POST https://pdrive.io/api/acme/-/projects \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "My Project"}'

Body fields: name (required), description, is_public (default false), is_members_only (default false).

Project settings

GET /api/:account/:project/-/settings View settings (manager+)
PATCH /api/:account/:project/-/settings Update settings (manager+)
PATCH /api/:account/:project/-/settings/visibility Update visibility (manager+)
POST /api/:account/:project/-/archive Archive project (manager+)
DELETE /api/:account/:project/-/archive Unarchive project (manager+)
DELETE /api/:account/:project/-/settings Soft-delete project (admin+)

Update settings body fields: name, slug, description (all optional).

Update visibility body fields: is_public, is_members_only (all optional, booleans).

Documents

GET /api/:account/:project/-/documents List documents
GET /api/:account/:project/:filename Read document with content
POST /api/:account/:project/:filename Create document (editor+, 409 if exists)
PUT /api/:account/:project/:filename Create or update (editor+)
PATCH /api/:account/:project/:filename Rename, move, or update content (see below)
DELETE /api/:account/:project/:filename Soft-delete document (editor+)

The list endpoint returns {items, next_cursor, has_more}. The document detail response includes version count and a urls map with links to the versions list and diff endpoints.

Publish a document:

curl -X PUT https://pdrive.io/api/acme/docs/deploy-guide.md \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"content": "# Deploy guide\n\nStep-by-step instructions.", "message": "Initial version"}'

POST and PUT body fields: content (required), title (optional, used on create only), message (optional, version message), tags (optional, array of strings). PUT also accepts force (optional boolean, bypasses publish conflict detection).

PATCH dispatch: The PATCH endpoint accepts one of three body shapes:

  • {"filename": "new-name.md"} to rename (editor+)
  • {"project_slug": "target-project"} to move (admin+)
  • {"content": "..."} to update content (editor+, also accepts optional message and tags)

Only one operation per request. Sending more than one of filename, project_slug, or content returns 422.

Versions

GET /api/:account/:project/:filename/versions List versions
GET /api/:account/:project/:filename/versions/:n Read specific version
DELETE /api/:account/:project/:filename/versions/:n Soft-delete a version (editor+)
GET /api/:account/:project/:filename/diff Diff between versions (?from=N&to=M)

The versions list returns {items, next_cursor, has_more}.

Document actions

POST /api/:account/:project/:filename/lock Lock document (manager+)
DELETE /api/:account/:project/:filename/lock Unlock document (manager+)
PATCH /api/:account/:project/:filename/versions/:n/tags Update version tags (editor+)

Update tags body fields: tags (required, array of strings, max 10 tags, max 30 characters each).

Account membership

GET /api/:account/-/members List members (admin+)
POST /api/:account/-/members Invite member (admin+)
PATCH /api/:account/-/members/:id Update role (admin+)
DELETE /api/:account/-/members/:id Remove or withdraw invite (admin+)
POST /api/:account/-/members/:id/accept Accept invite
POST /api/:account/-/members/:id/decline Decline invite

The list endpoint returns {items, next_cursor, has_more}.

Invite body fields: username or email (required), role (optional, default viewer). Valid roles: viewer, editor, manager, admin.

Update role body fields: role (required).

Project membership

GET /api/:account/:project/-/members List members (manager+)
POST /api/:account/:project/-/members Invite member (manager+)
PATCH /api/:account/:project/-/members/:id Update role (manager+)
DELETE /api/:account/:project/-/members/:id Remove or withdraw invite (manager+)
POST /api/:account/:project/-/members/:id/accept Accept invite
POST /api/:account/:project/-/members/:id/decline Decline invite

The list endpoint returns {items, next_cursor, has_more}.

Invite body fields: username or email (required), role (optional, default editor). Valid roles: viewer, editor, manager.

Update role body fields: role (required).

API key management

GET /api/:account/-/api-keys List API keys (admin+)
POST /api/:account/-/api-keys Create API key (admin+)
DELETE /api/:account/-/api-keys/:id Revoke API key (admin+)

The list endpoint returns {items, next_cursor, has_more}.

Create body fields: label (required), scope (optional, read or read write, default read), description (optional), expires_at (optional, ISO 8601 datetime).

Audit log

GET /api/:account/-/audit-log Query audit events (admin+)
GET /api/:account/-/audit-log/export?format=csv Export as CSV (admin+)

The query endpoint returns {items, next_cursor, has_more} and supports filters: ?category=, ?resource=, ?verb=, ?username=, ?project_slug=, ?from=, ?to=, ?search=.

The export endpoint supports a subset of filters: ?category=, ?resource=, ?verb=, ?search=, ?from=, ?to=. The format=csv parameter is required.

Soft-delete and restore

DELETE /api/:account/:project/:filename Soft-delete document
POST /api/:account/:project/:filename/-/restore Restore document (admin+)
POST /api/:account/:project/-/bulk-delete Bulk delete documents (editor+)
GET /api/:account/:project/-/deleted List deleted documents
POST /api/:account/:project/-/restore Restore project (admin+)
DELETE /api/:account/-/settings Soft-delete account (owner)
POST /api/:account/-/restore Restore account (owner)

The deleted documents list returns {items, next_cursor, has_more}.

Bulk delete body fields: filenames (required, array of filename strings).

Notifications

GET /api/notifications List notifications
POST /api/notifications/read-all Mark all as read
POST /api/notifications/:id/read Mark one as read

The list endpoint returns {items, next_cursor, has_more}. Supports ?unread_only=true to filter to unread notifications only.

User search

GET /api/users/search?q= Search users by username or email

Returns {"items": [...]} with up to 10 results. No cursor pagination.

Error format

Every error response uses the same shape:

{"error": "human-readable message", "code": "machine_readable_code"}
Status When
401 No auth, invalid API key, expired API key
403 Authenticated but lacks permission
404 Account, project, document, or version not found
409 Conflict (filename exists, publish conflict)
413 Size, storage, or count limits exceeded
422 Validation error (missing field, invalid filename, secret detected, malformed or wrong-type ID. Codes: invalid_id, wrong_id_type)
423 Document is locked
429 Rate limit exceeded

Rate limits

API requests are rate limited per API key. Write operations (POST, PUT, PATCH, DELETE) have a lower limit than reads. If you exceed the limit, the API returns 429 Too Many Requests with a Retry-After header indicating how many seconds to wait.