Portal support API
Support conversations are private app-scoped records. Dashboard staff use a WorkOS Bearer access token; customers use an email-verified Portal session. These endpoints do not use SDK write keys or subject tokens.
Support must be available in the environment. Customer access requires a published company portal and an included app; creating a new conversation additionally requires support to be enabled for that app. Disabling new support messages retains existing conversations and visitor replies. Read the support guide for setup and notification behavior.
Dashboard endpoints
Prefix: /v1/apps/:appId/support.
| Method and suffix | Role | Purpose |
|---|---|---|
GET /tickets | Viewer or higher | List conversations; optional status, cursor, limit |
GET /tickets/:ticketId | Viewer or higher | Read a conversation |
GET /tickets/:ticketId/messages | Viewer or higher | Paginated messages |
POST /tickets/:ticketId/messages | Editor or higher | Send { body, idempotencyKey } |
PATCH /tickets/:ticketId | Editor or higher | Update { status, expectedVersion } |
GET /settings | Viewer or higher | Read enabled state and notification recipient |
PUT /settings | Admin or owner | Save { enabled, notificationUserId } |
GET /summary | Viewer or higher | Read the open-conversation count |
POST /notifications/:notificationId/retry | Editor or higher | Retry an eligible failed notification |
Statuses are open, waiting_on_customer, and closed. A customer message sets the ticket to Open; a staff reply sets it to Waiting on customer. Status updates require the latest expectedVersion; refresh the ticket after a version conflict.
Enabling support requires an eligible notification recipient in the workspace. Notification state and retry eligibility are separate from whether a reply was saved.
Customer endpoints
Prefix: /v1/portal/:portalSlug/apps/:appSlug/support. All customer endpoints require Authorization: Portal <sessionToken> from the portal email verification flow.
| Method and suffix | Purpose |
|---|---|
GET /tickets | List only this visitor's conversations |
POST /tickets | Create { subject, body, idempotencyKey } |
GET /tickets/:ticketId | Read an owned conversation |
GET /tickets/:ticketId/messages | Read an owned conversation's messages |
POST /tickets/:ticketId/messages | Send { body, idempotencyKey } in an owned conversation |
Subjects allow 1–150 characters and message bodies 1–3,000 characters after trimming. Messages are plain text. Creation and sending use UUID idempotency keys; reuse the key when retrying the same operation.
POST /v1/portal/acme/apps/mobile/support/tickets
Authorization: Portal PORTAL_SESSION_TOKEN
Content-Type: application/json
{
"subject": "Help downloading a route",
"body": "My download stops before it completes. What should I check?",
"idempotencyKey": "d0948d6e-65d2-4db1-9dba-e46670a4d8ef"
}Customers cannot list or read another visitor's tickets, and have no ticket-status update endpoint. List endpoints accept cursor and limit (default 20, maximum 100). Use nextCursor from the response with unchanged filters to continue.
Canonical schemas are in packages/sdk-core/src/schemas/portal-support.ts; routes are in apps/api/src/routes/portal.ts.