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.
DeprecationandSunsettell 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_V1StGXR8Z5jdHi6BmyT0xq3k9fK2for a documentuser_abc123def456ghi789jkl012XyZqfor a useraudit_9fK2p8QnR4mL3tX6vN7wY8z1aBcDfor 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 optionalmessageandtags)
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.