Skip to main content

Errors and pagination

Common error shape

Most OpenDocs endpoints return JSON in one of these two forms.

The full shape, for failures that have something meaningful to say:

{
"error": "publish_failed",
"message": "Workspace not found."
}

The minimal shape, for failures where the code alone is self-explanatory:

{
"error": "unauthorized"
}

Structured-failure endpoints (like document_limit) add extra fields on top of this base shape — see the per-code table below.

Error codes

StatusError codeWhere it firesMeaning
400invalid_requestAny route with body validationRequest body failed Zod validation (missing fields, wrong types, unknown visibility values, etc.)
400publish_failedPOST /publish, POST /publish/batchBusiness rule failure during publish (workspace not found, slug collision that couldn't be resolved, markdown too large, etc.)
400update_failedPOST /posts/:id/updateUpdate succeeded past validation but failed at the service layer
400unpublish_failedPOST /posts/:id/unpublishUnpublish couldn't complete — usually because the post was already unpublished
400visibility_change_failedPOST /posts/:id/visibilityVisibility change rejected (typically: visibility: "public" without confirmPublic: true)
400invalid_visibilityPOST /posts/:id/visibilityvisibility was not one of private, workspace, public
400tag_update_failedPATCH /posts/:id/tagsTag mutation failed (e.g., exceeding the 20-tag cap)
400onboarding_failedPOST /onboardingInvalid/reserved username or workspace name couldn't be allocated
400missing_workspace_idWorkspace + team GET routesworkspaceId query param is absent. Every workspace-scoped route requires the caller to name the workspace explicitly.
401unauthorizedAll protected routesMissing, invalid, expired, or revoked API key / session
403forbiddenWorkspace, team, storage, api-key routesCaller is not an active member of workspaceId — or is a member but not an admin/owner where the action requires it
403document_limitPOST /publish, POST /publish/batchPublishing this document would push the workspace over its plan's document cap
403plan_limitPOST /workspacesUser is at the workspace cap for their account plan
403member_limitPOST /workspace/team/inviteFree/Pro workspace is at its member cap
402billing_failedPOST /workspace/team/accept-inviteTeam invite acceptance needed a Paddle seat increase, but billing could not be updated
404not_foundPost routes, api-key deleteTarget resource doesn't exist or isn't visible to the caller
409active_planDELETE /accountAccount deletion is blocked by an active subscription
409team_workspace_with_membersDELETE /accountAccount deletion is blocked by an owned Team workspace with other active members
413file_too_largeAvatar / workspace-logo uploadsUpload exceeded the per-file size cap
429rate_limitedAll routes (via middleware)Rate-limit bucket exhausted for this route group
502export_failedPOST /posts/:id/export/:format, POST /posts/batch/exportUpstream converter returned a non-2xx response
503storage_not_configuredUpload endpointsS3/object-storage env vars aren't set on the server

The document_limit error

This is the structured 403 emitted when a publish would exceed the workspace's document cap (Free: 10 docs per workspace). The body always includes plan, limit, used, and a human-readable message:

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

The batch variant (POST /publish/batch) adds requested and workspaceId so you can build a precise error message even when a batch spans multiple workspaces:

{
"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."
}

Batch pre-check is atomic: if the cap would be exceeded, no documents are written — you get one 403 and the whole batch is rejected. See Plans and limits for the full story.

Rate limits

The API enforces rate limits per route group:

Route groupLimit
General API traffic120 requests / minute
POST /publish30 requests / minute
POST /publish/batch5 requests / minute
POST /posts/:id/export/:format20 requests / minute
POST /posts/batch/export3 requests / minute

When a bucket is exhausted:

{
"error": "rate_limited",
"message": "Too many requests. Try again later."
}

Back off for at least the bucket window (usually one minute) before retrying.

Pagination

List endpoints (currently: GET /api/v1/posts) return a pagination block:

{
"posts": [ /* ... */ ],
"pagination": {
"page": 1,
"limit": 20,
"total": 42,
"totalPages": 3
}
}

Defaults:

  • page defaults to 1
  • limit defaults to 20, max 100

Practical guidance

  • Use modest limit values unless you truly need bigger pages.
  • Prefer --json in the CLI when a script or agent needs to read pagination data.
  • Call GET /api/v1/posts before POST /posts/:id/update when your automation needs to discover a postId.
  • On 403 document_limit: surface the message field directly — it's written for end users.