Public API

Yoofi API v1

Yoofi exposes versioned REST endpoints for public portal reads, authenticated idea workflows, and admin-scoped workspace queries.

Base URL

Use your deployment host plus /v1. This deployment uses https://www.yoofi.app/v1. Local development usually uses http://localhost:3000/v1.

Public contract

Routes under /api power the web app and can change without notice. Treat /v1 as the stable integration surface.

Quick start

The seed script creates a demo portal at /portal/public and an API key named yoofi_dev_key_123456789 for local development. Deployed workspaces can create scoped API keys from the Admin Console.

Read ideas

curl "https://www.yoofi.app/v1/portals/public/ideas?status=PLANNED&sort=top"

Query your workspace

curl -H "Authorization: Bearer YOOFI_ADMIN_KEY" \
  "https://www.yoofi.app/v1/workspace?portalSlug=public&status=PLANNED&limit=10"

Create an idea

curl -X POST "https://www.yoofi.app/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": []
  }'

Vote for an idea

curl -X POST "https://www.yoofi.app/v1/ideas/IDEA_ID/vote" \
  -H "X-API-Key: yoofi_dev_key_123456789"

List response excerpt

{
  "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
}

Authentication

Authenticated workspace and mutation endpoints accept either a browser session or an API key.

Session cookie

Use the same authenticated browser session as the web app. This is the easiest option for in-product integrations.

Bearer token

Send an API key in the Authorization header as Bearer <key>.

X-API-Key header

If you cannot set Authorization, send the same key in X-API-Key.

Admin-generated API key

Workspace admins can mint scoped keys from the Admin Console for server-to-server access.

Scopes

Yoofi checks scopes on authenticated workspace and mutation routes.

public:read

Read access for public resources. Current GET /v1 routes do not require authentication, but this scope is included on session and API-key principals.

ideas:write

Required for POST /v1/portals/:portalSlug/ideas.

votes:write

Required for vote, unvote, follow, and unfollow operations.

workspace:read

Required for GET /v1/workspace. This scope is available only on admin sessions and admin-created API keys.

admin:write

Reserved for future admin mutation routes and restricted to admin-created API keys.

Conventions

A few behaviors are shared across the public API.

TopicValueDetails
Base path/v1Public, versioned REST routes live under /v1. Internal web-app routes under /api are not part of the public contract.
Content typeapplication/jsonWrite routes expect JSON request bodies. Responses are JSON for both success and error cases.
PaginationpageThe ideas list accepts page. The current page size is fixed at 10 items per page.
Idea statusesUNDER_REVIEW, PLANNED, COMPLETEDUse these exact enum values when filtering or interpreting roadmap results.
Idea sort valuestop, new, trendingInvalid sort values fall back to top.
Admin workspace queryworkspace:readGET /v1/workspace is scoped to the authenticated workspace and supports admin sessions or admin-created keys.

Endpoint reference

The details below match the current route handlers in the app.

GET

/v1/workspace

Admin session or API key with workspace:read

Query the authenticated workspace, including products, taxonomy, recent announcements, and filtered ideas.

Query parameters

NameTypeRequiredDescription
portalSlugstringNoOptional product slug to limit ideas and announcements to one portal.
qstringNoOptional search term applied to idea title and description.
statusUNDER_REVIEW | PLANNED | COMPLETEDNoOptional idea status filter.
limitnumberNoMaximum number of idea records returned. Defaults to 25 and caps at 100.

Response

  • Returns { workspace, filters, portals, categories, tags, ideas, announcements, apiPrincipal }.
  • ideas returns { total, items } and items include portal, category, tags, and _count metadata.
  • Returns 404 with code portal_not_found when portalSlug is provided but is outside the authenticated workspace.

Notes

  • This route is workspace-scoped and never returns data from other workspaces.
  • Admin-generated keys should include workspace:read and only the additional scopes your integration needs.
GET

/v1/portals/:portalSlug/ideas

Public

List ideas for a portal with optional search, filtering, sorting, and pagination.

Query parameters

NameTypeRequiredDescription
qstringNoSearch title and description.
statusUNDER_REVIEW | PLANNED | COMPLETEDNoFilter by idea status.
tagstringNoFilter by tag slug.
categorystringNoFilter by category slug.
sorttop | new | trendingNoOrdering mode. Defaults to top.
pagenumberNo1-based page number. Values below 1 are treated as 1.

Response

  • Returns { items, page, pageSize, total, totalPages }.
  • Each item includes category, tags with nested tag records, and _count for votes and follows.
  • Returns 404 with code portal_not_found if the portal slug does not exist.

Notes

  • Only canonical, non-deleted ideas are returned.
  • The response does not currently include the full tag/category filter catalog even though the server resolves it internally.
POST

/v1/portals/:portalSlug/ideas

Session or API key with ideas:write

Create a new idea inside a portal that belongs to the authenticated workspace.

Request body

NameTypeRequiredDescription
titlestringYes5-140 characters.
descriptionstringYes10-5000 characters.
categoryIdstring | nullNoOptional category id from the same workspace.
tagIdsstring[]NoOptional tag ids from the same workspace.

Response

  • Returns 201 with { id } on success.
  • Returns 403 if the authenticated principal belongs to a different workspace than the portal.
  • Returns 400 validation_failed for malformed bodies or invalid category ids.

Notes

  • Unknown or archived tag ids are ignored rather than rejected.
  • The new idea is created in UNDER_REVIEW unless later changed by admin tooling.
GET

/v1/ideas/:ideaId

Public

Fetch a single idea record, including counts, tags, category, and the latest idea events.

Response

  • Returns { item, canonicalIdeaId, merged }.
  • item includes tags, category, _count.{votes,follows}, and up to 50 recent events ordered newest first.
  • Returns 404 with code idea_not_found if the idea does not exist or was deleted.

Notes

  • merged is true when canonicalIdeaId is set, which means the idea was merged into another record.
  • This route returns the idea by id directly and is not scoped by portal slug.
POST

/v1/ideas/:ideaId/vote

Session or API key with votes:write

Vote for an idea. Voting also ensures the caller follows the idea.

Response

  • Returns { voteCount } after the mutation.
  • The operation is idempotent: repeated POST requests keep a single vote.
  • Returns 404 if the idea is deleted, merged, missing, or outside the authenticated workspace.
DELETE

/v1/ideas/:ideaId/vote

Session or API key with votes:write

Remove the caller vote from an idea.

Response

  • Returns { voteCount } after deletion.
  • If the vote does not exist, the request still succeeds and returns the current count.
POST

/v1/ideas/:ideaId/follow

Session or API key with votes:write

Follow an idea without affecting its vote count.

Response

  • Returns { isFollowing: true } on success.
  • The operation is idempotent and keeps a single follow record per user.
DELETE

/v1/ideas/:ideaId/follow

Session or API key with votes:write

Stop following an idea.

Response

  • Returns { isFollowing: false } after deletion.
  • If the follow does not exist, the request still succeeds.
GET

/v1/tags

Public

List active tags for a workspace.

Query parameters

NameTypeRequiredDescription
workspacestringNoWorkspace slug. If omitted, the first workspace in the database is used.

Response

  • Returns { items } where items is an array of non-archived tag records.
  • If the workspace is missing, the endpoint returns { items: [] }.
GET

/v1/announcements

Public

List published announcements for a portal.

Query parameters

NameTypeRequiredDescription
portalSlugstringNoPortal slug. If omitted, the first public portal is used.

Response

  • Returns { items } ordered by publishedAt descending.
  • Each item includes createdBy.displayName.
  • Draft announcements and unpublished announcements are excluded.
GET

/v1/roadmap

Public

Return the current roadmap buckets for a portal.

Query parameters

NameTypeRequiredDescription
portalSlugstringNoPortal slug. If omitted, the first public portal is used.

Response

  • Returns { underReview, planned, completed }.
  • Each array contains canonical, non-deleted ideas with category, tags, and _count.
  • If the portal is missing, all three arrays are returned empty.

Idea detail response excerpt

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
}

Error format

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"
}

Common codes

  • auth_failed: Missing session, invalid API key, or missing required scope.
  • portal_not_found: Unknown portal slug on list or create routes.
  • workspace_not_found: Authenticated workspace no longer exists.
  • 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.