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)
| Name | Type | Required |
|---|---|---|
token | stringlength ≤ 1024 | No |
Response 200: string (text/html)
Errors: 400, 500, 503, all in the standard error shape.
curl 'https://api.upvoiced.com/v1/public/unsubscribe'const data = await upvoiced.get('/v1/public/unsubscribe');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
| Name | Type | Required |
|---|---|---|
token | stringlength ≤ 1024 | No |
Response 200: string (text/html)
Errors: 400, 500, 503, all in the standard error shape.
curl -X POST 'https://api.upvoiced.com/v1/public/unsubscribe'const data = await upvoiced.post('/v1/public/unsubscribe');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)
| Name | Type | Required |
|---|---|---|
token | stringlength ≤ 1024 | No |
Response 200: string (text/html)
Errors: 400, 500, all in the standard error shape.
curl 'https://api.upvoiced.com/v1/public/subscriptions/confirm'const data = await upvoiced.get('/v1/public/subscriptions/confirm');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)
| Name | Type | Required |
|---|---|---|
token | stringlength ≤ 1024 | No |
Response 200: string (text/html)
Errors: 400, 500, all in the standard error shape.
curl -X POST 'https://api.upvoiced.com/v1/public/subscriptions/confirm'const data = await upvoiced.post('/v1/public/subscriptions/confirm');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)
| Name | Type | Required |
|---|---|---|
workspace | stringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$ | No |
| Name | Type | Required |
|---|---|---|
x-workspace | stringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$ | No |
Response 200: 52 fields
| Name | Type | Required |
|---|---|---|
workspace | object | Yes |
workspace.name | stringlength 1–100 | Yes |
workspace.slug | stringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$ | Yes |
workspace.portal_locale | "en" | "pt-BR" | "es" | "de" | "fr" | Yes |
branding | object | Yes |
branding.preset | "ink" | "ocean" | "forest" | "citrus" | "plum" | "mono" | Yes |
branding.accent | stringpattern ^#[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_by | boolean | Yes |
branding.layout | object | Yes |
branding.layout.portal_nav | array of "board" | "roadmap" | "changelog" | "wall"items ≤ 4 | Yes |
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.logo | object | null | Yes |
branding.logo.id | string (uuid) | Yes |
branding.logo.url | string (uri) | Yes |
branding.logo.mime | "image/png" | "image/svg+xml" | "image/jpeg" | "image/webp" | Yes |
branding.logo.bytes | integervalue ≤ 2097152value > 0 | Yes |
branding.favicon | object | null | Yes |
branding.favicon.id | string (uuid) | Yes |
branding.favicon.url | string (uri) | Yes |
branding.favicon.mime | "image/png" | "image/svg+xml" | "image/jpeg" | "image/webp" | Yes |
branding.favicon.bytes | integervalue ≤ 2097152value > 0 | Yes |
boards | array of object | Yes |
boards[].id | string (uuid) | Yes |
boards[].name | stringlength 1–100 | Yes |
boards[].slug | stringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$ | Yes |
boards[].description | stringlength ≤ 2000 | Yes |
boards[].visibility | "public" | Yes |
statuses | array of object | Yes |
statuses[].id | string (uuid) | Yes |
statuses[].name | stringlength 1–100 | Yes |
statuses[].color | stringpattern ^#[0-9a-fA-F]{6}$ | Yes |
statuses[].category | "open" | "active" | "done" | "closed" | Yes |
statuses[].shown_on_roadmap | boolean | Yes |
features | object | Yes |
features.modules | array of "board" | "roadmap" | "changelog" | "wall" | Yes |
widget | object | Yes |
widget.tabs | array of "feedback" | "roadmap" | "whats-new"items ≤ 3 | Yes |
widget.launcher | "icon" | "pill" | "tab" | Yes |
widget.position | "bottom-right" | "bottom-left" | "right-middle" | "left-middle" | Yes |
custom_css | string | nulllength 1–65536 | No |
custom_domain | string | 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 'https://api.upvoiced.com/v1/public/workspace' \
-H "X-Workspace: acme"const data = await upvoiced.get('/v1/public/workspace');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)
| Name | Type | Required |
|---|---|---|
workspace | stringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$ | No |
q | stringlength ≤ 200 | Yes |
board | stringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$ | No |
| Name | Type | Required |
|---|---|---|
x-workspace | stringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$ | No |
Response 200: 12 fields
| Name | Type | Required |
|---|---|---|
items | array of objectitems ≤ 5 | Yes |
items[].id | string (uuid) | Yes |
items[].board_id | string (uuid) | Yes |
items[].slug | stringlength 1–200pattern ^[a-z0-9]+(?:-[a-z0-9]+)*$ | Yes |
items[].title | stringlength 1–140 | Yes |
items[].status | object | Yes |
items[].status.id | string (uuid) | Yes |
items[].status.name | stringlength 1–100 | Yes |
items[].status.color | stringpattern ^#[0-9a-fA-F]{6}$ | Yes |
items[].status.category | "open" | "active" | "done" | "closed" | Yes |
items[].vote_count | integervalue ≥ 0 | Yes |
items[].score | numbervalue 0–1 | Yes |
Errors: 400, 404, 429, 500, 503, all in the standard error shape.
curl 'https://api.upvoiced.com/v1/public/posts/similar?q=dark+mode' \
-H "X-Workspace: acme"const data = await upvoiced.get('/v1/public/posts/similar', { query: { q: 'dark mode' } });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)
| Name | Type | Required |
|---|---|---|
slug | stringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$ | Yes |
| Name | Type | Required |
|---|---|---|
workspace | stringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$ | No |
sort | "trending" | "top" | "new" | "relevance" | No |
status | string (uuid) | No |
tag | string (uuid) | No |
q | stringlength 1–200 | No |
cursor | stringlength 1–512 | No |
limit | integervalue 1–50default 20 | No |
| Name | Type | Required |
|---|---|---|
x-workspace | stringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$ | No |
Response 200: 35 fields
| Name | Type | Required |
|---|---|---|
board | object | Yes |
board.id | string (uuid) | Yes |
board.name | stringlength 1–100 | Yes |
board.slug | stringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$ | Yes |
board.description | stringlength ≤ 2000 | Yes |
board.visibility | "public" | Yes |
items | array of objectitems ≤ 50 | Yes |
items[].id | string (uuid) | Yes |
items[].board_id | string (uuid) | Yes |
items[].slug | stringlength 1–200pattern ^[a-z0-9]+(?:-[a-z0-9]+)*$ | Yes |
items[].title | stringlength 1–140 | Yes |
items[].body_md | stringlength ≤ 10000 | Yes |
items[].status | object | Yes |
items[].status.id | string (uuid) | Yes |
items[].status.name | stringlength 1–100 | Yes |
items[].status.color | stringpattern ^#[0-9a-fA-F]{6}$ | Yes |
items[].status.category | "open" | "active" | "done" | "closed" | Yes |
items[].tags | array of objectitems ≤ 50 | Yes |
items[].tags[].id | string (uuid) | Yes |
items[].tags[].name | stringlength 1–100 | Yes |
items[].tags[].color | stringpattern ^#[0-9a-fA-F]{6}$ | Yes |
items[].images | array of objectitems ≤ 4 | Yes |
items[].images[].id | string (uuid) | Yes |
items[].images[].url | string (uri) | Yes |
items[].images[].mime | "image/png" | "image/jpeg" | "image/webp" | Yes |
items[].images[].bytes | integervalue ≤ 5242880value > 0 | Yes |
items[].author | object | Yes |
items[].author.kind | "operator" | "end_user" | "deleted" | Yes |
items[].author.name | string | nulllength 1–100 | Yes |
items[].vote_count | integervalue ≥ 0 | Yes |
items[].comment_count | integervalue ≥ 0 | Yes |
items[].merged_into_post_id | string (uuid) | null | Yes |
items[].created_at | string (date-time) | Yes |
items[].updated_at | string (date-time) | Yes |
next_cursor | string | null | Yes |
Errors: 400, 404, 429, 500, 503, all in the standard error shape.
curl 'https://api.upvoiced.com/v1/public/boards/dark-mode/posts' \
-H "X-Workspace: acme"const data = await upvoiced.get('/v1/public/boards/{slug}/posts', { path: { slug: 'dark-mode' } });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)
| Name | Type | Required |
|---|---|---|
slug | stringlength 1–200pattern ^[a-z0-9]+(?:-[a-z0-9]+)*$ | Yes |
| Name | Type | Required |
|---|---|---|
workspace | stringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$ | No |
| Name | Type | Required |
|---|---|---|
x-workspace | stringlength 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 'https://api.upvoiced.com/v1/public/posts/dark-mode' \
-H "X-Workspace: acme"const data = await upvoiced.get('/v1/public/posts/{slug}', { path: { slug: 'dark-mode' } });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)
| Name | Type | Required |
|---|---|---|
slug | stringlength 1–200pattern ^[a-z0-9]+(?:-[a-z0-9]+)*$ | Yes |
| Name | Type | Required |
|---|---|---|
workspace | stringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$ | No |
cursor | stringlength 1–512 | No |
limit | integervalue 1–100default 50 | No |
| Name | Type | Required |
|---|---|---|
x-workspace | stringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$ | No |
Response 200: 14 fields
| Name | Type | Required |
|---|---|---|
items | array of object | Yes |
items[].id | string (uuid) | Yes |
items[].post_id | string (uuid) | Yes |
items[].parent_id | string (uuid) | null | Yes |
items[].author | object | Yes |
items[].author.kind | "operator" | "end_user" | "deleted" | Yes |
items[].author.name | string | nulllength ≤ 200 | Yes |
items[].body_md | string | Yes |
items[].is_official | boolean | Yes |
items[].pinned | boolean | Yes |
items[].edited_at | string (date-time) | null | Yes |
items[].deleted_at | string (date-time) | null | Yes |
items[].created_at | string (date-time) | Yes |
next_cursor | string | null | Yes |
Errors: 400, 404, 429, 500, 503, all in the standard error shape.
curl 'https://api.upvoiced.com/v1/public/posts/dark-mode/comments' \
-H "X-Workspace: acme"const data = await upvoiced.get('/v1/public/posts/{slug}/comments', { path: { slug: 'dark-mode' } });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)
| Name | Type | Required |
|---|---|---|
workspace | stringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$ | No |
board | stringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$ | No |
limit | integervalue 1–50default 20 | No |
| Name | Type | Required |
|---|---|---|
x-workspace | stringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$ | No |
Response 200: 22 fields
| Name | Type | Required |
|---|---|---|
boards | array of object | Yes |
boards[].id | string (uuid) | Yes |
boards[].name | stringlength 1–100 | Yes |
boards[].slug | stringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$ | Yes |
boards[].description | stringlength ≤ 2000 | Yes |
boards[].visibility | "public" | Yes |
columns | array of object | Yes |
columns[].status | object | Yes |
columns[].status.id | string (uuid) | Yes |
columns[].status.name | stringlength 1–100 | Yes |
columns[].status.color | stringpattern ^#[0-9a-fA-F]{6}$ | Yes |
columns[].status.category | "open" | "active" | "done" | "closed" | Yes |
columns[].cards | array of objectitems ≤ 50 | Yes |
columns[].cards[].id | string (uuid) | Yes |
columns[].cards[].slug | stringlength 1–200pattern ^[a-z0-9]+(?:-[a-z0-9]+)*$ | Yes |
columns[].cards[].title | stringlength 1–140 | Yes |
columns[].cards[].excerpt | string | Yes |
columns[].cards[].vote_count | integervalue ≥ 0 | Yes |
columns[].cards[].tags | array of objectitems ≤ 50 | Yes |
columns[].cards[].last_activity_at | string (date-time) | Yes |
columns[].cards[].board | object | Yes |
columns[].total | integervalue ≥ 0 | Yes |
Errors: 400, 404, 429, 500, 503, all in the standard error shape.
curl 'https://api.upvoiced.com/v1/public/roadmap' \
-H "X-Workspace: acme"const data = await upvoiced.get('/v1/public/roadmap');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)
| Name | Type | Required |
|---|---|---|
workspace | stringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$ | No |
category | stringlength 1–32pattern ^[^\u0000-\u001f\u007f]+$/u | No |
cursor | stringlength 1–512 | No |
limit | integervalue 1–50default 20 | No |
| Name | Type | Required |
|---|---|---|
x-workspace | stringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$ | No |
Response 200: 17 fields
| Name | Type | Required |
|---|---|---|
items | array of objectitems ≤ 50 | Yes |
items[].id | string (uuid) | Yes |
items[].slug | stringlength 1–200pattern ^[a-z0-9]+(?:-[a-z0-9]+)*$ | Yes |
items[].title | stringlength 1–140 | Yes |
items[].body_html | string | Yes |
items[].cover | object | null | Yes |
items[].cover.id | string (uuid) | Yes |
items[].cover.url | string (uri) | Yes |
items[].version | string | nullpattern ^[A-Za-z0-9][A-Za-z0-9._+-]{0,31}$ | Yes |
items[].categories | array of stringitems ≤ 5 | Yes |
items[].linked_posts | array of objectitems ≤ 50 | Yes |
items[].linked_posts[].id | string (uuid) | Yes |
items[].linked_posts[].slug | stringlength 1–200pattern ^[a-z0-9]+(?:-[a-z0-9]+)*$ | Yes |
items[].linked_posts[].title | stringlength 1–140 | Yes |
items[].linked_posts[].board | object | Yes |
items[].published_at | string (date-time) | Yes |
next_cursor | string | null | Yes |
Errors: 400, 404, 429, 500, 503, all in the standard error shape.
curl 'https://api.upvoiced.com/v1/public/changelog' \
-H "X-Workspace: acme"const data = await upvoiced.get('/v1/public/changelog');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)
| Name | Type | Required |
|---|---|---|
slug | stringlength 1–200pattern ^[a-z0-9]+(?:-[a-z0-9]+)*$ | Yes |
| Name | Type | Required |
|---|---|---|
workspace | stringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$ | No |
| Name | Type | Required |
|---|---|---|
x-workspace | stringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$ | No |
Response 200: 18 fields
| Name | Type | Required |
|---|---|---|
id | string (uuid) | Yes |
slug | stringlength 1–200pattern ^[a-z0-9]+(?:-[a-z0-9]+)*$ | Yes |
title | stringlength 1–140 | Yes |
body_html | string | Yes |
cover | object | null | Yes |
cover.id | string (uuid) | Yes |
cover.url | string (uri) | Yes |
version | string | nullpattern ^[A-Za-z0-9][A-Za-z0-9._+-]{0,31}$ | Yes |
categories | array of stringitems ≤ 5 | Yes |
linked_posts | array of objectitems ≤ 50 | Yes |
linked_posts[].id | string (uuid) | Yes |
linked_posts[].slug | stringlength 1–200pattern ^[a-z0-9]+(?:-[a-z0-9]+)*$ | Yes |
linked_posts[].title | stringlength 1–140 | Yes |
linked_posts[].board | object | Yes |
linked_posts[].board.id | string (uuid) | Yes |
linked_posts[].board.name | stringlength 1–100 | Yes |
linked_posts[].board.slug | stringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$ | Yes |
published_at | string (date-time) | Yes |
Errors: 400, 404, 429, 500, 503, all in the standard error shape.
curl 'https://api.upvoiced.com/v1/public/changelog/dark-mode' \
-H "X-Workspace: acme"const data = await upvoiced.get('/v1/public/changelog/{slug}', { path: { slug: 'dark-mode' } });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
| Name | Type | Required |
|---|---|---|
id | string (uuid) | Yes |
| Name | Type | Required |
|---|---|---|
workspace | stringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$ | No |
| Name | Type | Required |
|---|---|---|
x-workspace | stringlength 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 'https://api.upvoiced.com/v1/public/changelog/media/0190f5e2-4c1d-7a3b-8c2d-5e6f7a8b9c0d' \
-H "X-Workspace: acme"const data = await upvoiced.get('/v1/public/changelog/media/{id}', { path: { id: '0190f5e2-4c1d-7a3b-8c2d-5e6f7a8b9c0d' } });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)
| Name | Type | Required |
|---|---|---|
slug | stringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$ | Yes |
| Name | Type | Required |
|---|---|---|
workspace | stringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$ | No |
sort | "trending" | "top" | "new" | "relevance" | No |
status | string (uuid) | No |
tag | string (uuid) | No |
q | stringlength 1–200 | No |
cursor | stringlength 1–512 | No |
limit | integervalue 1–50default 20 | No |
| Name | Type | Required |
|---|---|---|
x-workspace | stringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$ | No |
Response 200: 35 fields
| Name | Type | Required |
|---|---|---|
board | object | Yes |
board.id | string (uuid) | Yes |
board.name | stringlength 1–100 | Yes |
board.slug | stringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$ | Yes |
board.description | stringlength ≤ 2000 | Yes |
board.visibility | "unlisted" | Yes |
items | array of objectitems ≤ 50 | Yes |
items[].id | string (uuid) | Yes |
items[].board_id | string (uuid) | Yes |
items[].slug | stringlength 1–200pattern ^[a-z0-9]+(?:-[a-z0-9]+)*$ | Yes |
items[].title | stringlength 1–140 | Yes |
items[].body_md | stringlength ≤ 10000 | Yes |
items[].status | object | Yes |
items[].status.id | string (uuid) | Yes |
items[].status.name | stringlength 1–100 | Yes |
items[].status.color | stringpattern ^#[0-9a-fA-F]{6}$ | Yes |
items[].status.category | "open" | "active" | "done" | "closed" | Yes |
items[].tags | array of objectitems ≤ 50 | Yes |
items[].tags[].id | string (uuid) | Yes |
items[].tags[].name | stringlength 1–100 | Yes |
items[].tags[].color | stringpattern ^#[0-9a-fA-F]{6}$ | Yes |
items[].images | array of objectitems ≤ 4 | Yes |
items[].images[].id | string (uuid) | Yes |
items[].images[].url | string (uri) | Yes |
items[].images[].mime | "image/png" | "image/jpeg" | "image/webp" | Yes |
items[].images[].bytes | integervalue ≤ 5242880value > 0 | Yes |
items[].author | object | Yes |
items[].author.kind | "operator" | "end_user" | "deleted" | Yes |
items[].author.name | string | nulllength 1–100 | Yes |
items[].vote_count | integervalue ≥ 0 | Yes |
items[].comment_count | integervalue ≥ 0 | Yes |
items[].merged_into_post_id | string (uuid) | null | Yes |
items[].created_at | string (date-time) | Yes |
items[].updated_at | string (date-time) | Yes |
next_cursor | string | null | Yes |
Errors: 400, 404, 429, 500, 503, all in the standard error shape.
curl 'https://api.upvoiced.com/v1/unlisted/boards/dark-mode/posts' \
-H "X-Workspace: acme"const data = await upvoiced.get('/v1/unlisted/boards/{slug}/posts', { path: { slug: 'dark-mode' } });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)
| Name | Type | Required |
|---|---|---|
id | string (uuid) | Yes |
| Name | Type | Required |
|---|---|---|
workspace | stringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$ | No |
| Name | Type | Required |
|---|---|---|
x-workspace | stringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$ | No |
Response 200: 60 fields
| Name | Type | Required |
|---|---|---|
board | object | Yes |
board.id | string (uuid) | Yes |
board.name | stringlength 1–100 | Yes |
board.slug | stringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$ | Yes |
board.description | stringlength ≤ 2000 | Yes |
board.visibility | "unlisted" | Yes |
post | object | Yes |
post.id | string (uuid) | Yes |
post.board_id | string (uuid) | Yes |
post.slug | stringlength 1–200pattern ^[a-z0-9]+(?:-[a-z0-9]+)*$ | Yes |
post.title | stringlength 1–140 | Yes |
post.body_md | stringlength ≤ 10000 | Yes |
post.status | object | Yes |
post.status.id | string (uuid) | Yes |
post.status.name | stringlength 1–100 | Yes |
post.status.color | stringpattern ^#[0-9a-fA-F]{6}$ | Yes |
post.status.category | "open" | "active" | "done" | "closed" | Yes |
post.tags | array of objectitems ≤ 50 | Yes |
post.tags[].id | string (uuid) | Yes |
post.tags[].name | stringlength 1–100 | Yes |
post.tags[].color | stringpattern ^#[0-9a-fA-F]{6}$ | Yes |
post.images | array of objectitems ≤ 4 | Yes |
post.images[].id | string (uuid) | Yes |
post.images[].url | string (uri) | Yes |
post.images[].mime | "image/png" | "image/jpeg" | "image/webp" | Yes |
post.images[].bytes | integervalue ≤ 5242880value > 0 | Yes |
post.author | object | Yes |
post.author.kind | "operator" | "end_user" | "deleted" | Yes |
post.author.name | string | nulllength 1–100 | Yes |
post.vote_count | integervalue ≥ 0 | Yes |
post.comment_count | integervalue ≥ 0 | Yes |
post.merged_into_post_id | string (uuid) | null | Yes |
post.created_at | string (date-time) | Yes |
post.updated_at | string (date-time) | Yes |
official_reply | object | null | Yes |
official_reply.id | string (uuid) | Yes |
official_reply.post_id | string (uuid) | Yes |
official_reply.parent_id | string (uuid) | null | Yes |
official_reply.author | object | Yes |
official_reply.author.kind | "operator" | "end_user" | "deleted" | Yes |
official_reply.author.name | string | nulllength ≤ 200 | Yes |
official_reply.body_md | string | Yes |
official_reply.is_official | boolean | Yes |
official_reply.pinned | boolean | Yes |
official_reply.edited_at | string (date-time) | null | Yes |
official_reply.deleted_at | string (date-time) | null | Yes |
official_reply.created_at | string (date-time) | Yes |
comments | object | Yes |
comments.items | array of object | Yes |
comments.items[].id | string (uuid) | Yes |
comments.items[].post_id | string (uuid) | Yes |
comments.items[].parent_id | string (uuid) | null | Yes |
comments.items[].author | object | Yes |
comments.items[].body_md | string | Yes |
comments.items[].is_official | boolean | Yes |
comments.items[].pinned | boolean | Yes |
comments.items[].edited_at | string (date-time) | null | Yes |
comments.items[].deleted_at | string (date-time) | null | Yes |
comments.items[].created_at | string (date-time) | Yes |
comments.next_cursor | string | null | Yes |
Errors: 400, 404, 429, 500, 503, all in the standard error shape.
curl 'https://api.upvoiced.com/v1/unlisted/posts/0190f5e2-4c1d-7a3b-8c2d-5e6f7a8b9c0d' \
-H "X-Workspace: acme"const data = await upvoiced.get('/v1/unlisted/posts/{id}', { path: { id: '0190f5e2-4c1d-7a3b-8c2d-5e6f7a8b9c0d' } });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)
| Name | Type | Required |
|---|---|---|
id | string (uuid) | Yes |
| Name | Type | Required |
|---|---|---|
workspace | stringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$ | No |
cursor | stringlength 1–512 | No |
limit | integervalue 1–100default 50 | No |
| Name | Type | Required |
|---|---|---|
x-workspace | stringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$ | No |
Response 200: 14 fields
| Name | Type | Required |
|---|---|---|
items | array of object | Yes |
items[].id | string (uuid) | Yes |
items[].post_id | string (uuid) | Yes |
items[].parent_id | string (uuid) | null | Yes |
items[].author | object | Yes |
items[].author.kind | "operator" | "end_user" | "deleted" | Yes |
items[].author.name | string | nulllength ≤ 200 | Yes |
items[].body_md | string | Yes |
items[].is_official | boolean | Yes |
items[].pinned | boolean | Yes |
items[].edited_at | string (date-time) | null | Yes |
items[].deleted_at | string (date-time) | null | Yes |
items[].created_at | string (date-time) | Yes |
next_cursor | string | null | Yes |
Errors: 400, 404, 429, 500, 503, all in the standard error shape.
curl 'https://api.upvoiced.com/v1/unlisted/posts/0190f5e2-4c1d-7a3b-8c2d-5e6f7a8b9c0d/comments' \
-H "X-Workspace: acme"const data = await upvoiced.get('/v1/unlisted/posts/{id}/comments', { path: { id: '0190f5e2-4c1d-7a3b-8c2d-5e6f7a8b9c0d' } });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)
| Name | Type | Required |
|---|---|---|
token | stringpattern ^[A-Za-z0-9_-]{43}$ | Yes |
| Name | Type | Required |
|---|---|---|
workspace | stringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$ | No |
| Name | Type | Required |
|---|---|---|
x-workspace | stringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$ | No |
Response 200: 20 fields
| Name | Type | Required |
|---|---|---|
id | string (uuid) | Yes |
slug | stringlength 1–200pattern ^[a-z0-9]+(?:-[a-z0-9]+)*$ | Yes |
title | stringlength 1–140 | Yes |
body_html | string | Yes |
cover | object | null | Yes |
cover.id | string (uuid) | Yes |
cover.url | string (uri) | Yes |
version | string | nullpattern ^[A-Za-z0-9][A-Za-z0-9._+-]{0,31}$ | Yes |
categories | array of stringitems ≤ 5 | Yes |
linked_posts | array of objectitems ≤ 50 | Yes |
linked_posts[].id | string (uuid) | Yes |
linked_posts[].slug | stringlength 1–200pattern ^[a-z0-9]+(?:-[a-z0-9]+)*$ | Yes |
linked_posts[].title | stringlength 1–140 | Yes |
linked_posts[].board | object | Yes |
linked_posts[].board.id | string (uuid) | Yes |
linked_posts[].board.name | stringlength 1–100 | Yes |
linked_posts[].board.slug | stringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$ | Yes |
state | "draft" | "scheduled" | Yes |
scheduled_for | string (date-time) | null | Yes |
preview | true | Yes |
Errors: 400, 404, 429, 500, 503, all in the standard error shape.
curl 'https://api.upvoiced.com/v1/unlisted/changelog/preview/example-token-00000000000000000000000000000' \
-H "X-Workspace: acme"const data = await upvoiced.get('/v1/unlisted/changelog/preview/{token}', {
path: { token: 'example-token-00000000000000000000000000000' },
});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
| Name | Type | Required |
|---|---|---|
workspace | stringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$ | No |
| Name | Type | Required |
|---|---|---|
x-workspace | stringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$ | Yes |
| Name | Type | Required |
|---|---|---|
email | string (email)length ≤ 254 | Yes |
locale | "en" | "pt-BR" | "es" | "de" | "fr" | No |
website | stringlength ≤ 2048 | No |
turnstile_token | stringlength 1–2048pattern ^[\x21-\x7e]+$ | No |
Response 202: 2 fields
| Name | Type | Required |
|---|---|---|
accepted | true | Yes |
confirmation_expires_in_hours | 48 | Yes |
Errors: 400, 401, 403, 404, 409, 413, 429, 500, 503, all in the standard error shape.
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"
}'const data = await upvoiced.post('/v1/public/subscriptions/changelog', { body: { email: 'ada@example.com' } });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
| Name | Type | Required |
|---|---|---|
workspace | stringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$ | No |
| Name | Type | Required |
|---|---|---|
x-workspace | stringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$ | Yes |
| Name | Type | Required |
|---|---|---|
email | string (email)length ≤ 254 | Yes |
website | stringlength ≤ 2048 | No |
turnstile_token | stringlength 1–2048pattern ^[\x21-\x7e]+$ | No |
Response 202: 3 fields
| Name | Type | Required |
|---|---|---|
accepted | true | Yes |
code_length | 6 | Yes |
expires_in_seconds | 900 | Yes |
Errors: 400, 401, 403, 404, 409, 413, 429, 500, 503, all in the standard error shape.
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"
}'const data = await upvoiced.post('/v1/public/auth/code', { body: { email: 'ada@example.com' } });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
| Name | Type | Required |
|---|---|---|
workspace | stringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$ | No |
| Name | Type | Required |
|---|---|---|
x-workspace | stringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$ | Yes |
Response 200: 16 fields
| Name | Type | Required |
|---|---|---|
token | stringpattern ^[A-Za-z0-9_-]{43}$ | Yes |
token_type | "Bearer" | Yes |
expires_at | string (date-time) | Yes |
end_user | object | Yes |
end_user.id | string (uuid) | Yes |
end_user.email | string | nulllength ≤ 254 | Yes |
end_user.name | string | nulllength 1–100 | Yes |
end_user.avatar_url | string | nulllength ≤ 2048 | Yes |
end_user.locale | "en" | "pt-BR" | "es" | "de" | "fr" | Yes |
end_user.created_at | string (date-time) | Yes |
pending_action | object | null | Yes |
pending_action.kind | "vote" | "follow" | "comment" | "post" | Yes |
pending_action.status | "done" | "failed" | Yes |
pending_action.error_code | string | nullpattern ^[a-z0-9_]{1,64}$ | Yes |
pending_action.post_id | string (uuid) | null | Yes |
pending_action.comment_id | string (uuid) | null | Yes |
Errors: 400, 401, 403, 404, 409, 413, 429, 500, 503, all in the standard error shape.
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"
}'const data = await upvoiced.post('/v1/public/auth/verify', { body: { email: 'ada@example.com', code: '123456' } });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
| Name | Type | Required |
|---|---|---|
workspace | stringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$ | No |
| Name | Type | Required |
|---|---|---|
x-workspace | stringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$ | Yes |
| Name | Type | Required |
|---|---|---|
token | stringlength ≤ 4096pattern ^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$ | Yes |
pending_action | object | No |
Response 200: 16 fields
| Name | Type | Required |
|---|---|---|
token | stringpattern ^[A-Za-z0-9_-]{43}$ | Yes |
token_type | "Bearer" | Yes |
expires_at | string (date-time) | Yes |
end_user | object | Yes |
end_user.id | string (uuid) | Yes |
end_user.email | string | nulllength ≤ 254 | Yes |
end_user.name | string | nulllength 1–100 | Yes |
end_user.avatar_url | string | nulllength ≤ 2048 | Yes |
end_user.locale | "en" | "pt-BR" | "es" | "de" | "fr" | Yes |
end_user.created_at | string (date-time) | Yes |
pending_action | object | null | Yes |
pending_action.kind | "vote" | "follow" | "comment" | "post" | Yes |
pending_action.status | "done" | "failed" | Yes |
pending_action.error_code | string | nullpattern ^[a-z0-9_]{1,64}$ | Yes |
pending_action.post_id | string (uuid) | null | Yes |
pending_action.comment_id | string (uuid) | null | Yes |
Errors: 400, 401, 403, 404, 409, 413, 429, 500, 503, all in the standard error shape.
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"
}'const data = await upvoiced.post('/v1/public/auth/sso', { body: { token: 'feedback.example.com' } });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
| Name | Type | Required |
|---|---|---|
workspace | stringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$ | No |
| Name | Type | Required |
|---|---|---|
x-workspace | stringlength 3–32pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$ | Yes |
| Name | Type | Required |
|---|---|---|
pending_action | object | No |
turnstile_token | stringlength 1–2048pattern ^[\x21-\x7e]+$ | No |
Response 200: 16 fields
| Name | Type | Required |
|---|---|---|
token | stringpattern ^[A-Za-z0-9_-]{43}$ | Yes |
token_type | "Bearer" | Yes |
expires_at | string (date-time) | Yes |
end_user | object | Yes |
end_user.id | string (uuid) | Yes |
end_user.email | string | nulllength ≤ 254 | Yes |
end_user.name | string | nulllength 1–100 | Yes |
end_user.avatar_url | string | nulllength ≤ 2048 | Yes |
end_user.locale | "en" | "pt-BR" | "es" | "de" | "fr" | Yes |
end_user.created_at | string (date-time) | Yes |
pending_action | object | null | Yes |
pending_action.kind | "vote" | "follow" | "comment" | "post" | Yes |
pending_action.status | "done" | "failed" | Yes |
pending_action.error_code | string | nullpattern ^[a-z0-9_]{1,64}$ | Yes |
pending_action.post_id | string (uuid) | null | Yes |
pending_action.comment_id | string (uuid) | null | Yes |
Errors: 400, 401, 403, 404, 409, 413, 429, 500, 503, all in the standard error shape.
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"
}
}'const data = await upvoiced.post('/v1/public/auth/demo', {
body: {
pending_action: {
kind: 'vote',
post_id: '0190f5e2-4c1d-7a3b-8c2d-5e6f7a8b9c0d',
},
},
});No MCP tool for this operation. The MCP server covers the everyday reads and writes.