Skip to main content

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.

  • :postId can 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.
  • :format must be docx or pdf. Anything else returns 404 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"
}
}
FieldTypeNotes
filenamestringOverrides the download filename. Defaults to the post's slug.
landscapebooleanRender in landscape orientation. Default false.
accent_colorstring6-digit hex, # optional. Overrides the workspace's configured brand color for this export.
brandingobjectFine-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

StatuserrorWhen
404not_foundPost doesn't exist, isn't owned by the caller, or format wasn't pdf/docx
400invalid_requestBody failed schema validation (e.g. bad hex in accent_color)
429rate_limitedExceeded 20/min
502converter_failedThe converter microservice returned an error
502converter_timeoutThe 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" }
}
FieldRequiredNotes
postIdsyesArray of post UUIDs or slugs, max 10 items.
formatyes"pdf" or "docx".
landscapenoApplied to every post in the batch.
accent_colornoApplied to every post in the batch.
brandingnoApplied 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) — true if the file came from the export cache without re-running the converter.
  • cacheHits / cacheMisses in summary — 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:

errorMeaning
not_foundPost not owned by the caller or doesn't exist
converter_failedConverter returned a non-2xx
converter_timeoutConverter didn't respond in time
export_failedAny other unexpected failure

Top-level errors

StatuserrorWhen
400invalid_requestMore than 10 postIds, bad format, etc.
429rate_limitedExceeded 3/min
413payload_too_largeBatch 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|MISS is only set when the cache is enabled. If absent, assume the export was freshly rendered.

Rate-limit buckets

EndpointBucketLimit
POST /posts/:postId/export/:formatexport20 / min
POST /posts/batch/exportbatch-export3 / min

See Errors and pagination → Rate limits for the full list.