Portal content API
Manage help articles, collections, changelog entries, and team roadmap items for an app. These are Customer Portal resources, separate from Engage campaigns and SDK ingest.
Dashboard routes require a WorkOS Bearer access token. Viewers can read; editors, admins, and owners can create, edit, and publish. Portal content must be enabled in the environment. Publication also requires a published company portal, an included app, and ready hosting.
Documents
Use one of these bases:
- Help articles:
/v1/apps/:appId/help/articles - Changelog entries:
/v1/apps/:appId/changelog
| Method and suffix | Purpose |
|---|---|
GET / | List documents |
POST / | Create a private draft |
GET /:documentId | Read the draft, published snapshot, revision, and publication readiness |
PUT /:documentId | Save { revision, draft } |
POST /:documentId/lifecycle | Apply { revision, action } |
POST /:documentId/position | Reorder a help article using { revision, direction } |
For / rows, use the base itself. position is available for articles only. List filters include q, state (draft, published, archived), collectionId, category, cursor, and limit (default 20, maximum 50). Paginated results contain items and nextCursor; pass the returned cursor back with the same filters.
Creation accepts a UUID idempotencyKey and optional title, collectionId, and links. Reuse the creation key for a retry of the same operation.
POST /v1/apps/APP_ID/changelog
Authorization: Bearer WORKOS_ACCESS_TOKEN
Content-Type: application/json
{
"idempotencyKey": "d0948d6e-65d2-4db1-9dba-e46670a4d8ef",
"title": "Offline maps are ready",
"links": []
}Draft format and publication
draft is a complete structured object with title, summary, body, collectionId, category, releaseDate, version, and links. Both document kinds use this schema. Start from the object returned by GET and retain its required fields when saving.
The body is a validated content tree rooted at type: doc, rather than HTML or a Markdown string. It supports paragraphs, headings at levels 2 and 3, lists, quotes, code blocks, text marks, links, and app-scoped images.
{
"type": "doc",
"content": [
{
"type": "paragraph",
"content": [{ "type": "text", "text": "Save your route before leaving Wi-Fi." }]
}
]
}links uses { kind: "request" | "roadmap", id: UUID } records from the same app. Articles need an active collection before publication. Changelog entries use collectionId: null, a category of new, improved, or fixed, and a YYYY-MM-DD release date no later than today. Future scheduled publication is unavailable.
Lifecycle actions are publish, unpublish, archive, and restore. Publishing copies the saved draft into the public snapshot. Later saves stay private until another publish. Restore returns archived content to a draft, and its stable public slug is retained.
Every save or lifecycle mutation uses the current revision. Use the revision returned by the last successful operation. On a conflict, fetch the current document, reconcile your edits, and submit its latest revision.
Help collections
Base: /v1/apps/:appId/help/collections.
| Method and suffix | Body or behavior |
|---|---|
GET / | Paginated list using the content query parameters |
POST / | { title, description, idempotencyKey } |
PATCH /:collectionId | { revision, title?, description?, archived? } |
POST /:collectionId/position | { revision, direction }, where direction is up or down |
Collections have revisions and immediate metadata updates. They do not use document publication actions. Archiving requires moving or archiving their active articles first. Public collection reads only expose non-archived collections with published articles.
Roadmap items
Base: /v1/apps/:appId/roadmap.
| Method and suffix | Body or behavior |
|---|---|
GET / | Required status; optional cursor, limit, archived |
POST / | { title, description, status, requestIds, idempotencyKey } |
GET /:itemId | Read one team item with its linked requests and revision |
PUT /:itemId | { revision, title, description, requestIds } |
POST /:itemId/lifecycle | { revision, action } using document lifecycle actions |
POST /:itemId/status | Move the item and explicitly selected requests |
Statuses are planned, in_progress, and shipped. A list can contain both request cards and team-item cards; inspect each card's kind. Team items have their own UUIDs and are not request IDs.
The status body is { revision, status, requests: [{ id, updatedAt }] }. Only those selected linked requests change status; include their current updatedAt values. Use requests: [] to move only the team item. Hidden requests cannot move this way. Request publication stays independent. Existing follower rules queue in-app SDK instructions; this operation does not send automatic native push.
Unlike documents, saving changes to an already published team item updates its live content immediately.
Related records and images
GET /v1/apps/:appId/portal-content/link-targets finds records to link. Supply kind (request or roadmap), optional q, ids (comma-separated UUIDs), cursor, and limit (maximum 100).
Upload images with POST /v1/apps/:appId/portal-content/assets/:kind/:documentId, where kind is article or changelog. Send multipart form data with a file part and UUID Idempotency-Key header. The document must belong to this app. Use the returned asset ID in an image content node. Read private images at GET /v1/apps/:appId/portal-content/assets/:assetId with dashboard authentication.
Public content
Public prefix: /v1/portal/:portalSlug/apps/:appSlug. No visitor session is needed to read published content.
GET /v1/portal/:portalSlug/apps/:appSlug/help/collections
GET /v1/portal/:portalSlug/apps/:appSlug/help/collections/:collectionId
GET /v1/portal/:portalSlug/apps/:appSlug/help/articles
GET /v1/portal/:portalSlug/apps/:appSlug/help/articles/:documentId
GET /v1/portal/:portalSlug/apps/:appSlug/changelog
GET /v1/portal/:portalSlug/apps/:appSlug/changelog/:documentId
GET /v1/portal/:portalSlug/apps/:appSlug/roadmap?status=planned
GET /v1/portal/:portalSlug/apps/:appSlug/roadmap/:itemId
GET /v1/portal/:portalSlug/apps/:appSlug/requests/:requestId/changelog
GET /v1/portal/:portalSlug/apps/:appSlug/roadmap/:itemId/changelog
GET /v1/portal/:portalSlug/apps/:appSlug/assets/:assetIdArticle and changelog lists support q, collection/category filters, and cursor pagination. The roadmap requires a column status. Related-changelog lists accept cursor and limit. Public reads exclude drafts, archived records, and private links; media is served only when eligible for public access.
Canonical schemas are in packages/sdk-core/src/schemas/portal-content.ts; routes are in apps/api/src/routes/portal-content.ts.