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
| Field | Type | Required | Notes |
|---|---|---|---|
markdown | string | Yes | The raw Markdown content |
title | string | No | Overrides the inferred title |
slug | string | No | Preferred URL slug; a numeric suffix is appended on collision |
visibility | string | No | private, workspace, or public; defaults to workspace |
workspaceId | string | Yes | Must be a workspace the caller belongs to |
projectId | string | No | Project to publish into. Omit to use the workspace Default project. |
projectSlug | string | No | Alternative to projectId; resolved inside workspaceId. |
tags | string[] | No | Up to 20 tags; normalized to lowercase |
sourcePath | string | No | Source-file hint (e.g., docs/launch.md) — stored alongside the post for later reference |
confirmPublic | boolean | No | Required 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
| Status | error | When |
|---|---|---|
400 | invalid_request | Body failed validation (missing markdown, bad visibility, etc.) |
400 | publish_failed | Business-rule failure (workspace not found, slug couldn't be resolved, markdown too large) |
401 | unauthorized | Missing/invalid API key |
403 | document_limit | Would exceed the workspace's plan cap — see below |
429 | rate_limited | 30/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) — returns207 Multi-Statuswith 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
resultframe per document, emitted as soon as that document finishes. - One final
summaryframe after the last result. - The response carries
Cache-Control: no-cacheandX-Accel-Buffering: noso nginx / Caddy don't buffer the stream.
Errors
| Status | error | When |
|---|---|---|
400 | invalid_request | documents empty, >10 entries, or any entry fails validation |
401 | unauthorized | Missing/invalid API key |
403 | document_limit | Atomic pre-check would exceed a workspace's cap — see above |
413 | — | Body exceeds the 32 MB route-specific limit (Express returns its own shape) |
429 | rate_limited | 5/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.
Related pages
- GET /api/v1/me — the live
quota.documentsfield for pre-checks - Errors and pagination — full error catalogue and rate-limit buckets
- Plans and limits — where the document cap comes from