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 suffix | Role | Purpose |
|---|---|---|
GET / | Viewer or higher | Read company details, publication state, hosting readiness, and apps |
PUT / | Admin or owner | Update displayName, slug, or setupDismissed |
GET /slug-available?slug=acme | Viewer or higher | Check a company address |
PUT /apps/:appId | Admin or owner | Set an app's slug and enabled state |
POST /publish | Admin or owner | Publish the company portal |
POST /unpublish | Admin or owner | Take it offline while retaining records |
GET /preview/:appId | Viewer or higher | Private 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.
| Endpoint | Body or credential | Purpose |
|---|---|---|
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/session | Portal session header | Read the session, or null if none exists |
DELETE /v1/portal/:portalSlug/session | Portal session header | Sign 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 suffix | Authentication | Purpose |
|---|---|---|
GET /requests | Public | List published, non-hidden requests |
GET /requests/counts | Public | Counts across public request statuses |
GET /requests/:requestId | Public | Read a public request |
GET /votes?ids=UUID,UUID | Portal session | Get the visitor's voted IDs; up to 100 |
POST /requests | Portal session | Submit an idea |
POST /requests/:requestId/vote | Portal session | Vote 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.