API Reference
Complete reference for all Featurely public REST API endpoints.
API Reference
All endpoints are under:
https://www.featurely.no/api/public/v1Use the demo API key (ft_demo_public, project demo) to try every example without signing in.
GET /changelog
Returns published changelog entries for your project.
Entries linked to an internal feature are omitted in SQL before limit and offset. Standalone entries (no feature) stay. count is the size of that public page, so it does not reveal hidden items.
Required permission: features:read
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
published | boolean | — | Filter by published status |
limit | number | 50 | Max entries to return (max 100) |
offset | number | 0 | Pagination offset |
curl
curl -X GET \
"https://www.featurely.no/api/public/v1/changelog?limit=10&published=true" \
-H "Authorization: Bearer ft_demo_public"JavaScript
const res = await fetch(
'https://www.featurely.no/api/public/v1/changelog?limit=10&published=true',
{
headers: {
'Authorization': 'Bearer ft_demo_public',
},
}
);
const data = await res.json();
console.log(data.entries);Response
{
"entries": [
{
"id": "clx...",
"featureId": "clx...",
"projectId": "demo",
"featureTitle": "Dark mode support",
"featureType": "feature",
"statusKey": "done",
"statusLabel": "Done",
"summary": "Added full dark mode support across the dashboard.",
"description": "<p>We've shipped dark mode...</p>",
"published": true,
"createdAt": "2024-01-15T10:00:00Z",
"publishedAt": "2024-01-16T09:00:00Z"
}
],
"count": 1,
"limit": 10,
"offset": 0
}GET /roadmap
Returns roadmap items grouped by status column.
Internal features are excluded in SQL before grouping, so column counts and total only include public items.
Required permission: features:read
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
status | string | — | Comma-separated statuses: open,planned,in-progress,in-review,accepted,done,declined |
limit | number | 100 | Max items to return (max 200) |
curl
curl -X GET \
"https://www.featurely.no/api/public/v1/roadmap?status=planned,in-progress" \
-H "Authorization: Bearer ft_demo_public"JavaScript
const res = await fetch(
'https://www.featurely.no/api/public/v1/roadmap?status=planned,in-progress',
{
headers: {
'Authorization': 'Bearer ft_demo_public',
},
}
);
const data = await res.json();
console.log(data.columns);Response
{
"columns": [
{
"status": "planned",
"items": [
{
"id": "clx...",
"title": "CSV export",
"status": "planned",
"voteCount": 42,
"commentCount": 7,
"createdAt": "2024-01-10T08:00:00Z"
}
]
}
],
"total": 1
}POST /features
Submit a feature request or a bug. The project is taken from the API key that authenticates the request. The body does not include a project id.
Required permission: features:write
Rate limit: 50 requests per 5 minutes per API key. When the limit is exceeded the response is 429.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
title | string | Yes | Title (4–120 characters) |
description | string | Yes | Description (12–1000 characters) |
contactEmail | string | No | Submitter's email address |
metadata | object | No | Arbitrary key/value metadata |
type | string | No | bug, feature, or error. Omitted defaults to feature. error is stored as a bug. Any other value returns 400. |
internal | boolean | No | Hide the item from the public board, feeds, and public read APIs. An explicit true or false always wins. When omitted, every type defaults to true for the Featurely platform project API key (iGBUmKRzcovPHJAP1Z6Y). Widget (channel: "widget"), SDK (channel: "sdk"), and every other project's API key default to false. |
channel | string | No | widget or sdk. Keeps a feedback-widget or SDK submission public even on the Featurely project key. Omit it for a normal API-key create. |
type: "bug" and type: "error" are stored as type: "bug" with status new. type: "feature" and a missing type are stored as type: "feature" with status open. In the Featurely project (iGBUmKRzcovPHJAP1Z6Y), an API-key create of any type defaults to internal: true. A widget or SDK submission stays public, as does every create on a customer API key. Pass internal to override. The response returns the stored type, status, and internal. In the dashboard, Internal (hidden from public board) starts checked only for the Featurely project.
Update internal
PATCH /api/public/v1/features/{featureId} (features:write) accepts internal together with status and developerNotes:
{ "internal": false }The patch still finds an item that is currently internal, so you can publish it again. After internal is false, the item appears on the public board, feeds, and public read APIs. Reads of an internal item (list, roadmap, changelog, status messages, votes, comments) return the same 404 as a missing item.
Any other type returns 400:
{
"error": "Invalid type. Allowed values: bug, feature, error"
}curl
curl -X POST \
"https://www.featurely.no/api/public/v1/features" \
-H "Authorization: Bearer ft_demo_public" \
-H "Content-Type: application/json" \
-d '{
"title": "Dark mode support",
"description": "It would be great to have a dark mode option for the dashboard.",
"contactEmail": "user@example.com"
}'JavaScript
const res = await fetch('https://www.featurely.no/api/public/v1/features', {
method: 'POST',
headers: {
'Authorization': 'Bearer ft_demo_public',
'Content-Type': 'application/json',
},
body: JSON.stringify({
title: 'Dark mode support',
description: 'It would be great to have a dark mode option for the dashboard.',
contactEmail: 'user@example.com',
}),
});
const data = await res.json();
console.log(data.id);Response (feature)
{
"id": "clx...",
"projectId": "demo",
"title": "Dark mode support",
"description": "It would be great to have a dark mode option for the dashboard.",
"status": "open",
"type": "feature",
"voteCount": 0,
"commentCount": 0,
"createdAt": "2024-01-15T10:00:00Z",
"message": "Feature request created successfully"
}Example: file a bug
curl -X POST \
"https://www.featurely.no/api/public/v1/features" \
-H "Authorization: Bearer ft_demo_public" \
-H "Content-Type: application/json" \
-d '{
"title": "Login button unresponsive",
"description": "Clicking the login button on Safari 17 does nothing.",
"type": "bug"
}'const res = await fetch('https://www.featurely.no/api/public/v1/features', {
method: 'POST',
headers: {
'Authorization': 'Bearer ft_demo_public',
'Content-Type': 'application/json',
},
body: JSON.stringify({
title: 'Login button unresponsive',
description: 'Clicking the login button on Safari 17 does nothing.',
type: 'bug',
}),
});
const data = await res.json();
console.log(data.type, data.status); // "bug" "new"201 response:
{
"id": "clx...",
"projectId": "demo",
"title": "Login button unresponsive",
"description": "Clicking the login button on Safari 17 does nothing.",
"status": "new",
"type": "bug",
"voteCount": 0,
"commentCount": 0,
"createdAt": "2024-01-15T10:00:00Z",
"message": "Feature request created successfully"
}GET /features
List feature requests for your project. Internal items are filtered in SQL before limit and offset. count is the number of public rows returned.
Required permission: features:read
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
status | string | — | Filter by status: open, planned, in-progress, in-review, accepted, done, declined |
type | string | — | Filter by type: feature or bug |
limit | number | 50 | Max items (max 100) |
offset | number | 0 | Pagination offset |
curl
curl -X GET \
"https://www.featurely.no/api/public/v1/features?status=open&type=feature&limit=20" \
-H "Authorization: Bearer ft_demo_public"JavaScript
const res = await fetch(
'https://www.featurely.no/api/public/v1/features?status=open&type=feature&limit=20',
{
headers: {
'Authorization': 'Bearer ft_demo_public',
},
}
);
const data = await res.json();
console.log(data.features);Response
{
"features": [
{
"id": "clx...",
"projectId": "demo",
"title": "Dark mode support",
"status": "open",
"type": "feature",
"voteCount": 42,
"commentCount": 7,
"createdAt": "2024-01-10T08:00:00Z"
}
],
"count": 1,
"limit": 20,
"offset": 0
}POST /bugs
Submit a bug report.
Required permission: bugs:write
Deduplication: if the same title + message combination is submitted within 1 hour, the occurrence count is incremented instead of creating a new record.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
title | string | Yes | Bug title (5–200 characters) |
message | string | Yes | Bug message / description (10–5000 characters) |
stackTrace | string | No | Stack trace string |
bugType | string | No | Category or type label |
severity | string | No | low, medium, high, or critical. Default: medium |
createFeature | boolean | No | Also create a linked feature request |
internal | boolean | No | With createFeature: true, set the linked board item. An explicit true or false always wins. Omitted stays public for customer API keys and for channel: "widget" or "sdk". The Featurely platform project's own API key (iGBUmKRzcovPHJAP1Z6Y) defaults that linked item to internal: true. |
channel | string | No | widget or sdk. Keeps the linked board item public even on the Featurely project key. |
metadata | object | No | Arbitrary key/value metadata |
curl
curl -X POST \
"https://www.featurely.no/api/public/v1/bugs" \
-H "Authorization: Bearer ft_demo_public" \
-H "Content-Type: application/json" \
-d '{
"title": "Login button unresponsive on Safari",
"message": "Clicking the login button on Safari 17 does nothing — no network request, no error.",
"severity": "high"
}'JavaScript
const res = await fetch('https://www.featurely.no/api/public/v1/bugs', {
method: 'POST',
headers: {
'Authorization': 'Bearer ft_demo_public',
'Content-Type': 'application/json',
},
body: JSON.stringify({
title: 'Login button unresponsive on Safari',
message: 'Clicking the login button on Safari 17 does nothing — no network request, no error.',
severity: 'high',
}),
});
const data = await res.json();
console.log(data.id);Response
{
"id": "clx...",
"message": "Bug report submitted successfully."
}POST /errors
Report an application error with full context.
Required permission: errors:write
Deduplication: if the same title + errorType combination is received within 1 hour, the occurrence count is incremented instead of creating a new record.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
title | string | Yes | Error title (5–200 characters) |
message | string | Yes | Error message (10–5000 characters) |
stackTrace | string | No | Full stack trace |
errorType | string | No | Error class or category |
severity | string | No | low, medium, high, or critical. Default: medium |
breadcrumbs | array | No | Up to 50 breadcrumb events leading up to the error |
device | object | No | Device and browser information |
network | object | No | Network conditions at time of error |
app | object | No | Application context (version, environment, session, user) |
url | string | No | URL where the error occurred |
referrer | string | No | Referring URL |
timestamp | string | No | ISO 8601 timestamp of the error |
metadata | object | No | Arbitrary key/value metadata |
Breadcrumb object
| Field | Type | Required | Description |
|---|---|---|---|
timestamp | string | Yes | ISO 8601 timestamp |
type | string | Yes | navigation, click, input, http, console, or custom |
message | string | Yes | Breadcrumb message |
category | string | No | Category label |
level | string | No | Severity level |
data | object | No | Additional data |
Device object
| Field | Type | Description |
|---|---|---|
userAgent | string | Full user agent string |
browser | string | Browser name |
browserVersion | string | Browser version |
os | string | Operating system |
osVersion | string | OS version |
device | string | Device type |
screenResolution | string | Screen resolution |
language | string | Browser language |
timezone | string | Timezone identifier |
Network object
| Field | Type | Description |
|---|---|---|
effectiveType | string | Connection type (e.g. 4g) |
downlink | number | Downlink speed in Mbps |
rtt | number | Round-trip time in ms |
online | boolean | Whether the device was online |
App object
| Field | Type | Description |
|---|---|---|
version | string | App version string |
environment | string | development, staging, or production |
releaseId | string | Release identifier (e.g. git commit hash) |
sessionId | string | Session identifier |
userId | string | User identifier |
userEmail | string | User email address |
curl
curl -X POST \
"https://www.featurely.no/api/public/v1/errors" \
-H "Authorization: Bearer ft_demo_public" \
-H "Content-Type: application/json" \
-d '{
"title": "TypeError: Cannot read properties of undefined",
"message": "Cannot read properties of undefined (reading '\''map'\'')",
"errorType": "TypeError",
"severity": "high",
"app": {
"version": "1.2.3",
"environment": "production",
"userId": "usr_123"
}
}'JavaScript
const res = await fetch('https://www.featurely.no/api/public/v1/errors', {
method: 'POST',
headers: {
'Authorization': 'Bearer ft_demo_public',
'Content-Type': 'application/json',
},
body: JSON.stringify({
title: 'TypeError: Cannot read properties of undefined',
message: "Cannot read properties of undefined (reading 'map')",
errorType: 'TypeError',
severity: 'high',
app: {
version: '1.2.3',
environment: 'production',
userId: 'usr_123',
},
}),
});
const data = await res.json();
console.log(data.id, data.duplicate);Response
{
"id": "clx...",
"message": "Error reported successfully.",
"duplicate": false
}POST /logs
Send structured log entries from any JavaScript or TypeScript application.
Required permission: logs:write
Accepts a batch of up to 100 log entries per request. If the same level + category + message fingerprint is received repeatedly, the server increments an occurrence counter rather than creating duplicate rows.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
logs | array | Yes | Array of log entry objects (max 100) |
Each log entry in the array:
| Field | Type | Required | Description |
|---|---|---|---|
level | string | Yes | error, warn, info, debug, or trace |
message | string | Yes | Log message (max 5,000 characters) |
category | string | No | Group label (e.g. "auth", "payment") |
source | string | No | Origin tag (e.g. "server", "worker") |
data | object | No | Arbitrary key/value metadata |
userId | string | No | User identifier for filtering in the dashboard |
appVersion | string | No | Application version string |
fingerprint | string | No | Custom dedup key — overrides the server-computed fingerprint |
timestamp | string | No | ISO 8601 timestamp. Defaults to server receive time |
curl
curl -X POST \
"https://www.featurely.no/api/public/v1/logs" \
-H "X-API-Key: ft_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"logs": [
{
"level": "error",
"message": "Stripe webhook failed",
"category": "billing",
"data": { "statusCode": 500 },
"userId": "u_123"
}
]
}'JavaScript
const res = await fetch('https://www.featurely.no/api/public/v1/logs', {
method: 'POST',
headers: {
'X-API-Key': 'ft_live_your_api_key',
'Content-Type': 'application/json',
},
body: JSON.stringify({
logs: [
{
level: 'error',
message: 'Stripe webhook failed',
category: 'billing',
data: { statusCode: 500 },
userId: 'u_123',
},
],
}),
});
const data = await res.json();
console.log(data.accepted); // number of entries storedResponse
{
"accepted": 1
}Use the
featurely-loggerSDK to batch and flush logs automatically rather than calling this endpoint directly.
POST /i18n-telemetry
Report missing translation keys detected at runtime. This endpoint is called automatically by the featurely-i18n SDK — you do not need to call it directly.
Required permission: public:read
Request body
| Field | Type | Required | Description |
|---|---|---|---|
locale | string | Yes | BCP 47 locale code (e.g. en, no) |
missingKeys | string[] | Yes | Translation keys that were missing (max 100 per request) |
Response
{ "ok": true }GET /site-config
Returns the full site configuration for a project — maintenance mode, status messages, feature flags, and environments. This endpoint is public and does not require an API key.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
projectId | string | Yes | Your project ID |
curl
curl -X GET \
"https://www.featurely.no/api/public/v1/site-config?projectId=demo"JavaScript
const res = await fetch(
'https://www.featurely.no/api/public/v1/site-config?projectId=demo'
);
const data = await res.json();
console.log(data.maintenance, data.featureFlags);Response
{
"maintenance": {
"enabled": false,
"type": "full",
"customHtml": null,
"expectedRestoration": null,
"showStatusLink": false,
"statusPageUrl": null,
"whitelist": {
"localStorageKeys": [],
"emails": [],
"ips": []
}
},
"messages": [
{
"id": "msg_...",
"projectId": "demo",
"type": "info",
"text": "Scheduled maintenance on Sunday 2 AM UTC.",
"isActive": true,
"priority": 1,
"startsAt": null,
"expiresAt": null
}
],
"featureFlags": [
{
"id": "flag_...",
"projectId": "demo",
"key": "new_dashboard",
"enabled": true
}
],
"debugMode": false,
"environments": [
{
"id": "env_...",
"name": "Production",
"slug": "production",
"url": "https://example.com",
"color": "#22c55e",
"debugEnabled": false
}
],
"lastUpdated": "2024-01-15T10:00:00Z"
}GET /translations
Returns translation strings for a given locale.
Required permission: public:read
Use the hash value from the response as If-None-Match on subsequent requests — the API returns 304 Not Modified when nothing has changed, saving bandwidth.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
locale | string | en | BCP 47 locale code (e.g. en, no, fr) |
namespace | string | — | Scope the response to a single namespace |
curl
curl -X GET \
"https://www.featurely.no/api/public/v1/translations?locale=en" \
-H "Authorization: Bearer ft_demo_public"JavaScript
const res = await fetch(
'https://www.featurely.no/api/public/v1/translations?locale=en',
{
headers: {
'Authorization': 'Bearer ft_demo_public',
},
}
);
const data = await res.json();
// On subsequent calls, pass the hash to avoid re-downloading unchanged data:
// headers: { 'If-None-Match': data.hash }
console.log(data.translations);Response
{
"translations": {
"nav.home": "Home",
"nav.features": "Features",
"hero.title": "Build better products"
},
"hash": "etag_abc123",
"locale": "en"
}GET /version-check
Checks whether a newer version of your application is available. This endpoint is public and does not require an API key.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
projectId | string | Yes | Your project ID |
currentVersion | string | No | Current version string — SemVer (e.g. 1.2.3) or git hash |
curl
curl -X GET \
"https://www.featurely.no/api/public/v1/version-check?projectId=demo¤tVersion=1.0.0"JavaScript
const res = await fetch(
'https://www.featurely.no/api/public/v1/version-check?projectId=demo¤tVersion=1.0.0'
);
const data = await res.json();
if (data.updateAvailable) {
console.log('New version:', data.latestVersion.version);
}Response
{
"updateAvailable": true,
"updateRequired": false,
"updateRecommended": true,
"updateType": "minor",
"latestVersion": {
"version": "1.2.0",
"title": "January release",
"releaseNotes": "New dashboard, bug fixes, performance improvements.",
"downloadUrl": "https://example.com/download/1.2.0",
"releaseDate": "2024-01-15T10:00:00Z",
"isBeta": false
}
}