Customer Portal API

The Customer Portal has workspace-level settings, app-specific public content, and its own visitor authentication. Dashboard operators manage it with a WorkOS access token; public readers and email-verified visitors use a separate surface.

Responses use the standard data envelope. UUIDs identify workspace and app records in dashboard routes. Public routes use the company portalSlug and included app's appSlug; use the URLs returned by the API rather than constructing them from display names.

Workspace settings

Prefix: /v1/workspaces/:wid/portal.

Method and suffixRolePurpose
GET /Viewer or higherRead company details, publication state, hosting readiness, and apps
PUT /Admin or ownerUpdate displayName, slug, or setupDismissed
GET /slug-available?slug=acmeViewer or higherCheck a company address
PUT /apps/:appIdAdmin or ownerSet an app's slug and enabled state
POST /publishAdmin or ownerPublish the company portal
POST /unpublishAdmin or ownerTake it offline while retaining records
GET /preview/:appIdViewer or higherPrivate request preview with request-list query parameters

For the / rows, use the prefix itself without an extra suffix.

PUT /v1/workspaces/WORKSPACE_ID/portal
Authorization: Bearer WORKOS_ACCESS_TOKEN
Content-Type: application/json
 
{ "displayName": "Acme", "slug": "acme" }
PUT /v1/workspaces/WORKSPACE_ID/portal/apps/APP_ID
Authorization: Bearer WORKOS_ACCESS_TOKEN
Content-Type: application/json
 
{ "slug": "mobile", "enabled": true }

Slugs use 3–48 lowercase letters, digits, or interior hyphens and cannot be reserved names. Company and app addresses lock after their first publication. Publishing requires ready hosting and at least one included app. An included app's content still needs its own publication decision.

Per-app appearance is stored through the app's request settings and brand configuration, rather than the company settings payload. See Setup & branding.

Public directory

GET /v1/portal/:portalSlug returns the published company's included apps, branding, section URLs, and content/support availability. A draft portal or excluded app is not a public resource.

Use the public app prefix /v1/portal/:portalSlug/apps/:appSlug for requests, roadmap, help, changelog, and support. Portal content documents content routes; Portal support documents private conversations.

Visitor authentication

Portal visitor sessions use Authorization: Portal <sessionToken>, rather than a Bearer write key or dashboard access token.

EndpointBody or credentialPurpose
POST /v1/portal/:portalSlug/auth/start{ "email": "[email protected]" }Send an email verification code
POST /v1/portal/:portalSlug/auth/verify{ "email": "[email protected]", "code": "123456" }Verify the six-digit code and obtain a session
GET /v1/portal/:portalSlug/sessionPortal session headerRead the session, or null if none exists
DELETE /v1/portal/:portalSlug/sessionPortal session headerSign out

Sessions belong to the company portal. Participation creates a separate portal-origin identity per app. It does not automatically identify an SDK user or grant workspace membership. Do not forward a portal session to another company's portal.

Public requests

Prefix: /v1/portal/:portalSlug/apps/:appSlug.

Method and suffixAuthenticationPurpose
GET /requestsPublicList published, non-hidden requests
GET /requests/countsPublicCounts across public request statuses
GET /requests/:requestIdPublicRead a public request
GET /votes?ids=UUID,UUIDPortal sessionGet the visitor's voted IDs; up to 100
POST /requestsPortal sessionSubmit an idea
POST /requests/:requestId/votePortal sessionVote or remove a vote

List parameters: q, one status, sort (top, newest, or status_changed), page (default 1), and limit (default 20, maximum 100). The data contains items, total, page, and hasMore.

Submission requires title (1–120 characters), description (1–1,500), a UUID idempotencyKey, and feedbackConsent: true. Vote requests require vote and feedbackConsent: true. Reuse the same idempotency key when retrying the same submission.

POST /v1/portal/acme/apps/mobile/requests
Authorization: Portal PORTAL_SESSION_TOKEN
Content-Type: application/json
 
{
  "title": "Offline maps",
  "description": "Let me save a route before leaving Wi-Fi.",
  "idempotencyKey": "d0948d6e-65d2-4db1-9dba-e46670a4d8ef",
  "feedbackConsent": true
}

The current portal participant API exposes submission and voting. SDK comment and follow methods are documented separately in Feature requests API; they are not portal visitor endpoints.

Request publication

Editors or higher can call:

PATCH /v1/apps/APP_ID/requests/portal-visibility
Authorization: Bearer WORKOS_ACCESS_TOKEN
Content-Type: application/json
 
{ "ids": ["d0948d6e-65d2-4db1-9dba-e46670a4d8ef"], "visible": true }

Send up to 100 request UUIDs; visible: false removes them from the portal. Global moderation still applies. SDK visibility and portal visibility are independent, and publication does not expose private comments or participant identities.

Errors and availability

These routes enforce membership or visitor authentication, app scoping, publication state, and rate limits. Private resources are unavailable through public reads. Stale addresses or attempted changes to locked addresses can return a conflict. Read the server's error message and refresh settings before retrying. See API errors.

Canonical request definitions are in packages/sdk-core/src/schemas/portal.ts; routes are in apps/api/src/routes/portal.ts.