Skip to content
UpVoiced Docs

API reference

Public portal API

Unauthenticated reads of public boards, posts, the roadmap and the changelog, plus end-user sign-in and changelog subscriptions. What your portal uses.

Set up the SDK client used in the examples

None (public):

import { UpVoiced } from '@upvoiced/sdk';

// Public reads need no credentials: only your workspace slug.
const upvoiced = new UpVoiced({ workspace: 'acme' });

curl examples read $UPVOICED_API_KEY and $UPVOICED_WORKSPACE_IDfrom your shell. MCP examples are the tools/call request an MCP client sends to the MCP server with the same API key.

GET/v1/public/unsubscribe

Unsubscribe confirmation page for a signed email link (no state change)

Authentication
None (public)
Operation id
unsubscribePage
Query parameters
NameTypeRequired
tokenstringlength ≤ 1024No

Response 200: string (text/html)

Errors: 400, 500, 503, all in the standard error shape.

curl
curl 'https://api.upvoiced.com/v1/public/unsubscribe'
TypeScript SDK
const data = await upvoiced.get('/v1/public/unsubscribe');
MCP

No MCP tool for this operation. The MCP server covers the everyday reads and writes.

POST/v1/public/unsubscribe

One-click unsubscribe (RFC 8058 List-Unsubscribe-Post) for a signed email link

Authentication
None (public)
Operation id
unsubscribeOneClick
Query parameters
NameTypeRequired
tokenstringlength ≤ 1024No

Response 200: string (text/html)

Errors: 400, 500, 503, all in the standard error shape.

curl
curl -X POST 'https://api.upvoiced.com/v1/public/unsubscribe'
TypeScript SDK
const data = await upvoiced.post('/v1/public/unsubscribe');
MCP

No MCP tool for this operation. The MCP server covers the everyday reads and writes.

GET/v1/public/subscriptions/confirm

Changelog subscription confirmation page for a double opt-in link (no state change)

Authentication
None (public)
Operation id
changelogConfirmPage
Query parameters
NameTypeRequired
tokenstringlength ≤ 1024No

Response 200: string (text/html)

Errors: 400, 500, all in the standard error shape.

curl
curl 'https://api.upvoiced.com/v1/public/subscriptions/confirm'
TypeScript SDK
const data = await upvoiced.get('/v1/public/subscriptions/confirm');
MCP

No MCP tool for this operation. The MCP server covers the everyday reads and writes.

POST/v1/public/subscriptions/confirm

Confirm a changelog email subscription (double opt-in, single use, 48 h)

Authentication
None (public)
Operation id
confirmChangelogSubscription
Query parameters
NameTypeRequired
tokenstringlength ≤ 1024No

Response 200: string (text/html)

Errors: 400, 500, all in the standard error shape.

curl
curl -X POST 'https://api.upvoiced.com/v1/public/subscriptions/confirm'
TypeScript SDK
const data = await upvoiced.post('/v1/public/subscriptions/confirm');
MCP

No MCP tool for this operation. The MCP server covers the everyday reads and writes.

GET/v1/public/workspace

Public branding, public boards, statuses, enabled portal modules and widget tabs/launcher (cacheable)

Authentication
None (public)
Operation id
getPublicWorkspace
Query parameters
NameTypeRequired
workspacestringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$No
Headers
NameTypeRequired
x-workspacestringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$No
Response 200: 52 fields
Response 200 (application/json)
NameTypeRequired
workspaceobjectYes
workspace.namestringlength 1–100Yes
workspace.slugstringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$Yes
workspace.portal_locale"en" | "pt-BR" | "es" | "de" | "fr"Yes
brandingobjectYes
branding.preset"ink" | "ocean" | "forest" | "citrus" | "plum" | "mono"Yes
branding.accentstringpattern ^#[0-9a-fA-F]{6}$Yes
branding.scheme"light" | "dark" | "auto"Yes
branding.font"system" | "source-sans" | "inter" | "geist" | "manrope" | "source-serif" | "fraunces" | "ibm-plex" | "jetbrains-mono"Yes
branding.radius"none" | "sm" | "md" | "lg" | "full"Yes
branding.density"comfortable" | "compact"Yes
branding.surface"flat" | "elevated"Yes
branding.show_powered_bybooleanYes
branding.layoutobjectYes
branding.layout.portal_navarray of "board" | "roadmap" | "changelog" | "wall"items ≤ 4Yes
branding.layout.portal_header"logo-left" | "centered" | "minimal"Yes
branding.layout.post_card"compact" | "comfortable"Yes
branding.layout.roadmap_card"title-only" | "with-excerpt"Yes
branding.layout.changelog"timeline" | "cards"Yes
branding.layout.wall_default"grid" | "masonry" | "carousel" | "list" | "spotlight"Yes
branding.layout.widget_launcher"icon" | "pill" | "tab"Yes
branding.layout.widget_position"bottom-right" | "bottom-left" | "right-middle" | "left-middle"Yes
branding.logoobject | nullYes
branding.logo.idstring (uuid)Yes
branding.logo.urlstring (uri)Yes
branding.logo.mime"image/png" | "image/svg+xml" | "image/jpeg" | "image/webp"Yes
branding.logo.bytesintegervalue ≤ 2097152value > 0Yes
branding.faviconobject | nullYes
branding.favicon.idstring (uuid)Yes
branding.favicon.urlstring (uri)Yes
branding.favicon.mime"image/png" | "image/svg+xml" | "image/jpeg" | "image/webp"Yes
branding.favicon.bytesintegervalue ≤ 2097152value > 0Yes
boardsarray of objectYes
boards[].idstring (uuid)Yes
boards[].namestringlength 1–100Yes
boards[].slugstringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$Yes
boards[].descriptionstringlength ≤ 2000Yes
boards[].visibility"public"Yes
statusesarray of objectYes
statuses[].idstring (uuid)Yes
statuses[].namestringlength 1–100Yes
statuses[].colorstringpattern ^#[0-9a-fA-F]{6}$Yes
statuses[].category"open" | "active" | "done" | "closed"Yes
statuses[].shown_on_roadmapbooleanYes
featuresobjectYes
features.modulesarray of "board" | "roadmap" | "changelog" | "wall"Yes
widgetobjectYes
widget.tabsarray of "feedback" | "roadmap" | "whats-new"items ≤ 3Yes
widget.launcher"icon" | "pill" | "tab"Yes
widget.position"bottom-right" | "bottom-left" | "right-middle" | "left-middle"Yes
custom_cssstring | nulllength 1–65536No
custom_domainstring | nulllength 4–253pattern ^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+(?:[a-z]{2,63}|xn--[a-z0-9-]{1,59})$No

Errors: 400, 404, 429, 500, 503, all in the standard error shape.

curl
curl 'https://api.upvoiced.com/v1/public/workspace' \
  -H "X-Workspace: acme"
TypeScript SDK
const data = await upvoiced.get('/v1/public/workspace');
MCP

No MCP tool for this operation. The MCP server covers the everyday reads and writes.

GET/v1/public/posts/similar

Duplicate hints while typing: up to 5 approved posts of public boards, no author data (cacheable)

Authentication
None (public)
Operation id
listPublicSimilarPosts
Query parameters
NameTypeRequired
workspacestringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$No
qstringlength ≤ 200Yes
boardstringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$No
Headers
NameTypeRequired
x-workspacestringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$No
Response 200: 12 fields
Response 200 (application/json)
NameTypeRequired
itemsarray of objectitems ≤ 5Yes
items[].idstring (uuid)Yes
items[].board_idstring (uuid)Yes
items[].slugstringlength 1–200pattern ^[a-z0-9]+(?:-[a-z0-9]+)*$Yes
items[].titlestringlength 1–140Yes
items[].statusobjectYes
items[].status.idstring (uuid)Yes
items[].status.namestringlength 1–100Yes
items[].status.colorstringpattern ^#[0-9a-fA-F]{6}$Yes
items[].status.category"open" | "active" | "done" | "closed"Yes
items[].vote_countintegervalue ≥ 0Yes
items[].scorenumbervalue 0–1Yes

Errors: 400, 404, 429, 500, 503, all in the standard error shape.

curl
curl 'https://api.upvoiced.com/v1/public/posts/similar?q=dark+mode' \
  -H "X-Workspace: acme"
TypeScript SDK
const data = await upvoiced.get('/v1/public/posts/similar', { query: { q: 'dark mode' } });
MCP

No MCP tool for this operation. The MCP server covers the everyday reads and writes.

GET/v1/public/boards/{slug}/posts

Board page: approved posts of a public board, trending/top/new/relevance, filters, cursor (cacheable)

Authentication
None (public)
Operation id
listPublicBoardPosts
Path parameters
NameTypeRequired
slugstringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$Yes
Query parameters
NameTypeRequired
workspacestringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$No
sort"trending" | "top" | "new" | "relevance"No
statusstring (uuid)No
tagstring (uuid)No
qstringlength 1–200No
cursorstringlength 1–512No
limitintegervalue 1–50default 20No
Headers
NameTypeRequired
x-workspacestringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$No
Response 200: 35 fields
Response 200 (application/json)
NameTypeRequired
boardobjectYes
board.idstring (uuid)Yes
board.namestringlength 1–100Yes
board.slugstringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$Yes
board.descriptionstringlength ≤ 2000Yes
board.visibility"public"Yes
itemsarray of objectitems ≤ 50Yes
items[].idstring (uuid)Yes
items[].board_idstring (uuid)Yes
items[].slugstringlength 1–200pattern ^[a-z0-9]+(?:-[a-z0-9]+)*$Yes
items[].titlestringlength 1–140Yes
items[].body_mdstringlength ≤ 10000Yes
items[].statusobjectYes
items[].status.idstring (uuid)Yes
items[].status.namestringlength 1–100Yes
items[].status.colorstringpattern ^#[0-9a-fA-F]{6}$Yes
items[].status.category"open" | "active" | "done" | "closed"Yes
items[].tagsarray of objectitems ≤ 50Yes
items[].tags[].idstring (uuid)Yes
items[].tags[].namestringlength 1–100Yes
items[].tags[].colorstringpattern ^#[0-9a-fA-F]{6}$Yes
items[].imagesarray of objectitems ≤ 4Yes
items[].images[].idstring (uuid)Yes
items[].images[].urlstring (uri)Yes
items[].images[].mime"image/png" | "image/jpeg" | "image/webp"Yes
items[].images[].bytesintegervalue ≤ 5242880value > 0Yes
items[].authorobjectYes
items[].author.kind"operator" | "end_user" | "deleted"Yes
items[].author.namestring | nulllength 1–100Yes
items[].vote_countintegervalue ≥ 0Yes
items[].comment_countintegervalue ≥ 0Yes
items[].merged_into_post_idstring (uuid) | nullYes
items[].created_atstring (date-time)Yes
items[].updated_atstring (date-time)Yes
next_cursorstring | nullYes

Errors: 400, 404, 429, 500, 503, all in the standard error shape.

curl
curl 'https://api.upvoiced.com/v1/public/boards/dark-mode/posts' \
  -H "X-Workspace: acme"
TypeScript SDK
const data = await upvoiced.get('/v1/public/boards/{slug}/posts', { path: { slug: 'dark-mode' } });
MCP

No MCP tool for this operation. The MCP server covers the everyday reads and writes.

GET/v1/public/posts/{slug}

Post page by slug: post, official reply and first comments, or a redirect for merged posts (cacheable)

Authentication
None (public)
Operation id
getPublicPost
Path parameters
NameTypeRequired
slugstringlength 1–200pattern ^[a-z0-9]+(?:-[a-z0-9]+)*$Yes
Query parameters
NameTypeRequired
workspacestringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$No
Headers
NameTypeRequired
x-workspacestringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$No

Response 200: object (application/json)

Errors: 400, 404, 429, 500, 503, all in the standard error shape.

curl
curl 'https://api.upvoiced.com/v1/public/posts/dark-mode' \
  -H "X-Workspace: acme"
TypeScript SDK
const data = await upvoiced.get('/v1/public/posts/{slug}', { path: { slug: 'dark-mode' } });
MCP

No MCP tool for this operation. The MCP server covers the everyday reads and writes.

GET/v1/public/posts/{slug}/comments

Further comment pages of a public post, oldest first (cacheable)

Authentication
None (public)
Operation id
listPublicPostComments
Path parameters
NameTypeRequired
slugstringlength 1–200pattern ^[a-z0-9]+(?:-[a-z0-9]+)*$Yes
Query parameters
NameTypeRequired
workspacestringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$No
cursorstringlength 1–512No
limitintegervalue 1–100default 50No
Headers
NameTypeRequired
x-workspacestringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$No
Response 200: 14 fields
Response 200 (application/json)
NameTypeRequired
itemsarray of objectYes
items[].idstring (uuid)Yes
items[].post_idstring (uuid)Yes
items[].parent_idstring (uuid) | nullYes
items[].authorobjectYes
items[].author.kind"operator" | "end_user" | "deleted"Yes
items[].author.namestring | nulllength ≤ 200Yes
items[].body_mdstringYes
items[].is_officialbooleanYes
items[].pinnedbooleanYes
items[].edited_atstring (date-time) | nullYes
items[].deleted_atstring (date-time) | nullYes
items[].created_atstring (date-time)Yes
next_cursorstring | nullYes

Errors: 400, 404, 429, 500, 503, all in the standard error shape.

curl
curl 'https://api.upvoiced.com/v1/public/posts/dark-mode/comments' \
  -H "X-Workspace: acme"
TypeScript SDK
const data = await upvoiced.get('/v1/public/posts/{slug}/comments', { path: { slug: 'dark-mode' } });
MCP

No MCP tool for this operation. The MCP server covers the everyday reads and writes.

GET/v1/public/roadmap

Public roadmap: statuses shown on the roadmap with approved posts of public, non-excluded boards; filter by board slug; 404 while hidden (cacheable)

Authentication
None (public)
Operation id
getPublicRoadmap
Query parameters
NameTypeRequired
workspacestringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$No
boardstringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$No
limitintegervalue 1–50default 20No
Headers
NameTypeRequired
x-workspacestringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$No
Response 200: 22 fields
Response 200 (application/json)
NameTypeRequired
boardsarray of objectYes
boards[].idstring (uuid)Yes
boards[].namestringlength 1–100Yes
boards[].slugstringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$Yes
boards[].descriptionstringlength ≤ 2000Yes
boards[].visibility"public"Yes
columnsarray of objectYes
columns[].statusobjectYes
columns[].status.idstring (uuid)Yes
columns[].status.namestringlength 1–100Yes
columns[].status.colorstringpattern ^#[0-9a-fA-F]{6}$Yes
columns[].status.category"open" | "active" | "done" | "closed"Yes
columns[].cardsarray of objectitems ≤ 50Yes
columns[].cards[].idstring (uuid)Yes
columns[].cards[].slugstringlength 1–200pattern ^[a-z0-9]+(?:-[a-z0-9]+)*$Yes
columns[].cards[].titlestringlength 1–140Yes
columns[].cards[].excerptstringYes
columns[].cards[].vote_countintegervalue ≥ 0Yes
columns[].cards[].tagsarray of objectitems ≤ 50Yes
columns[].cards[].last_activity_atstring (date-time)Yes
columns[].cards[].boardobjectYes
columns[].totalintegervalue ≥ 0Yes

Errors: 400, 404, 429, 500, 503, all in the standard error shape.

curl
curl 'https://api.upvoiced.com/v1/public/roadmap' \
  -H "X-Workspace: acme"
TypeScript SDK
const data = await upvoiced.get('/v1/public/roadmap');
MCP

No MCP tool for this operation. The MCP server covers the everyday reads and writes.

GET/v1/public/changelog

Published changelog entries, newest first, cursor (cacheable)

Authentication
None (public)
Operation id
listPublicChangelog
Query parameters
NameTypeRequired
workspacestringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$No
categorystringlength 1–32pattern ^[^\u0000-\u001f\u007f]+$/uNo
cursorstringlength 1–512No
limitintegervalue 1–50default 20No
Headers
NameTypeRequired
x-workspacestringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$No
Response 200: 17 fields
Response 200 (application/json)
NameTypeRequired
itemsarray of objectitems ≤ 50Yes
items[].idstring (uuid)Yes
items[].slugstringlength 1–200pattern ^[a-z0-9]+(?:-[a-z0-9]+)*$Yes
items[].titlestringlength 1–140Yes
items[].body_htmlstringYes
items[].coverobject | nullYes
items[].cover.idstring (uuid)Yes
items[].cover.urlstring (uri)Yes
items[].versionstring | nullpattern ^[A-Za-z0-9][A-Za-z0-9._+-]{0,31}$Yes
items[].categoriesarray of stringitems ≤ 5Yes
items[].linked_postsarray of objectitems ≤ 50Yes
items[].linked_posts[].idstring (uuid)Yes
items[].linked_posts[].slugstringlength 1–200pattern ^[a-z0-9]+(?:-[a-z0-9]+)*$Yes
items[].linked_posts[].titlestringlength 1–140Yes
items[].linked_posts[].boardobjectYes
items[].published_atstring (date-time)Yes
next_cursorstring | nullYes

Errors: 400, 404, 429, 500, 503, all in the standard error shape.

curl
curl 'https://api.upvoiced.com/v1/public/changelog' \
  -H "X-Workspace: acme"
TypeScript SDK
const data = await upvoiced.get('/v1/public/changelog');
MCP

No MCP tool for this operation. The MCP server covers the everyday reads and writes.

GET/v1/public/changelog/{slug}

One published entry with its public linked posts ('You asked, we shipped'; cacheable)

Authentication
None (public)
Operation id
getPublicChangelogEntry
Path parameters
NameTypeRequired
slugstringlength 1–200pattern ^[a-z0-9]+(?:-[a-z0-9]+)*$Yes
Query parameters
NameTypeRequired
workspacestringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$No
Headers
NameTypeRequired
x-workspacestringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$No
Response 200: 18 fields
Response 200 (application/json)
NameTypeRequired
idstring (uuid)Yes
slugstringlength 1–200pattern ^[a-z0-9]+(?:-[a-z0-9]+)*$Yes
titlestringlength 1–140Yes
body_htmlstringYes
coverobject | nullYes
cover.idstring (uuid)Yes
cover.urlstring (uri)Yes
versionstring | nullpattern ^[A-Za-z0-9][A-Za-z0-9._+-]{0,31}$Yes
categoriesarray of stringitems ≤ 5Yes
linked_postsarray of objectitems ≤ 50Yes
linked_posts[].idstring (uuid)Yes
linked_posts[].slugstringlength 1–200pattern ^[a-z0-9]+(?:-[a-z0-9]+)*$Yes
linked_posts[].titlestringlength 1–140Yes
linked_posts[].boardobjectYes
linked_posts[].board.idstring (uuid)Yes
linked_posts[].board.namestringlength 1–100Yes
linked_posts[].board.slugstringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$Yes
published_atstring (date-time)Yes

Errors: 400, 404, 429, 500, 503, all in the standard error shape.

curl
curl 'https://api.upvoiced.com/v1/public/changelog/dark-mode' \
  -H "X-Workspace: acme"
TypeScript SDK
const data = await upvoiced.get('/v1/public/changelog/{slug}', { path: { slug: 'dark-mode' } });
MCP

No MCP tool for this operation. The MCP server covers the everyday reads and writes.

GET/v1/public/changelog/media/{id}

Workspace media host: 302 to a signed link of an image a published entry shows (body image or cover); 404 otherwise

Authentication
None (public)
Operation id
getPublicChangelogMedia
Path parameters
NameTypeRequired
idstring (uuid)Yes
Query parameters
NameTypeRequired
workspacestringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$No
Headers
NameTypeRequired
x-workspacestringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$No

Errors: 400, 404, 429, 500, 503, all in the standard error shape.

curl
curl 'https://api.upvoiced.com/v1/public/changelog/media/0190f5e2-4c1d-7a3b-8c2d-5e6f7a8b9c0d' \
  -H "X-Workspace: acme"
TypeScript SDK
const data = await upvoiced.get('/v1/public/changelog/media/{id}', { path: { id: '0190f5e2-4c1d-7a3b-8c2d-5e6f7a8b9c0d' } });
MCP

No MCP tool for this operation. The MCP server covers the everyday reads and writes.

GET/v1/unlisted/boards/{slug}/posts

Board page of an unlisted board addressed by slug: approved posts, same sorts/filters as the public board page (never cached)

Authentication
None (public)
Operation id
listUnlistedBoardPosts
Path parameters
NameTypeRequired
slugstringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$Yes
Query parameters
NameTypeRequired
workspacestringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$No
sort"trending" | "top" | "new" | "relevance"No
statusstring (uuid)No
tagstring (uuid)No
qstringlength 1–200No
cursorstringlength 1–512No
limitintegervalue 1–50default 20No
Headers
NameTypeRequired
x-workspacestringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$No
Response 200: 35 fields
Response 200 (application/json)
NameTypeRequired
boardobjectYes
board.idstring (uuid)Yes
board.namestringlength 1–100Yes
board.slugstringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$Yes
board.descriptionstringlength ≤ 2000Yes
board.visibility"unlisted"Yes
itemsarray of objectitems ≤ 50Yes
items[].idstring (uuid)Yes
items[].board_idstring (uuid)Yes
items[].slugstringlength 1–200pattern ^[a-z0-9]+(?:-[a-z0-9]+)*$Yes
items[].titlestringlength 1–140Yes
items[].body_mdstringlength ≤ 10000Yes
items[].statusobjectYes
items[].status.idstring (uuid)Yes
items[].status.namestringlength 1–100Yes
items[].status.colorstringpattern ^#[0-9a-fA-F]{6}$Yes
items[].status.category"open" | "active" | "done" | "closed"Yes
items[].tagsarray of objectitems ≤ 50Yes
items[].tags[].idstring (uuid)Yes
items[].tags[].namestringlength 1–100Yes
items[].tags[].colorstringpattern ^#[0-9a-fA-F]{6}$Yes
items[].imagesarray of objectitems ≤ 4Yes
items[].images[].idstring (uuid)Yes
items[].images[].urlstring (uri)Yes
items[].images[].mime"image/png" | "image/jpeg" | "image/webp"Yes
items[].images[].bytesintegervalue ≤ 5242880value > 0Yes
items[].authorobjectYes
items[].author.kind"operator" | "end_user" | "deleted"Yes
items[].author.namestring | nulllength 1–100Yes
items[].vote_countintegervalue ≥ 0Yes
items[].comment_countintegervalue ≥ 0Yes
items[].merged_into_post_idstring (uuid) | nullYes
items[].created_atstring (date-time)Yes
items[].updated_atstring (date-time)Yes
next_cursorstring | nullYes

Errors: 400, 404, 429, 500, 503, all in the standard error shape.

curl
curl 'https://api.upvoiced.com/v1/unlisted/boards/dark-mode/posts' \
  -H "X-Workspace: acme"
TypeScript SDK
const data = await upvoiced.get('/v1/unlisted/boards/{slug}/posts', { path: { slug: 'dark-mode' } });
MCP

No MCP tool for this operation. The MCP server covers the everyday reads and writes.

GET/v1/unlisted/posts/{id}

Post of an unlisted board by id: post, board, official reply and first comments (never cached)

Authentication
None (public)
Operation id
getUnlistedPost
Path parameters
NameTypeRequired
idstring (uuid)Yes
Query parameters
NameTypeRequired
workspacestringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$No
Headers
NameTypeRequired
x-workspacestringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$No
Response 200: 60 fields
Response 200 (application/json)
NameTypeRequired
boardobjectYes
board.idstring (uuid)Yes
board.namestringlength 1–100Yes
board.slugstringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$Yes
board.descriptionstringlength ≤ 2000Yes
board.visibility"unlisted"Yes
postobjectYes
post.idstring (uuid)Yes
post.board_idstring (uuid)Yes
post.slugstringlength 1–200pattern ^[a-z0-9]+(?:-[a-z0-9]+)*$Yes
post.titlestringlength 1–140Yes
post.body_mdstringlength ≤ 10000Yes
post.statusobjectYes
post.status.idstring (uuid)Yes
post.status.namestringlength 1–100Yes
post.status.colorstringpattern ^#[0-9a-fA-F]{6}$Yes
post.status.category"open" | "active" | "done" | "closed"Yes
post.tagsarray of objectitems ≤ 50Yes
post.tags[].idstring (uuid)Yes
post.tags[].namestringlength 1–100Yes
post.tags[].colorstringpattern ^#[0-9a-fA-F]{6}$Yes
post.imagesarray of objectitems ≤ 4Yes
post.images[].idstring (uuid)Yes
post.images[].urlstring (uri)Yes
post.images[].mime"image/png" | "image/jpeg" | "image/webp"Yes
post.images[].bytesintegervalue ≤ 5242880value > 0Yes
post.authorobjectYes
post.author.kind"operator" | "end_user" | "deleted"Yes
post.author.namestring | nulllength 1–100Yes
post.vote_countintegervalue ≥ 0Yes
post.comment_countintegervalue ≥ 0Yes
post.merged_into_post_idstring (uuid) | nullYes
post.created_atstring (date-time)Yes
post.updated_atstring (date-time)Yes
official_replyobject | nullYes
official_reply.idstring (uuid)Yes
official_reply.post_idstring (uuid)Yes
official_reply.parent_idstring (uuid) | nullYes
official_reply.authorobjectYes
official_reply.author.kind"operator" | "end_user" | "deleted"Yes
official_reply.author.namestring | nulllength ≤ 200Yes
official_reply.body_mdstringYes
official_reply.is_officialbooleanYes
official_reply.pinnedbooleanYes
official_reply.edited_atstring (date-time) | nullYes
official_reply.deleted_atstring (date-time) | nullYes
official_reply.created_atstring (date-time)Yes
commentsobjectYes
comments.itemsarray of objectYes
comments.items[].idstring (uuid)Yes
comments.items[].post_idstring (uuid)Yes
comments.items[].parent_idstring (uuid) | nullYes
comments.items[].authorobjectYes
comments.items[].body_mdstringYes
comments.items[].is_officialbooleanYes
comments.items[].pinnedbooleanYes
comments.items[].edited_atstring (date-time) | nullYes
comments.items[].deleted_atstring (date-time) | nullYes
comments.items[].created_atstring (date-time)Yes
comments.next_cursorstring | nullYes

Errors: 400, 404, 429, 500, 503, all in the standard error shape.

curl
curl 'https://api.upvoiced.com/v1/unlisted/posts/0190f5e2-4c1d-7a3b-8c2d-5e6f7a8b9c0d' \
  -H "X-Workspace: acme"
TypeScript SDK
const data = await upvoiced.get('/v1/unlisted/posts/{id}', { path: { id: '0190f5e2-4c1d-7a3b-8c2d-5e6f7a8b9c0d' } });
MCP

No MCP tool for this operation. The MCP server covers the everyday reads and writes.

GET/v1/unlisted/posts/{id}/comments

Further comment pages of an unlisted-board post, oldest first (never cached)

Authentication
None (public)
Operation id
listUnlistedComments
Path parameters
NameTypeRequired
idstring (uuid)Yes
Query parameters
NameTypeRequired
workspacestringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$No
cursorstringlength 1–512No
limitintegervalue 1–100default 50No
Headers
NameTypeRequired
x-workspacestringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$No
Response 200: 14 fields
Response 200 (application/json)
NameTypeRequired
itemsarray of objectYes
items[].idstring (uuid)Yes
items[].post_idstring (uuid)Yes
items[].parent_idstring (uuid) | nullYes
items[].authorobjectYes
items[].author.kind"operator" | "end_user" | "deleted"Yes
items[].author.namestring | nulllength ≤ 200Yes
items[].body_mdstringYes
items[].is_officialbooleanYes
items[].pinnedbooleanYes
items[].edited_atstring (date-time) | nullYes
items[].deleted_atstring (date-time) | nullYes
items[].created_atstring (date-time)Yes
next_cursorstring | nullYes

Errors: 400, 404, 429, 500, 503, all in the standard error shape.

curl
curl 'https://api.upvoiced.com/v1/unlisted/posts/0190f5e2-4c1d-7a3b-8c2d-5e6f7a8b9c0d/comments' \
  -H "X-Workspace: acme"
TypeScript SDK
const data = await upvoiced.get('/v1/unlisted/posts/{id}/comments', { path: { id: '0190f5e2-4c1d-7a3b-8c2d-5e6f7a8b9c0d' } });
MCP

No MCP tool for this operation. The MCP server covers the everyday reads and writes.

GET/v1/unlisted/changelog/preview/{token}

Preview of a draft or scheduled entry by its signed, expiring token (never cached, noindex)

Authentication
None (public)
Operation id
getChangelogPreview
Path parameters
NameTypeRequired
tokenstringpattern ^[A-Za-z0-9_-]{43}$Yes
Query parameters
NameTypeRequired
workspacestringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$No
Headers
NameTypeRequired
x-workspacestringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$No
Response 200: 20 fields
Response 200 (application/json)
NameTypeRequired
idstring (uuid)Yes
slugstringlength 1–200pattern ^[a-z0-9]+(?:-[a-z0-9]+)*$Yes
titlestringlength 1–140Yes
body_htmlstringYes
coverobject | nullYes
cover.idstring (uuid)Yes
cover.urlstring (uri)Yes
versionstring | nullpattern ^[A-Za-z0-9][A-Za-z0-9._+-]{0,31}$Yes
categoriesarray of stringitems ≤ 5Yes
linked_postsarray of objectitems ≤ 50Yes
linked_posts[].idstring (uuid)Yes
linked_posts[].slugstringlength 1–200pattern ^[a-z0-9]+(?:-[a-z0-9]+)*$Yes
linked_posts[].titlestringlength 1–140Yes
linked_posts[].boardobjectYes
linked_posts[].board.idstring (uuid)Yes
linked_posts[].board.namestringlength 1–100Yes
linked_posts[].board.slugstringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$Yes
state"draft" | "scheduled"Yes
scheduled_forstring (date-time) | nullYes
previewtrueYes

Errors: 400, 404, 429, 500, 503, all in the standard error shape.

curl
curl 'https://api.upvoiced.com/v1/unlisted/changelog/preview/example-token-00000000000000000000000000000' \
  -H "X-Workspace: acme"
TypeScript SDK
const data = await upvoiced.get('/v1/unlisted/changelog/preview/{token}', {
  path: { token: 'example-token-00000000000000000000000000000' },
});
MCP

No MCP tool for this operation. The MCP server covers the everyday reads and writes.

POST/v1/public/subscriptions/changelog

Changelog subscribe box: email a double opt-in link (48 h); always 202, nothing is mailed before confirmation

Authentication
None (public)
Operation id
subscribeChangelogEmail
Query parameters
NameTypeRequired
workspacestringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$No
Headers
NameTypeRequired
x-workspacestringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$Yes
Request body (JSON)
NameTypeRequired
emailstring (email)length ≤ 254Yes
locale"en" | "pt-BR" | "es" | "de" | "fr"No
websitestringlength ≤ 2048No
turnstile_tokenstringlength 1–2048pattern ^[\x21-\x7e]+$No
Response 202: 2 fields
Response 202 (application/json)
NameTypeRequired
acceptedtrueYes
confirmation_expires_in_hours48Yes

Errors: 400, 401, 403, 404, 409, 413, 429, 500, 503, all in the standard error shape.

curl
curl -X POST 'https://api.upvoiced.com/v1/public/subscriptions/changelog' \
  -H "X-Workspace: acme" \
  -H "Content-Type: application/json" \
  -d '{
  "email": "ada@example.com"
}'
TypeScript SDK
const data = await upvoiced.post('/v1/public/subscriptions/changelog', { body: { email: 'ada@example.com' } });
MCP

No MCP tool for this operation. The MCP server covers the everyday reads and writes.

POST/v1/public/auth/code

Email a single-use six-digit code and sign-in link (15 min); always 202 for any address

Authentication
None (public)
Operation id
requestSignInCode
Query parameters
NameTypeRequired
workspacestringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$No
Headers
NameTypeRequired
x-workspacestringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$Yes
Request body (JSON)
NameTypeRequired
emailstring (email)length ≤ 254Yes
websitestringlength ≤ 2048No
turnstile_tokenstringlength 1–2048pattern ^[\x21-\x7e]+$No
Response 202: 3 fields
Response 202 (application/json)
NameTypeRequired
acceptedtrueYes
code_length6Yes
expires_in_seconds900Yes

Errors: 400, 401, 403, 404, 409, 413, 429, 500, 503, all in the standard error shape.

curl
curl -X POST 'https://api.upvoiced.com/v1/public/auth/code' \
  -H "X-Workspace: acme" \
  -H "Content-Type: application/json" \
  -d '{
  "email": "ada@example.com"
}'
TypeScript SDK
const data = await upvoiced.post('/v1/public/auth/code', { body: { email: 'ada@example.com' } });
MCP

No MCP tool for this operation. The MCP server covers the everyday reads and writes.

POST/v1/public/auth/verify

Exchange {email, code} or the link {token} for an end-user session; optional pending action

Authentication
None (public)
Operation id
verifySignIn
Query parameters
NameTypeRequired
workspacestringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$No
Headers
NameTypeRequired
x-workspacestringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$Yes
Response 200: 16 fields
Response 200 (application/json)
NameTypeRequired
tokenstringpattern ^[A-Za-z0-9_-]{43}$Yes
token_type"Bearer"Yes
expires_atstring (date-time)Yes
end_userobjectYes
end_user.idstring (uuid)Yes
end_user.emailstring | nulllength ≤ 254Yes
end_user.namestring | nulllength 1–100Yes
end_user.avatar_urlstring | nulllength ≤ 2048Yes
end_user.locale"en" | "pt-BR" | "es" | "de" | "fr"Yes
end_user.created_atstring (date-time)Yes
pending_actionobject | nullYes
pending_action.kind"vote" | "follow" | "comment" | "post"Yes
pending_action.status"done" | "failed"Yes
pending_action.error_codestring | nullpattern ^[a-z0-9_]{1,64}$Yes
pending_action.post_idstring (uuid) | nullYes
pending_action.comment_idstring (uuid) | nullYes

Errors: 400, 401, 403, 404, 409, 413, 429, 500, 503, all in the standard error shape.

curl
curl -X POST 'https://api.upvoiced.com/v1/public/auth/verify' \
  -H "X-Workspace: acme" \
  -H "Content-Type: application/json" \
  -d '{
  "email": "ada@example.com",
  "code": "123456"
}'
TypeScript SDK
const data = await upvoiced.post('/v1/public/auth/verify', { body: { email: 'ada@example.com', code: '123456' } });
MCP

No MCP tool for this operation. The MCP server covers the everyday reads and writes.

POST/v1/public/auth/sso

Exchange a host-signed HS256 SSO token (aud = workspace id, exp ≤ 10 min, single-use jti) for a 12 h end-user session

Authentication
None (public)
Operation id
exchangeSsoToken
Query parameters
NameTypeRequired
workspacestringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$No
Headers
NameTypeRequired
x-workspacestringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$Yes
Request body (JSON)
NameTypeRequired
tokenstringlength ≤ 4096pattern ^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$Yes
pending_actionobjectNo
Response 200: 16 fields
Response 200 (application/json)
NameTypeRequired
tokenstringpattern ^[A-Za-z0-9_-]{43}$Yes
token_type"Bearer"Yes
expires_atstring (date-time)Yes
end_userobjectYes
end_user.idstring (uuid)Yes
end_user.emailstring | nulllength ≤ 254Yes
end_user.namestring | nulllength 1–100Yes
end_user.avatar_urlstring | nulllength ≤ 2048Yes
end_user.locale"en" | "pt-BR" | "es" | "de" | "fr"Yes
end_user.created_atstring (date-time)Yes
pending_actionobject | nullYes
pending_action.kind"vote" | "follow" | "comment" | "post"Yes
pending_action.status"done" | "failed"Yes
pending_action.error_codestring | nullpattern ^[a-z0-9_]{1,64}$Yes
pending_action.post_idstring (uuid) | nullYes
pending_action.comment_idstring (uuid) | nullYes

Errors: 400, 401, 403, 404, 409, 413, 429, 500, 503, all in the standard error shape.

curl
curl -X POST 'https://api.upvoiced.com/v1/public/auth/sso' \
  -H "X-Workspace: acme" \
  -H "Content-Type: application/json" \
  -d '{
  "token": "feedback.example.com"
}'
TypeScript SDK
const data = await upvoiced.post('/v1/public/auth/sso', { body: { token: 'feedback.example.com' } });
MCP

No MCP tool for this operation. The MCP server covers the everyday reads and writes.

POST/v1/public/auth/demo

Platform demo workspace only: a guest end-user session without email (no identity, suppressed); 404 elsewhere

Authentication
None (public)
Operation id
startDemoGuestSession
Query parameters
NameTypeRequired
workspacestringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$No
Headers
NameTypeRequired
x-workspacestringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$Yes
Request body (JSON)
NameTypeRequired
pending_actionobjectNo
turnstile_tokenstringlength 1–2048pattern ^[\x21-\x7e]+$No
Response 200: 16 fields
Response 200 (application/json)
NameTypeRequired
tokenstringpattern ^[A-Za-z0-9_-]{43}$Yes
token_type"Bearer"Yes
expires_atstring (date-time)Yes
end_userobjectYes
end_user.idstring (uuid)Yes
end_user.emailstring | nulllength ≤ 254Yes
end_user.namestring | nulllength 1–100Yes
end_user.avatar_urlstring | nulllength ≤ 2048Yes
end_user.locale"en" | "pt-BR" | "es" | "de" | "fr"Yes
end_user.created_atstring (date-time)Yes
pending_actionobject | nullYes
pending_action.kind"vote" | "follow" | "comment" | "post"Yes
pending_action.status"done" | "failed"Yes
pending_action.error_codestring | nullpattern ^[a-z0-9_]{1,64}$Yes
pending_action.post_idstring (uuid) | nullYes
pending_action.comment_idstring (uuid) | nullYes

Errors: 400, 401, 403, 404, 409, 413, 429, 500, 503, all in the standard error shape.

curl
curl -X POST 'https://api.upvoiced.com/v1/public/auth/demo' \
  -H "X-Workspace: acme" \
  -H "Content-Type: application/json" \
  -d '{
  "pending_action": {
    "kind": "vote",
    "post_id": "0190f5e2-4c1d-7a3b-8c2d-5e6f7a8b9c0d"
  }
}'
TypeScript SDK
const data = await upvoiced.post('/v1/public/auth/demo', {
  body: {
    pending_action: {
      kind: 'vote',
      post_id: '0190f5e2-4c1d-7a3b-8c2d-5e6f7a8b9c0d',
    },
  },
});
MCP

No MCP tool for this operation. The MCP server covers the everyday reads and writes.