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.