Base URL
Use your deployment host plus /v1. Locally that is usually http://localhost:3000/v1.
Yoofi exposes versioned REST endpoints for public portal reads plus authenticated idea creation, voting, and follow workflows.
Use your deployment host plus /v1. Locally that is usually http://localhost:3000/v1.
Routes under /api power the web app and can change without notice. Treat /v1 as the stable integration surface.
The seed script creates a demo portal at /portal/public and an API key named yoofi_dev_key_123456789 for local development.
curl "http://localhost:3000/v1/portals/public/ideas?status=PLANNED&sort=top"curl -X POST "http://localhost:3000/v1/portals/public/ideas" \
-H "Authorization: Bearer yoofi_dev_key_123456789" \
-H "Content-Type: application/json" \
-d '{
"title": "Add dark mode for portal pages",
"description": "Customers need a dark theme for public portal browsing on low-light devices.",
"categoryId": null,
"tagIds": []
}'curl -X POST "http://localhost:3000/v1/ideas/IDEA_ID/vote" \
-H "X-API-Key: yoofi_dev_key_123456789"{
"items": [
{
"id": "cm9exampleidea",
"title": "Publish webhook retries with delivery logs",
"description": "Expose webhook retry history so integration teams can debug failures without support tickets.",
"status": "PLANNED",
"targetTimeframe": "Q2 2026",
"category": {
"id": "cm9category",
"name": "Integrations",
"slug": "integrations"
},
"tags": [
{
"tag": {
"id": "cm9tag",
"name": "API",
"slug": "api"
}
}
],
"_count": {
"votes": 1,
"follows": 1
}
}
],
"page": 1,
"pageSize": 10,
"total": 1,
"totalPages": 1
}Write endpoints accept either a browser session or an API key.
Use the same authenticated browser session as the web app. This is the easiest option for in-product integrations.
Send an API key in the Authorization header as Bearer <key>.
If you cannot set Authorization, send the same key in X-API-Key.
Yoofi checks scopes only on authenticated write operations today.
public:readRead access for public resources. Current GET /v1 routes do not require authentication, but this scope is included on session and API-key principals.
ideas:writeRequired for POST /v1/portals/:portalSlug/ideas.
votes:writeRequired for vote, unvote, follow, and unfollow operations.
A few behaviors are shared across the public API.
| Topic | Value | Details |
|---|---|---|
| Base path | /v1 | Public, versioned REST routes live under /v1. Internal web-app routes under /api are not part of the public contract. |
| Content type | application/json | Write routes expect JSON request bodies. Responses are JSON for both success and error cases. |
| Pagination | page | The ideas list accepts page. The current page size is fixed at 10 items per page. |
| Idea statuses | UNDER_REVIEW, PLANNED, COMPLETED | Use these exact enum values when filtering or interpreting roadmap results. |
| Idea sort values | top, new, trending | Invalid sort values fall back to top. |
The details below match the current route handlers in the app.
List ideas for a portal with optional search, filtering, sorting, and pagination.
| Name | Type | Required | Description |
|---|---|---|---|
q | string | No | Search title and description. |
status | UNDER_REVIEW | PLANNED | COMPLETED | No | Filter by idea status. |
tag | string | No | Filter by tag slug. |
category | string | No | Filter by category slug. |
sort | top | new | trending | No | Ordering mode. Defaults to top. |
page | number | No | 1-based page number. Values below 1 are treated as 1. |
Create a new idea inside a portal that belongs to the authenticated workspace.
| Name | Type | Required | Description |
|---|---|---|---|
title | string | Yes | 5-140 characters. |
description | string | Yes | 10-5000 characters. |
categoryId | string | null | No | Optional category id from the same workspace. |
tagIds | string[] | No | Optional tag ids from the same workspace. |
Fetch a single idea record, including counts, tags, category, and the latest idea events.
Vote for an idea. Voting also ensures the caller follows the idea.
Remove the caller vote from an idea.
Follow an idea without affecting its vote count.
Stop following an idea.
List active tags for a workspace.
| Name | Type | Required | Description |
|---|---|---|---|
workspace | string | No | Workspace slug. If omitted, the first workspace in the database is used. |
List published announcements for a portal.
| Name | Type | Required | Description |
|---|---|---|---|
portalSlug | string | No | Portal slug. If omitted, the first public portal is used. |
Return the current roadmap buckets for a portal.
| Name | Type | Required | Description |
|---|---|---|---|
portalSlug | string | No | Portal slug. If omitted, the first public portal is used. |
GET /v1/ideas/:ideaId returns the idea record plus merge metadata.
{
"item": {
"id": "cm9exampleidea",
"title": "Add advanced trend filters on analytics dashboard",
"status": "UNDER_REVIEW",
"canonicalIdeaId": null,
"category": {
"id": "cm9category",
"name": "User Experience",
"slug": "ux"
},
"tags": [
{
"tag": {
"id": "cm9tag",
"name": "Analytics",
"slug": "analytics"
}
}
],
"_count": {
"votes": 2,
"follows": 2
},
"events": [
{
"id": "cm9event",
"type": "CREATED",
"payload": "{\"message\":\"Idea submitted\"}"
}
]
},
"canonicalIdeaId": null,
"merged": false
}Public API errors use a consistent JSON envelope.
{
"code": "validation_failed",
"message": "Title must contain at least 5 character(s)",
"requestId": "0f5b5cc4-5db1-40bb-9f74-df0d497f0eb4"
}auth_failed: Missing session, invalid API key, or missing required scope.portal_not_found: Unknown portal slug on list or create routes.idea_not_found: Unknown, deleted, merged, or out-of-workspace idea for the current route.validation_failed: Body parsing failed or a field failed validation.forbidden: Authenticated principal tried to write into a different workspace.