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.