Export endpoints
Two endpoints for rendering published posts to PDF or DOCX. Both use the same converter and the same per-workspace branding — the difference is single-doc vs. batch.
Both endpoints accept either API-key or session auth.
POST /api/v1/posts/:postId/export/:format
Export a single post.
:postIdcan be either the post UUID (post_abc123) or the post's slug (checkout-api-reference). Slug lookups are scoped to the caller — a slug collision across workspaces is impossible.:formatmust bedocxorpdf. Anything else returns404 not_found.- Rate limit: 20 requests / minute.
Request body
Every field is optional.
{
"filename": "checkout-api",
"landscape": false,
"accent_color": "#3366ff",
"branding": {
"font_main": "Inter",
"logo_url": "https://cdn.acme.com/logo.png"
}
}
| Field | Type | Notes |
|---|---|---|
filename | string | Overrides the download filename. Defaults to the post's slug. |
landscape | boolean | Render in landscape orientation. Default false. |
accent_color | string | 6-digit hex, # optional. Overrides the workspace's configured brand color for this export. |
branding | object | Fine-grained branding overrides passed through to the converter (see below). |
Workspace defaults from
POST /api/v1/workspace/brand-color,
POST /api/v1/workspace/export-font,
and
POST /api/v1/workspace/export-footer
are applied first; anything in the request body wins on top. An empty
request body exports with the workspace's configured branding.
Success response
A binary body with these headers:
Content-Type: application/pdf # or application/vnd.openxmlformats-officedocument.wordprocessingml.document
Content-Disposition: attachment; filename="checkout-api.pdf"
X-Cache: HIT # or MISS
X-Cache is only set when the export cache is enabled on the server —
see below.
Errors
| Status | error | When |
|---|---|---|
404 | not_found | Post doesn't exist, isn't owned by the caller, or format wasn't pdf/docx |
400 | invalid_request | Body failed schema validation (e.g. bad hex in accent_color) |
429 | rate_limited | Exceeded 20/min |
502 | converter_failed | The converter microservice returned an error |
502 | converter_timeout | The converter didn't respond in time |
POST /api/v1/posts/batch/export
Export up to 10 posts in one request. Returns a zip containing every
successful export plus a manifest.json at the root describing every
entry (including failures).
- Max batch size: 10.
- Rate limit: 3 requests / minute.
- Posts may be referenced by UUID or slug, mixed within one call.
- Runs sequentially through the converter — one slow post doesn't let a single caller hog the converter's worker pool.
Request body
{
"postIds": ["checkout-api-reference", "post_abc123", "rollout-plan"],
"format": "pdf",
"landscape": false,
"accent_color": "#3366ff",
"branding": { "font_main": "Inter" }
}
| Field | Required | Notes |
|---|---|---|
postIds | yes | Array of post UUIDs or slugs, max 10 items. |
format | yes | "pdf" or "docx". |
landscape | no | Applied to every post in the batch. |
accent_color | no | Applied to every post in the batch. |
branding | no | Applied to every post in the batch. |
Success response
Status 200. A zip body with these headers:
Content-Type: application/zip
Content-Disposition: attachment; filename="opendocs-export-2026-04-17.zip"
X-Batch-Manifest: eyJmb3JtYXQiOiJwZGYiLCJyZXN1bHRzIjpbey...=
Inside the zip:
opendocs-export-2026-04-17.zip
├── manifest.json
├── checkout-api-reference.pdf
├── rollout-plan.pdf
└── …
manifest.json shape:
{
"format": "pdf",
"results": [
{
"postId": "post_abc123",
"success": true,
"slug": "checkout-api-reference",
"filename": "checkout-api-reference.pdf",
"bytes": 84213,
"cached": false
},
{
"postId": "rollout-plan",
"success": false,
"error": "not_found",
"message": "Post not found or not owned by user."
}
],
"summary": {
"total": 3,
"succeeded": 2,
"failed": 1,
"cacheHits": 1,
"cacheMisses": 1
}
}
cached(per result) —trueif the file came from the export cache without re-running the converter.cacheHits/cacheMissesinsummary— only present when the server has the export cache enabled.
The X-Batch-Manifest header is a base64-encoded copy of that same
JSON. The CLI reads it before opening the zip so it can print per-doc
progress without decompressing first.
Partial-failure semantics
Batch export never aborts on a single failing post. Every input row is
processed and every result shows up in the manifest — success: false
entries include a short error code and message. The response is always
200 even when some posts failed; inspect the manifest to decide what
to retry.
Known per-doc error codes:
error | Meaning |
|---|---|
not_found | Post not owned by the caller or doesn't exist |
converter_failed | Converter returned a non-2xx |
converter_timeout | Converter didn't respond in time |
export_failed | Any other unexpected failure |
Top-level errors
| Status | error | When |
|---|---|---|
400 | invalid_request | More than 10 postIds, bad format, etc. |
429 | rate_limited | Exceeded 3/min |
413 | payload_too_large | Batch body exceeded the body-size limit |
Caching
Both endpoints are backed by a content-addressed cache. The cache key covers everything that affects converter output — markdown body, workspace, format, landscape, resolved branding, and accent color — so the same inputs always return the same bytes.
- Cache hits don't bill converter time and are dramatically faster.
- Cache writes are fire-and-forget; a failed write is just a next-call re-render.
X-Cache: HIT|MISSis only set when the cache is enabled. If absent, assume the export was freshly rendered.
Rate-limit buckets
| Endpoint | Bucket | Limit |
|---|---|---|
POST /posts/:postId/export/:format | export | 20 / min |
POST /posts/batch/export | batch-export | 3 / min |
See Errors and pagination → Rate limits for the full list.
Related pages
- Posts endpoints — list, get, raw
- Post mutation endpoints — update, unpublish, visibility, tags
- Workspaces API — where export branding defaults live