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_state: "ready"→ the site is live; usepublic_url. Owner responses include the access password / share text for protected sites here (it is no longer in the immediate publish response — fetch it after the build).publish_state: "failed"→ readpublish_error; recover withPOST /api/v1/buckets/bkt_.../publication/retry(reuses the saved request, no need to resend settings). The publish-failed notification email is also sent.publish_state: "queued" | "processing"→ wait ~1–2s and poll again. Waiting is safe; a stuck build is auto-recovered by a background sweeper.
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
| Method | Path | Purpose |
|---|---|---|
POST | /api/v1/agent_auth/request_code | Request an email verification code to sign in or create a Revdoku account, without revealing prior account state. |
POST | /api/v1/agent_auth/verify_code | Verify the email code and create an API key when the code is valid. |
POST | /api/v1/agent_auth/exchange_grant | Exchange an app-created grant for an API key. |
POST | /api/v1/agent_auth/browser_login_link | Create 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:
| Path | Destination |
|---|---|
/buckets | Bucket dashboard. |
/library | Library settings. |
/account/access | Members, agents, and API keys. |
Bucket Endpoints
| Method | Path | Purpose |
|---|---|---|
GET | /api/v1/buckets | List active buckets by default. Use ?archived=true to list archived buckets. |
POST | /api/v1/buckets | Create a bucket. |
GET | /api/v1/buckets/:id | Read a bucket. |
PATCH | /api/v1/buckets/:id | Update bucket metadata. |
POST | /api/v1/buckets/:id/archive | Archive a normal unpublished bucket. |
POST | /api/v1/buckets/:id/unarchive | Restore an archived normal bucket. |
DELETE | /api/v1/buckets/:id | Permanently delete a normal unpublished bucket with confirmation. |
GET | /api/v1/tags | List 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:
| Field | Meaning |
|---|---|
website | Current or latest website publication metadata, including public_url, status, published, and lifecycle_active. |
publication_lifecycle_active | true when a publication is active enough to block archive/delete, even if the public artifacts are unavailable. |
archive.allowed | Whether the current principal can archive now. |
archive.required_action | unpublish_first when the bucket must be unpublished before archive. |
unarchive.allowed | Whether the current principal can restore an archived bucket now. |
delete.allowed | Whether the current principal can permanently delete now. |
delete.required_action | unpublish_first when the bucket must be unpublished before permanent delete. |
delete.confirmation | Confirmation 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
| Method | Path | Purpose |
|---|---|---|
GET | /api/v1/buckets/:bucket_id/files | List files. |
POST | /api/v1/buckets/:bucket_id/files | Attach one completed direct-upload blob. |
POST | /api/v1/buckets/:bucket_id/upload_sessions | Open and lock a multi-file bucket upload session. |
POST | /api/v1/buckets/:bucket_id/upload_sessions/:id/uploads | Create direct-upload descriptors for one file subbatch. |
POST | /api/v1/buckets/:bucket_id/upload_sessions/:id/finalize_batch | Commit a bounded subbatch of uploaded files. |
POST | /api/v1/buckets/:bucket_id/upload_sessions/:id/finalize | Continue finalization and close the session when no uploaded files remain. |
GET | /api/v1/buckets/:bucket_id/files/:id | Read file metadata. |
GET | /api/v1/buckets/:bucket_id/files/:id/download | Download file bytes. |
GET | /api/v1/buckets/:bucket_id/files/:id/text | Read a text file. |
DELETE | /api/v1/buckets/:bucket_id/files/:id | Delete a file. |
POST | /api/v1/buckets/:bucket_id/lock | Lock the whole bucket. |
DELETE | /api/v1/buckets/:bucket_id/lock | Unlock the bucket. |
POST | /api/v1/buckets/:bucket_id/files/lock | Lock by path. |
POST | /api/v1/buckets/:bucket_id/files/:id/lock | Lock by file id. |
DELETE | /api/v1/buckets/:bucket_id/files/:id/lock | Unlock a file. |
POST | /api/v1/direct_uploads | Create 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
| Method | Path | Purpose |
|---|---|---|
GET | /api/v1/buckets/:id/versions | List bucket versions. |
GET | /api/v1/buckets/:id/versions/:version_id | Read one bucket version. |
POST | /api/v1/buckets/:id/versions/restore | Restore a historical version. |
GET | /api/v1/buckets/:bucket_id/files/:id/versions | List file versions. |
GET | /api/v1/buckets/:bucket_id/files/:id/versions/:version_id/content | Download 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
| Method | Path | Purpose |
|---|---|---|
POST | /api/v1/buckets/:id/publication | Publish a bucket. |
DELETE | /api/v1/buckets/:id/publication | Stop serving a bucket. |
GET | /api/v1/publications | List public bucket links. |
GET | /api/v1/publications/:id | Read one publication. |
PATCH | /api/v1/publications/:id | Update publication settings. |
DELETE | /api/v1/publications/:id | Revoke a publication. |
GET | /api/v1/publications/:id/manifest | Read the published file manifest. |
POST | /api/v1/publish_sessions | Create a publish session. |
POST | /api/v1/publish_sessions/:id/uploads/refresh | Refresh upload URLs. |
POST | /api/v1/publish_sessions/:id/finalize | Finalize 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:
| Field | Meaning |
|---|---|
public_url | Same public website URL returned for users and agents. |
asset_base_url | Direct public object-storage/CDN directory. |
public_slug | Stable DNS-safe bucket publication slug. |
status | published, unpublished, or another lifecycle status. |
permanent | true when there is no expiration. |
expires_at | Expiration timestamp for temporary publications. |
site_mode | Whether deep links fall back to the entrypoint. |
access_mode | public, password, or password_ask_info. Password-protected websites are a Pro entitlement; password_ask_info asks visitors for email plus password. |
password_configured | Whether a protected website password is configured. |
access_password | Copyable stored password, returned only to account-owner publish keys. |
generated_password | Newly generated password, returned only to account-owner publish keys. |
share_text | Copyable owner-facing text containing the website link and password when visible. |
publication_analytics_enabled | Whether Revdoku records website analytics for this publication. |
publication_client_events_enabled | Whether browser-side Revdoku event tracking is enabled for this publication. |
analytics.hits_all_time | Cached all-time website hits; null when analytics numbers are hidden. |
analytics.last_event_at | Latest 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:
| Field | Meaning |
|---|---|
publish_session | Session id, files, uploads, and status. |
publish_session.uploads | Direct upload URLs for changed files only. |
finalize.url | URL to finalize after uploads finish. |
deploy_summary | Short 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
| Method | Path | Purpose |
|---|---|---|
GET | /api/v1/buckets/:bucket_id/custom_domains | Read the bucket custom-domain state. |
POST | /api/v1/buckets/:bucket_id/custom_domains | Create or replace a custom domain. |
GET | /api/v1/buckets/:bucket_id/custom_domains/:id | Read one custom domain. |
POST | /api/v1/buckets/:bucket_id/custom_domains/:id/refresh | Refresh DNS and certificate state. |
DELETE | /api/v1/buckets/:bucket_id/custom_domains/:id | Remove a custom domain. |
POST /api/v1/buckets/:bucket_id/custom_domains
{
"hostname": "example.com"
}
Plan rules:
| Plan | Custom-domain behavior |
|---|---|
| Free | max_custom_domains is 0; custom domains are disabled. |
| Paid | max_custom_domains equals the plan’s max live public websites. |
| Downgrade | Domains 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
| Method | Path | Purpose |
|---|---|---|
GET | /api/v1/analytics?range=30d | Account-wide publication analytics. |
GET | /api/v1/publications/:id/analytics?range=30d | Analytics for one publication. |
GET /api/v1/analytics
Supported ranges are 7d, 30d, and 90d.
Paid responses include:
| Field | Meaning |
|---|---|
first_event_at | First recorded event timestamp in the selected range. |
last_event_at | Last recorded event timestamp in the selected range. |
totals.hits_all_time | Total recorded website hits. |
totals.hits | Website hits in the selected range. |
totals.visitors | Sum of daily unique visitors in the selected range. |
totals.hits_not_found | Missing-path hits. |
totals.hits_bots | Likely or known bot hits. |
daily | Daily website hits and visitors. |
buckets | Highest-traffic published buckets. |
paths | Highest-traffic paths. |
referrers | Referrer hosts, with direct for no referrer. |
countries | Country codes. |
bots | Bot hits grouped by bot name. |
paths_not_found | Highest-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.
| HTTP | Code | Meaning |
|---|---|---|
409 | DATABASE_BUSY_RETRY | Related bucket changes are still committing; retry after the advertised delay. |
409 | BUCKET_FILE_PATH_INDEX_BACKFILL_PENDING | Existing bucket file path lookup keys are being prepared; retry after the advertised delay. |
429 | RATE_LIMIT_EXCEEDED | General account API rate limit exceeded. |
429 | PUBLISH_RATE_LIMIT_EXCEEDED | Publishing API rate limit exceeded. |
429 | UPLOAD_RATE_LIMIT_EXCEEDED | Upload-control API rate limit exceeded. |
Authentication Errors
| HTTP | Code | Meaning |
|---|---|---|
401 | UNAUTHORIZED | Missing, invalid, or expired API key. |
403 | FORBIDDEN | API key is valid but not allowed for this action. |
Bucket and File Errors
| HTTP | Code | Meaning |
|---|---|---|
404 | BUCKET_NOT_FOUND | Bucket does not exist or is not visible to this key. |
404 | FILE_NOT_FOUND | File does not exist or is not visible to this key. |
403 | LIBRARY_BUCKET_IMMUTABLE | Library bucket cannot be archived, unarchived, or deleted. |
403 | BUCKET_DELETE_ADMIN_REQUIRED | Only an account administrator can permanently delete this bucket, except for empty unpublished cleanup buckets created by the same user. |
409 | BUCKET_PUBLICATION_ACTIVE | Unpublish this bucket before archiving or deleting it. |
409 | BUCKET_ALREADY_ARCHIVED | Bucket is already archived. |
409 | BUCKET_NOT_ARCHIVED | Bucket is not archived; only unarchive archived buckets. |
422 | BUCKET_DELETE_CONFIRMATION_REQUIRED | Pass the delete.confirmation value returned by bucket list/detail with the delete request. |
403 | BUCKET_ARCHIVED | Bucket is archived and cannot be edited until it is unarchived. |
423 | FILE_LOCKED | Another key owns an active file lock. |
Publishing Errors
| HTTP | Code | Meaning |
|---|---|---|
403 | PUBLICATION_LIMIT_REACHED | Account is at the public-site limit. |
403 | LIBRARY_BUCKET_PUBLISH_FORBIDDEN | Library bucket cannot be published. |
409 | PUBLISH_SESSION_STALE | Publish session is out of date; recreate or refresh. |
410 | PUBLISH_SESSION_EXPIRED | Publish session expired; create a new one. |
503 | PUBLIC_STORAGE_NOT_CONFIGURED | Public publishing is not configured for this deployment. |
Custom Domain Errors
| HTTP | Code | Meaning |
|---|---|---|
403 | CUSTOM_DOMAIN_PLAN_REQUIRED | Current plan has no custom domains. |
403 | CUSTOM_DOMAIN_LIMIT_REACHED | Account has reached its custom-domain limit. |
422 | CUSTOM_DOMAIN_INVALID | Hostname is invalid or already assigned. |
422 | CUSTOM_DOMAIN_REQUIRES_PUBLICATION | Publish the bucket before assigning a domain. |
503 | CUSTOM_DOMAINS_NOT_CONFIGURED | Deployment 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.