Skip to content
Feeblo Docs
Esc
navigateopen⌘Jpreview
On this page

Widget HTTP API

The four public HTTP endpoints behind the feedback widget, with request and response shapes, the auth model, error codes, and rate limits.

The embedded widget talks to your Feeblo server over a small public API. Your backend can call the same four endpoints directly to collect feedback without loading the SDK in a browser.

Base URL

Every widget route lives under /api/widget/v1 on your Feeblo server. With the default development setup that is http://localhost:3000, the API_URL value from .env.example. In production, use whatever origin serves your API, for example https://api.example.com.

Each running Feeblo server also publishes its full HTTP surface as an OpenAPI document. Open /docs in a browser for the Scalar UI, or fetch /docs/openapi.json to generate a client for your language.

Authentication

No endpoint on this API requires an API key or session. The endpoints exist so anonymous visitors can browse boards, read updates, and file feedback. Every request is rate limited by client IP instead (see Rate limits).

Identity comes from one place: the optional token field on POST /feedback. That token is the same HS256 JWT you mint for widget SSO, signed with your organization secret. When you send it, Feeblo verifies the signature against the secrets stored for your organization, binds it to your organization via its audience claim, then upserts a contact from the claims (email, name, avatar, custom fields, companies) and attaches the new post to that contact. Omit the token and the post stays anonymous.

See Widget SSO for how to sign the token on your server.

Endpoints

POST /suggestions

Given a draft title and body, returns up to five similar public posts on the board. The widget shows these before submission so visitors can react to an existing discussion instead of filing a duplicate. Matching runs on embeddings first and falls back to lexical scoring, so you get results even while the embedding backend is unavailable.

curl -X POST https://api.example.com/api/widget/v1/suggestions \
  -H "Content-Type: application/json" \
  -d '{
    "organizationId": "org_123",
    "boardId": "brd_456",
    "title": "Dark mode",
    "content": "A dark theme would be easier on my eyes at night."
  }'

Request body:

Field Type Required Notes
organizationId string yes Your Feeblo organization ID
boardId string yes Board to search
title string yes Draft title, max 200 chars
content string yes Draft body, max 20,000 chars

Response 200 OK, an array of matches (possibly empty):

[
  {
    "id": "pst_9f2ka81m",
    "title": "Dark mode support",
    "excerpt": "My eyes hurt after sunset, please add a dark theme…",
    "slug": "dark-mode-support"
  }
]

POST /feedback

Creates a feedback post on a public board. Feeblo assigns your organization’s default status automatically, sanitizes content before storage, and fires the feedback.post.created webhook event (see Webhooks).

curl -X POST https://api.example.com/api/widget/v1/feedback \
  -H "Content-Type: application/json" \
  -d '{
    "organizationId": "org_123",
    "boardId": "brd_456",
    "title": "Dark mode",
    "content": "A dark theme would be easier on my eyes at night.",
    "metadata": {
      "plan": "pro",
      "host": "app.example.com"
    },
    "token": "<signed-JWT>"
  }'

Request body:

Field Type Required Notes
organizationId string yes Your Feeblo organization ID
boardId string yes Must reference a public board
title string yes Max 200 chars
content string yes Max 20,000 chars
metadata map of string → string no Up to 20 properties, keys max 64 chars, values max 500 chars
token string no HS256 identity JWT, max 8,192 chars

Response 200 OK:

{
  "id": "pst_7tqz04vd",
  "slug": "dark-mode",
  "title": "Dark mode",
  "boardId": "brd_456",
  "organizationId": "org_123",
  "createdAt": "2025-06-01T12:00:00.000Z"
}

GET /boards

Lists the organization’s public boards with names, slugs, and timestamps, so you can render a board picker without the SDK.

curl "https://api.example.com/api/widget/v1/boards?organizationId=org_123"

Query parameters:

Field Type Required Notes
organizationId string yes Your Feeblo organization ID

Response 200 OK, an array of boards:

[
  {
    "id": "brd_456",
    "name": "Feature requests",
    "slug": "feature-requests",
    "organizationId": "org_123",
    "createdAt": "2025-03-14T09:30:00.000Z",
    "updatedAt": "2025-05-20T16:45:00.000Z"
  }
]

Private boards never appear here.

GET /updates

Returns published changelog entries for the organization, newest first. content holds sanitized HTML you can render directly; imageUrl is the cover image or null.

curl "https://api.example.com/api/widget/v1/updates?organizationId=org_123"

Query parameters:

Field Type Required Notes
organizationId string yes Your Feeblo organization ID

Response 200 OK, an array of updates:

[
  {
    "id": "chg_31kx8p2n",
    "title": "Changelog categories are here",
    "slug": "changelog-categories-are-here",
    "content": "<p>You can now group entries by category.</p>",
    "excerpt": "You can now group entries by category.",
    "imageUrl": null,
    "publishedAt": "2025-05-28T10:00:00.000Z"
  }
]

Errors

Errors return a JSON body carrying the error tag and usually a message:

{
  "_tag": "NotFoundError",
  "message": "Board not found"
}
Status _tag Where it comes from
400 DataValidationError Malformed body or query params; a private board on /feedback
401 UnauthorizedError A token that fails verification on /feedback
404 NotFoundError No board matching boardId and organizationId
500 InternalServerError Something failed on the server
429 RateLimitExceededError You crossed an endpoint’s limit
503 RateLimitUnavailableError The rate limiter itself is unreachable

message is optional on the 400, 401, and 404 bodies. Handle its absence in client code. InternalServerError always carries a message and may add a detail.

Rate limits

Limits apply per client IP, per minute:

Endpoint Limit
GET /boards 120 requests
GET /updates 120 requests
POST /feedback 20 requests
POST /suggestions 5 requests

Suggestions cost more because each call runs a similarity search. If you batch-import old feedback, keep every source IP under twenty POST /feedback calls per minute. Exceeding a limit returns 429. A 503 means the limiter itself was unreachable; back off and retry.

Next steps

Was this page helpful?