Revdoku API

Use the Revdoku API to create buckets, store files, publish static websites, attach custom domains, and read publication analytics.

Most AI-agent users should start with the Revdoku app’s copied prompt or the Revdoku MCP tool. Use this HTTP API for custom clients, CI jobs, backend workers, or direct integrations.

Quick Start

Base URL

export REVDOKU_URL=https://app.revdoku.com
export REVDOKU_API_KEY=revdoku_...

Authentication Header

Send the API key as a bearer token:

Authorization: Bearer $REVDOKU_API_KEY

JSON Headers

Use JSON for request bodies. File bytes are uploaded to the object-storage upload URLs returned by Revdoku, not posted through Rails:

Content-Type: application/json
Accept: application/json

Agent Headers

Agent clients should identify themselves. These headers are used for audit logs and user-visible activity history.

User-Agent: RevdokuMCP/0.1.0 (codex)
X-Revdoku-Agent: codex
X-Revdoku-Agent-Client: chatgpt
X-Revdoku-Agent-Version: 0.1.0
X-Revdoku-Agent-Run-Id: run_20260520_001
X-Revdoku-Agent-Project: marketing-site
X-Revdoku-Agent-Task: landing-page-refresh

Response Format

Successful responses are wrapped in data:

{
  "data": {
    "id": "bkt_..."
  }
}

Errors are wrapped in error:

{
  "error": {
    "message": "Bucket not found",
    "code": "BUCKET_NOT_FOUND",
    "request_id": "req_...",
    "docs_url": "https://revdoku.com/api.md"
  }
}

Use error.code for recovery logic. Use request_id when debugging with support.

Hosted MCP for Claude/ChatGPT Cloud

Cloud agents that support custom remote MCP connectors connect to Revdoku through the production remote MCP endpoint:

https://app.revdoku.com/mcp

Add that URL as a Claude custom connector, or in ChatGPT use the custom connector/custom MCP app/developer-mode MCP surface available to the account. If that ChatGPT surface is not available, use the local CLI or local stdio MCP instead. The connector uses Revdoku OAuth discovery, authorization-code PKCE, and Bearer tokens. Users approve the connection in Revdoku and can revoke it later from /account/access.

Hosted MCP supports JSON-response Streamable HTTP and stateful Streamable HTTP/SSE sessions. OAuth metadata uses REVDOKU_MCP_PUBLIC_BASE_URL when set, so local HTTPS tunnels and reverse-proxy deployments can publish a stable public resource URL.

Hosted MCP exposes cloud-safe bucket tools for reading, creating, updating, archiving, unarchiving, permanent delete, publishing, republishing, and analytics. It intentionally does not expose local-path tools because cloud connectors cannot read a user’s local filesystem. Use the Revdoku CLI or local stdio MCP for local folder uploads; hosted MCP can then update and republish the same bucket_id. bucket_list and bucket_get include bucket ids, website metadata, publication lifecycle state, and action metadata such as archive.required_action and delete.confirmation so agents can handle ids internally instead of asking users to type them.

Common Workflows

Connect an Agent

The lowest-friction flow is the app’s Copy prompt button. It gives the agent a one-time grant token. Exchange it for a normal API key:

curl -fsS "$REVDOKU_URL/api/v1/agent_auth/exchange_grant" \
  -H "Content-Type: application/json" \
  -d '{
    "grant_token": "GRANT_TOKEN_FROM_REVDOKU",
    "label": "Codex on laptop"
  }'

Fallback email-code flow:

curl -fsS "$REVDOKU_URL/api/v1/agent_auth/request_code" \
  -H "Content-Type: application/json" \
  -d '{ "email": "person@example.com" }'

curl -fsS "$REVDOKU_URL/api/v1/agent_auth/verify_code" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "person@example.com",
    "code": "123456",
    "label": "Codex on laptop",
    "bucket_access": "all"
  }'

Store the returned data.api_key securely. Follow data.guidance when the server includes it. Do not print or log the key.

Create a Bucket

Bucket tags are user-facing labels for organization, not filesystem breadcrumbs. Do not derive tag_paths from local parent folders, the current working directory, bucket titles, or domain/folder names. For website uploads, use a simple website tag only when a type label is useful; store project or task context in metadata.

curl -fsS "$REVDOKU_URL/api/v1/buckets" \
  -H "Authorization: Bearer $REVDOKU_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "bucket": {
      "title": "Marketing site",
      "description": "Generated launch assets",
      "tag_paths": ["website"],
      "metadata": {
        "project": "marketing-site",
        "task": "landing-page"
      }
    }
  }'

Example response:

{
  "data": {
    "id": "bkt_...",
    "title": "Marketing site",
    "published": false
  }
}

Upload a File

For a single file, create a direct-upload descriptor, upload bytes to the returned object-storage URL, then attach the signed blob id to the bucket. The server opens and finalizes a one-file bucket upload session automatically.

curl -fsS "$REVDOKU_URL/api/v1/direct_uploads" \
  -H "Authorization: Bearer $REVDOKU_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "bucket_id": "bkt_...",
    "path": "index.html",
    "blob": {
      "filename": "index.html",
      "byte_size": 1234,
      "checksum": "BASE64_MD5",
      "content_type": "text/html",
      "sha256": "HEX_SHA256",
      "purpose": "bucket_file"
    }
  }'

Uploading the same path creates a new version of that file.

Upload Multiple Files

For folders or multi-file updates, open one bucket upload session, then request upload descriptors in client-side subbatches. Revdoku’s CLI and MCP clients use 12 files per descriptor batch. Upload each returned descriptor to object storage, then call finalize_batch for that subbatch before requesting much more work. This keeps each server-side commit bounded and resilient for large folders.

If the client disconnects after some object-storage uploads complete, Revdoku keeps files that were already finalized by finalize_batch. Unfinalized staged uploads are abandoned when the session expires, and the bucket write lock is released automatically.

curl -fsS "$REVDOKU_URL/api/v1/buckets/bkt_.../upload_sessions" \
  -H "Authorization: Bearer $REVDOKU_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'

Then request descriptors for one subbatch:

curl -fsS "$REVDOKU_URL/api/v1/buckets/bkt_.../upload_sessions/bus_.../uploads" \
  -H "Authorization: Bearer $REVDOKU_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "files": [
      {
        "path": "index.html",
        "name": "index.html",
        "byte_size": 1234,
        "checksum": "BASE64_MD5",
        "content_type": "text/html",
        "sha256": "HEX_SHA256"
      }
    ]
}'

Use data.uploads[].upload.url and data.uploads[].upload.headers for the object-storage PUT. Do not send Revdoku authorization headers to object storage. After each successful descriptor subbatch, commit a bounded batch:

curl -fsS -X POST "$REVDOKU_URL/api/v1/buckets/bkt_.../upload_sessions/bus_.../finalize_batch" \
  -H "Authorization: Bearer $REVDOKU_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"limit":12}'

Repeat descriptor and finalize subbatches until all selected files are uploaded.

Close the session when all uploads are done. Use complete:false only when canceling or interrupting the upload; it closes the session and releases the lock without committing any unfinalized staged uploads.

curl -fsS -X POST "$REVDOKU_URL/api/v1/buckets/bkt_.../upload_sessions/bus_.../finalize" \
  -H "Authorization: Bearer $REVDOKU_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"complete":true}'

For large sessions, finalize may return HTTP 202 with data.finalize_pending:true, data.remaining_files_count, and a Retry-After header. Wait for the retry interval and call the same finalize endpoint again until the response no longer includes finalize_pending:true.

Publish a Bucket

Publish explicitly when the bucket should have a website URL:

curl -fsS "$REVDOKU_URL/api/v1/buckets/bkt_.../publication" \
  -H "Authorization: Bearer $REVDOKU_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "entrypoint": "index.html",
    "site_mode": "spa",
    "access_mode": "public",
    "tracking_enabled": false,
    "permanent": true
  }'

For a Pro protected website, use "access_mode": "password". Use "access_mode": "password_ask_info" when visitors should enter email before the password. Omit password; Revdoku generates a copyable password the first time protected access is enabled. Set "regenerate_password": true only when the owner explicitly wants to rotate the protected-site password. Agents should not ask users to type protected-site passwords in chat. Never put the password in the URL. Owner publish responses include the website URL and copyable password/share text when the authenticated key is allowed to see it.

Website slug (paid plans). Pass "slug_suggestions": ["California Weather", "cali weather", "weather-california"] to steer the public URL slug. Revdoku sanitizes each name to a slug and uses the first available one; if all are taken it appends a numeric suffix (california-weather-1). On free plans slug_suggestions is ignored and a random slug is generated (the owner can rename the slug later after upgrading). Slug selection only applies when first creating a publication; republishing keeps the existing slug.

Publishing is asynchronous. The request returns HTTP 202 Accepted with the publication in a queued/processing state — the bundle is built in the background (this is why large, 4k-file buckets no longer time out). Example response:

{
  "data": {
    "id": "pub_...",
    "bucket_id": "bkt_...",
    "public_slug": "bright-canvas-meadow",
    "public_url": "https://bright-canvas-meadow.revdoku.site/",
    "status": "publishing",
    "publish_state": "queued",
    "publish_pending": true,
    "site_mode": "spa",
    "access_mode": "public",
    "permanent": true
  }
}

Wait for the build (poll until live)

Do not hand out public_url while publish_state is queued or processing — it 404s until the build finishes. Poll the publication until it is terminal:

curl -fsS "$REVDOKU_URL/api/v1/publications/pub_..." \
  -H "Authorization: Bearer $REVDOKU_API_KEY"

publish_enqueued_at / publish_started_at / publish_completed_at are exposed for progress/age. Changing only settings/access (no file changes) reuses the existing bundle and does not re-upload files.

Use site_mode: "static" for ordinary static sites. Use site_mode: "spa" for React/Vite-style apps where deep links should fall back to index.html. Website analytics and browser-side Revdoku event tracking are enabled by default. Set "tracking_enabled": false to disable both, or use "publication_analytics_enabled" and "publication_client_events_enabled" for separate control. "analytics_enabled" and "client_events_enabled" are accepted aliases.

Publish a Folder Efficiently

Use publish sessions for larger folders. Revdoku compares file hashes, uploads only changed bytes, then finalizes the publication.

Create the session:

curl -fsS "$REVDOKU_URL/api/v1/publish_sessions" \
  -H "Authorization: Bearer $REVDOKU_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "bucket_title": "Marketing site",
    "entrypoint": "index.html",
    "site_mode": "spa",
    "access_mode": "password",
    "tracking_enabled": false,
    "permanent": true,
    "files": [
      {
        "path": "index.html",
        "byte_size": 1234,
        "content_type": "text/html",
        "checksum": "BASE64_MD5",
        "sha256": "HEX_SHA256"
      }
    ]
  }'

Upload each file to data.publish_session.uploads[].upload.url using exactly the returned upload headers. Do not send Revdoku auth headers to object-storage upload URLs.

Finalize the session:

curl -fsS -X POST "$REVDOKU_URL/api/v1/publish_sessions/pus_.../finalize" \
  -H "Authorization: Bearer $REVDOKU_API_KEY"

Finalize returns 202 with the publication in publish_state: "queued" — the uploaded files are written into the bucket and the bundle is built in the background. Poll GET /api/v1/publications/pub_... until publish_state is ready before using public_url (see “Wait for the build” above). Bad input (a stale session, a file locked by another agent, missing storage) still fails fast at finalize with 409/423/503.

If an upload URL expires, refresh it:

curl -fsS -X POST "$REVDOKU_URL/api/v1/publish_sessions/pus_.../uploads/refresh" \
  -H "Authorization: Bearer $REVDOKU_API_KEY"

Add a Custom Domain

Custom domains are available on paid plans. Publish the bucket first.

curl -fsS "$REVDOKU_URL/api/v1/buckets/bkt_.../custom_domains" \
  -H "Authorization: Bearer $REVDOKU_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "hostname": "example.com" }'

Example response while DNS is still pending:

{
  "data": {
    "custom_domain": {
      "id": "pcd_...",
      "hostname": "example.com",
      "status": "pending_validation",
      "ssl_status": "pending_validation",
      "public_url": null,
      "required_dns_records": [
        {
          "type": "CNAME",
          "name": "example.com",
          "value": "custom.revdoku.site",
          "purpose": "traffic",
          "apex": true,
          "supported_types": ["ALIAS", "ANAME", "CNAME flattening"]
        },
        {
          "type": "TXT",
          "name": "_cf-custom-hostname.example.com",
          "value": "...",
          "purpose": "ownership"
        }
      ]
    },
    "publication": {
      "public_url": "https://bright-canvas-meadow.revdoku.site/"
    },
    "limits": {
      "active_count": 1,
      "max_custom_domains": 25
    }
  }
}

Add every returned DNS record. Then refresh until custom_domain.status is active:

curl -fsS -X POST "$REVDOKU_URL/api/v1/buckets/bkt_.../custom_domains/pcd_.../refresh" \
  -H "Authorization: Bearer $REVDOKU_API_KEY"

When active, the publication public_url switches to the custom domain. The managed https://<bucket-slug>.revdoku.site/ URL keeps working.

For apex domains such as example.com, the DNS provider must support ALIAS, ANAME, or CNAME flattening. If it does not, use www.example.com as the custom domain and redirect example.com to www.example.com at the DNS/hosting provider.

Read Analytics

Publication analytics are visible on paid plans. Free plans receive the same shape with numbers hidden.

curl -fsS "$REVDOKU_URL/api/v1/analytics?range=30d" \
  -H "Authorization: Bearer $REVDOKU_API_KEY"

Example paid response:

{
  "data": {
    "range": "30d",
    "first_event_at": "2026-05-22T09:12:33.000Z",
    "last_event_at": "2026-05-26T18:32:14.000Z",
    "totals": {
      "hits_all_time": 8420,
      "hits": 1204,
      "visitors": 822,
      "hits_not_found": 18,
      "hits_bots": 91
    },
    "daily": [
      { "date": "2026-05-26", "hits": 120, "visitors": 84, "hits_not_found": 2, "hits_bots": 9 }
    ],
    "buckets": [
      {
        "bucket_id": "bkt_abc123",
        "bucket_title": "Docs",
        "publication_id": "pub_abc123",
        "public_slug": "docs",
        "url": "https://docs.revdoku.site/",
        "hits": 1204
      }
    ],
    "paths": [
      { "path": "/", "hits": 650 }
    ],
    "referrers": [
      { "referrer": "direct", "hits": 420 }
    ],
    "countries": [
      { "country": "US", "hits": 510 }
    ],
    "bots": [
      { "bot": "GPTBot", "hits": 91 }
    ],
    "paths_not_found": [
      { "bucket_id": "bkt_abc123", "publication_id": "pub_abc123", "public_slug": "docs", "path": "/old-page", "hits": 18 }
    ]
  }
}

visitors is a sum of each day’s unique visitor count, not a global unique visitor count across the whole range.

API Reference

Authentication Endpoints

MethodPathPurpose
POST/api/v1/agent_auth/request_codeRequest an email verification code to sign in or create a Revdoku account, without revealing prior account state.
POST/api/v1/agent_auth/verify_codeVerify the email code and create an API key when the code is valid.
POST/api/v1/agent_auth/exchange_grantExchange an app-created grant for an API key.
POST/api/v1/agent_auth/browser_login_linkCreate a one-time dashboard login link.

POST /api/v1/agent_auth/request_code

This endpoint signs in an existing Revdoku account, or creates a new account when the email does not have one yet (subject to signup eligibility, e.g. disposable or blocked domains are rejected with SIGNUP_BLOCKED). It does not reveal whether the email already had an account, whether that account is locked, or whether browser sign-in is required: it returns the same success shape for syntactically valid, eligible email requests. If no code arrives or verification fails, ask the user to sign in to Revdoku in the browser and copy a one-time connection prompt/grant from the app, then exchange it. Do not ask for a Revdoku password, TOTP, backup code, payment details, or full chat history.

{
  "email": "person@example.com"
}

POST /api/v1/agent_auth/verify_code

Verifies the email code and returns a revdoku_... API key when the code is valid for an account that can use email-code agent sign-in. A new account created through request_code is confirmed and its default account is set up on the first successful verification. INVALID_CODE is privacy-preserving and can also mean the account needs browser sign-in.

{
  "email": "person@example.com",
  "code": "123456",
  "label": "Codex on laptop",
  "bucket_access": "all"
}

For selected-bucket access, use:

{
  "bucket_access": "selected",
  "bucket_ids": ["bkt_..."],
  "bucket_permissions": {
    "bkt_...": "write"
  }
}

POST /api/v1/agent_auth/exchange_grant

{
  "grant_token": "GRANT_TOKEN_FROM_REVDOKU",
  "label": "Codex on laptop"
}

POST /api/v1/agent_auth/browser_login_link

Requires Authorization. Disabled when the authenticated user has two-factor authentication enabled or the account requires two-factor authentication. In that case, open the Revdoku dashboard through the normal browser sign-in flow.

{
  "redirect_path": "/account/access"
}

Common redirect_path values:

PathDestination
/bucketsBucket dashboard.
/libraryLibrary settings.
/account/accessMembers, agents, and API keys.

Bucket Endpoints

MethodPathPurpose
GET/api/v1/bucketsList active buckets by default. Use ?archived=true to list archived buckets.
POST/api/v1/bucketsCreate a bucket.
GET/api/v1/buckets/:idRead a bucket.
PATCH/api/v1/buckets/:idUpdate bucket metadata.
POST/api/v1/buckets/:id/archiveArchive a normal unpublished bucket.
POST/api/v1/buckets/:id/unarchiveRestore an archived normal bucket.
DELETE/api/v1/buckets/:idPermanently delete a normal unpublished bucket with confirmation.
GET/api/v1/tagsList reusable bucket labels.

GET /api/v1/buckets

curl -fsS "$REVDOKU_URL/api/v1/buckets" \
  -H "Authorization: Bearer $REVDOKU_API_KEY"

By default, this returns active buckets. To list archived buckets, call:

curl -fsS "$REVDOKU_URL/api/v1/buckets?archived=true" \
  -H "Authorization: Bearer $REVDOKU_API_KEY"

Bucket list/detail responses include effective lifecycle action metadata:

FieldMeaning
websiteCurrent or latest website publication metadata, including public_url, status, published, and lifecycle_active.
publication_lifecycle_activetrue when a publication is active enough to block archive/delete, even if the public artifacts are unavailable.
archive.allowedWhether the current principal can archive now.
archive.required_actionunpublish_first when the bucket must be unpublished before archive.
unarchive.allowedWhether the current principal can restore an archived bucket now.
delete.allowedWhether the current principal can permanently delete now.
delete.required_actionunpublish_first when the bucket must be unpublished before permanent delete.
delete.confirmationConfirmation phrase returned by the API; clients should pass it exactly to DELETE after human confirmation, not ask users to type bucket ids.

Archived buckets are read-only until unarchived. Metadata edits, label changes, file changes, direct upload targets, reference file uploads, thumbnail uploads, bucket duplication, publication updates, and custom-domain mutations return BUCKET_ARCHIVED. Read/list endpoints, unarchive, permanent delete, and publication cleanup remain available when otherwise permitted. Copying files out of an archived bucket is allowed when the caller has read access to the source and write access to an active target bucket.

POST /api/v1/buckets

Bucket tags are user-facing labels, not filesystem breadcrumbs. Use tag_paths only for explicit reusable labels such as website; store project, source, task, or local-folder context in metadata.

{
  "bucket": {
    "title": "Marketing site",
    "description": "Generated launch assets",
    "tag_paths": ["website"],
    "metadata": {
      "project": "marketing-site"
    }
  }
}

PATCH /api/v1/buckets/:id

{
  "bucket": {
    "description": "Updated purpose",
    "metadata": {
      "run": "revision-2"
    }
  }
}

Bucket locks

Use a bucket lock for broad folder uploads, full-site rewrites, or coordinated multi-file edits. Use file locks for narrow edits to specific paths.

curl -fsS -X POST "$REVDOKU_URL/api/v1/buckets/bkt_.../lock" \
  -H "Authorization: Bearer $REVDOKU_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "message": "Uploading website folder", "duration_seconds": 900 }'
curl -fsS -X DELETE "$REVDOKU_URL/api/v1/buckets/bkt_.../lock" \
  -H "Authorization: Bearer $REVDOKU_API_KEY"

Active bucket locks block writes, deletes, publishing changes, direct uploads, and file locks by other API keys. Revdoku checks the bucket lock before checking specific file locks. Conflicts return HTTP 423 with code BUCKET_LOCKED.

Archive, unarchive, and permanent delete

Library buckets cannot be archived, unarchived, or deleted. Normal buckets with active published websites must be unpublished first.

curl -fsS -X POST "$REVDOKU_URL/api/v1/buckets/bkt_.../archive" \
  -H "Authorization: Bearer $REVDOKU_API_KEY"
curl -fsS -X POST "$REVDOKU_URL/api/v1/buckets/bkt_.../unarchive" \
  -H "Authorization: Bearer $REVDOKU_API_KEY"

Permanent delete requires the confirmation phrase returned by GET /api/v1/buckets or GET /api/v1/buckets/:id in delete.confirmation.

curl -fsS -X DELETE "$REVDOKU_URL/api/v1/buckets/bkt_..." \
  -H "Authorization: Bearer $REVDOKU_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "confirmation": "<delete.confirmation from bucket list/detail>" }'

UI and agent clients should ask users to confirm by bucket title or natural language, then pass delete.confirmation internally.

Permanent deletion is not a bulk operation. Buckets must be deleted one at a time via DELETE /api/v1/buckets/:id so each removal is confirmed individually. The POST /api/v1/buckets/bulk endpoint accepts only archive and unarchive operations and rejects delete.

Large bucket deletes can return HTTP 202 with data.bucket.deletion_started and data.delete_progress. The bucket remains visible while the background job runs, with lock.kind:"bucket_delete" and progress fields such as phase, total_files, total_versions, and total_items. Poll bucket list/detail to show progress until the bucket disappears or a delete notification is delivered. If background deletion fails, the bucket is unlocked and a failed delete notification is sent so clients can retry.

File Endpoints

MethodPathPurpose
GET/api/v1/buckets/:bucket_id/filesList files.
POST/api/v1/buckets/:bucket_id/filesAttach one completed direct-upload blob.
POST/api/v1/buckets/:bucket_id/upload_sessionsOpen and lock a multi-file bucket upload session.
POST/api/v1/buckets/:bucket_id/upload_sessions/:id/uploadsCreate direct-upload descriptors for one file subbatch.
POST/api/v1/buckets/:bucket_id/upload_sessions/:id/finalize_batchCommit a bounded subbatch of uploaded files.
POST/api/v1/buckets/:bucket_id/upload_sessions/:id/finalizeContinue finalization and close the session when no uploaded files remain.
GET/api/v1/buckets/:bucket_id/files/:idRead file metadata.
GET/api/v1/buckets/:bucket_id/files/:id/downloadDownload file bytes.
GET/api/v1/buckets/:bucket_id/files/:id/textRead a text file.
DELETE/api/v1/buckets/:bucket_id/files/:idDelete a file.
POST/api/v1/buckets/:bucket_id/lockLock the whole bucket.
DELETE/api/v1/buckets/:bucket_id/lockUnlock the bucket.
POST/api/v1/buckets/:bucket_id/files/lockLock by path.
POST/api/v1/buckets/:bucket_id/files/:id/lockLock by file id.
DELETE/api/v1/buckets/:bucket_id/files/:id/lockUnlock a file.
POST/api/v1/direct_uploadsCreate a direct-upload URL.

POST /api/v1/buckets/:bucket_id/files

Attach one completed direct-upload blob. Revdoku opens and finalizes a server-owned upload session automatically for this single-file write.

{
  "signed_blob_id": "...",
  "path": "assets/app.js",
  "name": "app.js"
}

POST /api/v1/buckets/:bucket_id/upload_sessions

Open a durable multi-file upload session. Revdoku locks the bucket for writes until the session is finalized or expires. The upload-session TTL is sliding: successful descriptor/finalize progress refreshes the session expiry and bucket lock expiry. The request body can be empty.

{}

POST /api/v1/buckets/:bucket_id/upload_sessions/:id/uploads

Create direct-upload descriptors for one file subbatch.

{
  "files": [
    {
      "input_index": 0,
      "path": "assets/app.js",
      "name": "app.js",
      "byte_size": 1234,
      "checksum": "BASE64_MD5",
      "content_type": "application/javascript",
      "sha256": "HEX_SHA256"
    }
  ]
}

Upload each returned data.uploads[].upload descriptor to object storage. Identical duplicates may be returned in data.skipped without an upload URL.

Then call POST /api/v1/buckets/:bucket_id/upload_sessions/:id/finalize_batch after each uploaded subbatch. Finish with POST /api/v1/buckets/:bucket_id/upload_sessions/:id/finalize and {"complete":true}. Expired sessions are auto-closed; files already committed with finalize_batch remain in the bucket, while unfinalized staged uploads are abandoned.

Finalize responses include authoritative aggregate counts such as uploaded_count, created_count, updated_count, and skipped_count. The uploaded, staged, and skipped arrays are capped detail samples for large sessions; clients should use the count fields for totals. For large remaining work, finalize can return HTTP 202 with finalize_pending:true; retry the same finalize call after the Retry-After delay until the session closes.

POST /api/v1/buckets/:bucket_id/files/lock

{
  "path": "index.html",
  "message": "Updating the landing page",
  "duration_seconds": 900
}

Active locks block writes and deletes by other API keys. Lock conflicts return HTTP 423 with code FILE_LOCKED, unless the bucket is locked first, in which case the API returns BUCKET_LOCKED.

POST /api/v1/direct_uploads

Create an upload descriptor:

{
  "bucket_id": "bkt_...",
  "path": "dist/index.html",
  "blob": {
    "filename": "index.html",
    "byte_size": 1234,
    "checksum": "BASE64_MD5",
    "content_type": "text/html",
    "sha256": "HEX_SHA256",
    "purpose": "bucket_file"
  }
}

Upload bytes to data.direct_upload.url using data.direct_upload.headers. Then attach data.signed_id with POST /api/v1/buckets/:bucket_id/files.

Version Endpoints

MethodPathPurpose
GET/api/v1/buckets/:id/versionsList bucket versions.
GET/api/v1/buckets/:id/versions/:version_idRead one bucket version.
POST/api/v1/buckets/:id/versions/restoreRestore a historical version.
GET/api/v1/buckets/:bucket_id/files/:id/versionsList file versions.
GET/api/v1/buckets/:bucket_id/files/:id/versions/:version_id/contentDownload one file version.

GET /api/v1/buckets/:bucket_id/files

Lists active bucket files. By default the response includes every file for backward compatibility. For large buckets, pass limit and optional offset to page through the list:

curl -fsS "$REVDOKU_URL/api/v1/buckets/bkt_.../files?limit=100&offset=0" \
  -H "Authorization: Bearer $REVDOKU_API_KEY"

Paginated responses include data.pagination with limit, offset, count, total, has_more, and next_offset.

GET /api/v1/buckets/:id also accepts include=files to return bucket metadata plus current files without historical versions or legacy source-file payloads. Combine it with file_limit and file_offset when a bucket detail view should page data.bucket.files.

POST /api/v1/buckets/:id/versions/restore

{
  "version_id": "bktrv_...",
  "comment": "Return to the first published draft"
}

Restore is non-destructive. Revdoku creates a new latest version linked to the selected historical version.

Publishing Endpoints

MethodPathPurpose
POST/api/v1/buckets/:id/publicationPublish a bucket.
DELETE/api/v1/buckets/:id/publicationStop serving a bucket.
GET/api/v1/publicationsList public bucket links.
GET/api/v1/publications/:idRead one publication.
PATCH/api/v1/publications/:idUpdate publication settings.
DELETE/api/v1/publications/:idRevoke a publication.
GET/api/v1/publications/:id/manifestRead the published file manifest.
POST/api/v1/publish_sessionsCreate a publish session.
POST/api/v1/publish_sessions/:id/uploads/refreshRefresh upload URLs.
POST/api/v1/publish_sessions/:id/finalizeFinalize a publish session.

Archived buckets cannot be published, republished, direct-publish finalized, or have publication settings updated until they are unarchived. Unpublish and publication revoke endpoints remain available for cleanup.

POST /api/v1/buckets/:id/publication

{
  "entrypoint": "index.html",
  "site_mode": "spa",
  "access_mode": "password",
  "tracking_enabled": false,
  "permanent": true
}

Publication response fields:

FieldMeaning
public_urlSame public website URL returned for users and agents.
asset_base_urlDirect public object-storage/CDN directory.
public_slugStable DNS-safe bucket publication slug.
statuspublished, unpublished, or another lifecycle status.
permanenttrue when there is no expiration.
expires_atExpiration timestamp for temporary publications.
site_modeWhether deep links fall back to the entrypoint.
access_modepublic, password, or password_ask_info. Password-protected websites are a Pro entitlement; password_ask_info asks visitors for email plus password.
password_configuredWhether a protected website password is configured.
access_passwordCopyable stored password, returned only to account-owner publish keys.
generated_passwordNewly generated password, returned only to account-owner publish keys.
share_textCopyable owner-facing text containing the website link and password when visible.
publication_analytics_enabledWhether Revdoku records website analytics for this publication.
publication_client_events_enabledWhether browser-side Revdoku event tracking is enabled for this publication.
analytics.hits_all_timeCached all-time website hits; null when analytics numbers are hidden.
analytics.last_event_atLatest recorded analytics event timestamp; null when hidden or not recorded yet.

POST /api/v1/publish_sessions

Use this for larger folders and AI-generated websites. It accepts the same access and analytics/tracking fields as bucket publishing, including tracking_enabled, publication_analytics_enabled, and publication_client_events_enabled.

{
  "bucket_title": "Marketing site",
  "bucket_description": "Generated launch assets",
  "bucket_tag_paths": ["website"],
  "entrypoint": "index.html",
  "site_mode": "spa",
  "access_mode": "password",
  "permanent": true,
  "files": [
    {
      "path": "index.html",
      "byte_size": 1234,
      "content_type": "text/html",
      "checksum": "BASE64_MD5",
      "sha256": "HEX_SHA256"
    }
  ]
}

The response includes:

FieldMeaning
publish_sessionSession id, files, uploads, and status.
publish_session.uploadsDirect upload URLs for changed files only.
finalize.urlURL to finalize after uploads finish.
deploy_summaryShort user-facing deployment summary.

If finalize returns 409 with PUBLISH_SESSION_STALE, PUBLISH_SESSION_EXPIRED, or PUBLISH_SESSION_NOT_PENDING, recreate the publish session from the same manifest and retry once.

Custom Domain Endpoints

MethodPathPurpose
GET/api/v1/buckets/:bucket_id/custom_domainsRead the bucket custom-domain state.
POST/api/v1/buckets/:bucket_id/custom_domainsCreate or replace a custom domain.
GET/api/v1/buckets/:bucket_id/custom_domains/:idRead one custom domain.
POST/api/v1/buckets/:bucket_id/custom_domains/:id/refreshRefresh DNS and certificate state.
DELETE/api/v1/buckets/:bucket_id/custom_domains/:idRemove a custom domain.

POST /api/v1/buckets/:bucket_id/custom_domains

{
  "hostname": "example.com"
}

Plan rules:

PlanCustom-domain behavior
Freemax_custom_domains is 0; custom domains are disabled.
Paidmax_custom_domains equals the plan’s max live public websites.
DowngradeDomains above the new limit are disabled. On Free, all custom domains are disabled.

Replacing a custom domain keeps the previous active domain serving until the new domain becomes active.

Analytics Endpoints

MethodPathPurpose
GET/api/v1/analytics?range=30dAccount-wide publication analytics.
GET/api/v1/publications/:id/analytics?range=30dAnalytics for one publication.

GET /api/v1/analytics

Supported ranges are 7d, 30d, and 90d.

Paid responses include:

FieldMeaning
first_event_atFirst recorded event timestamp in the selected range.
last_event_atLast recorded event timestamp in the selected range.
totals.hits_all_timeTotal recorded website hits.
totals.hitsWebsite hits in the selected range.
totals.visitorsSum of daily unique visitors in the selected range.
totals.hits_not_foundMissing-path hits.
totals.hits_botsLikely or known bot hits.
dailyDaily website hits and visitors.
bucketsHighest-traffic published buckets.
pathsHighest-traffic paths.
referrersReferrer hosts, with direct for no referrer.
countriesCountry codes.
botsBot hits grouped by bot name.
paths_not_foundHighest-traffic missing paths.

Free responses hide numbers:

{
  "data": {
    "range": "30d",
    "first_event_at": null,
    "last_event_at": null,
    "totals": {
      "hits_all_time": null,
      "hits": null,
      "visitors": null,
      "hits_not_found": null,
      "hits_bots": null
    },
    "daily": [],
    "buckets": [],
    "paths": [],
    "referrers": [],
    "countries": [],
    "bots": [],
    "paths_not_found": []
  }
}

Common Errors

Rate Limits

Upload-control endpoints such as direct-upload creation and bucket upload sessions are account-throttled. On HTTP 429, honor the Retry-After header or error.details.retry_after before retrying. Clients should use bounded exponential backoff with jitter and should not retry indefinitely.

Concurrent large uploads, finalization, deletes, and storage-counter refreshes can also return HTTP 409 with DATABASE_BUSY_RETRY. Treat this as a temporary contention signal: honor Retry-After or error.details.retry_after, use bounded exponential backoff with jitter, and retry only idempotent or session-keyed upload/delete control calls.

HTTPCodeMeaning
409DATABASE_BUSY_RETRYRelated bucket changes are still committing; retry after the advertised delay.
409BUCKET_FILE_PATH_INDEX_BACKFILL_PENDINGExisting bucket file path lookup keys are being prepared; retry after the advertised delay.
429RATE_LIMIT_EXCEEDEDGeneral account API rate limit exceeded.
429PUBLISH_RATE_LIMIT_EXCEEDEDPublishing API rate limit exceeded.
429UPLOAD_RATE_LIMIT_EXCEEDEDUpload-control API rate limit exceeded.

Authentication Errors

HTTPCodeMeaning
401UNAUTHORIZEDMissing, invalid, or expired API key.
403FORBIDDENAPI key is valid but not allowed for this action.

Bucket and File Errors

HTTPCodeMeaning
404BUCKET_NOT_FOUNDBucket does not exist or is not visible to this key.
404FILE_NOT_FOUNDFile does not exist or is not visible to this key.
403LIBRARY_BUCKET_IMMUTABLELibrary bucket cannot be archived, unarchived, or deleted.
403BUCKET_DELETE_ADMIN_REQUIREDOnly an account administrator can permanently delete this bucket, except for empty unpublished cleanup buckets created by the same user.
409BUCKET_PUBLICATION_ACTIVEUnpublish this bucket before archiving or deleting it.
409BUCKET_ALREADY_ARCHIVEDBucket is already archived.
409BUCKET_NOT_ARCHIVEDBucket is not archived; only unarchive archived buckets.
422BUCKET_DELETE_CONFIRMATION_REQUIREDPass the delete.confirmation value returned by bucket list/detail with the delete request.
403BUCKET_ARCHIVEDBucket is archived and cannot be edited until it is unarchived.
423FILE_LOCKEDAnother key owns an active file lock.

Publishing Errors

HTTPCodeMeaning
403PUBLICATION_LIMIT_REACHEDAccount is at the public-site limit.
403LIBRARY_BUCKET_PUBLISH_FORBIDDENLibrary bucket cannot be published.
409PUBLISH_SESSION_STALEPublish session is out of date; recreate or refresh.
410PUBLISH_SESSION_EXPIREDPublish session expired; create a new one.
503PUBLIC_STORAGE_NOT_CONFIGUREDPublic publishing is not configured for this deployment.

Custom Domain Errors

HTTPCodeMeaning
403CUSTOM_DOMAIN_PLAN_REQUIREDCurrent plan has no custom domains.
403CUSTOM_DOMAIN_LIMIT_REACHEDAccount has reached its custom-domain limit.
422CUSTOM_DOMAIN_INVALIDHostname is invalid or already assigned.
422CUSTOM_DOMAIN_REQUIRES_PUBLICATIONPublish the bucket before assigning a domain.
503CUSTOM_DOMAINS_NOT_CONFIGUREDDeployment custom-domain support is not configured.

Integration Guidelines

Keep Bucket URLs Stable

Republish the same bucket when updating a website. Revdoku keeps the same public_slug and public URL across unpublish and republish.

Prefer Publish Sessions for Agents

Agents publishing generated sites should use POST /api/v1/publish_sessions instead of uploading every file manually. Publish sessions reuse unchanged files and return a short deploy_summary that is easy to show to users.

Surface Plan Limits Clearly

When the API returns a limit error, tell the user what happened and suggest the least disruptive next action: unpublish an older site, remove an unused custom domain, or visit Revdoku in the browser to review plan capacity.

Do Not Leak Secrets

Never print, paste, commit, or log revdoku_... API keys, one-time grant tokens, direct-upload URLs, or browser login links.

Loading PDF…