JavaScript SDK
The FeedbackKit JavaScript SDK is a TypeScript client for listing, submitting and voting on feedback from any web app, in the browser or in Node.js. Create a client with your project API key and build the feedback UI your site needs on top of it.
Installation
Install the FeedbackKit JavaScript SDK using your preferred package manager.
# Using npm
npm install feedbackkit-js
# Using yarn
yarn add feedbackkit-js
# Using pnpm
pnpm add feedbackkit-js
The SDK is written in TypeScript and ships with full type definitions. No additional @types package is needed. There is no UMD or IIFE build and no global. Node.js 18+ is required because the client uses global fetch. The registry copy is 1.0.1. Snippets on this page are tag 2.1.0. That tag does not commit dist/, so a git dependency on it has to be built before it can be imported.
Quick Start
Initialize the client with your project API key and start collecting feedback in minutes.
import { FeedbackKit, FeedbackCategory } from 'feedbackkit-js';
// Initialize the client
const client = new FeedbackKit({
apiKey: 'your-api-key',
userId: 'user_12345'
});
// List feedback
const feedbacks = await client.feedback.list();
// Submit feedback
const newFeedback = await client.feedback.create({
title: 'Add dark mode',
description: 'Would love a dark mode option',
category: FeedbackCategory.FeatureRequest,
userId: 'user_12345'
});
Configuration
The FeedbackKitConfig interface accepts the following options when creating a new client instance.
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
| apiKey | string | Yes | — | Your project API key |
| userId | string | No | local_<uuid> | Session id. Omit it and a local_ UUID is generated. 1.0.1 does not generate one. |
| baseUrl | string | No | https://api.prod.getfeedbackkit.com/api/v1 | https only, except http on localhost, 127.0.0.1, and ::1. Redirects are refused. 1.0.1 defaults to https://feedbackkit.swiftly-workspace.com/api/v1 and allows any http URL. |
| bearerToken | string | No | — | Server credential for admin replies. Leave unset in a browser app. Not in 1.0.1. |
| timeout | number | No | 30000 | Request timeout in ms |
Anonymous id, https, and where it runs
With no userId, 2.1.0 stores local_<uuid> in localStorage under feedbackkit_user_id when storage works. Node and a throwing localStorage keep it in memory. setUserId(undefined) clears it. The next request generates a new one and sends it as X-User-Id. Create, vote, unvote, and comment copy it into user_id when that call omits userId. A different userId on the call does not replace the session id, and the header is omitted for that call so the two cannot disagree. users.register and events.track still require their own userId.
A non-loopback http:// base URL throws ValidationError. fetch uses redirect: 'error'. The client does not retry. A 429 is RateLimitError, and retryAfterSeconds is set from Retry-After when that header is delta-seconds or an HTTP date. 1.0.1 turns 429 and 5xx into the base FeedbackKitError with code SERVER_ERROR and ignores Retry-After.
In the browser the project API key is in the bundle. It identifies a project, not a person. On the SDK API this package calls, CORS allows FeedbackKit's own origins and a few localhost ports. A customer origin is not on that list, and there is no project setting that adds one, so a browser fetch from your production site fails before you see a status. Call the SDK from Node on your server instead. The embed API is the exception: /api/v1/embed/, which the script widget (/embed/feedbackkit-embed.js) calls, accepts cross-origin requests from any site, without cookies or credentials. So the script widget, or an iframe of /embed/<apiKey>/kanban, /roadmap, or /form, works on any site where embedding is turned on for the project. 1.0.1 does not generate an anonymous id, so a client with no userId sends no X-User-Id and every row has hasVoted: false. 1.0.1 also returns JSON keys as the server sent them (vote_count). This source camelCases response keys. values and event properties stay verbatim.
Client Methods
import { FeedbackKit } from 'feedbackkit-js';
const client = new FeedbackKit({
apiKey: 'sf_your_api_key',
userId: 'user_12345',
timeout: 10000
});
// Update user ID after login
client.setUserId('user_12345');
// Get current user ID
const userId = client.getUserId();
// Clear user ID on logout
client.setUserId(undefined);
API Reference
Feedback API
client.feedback
| Method | Description |
|---|---|
| list(options?: ListFeedbackOptions): Promise<Feedback[]> | List all feedback with optional filters |
| get(feedbackId: string): Promise<Feedback> | Get single feedback by ID |
| create(request: CreateFeedbackRequest): Promise<Feedback> | Submit new feedback |
import { FeedbackKit, FeedbackCategory, FeedbackSort, FeedbackStatus } from 'feedbackkit-js';
const client = new FeedbackKit({ apiKey: 'sf_your_api_key' });
// Get all feedback
const all = await client.feedback.list();
// Filter by status
const pending = await client.feedback.list({
status: FeedbackStatus.Pending
});
// Filter by category
const bugs = await client.feedback.list({
category: FeedbackCategory.BugReport
});
// Include merged items
const withMerged = await client.feedback.list({
includeMerged: true,
sort: FeedbackSort.Newest
});
// Get single feedback
const feedback = await client.feedback.get('feedback-uuid');
console.log(feedback.title, feedback.voteCount);
// Submit new feedback
const created = await client.feedback.create({
title: 'Add dark mode',
description: 'Please add a dark mode option for night time use.',
category: FeedbackCategory.FeatureRequest,
userId: 'user_12345',
userEmail: 'user@example.com' // optional
});
Votes API
client.votes
| Method | Description |
|---|---|
| vote(feedbackId: string, request?: VoteRequest): Promise<VoteResponse> | Vote for feedback. The request defaults to an empty object. On npm 1.0.1 the request argument and its userId are required. |
| unvote(feedbackId: string, request?: UnvoteRequest): Promise<VoteResponse> | Remove a vote. The request defaults to an empty object. On npm 1.0.1 the request argument and its userId are required. |
import { FeedbackKit } from 'feedbackkit-js';
const client = new FeedbackKit({ apiKey: 'sf_your_api_key' });
// Simple vote. userId is optional in 2.1.0. It is required on npm 1.0.1.
const voted = await client.votes.vote('feedback-id', {
userId: 'user_12345'
});
// Vote with email notification opt-in. Stored only on the Team tier, and only with an email.
const notified = await client.votes.vote('feedback-id', {
userId: 'user_12345',
email: 'user@example.com',
notifyStatusChange: true
});
// Remove a vote
const removed = await client.votes.unvote('feedback-id', {
userId: 'user_12345'
});
console.log(removed.hasVoted); // false
Comments API
client.comments
| Method | Description |
|---|---|
| list(feedbackId: string): Promise<Comment[]> | List comments for feedback |
| create(feedbackId: string, request: CreateCommentRequest): Promise<Comment> | Add comment |
import { FeedbackKit } from 'feedbackkit-js';
const client = new FeedbackKit({
apiKey: 'sf_your_api_key',
bearerToken: 'server-bearer-token'
});
// List all comments
const comments = await client.comments.list('feedback-id');
for (const comment of comments) {
console.log(`${comment.userId}: ${comment.content}`);
if (comment.isAdmin) console.log('(Admin response)');
}
// Add a user comment
const comment = await client.comments.create('feedback-id', {
content: 'This would be really helpful!',
userId: 'user_12345'
});
// Admin response. bearerToken is required. Do not ship it in a browser bundle.
const adminComment = await client.comments.create('feedback-id', {
content: 'Thanks for the feedback! We are working on this.',
userId: 'admin_user',
isAdmin: true
});
Users API
client.users
| Method | Description |
|---|---|
| register(request: RegisterUserRequest): Promise<SDKUser> | Register SDK user |
import { FeedbackKit } from 'feedbackkit-js';
const client = new FeedbackKit({ apiKey: 'sf_your_api_key' });
// Register a free user. userId is required. The session id is not copied in.
const user = await client.users.register({
userId: 'user_12345'
});
// Register a paying user with MRR
const payingUser = await client.users.register({
userId: 'user_67890',
mrr: 9.99
});
// Update MRR when subscription changes
const upgraded = await client.users.register({
userId: 'user_67890',
mrr: 19.99
});
Events API
client.events
| Method | Description |
|---|---|
| track(request: TrackEventRequest): Promise<TrackedEvent> | Track analytics event |
import { FeedbackKit, SDKEvents } from 'feedbackkit-js';
const client = new FeedbackKit({ apiKey: 'sf_your_api_key' });
// Track a simple event
await client.events.track({
eventName: 'feedback_list',
userId: 'user_12345'
});
// Track with properties
await client.events.track({
eventName: 'feedback_list',
userId: 'user_12345',
properties: {
filter: 'feature_request',
sort: 'votes'
}
});
// Using predefined SDK events
await client.events.track({
eventName: SDKEvents.FeedbackList,
userId: 'user_12345'
});
// Track a custom event
await client.events.track({
eventName: 'onboarding_completed',
userId: 'user_12345',
properties: {
step_count: 5,
duration_seconds: 120
}
});
Predefined SDK Events:
SDKEvents.FeedbackList,
SDKEvents.FeedbackDetail,
SDKEvents.SubmitFeedback
Models & Types
Feedback
| Field | Type | Description |
|---|---|---|
| id | string | Unique identifier |
| title | string | Feedback title |
| description | string | Detailed description |
| status | FeedbackStatus | Current status |
| category | FeedbackCategory | Feedback category |
| userId | string | ID of the user who submitted |
| userEmail | string? | Email of the submitter (if provided) |
| voteCount | number | Total number of votes |
| hasVoted | boolean | Whether the current user has voted |
| commentCount | number | Total number of comments |
| totalMrr | number? | Combined MRR of all voters |
| createdAt | string | When the feedback was created |
| updatedAt | string | When the feedback was last updated |
| rejectionReason | string? | Explanation for rejection |
| mergedIntoId | string? | ID of feedback this was merged into |
| mergedAt | string? | When this feedback was merged |
| mergedFeedbackIds | string[]? | IDs of feedback items merged into this one |
| isHiddenOverLimit | boolean? | Free-tier cap. The write still returns 2xx. Later reads hide it except from that submitter. Not on the npm 1.0.1 type. |
Enums
FeedbackStatus
| Value | Raw |
|---|---|
| Pending | "pending" |
| Approved | "approved" |
| InProgress | "in_progress" |
| TestFlight | "testflight" |
| Completed | "completed" |
| Rejected | "rejected" |
FeedbackCategory
| Value | Raw |
|---|---|
| FeatureRequest | "feature_request" |
| BugReport | "bug_report" |
| Improvement | "improvement" |
| Other | "other" |
Comment
| Field | Type | Description |
|---|---|---|
| id | string | Unique identifier |
| content | string | Comment text |
| userId | string | ID of the commenting user |
| isAdmin | boolean | Whether this is an admin comment |
| createdAt | string | When the comment was created |
VoteResponse
| Field | Type | Description |
|---|---|---|
| feedbackId | string | ID of the feedback item |
| voteCount | number | Updated vote count |
| hasVoted | boolean | Whether the user has voted after this action |
Request Types
CreateFeedbackRequest
| Field | Type | Required | Description |
|---|---|---|---|
| title | string | Yes | Brief title (1-200 chars) |
| description | string | Yes | Detailed description (1-5000 chars) |
| category | FeedbackCategory | Yes | Feedback category |
| userId | string | No | Defaults to the session id. Required on npm 1.0.1. |
| userEmail | string | No | Optional email for status update notifications |
VoteRequest
| Field | Type | Required | Description |
|---|---|---|---|
| userId | string | No | Defaults to the session id. Required on npm 1.0.1. Status emails need Team and a non-empty email. |
| string | No | Email for status change notifications | |
| notifyStatusChange | boolean | No | Opt-in to receive email notifications |
UnvoteRequest
| Field | Type | Required | Description |
|---|---|---|---|
| userId | string | No | Defaults to the session id. Required on npm 1.0.1. |
CreateCommentRequest
| Field | Type | Required | Description |
|---|---|---|---|
| content | string | Yes | Comment text (1-2000 chars) |
| userId | string | No | Defaults to the session id. Required on npm 1.0.1. isAdmin needs bearerToken in 2.1.0. |
| isAdmin | boolean | No | Whether this comment is from an admin/developer |
ListFeedbackOptions
| Field | Type | Required | Description |
|---|---|---|---|
| status | FeedbackStatus | No | Filter by status |
| category | FeedbackCategory | No | Filter by category |
| includeMerged | boolean | No | Include merged feedback items. Sent as the query key includeMerged. |
| sort | FeedbackSort | No | votes (default), newest, oldest, or comments. Not on npm 1.0.1. |
RegisterUserRequest
| Field | Type | Required | Description |
|---|---|---|---|
| userId | string | Yes | Unique identifier of the SDK user |
| mrr | number | No | Monthly Recurring Revenue |
TrackEventRequest
| Field | Type | Required | Description |
|---|---|---|---|
| eventName | string | Yes | Name of the event to track |
| userId | string | Yes | Unique identifier of the user |
| properties | Record<string, unknown> | No | Optional key-value properties for the event |
Metrics
feedbackKit.metrics first ships in 2.1.0. It is not in npm 1.0.1 or in tags 1.1.1 and 1.2.0. It exposes four reads for the project the API key identifies. Pass days or dateFrom / dateTo. A window wider than the plan allows returns HTTP 200 with meta.retentionClamped. Read meta.retentionWindowDays from the response. The windows are Free 30 days, Pro 90 days, and Team 365 days. Below Pro, custom event names are stored and a read sets meta.eventScope to predefined_only. A breakdown by event name, a custom event-name filter, or the raw event feed is 402 METRICS_CUSTOM_EVENTS_REQUIRE_PRO. The four reads share a cap of 60 requests per 60 seconds. The client does not retry that 429. Bearer-only measures (sdk_user_count, mrr_sum, mrr_average, users_with_mrr) are 403 METRICS_MEASURE_REQUIRES_BEARER.
summary,timeseries,breakdown, andevents.- Measure maps are sparse. Use
metricValue. A breakdown overflow row omitsunique_event_users. - Empty timeseries buckets are present and zero.
import { FeedbackKit, MetricMeasure, metricValue } from 'feedbackkit-js';
const feedbackKit = new FeedbackKit({ apiKey: 'sf_your_api_key' });
const summary = await feedbackKit.metrics.summary({
measures: [MetricMeasure.FeedbackCount, MetricMeasure.EventCount],
days: 7
});
metricValue(summary.values, MetricMeasure.FeedbackCount);
Error Handling
The SDK provides a typed error hierarchy so you can handle specific error cases. All errors extend the base FeedbackKitError class.
| Error Class | Status Code | Code | Description |
|---|---|---|---|
| FeedbackKitError | — | — | Base error class (statusCode, code, message) |
| AuthenticationError | 401 | UNAUTHORIZED | Invalid or missing API key |
| PaymentRequiredError | 402 | PAYMENT_REQUIRED | A subscription gate. The Free feedback cap is not a 402. |
| ForbiddenError | 403 | FORBIDDEN | Action not allowed (archived project, voting blocked) |
| NotFoundError | 404 | NOT_FOUND | Resource not found |
| ConflictError | 409 | CONFLICT | Duplicate action (e.g., already voted) |
| ValidationError | 400 or 422 | BAD_REQUEST / UNPROCESSABLE_ENTITY | Validation failed. 422 when the body has no machine code. A missing apiKey throws this before the request. |
| RateLimitError | 429 | RATE_LIMITED | retryAfterSeconds from Retry-After. The client does not retry. Not in npm 1.0.1. |
| ServerError | 5xx | SERVER_ERROR | The server failed. Not in npm 1.0.1, where 5xx is the base class. |
| NetworkError | 0 | NETWORK_ERROR | Network connectivity issue |
import {
FeedbackKit,
FeedbackCategory,
FeedbackKitError,
AuthenticationError,
PaymentRequiredError,
NotFoundError,
ConflictError,
RateLimitError,
ServerError,
NetworkError
} from 'feedbackkit-js';
const client = new FeedbackKit({ apiKey: 'sf_your_api_key' });
try {
const feedback = await client.feedback.create({
title: 'My feedback',
description: 'Details here',
category: FeedbackCategory.FeatureRequest,
userId: 'user_12345'
});
} catch (error) {
if (error instanceof AuthenticationError) {
console.error('Invalid API key');
} else if (error instanceof PaymentRequiredError) {
console.error('Upgrade your plan to continue');
} else if (error instanceof NotFoundError) {
console.error('Resource not found');
} else if (error instanceof ConflictError) {
console.error('Action already performed');
} else if (error instanceof RateLimitError) {
console.error(error.retryAfterSeconds);
} else if (error instanceof ServerError) {
console.error(error.statusCode);
} else if (error instanceof NetworkError) {
console.error('Check your internet connection');
} else if (error instanceof FeedbackKitError) {
// Catch-all for other API errors
console.error(`Error ${error.statusCode}: ${error.message}`);
}
}
Guides
How to Add a Feedback Widget to a JavaScript Web App
Build a feedback widget in a JS web app with feedbackkit-js 1.0.1 from npm: user ids, list, vote, submit, comments, MRR, and what the browser API key can do.
11 min read
How to Embed a Feedback Board and Roadmap on Your Website
Turn on FeedbackKit's iframe embeds, copy the code for the board, roadmap and form, and place it in HTML, React or a static site, with theme and sizing tips.
9 min read