Skip to main content

Publish endpoints

Two endpoints cover every publish path:

  • POST /api/v1/publish — publish one document.
  • POST /api/v1/publish/batch — publish up to 25 documents in one call, with optional SSE progress streaming.

Both return 403 document_limit if the workspace is at its plan's document cap (10 on Free; unlimited on Pro/Team). See Plans and limits for how that works.


POST /api/v1/publish

Publish a Markdown file and receive a stable URL.

Auth

API key or session auth.

Rate limit

30 requests per minute.

Request body

{
"markdown": "# Launch note\n\nShip it.",
"title": "Launch note",
"slug": "launch-note",
"visibility": "workspace",
"workspaceId": "ws_123",
"projectId": "proj_123",
"tags": ["launch", "release"],
"sourcePath": "launch-note.md",
"confirmPublic": false
}

Fields

FieldTypeRequiredNotes
markdownstringYesThe raw Markdown content
titlestringNoOverrides the inferred title
slugstringNoPreferred URL slug; a numeric suffix is appended on collision
visibilitystringNoprivate, workspace, or public; defaults to workspace
workspaceIdstringYesMust be a workspace the caller belongs to
projectIdstringNoProject to publish into. Omit to use the workspace Default project.
projectSlugstringNoAlternative to projectId; resolved inside workspaceId.
tagsstring[]NoUp to 20 tags; normalized to lowercase
sourcePathstringNoSource-file hint (e.g., docs/launch.md) — stored alongside the post for later reference
confirmPublicbooleanNoRequired when visibility is public — the API rejects public publishes without this flag

Example request

curl https://api.opendocs.cc/api/v1/publish \
-X POST \
-H "Authorization: Bearer od_live_xxxxx" \
-H "Content-Type: application/json" \
-d '{
"markdown": "# Checkout API reference\n\nEndpoint summary.",
"visibility": "workspace",
"workspaceId": "ws_123",
"tags": ["api", "checkout"]
}'

Success response

{
"postId": "post_123",
"slug": "checkout-api-reference",
"url": "/acme/checkout-api-reference",
"title": "Checkout API reference",
"projectId": "proj_default",
"projectSlug": "default",
"projectName": "Default",
"isDefaultProject": true,
"tags": ["api", "checkout"]
}

Returns 201 Created.

Default project URLs are workspace-slug prefixed (/<workspace-slug>/<post-slug>). Non-default project URLs include the project: /<workspace-slug>/projects/<project-slug>/<post-slug>. Prepend https://opendocs.cc to turn either into an absolute URL.

Public publishing

This fails with 400 invalid_request:

{ "visibility": "public", "confirmPublic": false }

Use:

{ "visibility": "public", "confirmPublic": true }

Errors

StatuserrorWhen
400invalid_requestBody failed validation (missing markdown, bad visibility, etc.)
400publish_failedBusiness-rule failure (workspace not found, slug couldn't be resolved, markdown too large)
401unauthorizedMissing/invalid API key
403document_limitWould exceed the workspace's plan cap — see below
429rate_limited30/min bucket exhausted

The document_limit body:

{
"error": "document_limit",
"plan": "free",
"limit": 10,
"used": 10,
"message": "Your free plan allows 10 published documents. You currently have 10."
}

POST /api/v1/publish/batch

Publish up to 25 documents in one call. Two response modes based on the Accept header:

  • application/json (default) — returns 207 Multi-Status with per-doc results.
  • text/event-stream — streams Server-Sent Events so the CLI can show live per-doc progress.

Auth

API key or session auth.

Rate limit

5 requests per minute (stricter than single publish — each call is up to 25 documents of work).

Limits

  • Max 25 documents per request.
  • Max body size 32 MB (room for 25 × ~1 MB markdown + overhead).
  • Documents are processed sequentially in list order. Each publish is retried once with a 100 ms backoff before being recorded as a failure.

Request body

{
"documents": [
{
"title": "Batch Doc One",
"markdown": "# One\n\nBody",
"workspaceId": "ws_123",
"visibility": "workspace",
"tags": ["batch"]
},
{
"title": "Batch Doc Two",
"markdown": "# Two\n\nBody",
"workspaceId": "ws_123",
"visibility": "workspace"
}
]
}

Each element uses the same shape as POST /api/v1/publish's request body.

Atomic document-limit pre-check

Before any document is written, the server computes the batch's per-workspace delta and checks it against each workspace's cap. If any workspace would be pushed over, the entire batch is rejected up front:

{
"error": "document_limit",
"plan": "free",
"limit": 10,
"used": 9,
"requested": 3,
"workspaceId": "ws_123",
"message": "This batch would publish 3 documents, but your free plan allows 10 total. You have 1 slot left."
}

Returns 403. No documents from the batch are created — partial publishes are considered worse than a clean "no."

JSON response (default)

{
"summary": { "total": 2, "succeeded": 2, "failed": 0 },
"results": [
{
"success": true,
"index": 0,
"postId": "post_abc",
"slug": "batch-doc-one",
"title": "Batch Doc One",
"url": "/acme/batch-doc-one"
},
{
"success": true,
"index": 1,
"postId": "post_def",
"slug": "batch-doc-two",
"title": "Batch Doc Two",
"url": "/acme/batch-doc-two"
}
]
}

Returns 207 Multi-Status.

Partial-success results preserve the input index so the caller can align error messages back to the original document:

{
"success": false,
"index": 1,
"error": "publish_failed",
"message": "Workspace not found."
}

SSE response (Accept: text/event-stream)

curl -N https://api.opendocs.cc/api/v1/publish/batch \
-X POST \
-H "Authorization: Bearer od_live_xxxxx" \
-H "Content-Type: application/json" \
-H "Accept: text/event-stream" \
-d @batch.json

Frames:

event: result
data: {"success":true,"index":0,"postId":"post_abc","slug":"batch-doc-one", ...}

event: result
data: {"success":true,"index":1,"postId":"post_def","slug":"batch-doc-two", ...}

event: summary
data: {"total":2,"succeeded":2,"failed":0}
  • One result frame per document, emitted as soon as that document finishes.
  • One final summary frame after the last result.
  • The response carries Cache-Control: no-cache and X-Accel-Buffering: no so nginx / Caddy don't buffer the stream.

Errors

StatuserrorWhen
400invalid_requestdocuments empty, >10 entries, or any entry fails validation
401unauthorizedMissing/invalid API key
403document_limitAtomic pre-check would exceed a workspace's cap — see above
413Body exceeds the 32 MB route-specific limit (Express returns its own shape)
429rate_limited5/min bucket exhausted

Per-document failures inside a successful 207 response do not use a non-200 status — they're reported as { success: false, ... } entries inside results.