Featurely Docs

API Reference

Complete reference for all Featurely public REST API endpoints.

API Reference

All endpoints are under:

https://www.featurely.no/api/public/v1

Use 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

NameTypeDefaultDescription
publishedboolean—Filter by published status
limitnumber50Max entries to return (max 100)
offsetnumber0Pagination 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

NameTypeDefaultDescription
statusstring—Comma-separated statuses: open,planned,in-progress,in-review,accepted,done,declined
limitnumber100Max 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

FieldTypeRequiredDescription
titlestringYesTitle (4–120 characters)
descriptionstringYesDescription (12–1000 characters)
contactEmailstringNoSubmitter's email address
metadataobjectNoArbitrary key/value metadata
typestringNobug, feature, or error. Omitted defaults to feature. error is stored as a bug. Any other value returns 400.
internalbooleanNoHide 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.
channelstringNowidget 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

NameTypeDefaultDescription
statusstring—Filter by status: open, planned, in-progress, in-review, accepted, done, declined
typestring—Filter by type: feature or bug
limitnumber50Max items (max 100)
offsetnumber0Pagination 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

FieldTypeRequiredDescription
titlestringYesBug title (5–200 characters)
messagestringYesBug message / description (10–5000 characters)
stackTracestringNoStack trace string
bugTypestringNoCategory or type label
severitystringNolow, medium, high, or critical. Default: medium
createFeaturebooleanNoAlso create a linked feature request
internalbooleanNoWith 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.
channelstringNowidget or sdk. Keeps the linked board item public even on the Featurely project key.
metadataobjectNoArbitrary 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

FieldTypeRequiredDescription
titlestringYesError title (5–200 characters)
messagestringYesError message (10–5000 characters)
stackTracestringNoFull stack trace
errorTypestringNoError class or category
severitystringNolow, medium, high, or critical. Default: medium
breadcrumbsarrayNoUp to 50 breadcrumb events leading up to the error
deviceobjectNoDevice and browser information
networkobjectNoNetwork conditions at time of error
appobjectNoApplication context (version, environment, session, user)
urlstringNoURL where the error occurred
referrerstringNoReferring URL
timestampstringNoISO 8601 timestamp of the error
metadataobjectNoArbitrary key/value metadata
FieldTypeRequiredDescription
timestampstringYesISO 8601 timestamp
typestringYesnavigation, click, input, http, console, or custom
messagestringYesBreadcrumb message
categorystringNoCategory label
levelstringNoSeverity level
dataobjectNoAdditional data

Device object

FieldTypeDescription
userAgentstringFull user agent string
browserstringBrowser name
browserVersionstringBrowser version
osstringOperating system
osVersionstringOS version
devicestringDevice type
screenResolutionstringScreen resolution
languagestringBrowser language
timezonestringTimezone identifier

Network object

FieldTypeDescription
effectiveTypestringConnection type (e.g. 4g)
downlinknumberDownlink speed in Mbps
rttnumberRound-trip time in ms
onlinebooleanWhether the device was online

App object

FieldTypeDescription
versionstringApp version string
environmentstringdevelopment, staging, or production
releaseIdstringRelease identifier (e.g. git commit hash)
sessionIdstringSession identifier
userIdstringUser identifier
userEmailstringUser 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

FieldTypeRequiredDescription
logsarrayYesArray of log entry objects (max 100)

Each log entry in the array:

FieldTypeRequiredDescription
levelstringYeserror, warn, info, debug, or trace
messagestringYesLog message (max 5,000 characters)
categorystringNoGroup label (e.g. "auth", "payment")
sourcestringNoOrigin tag (e.g. "server", "worker")
dataobjectNoArbitrary key/value metadata
userIdstringNoUser identifier for filtering in the dashboard
appVersionstringNoApplication version string
fingerprintstringNoCustom dedup key — overrides the server-computed fingerprint
timestampstringNoISO 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 stored

Response

{
  "accepted": 1
}

Use the featurely-logger SDK 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

FieldTypeRequiredDescription
localestringYesBCP 47 locale code (e.g. en, no)
missingKeysstring[]YesTranslation 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

NameTypeRequiredDescription
projectIdstringYesYour 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

NameTypeDefaultDescription
localestringenBCP 47 locale code (e.g. en, no, fr)
namespacestring—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

NameTypeRequiredDescription
projectIdstringYesYour project ID
currentVersionstringNoCurrent 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&currentVersion=1.0.0"

JavaScript

const res = await fetch(
  'https://www.featurely.no/api/public/v1/version-check?projectId=demo&currentVersion=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
  }
}