What's New in JavaScript SDK 2.1
JavaScript SDK 2.1.0 and 1.2.0 were both tagged on 14 October 2026. There is no 2.0.0 tag: 2.1.0 is the release after 1.2.0. This post covers both, starting from JavaScript SDK 1.1. 1.2.0 is a single fix. 2.1.0 is the big one: a feedback list you can mount on any web page, user ids you no longer have to invent, and a client that is stricter about what it sends.
The short version
- New:
feedbackkit-js/ui, an optional entry point whosemountFeedbackListrenders the Showcase card list into any element, with no framework. - New:
userIdis optional on create, vote, unvote and comment. Leave it out and the client makes alocal_id and keeps it. - New:
pageandperPageonfeedback.list()andcomments.list(). - New:
RateLimitErrorfor a 429 andServerErrorfor a 5xx. - Fixed in 1.2:
includeMerged: truereaches the server. - Breaking:
TrackedEvent.createdAtmay beundefined, plainhttp://addresses are refused, admin replies need a server token, and event properties must be flat values.
Installing 2.1
Nothing from 1.1.0 to 2.1.0 is on the npm registry. npm install feedbackkit-js still installs 1.0.1. The 1.2.0 and 2.1.0 tags are in the SwiftlyFeedbackKit-Javascript-SDK repository on GitHub, and as with 1.1 they hold the TypeScript source with no dist folder and no prepare script, so a github: dependency gives you a package with nothing to import. Build it from the tag and install the tarball:
git clone --branch 2.1.0 https://github.com/Swiftly-Developed/SwiftlyFeedbackKit-Javascript-SDK.git
cd SwiftlyFeedbackKit-Javascript-SDK
npm install
npm run build
npm pack
# then, in your app:
npm install ../SwiftlyFeedbackKit-Javascript-SDK/feedbackkit-js-2.1.0.tgz
The package name is still feedbackkit-js. The build also writes dist/feedbackkit-ui.global.js for pages without a bundler. A CDN that mirrors the npm package serves 1.0.1, which has no such file, so copy that file from your build and host it yourself. If you want to stay on the registry, 1.0.1 keeps working, and the JavaScript SDK 1.0 tutorial covers it.
1.2: merged requests on request
The 1.1 notes warned that includeMerged did nothing, because the client renamed query keys to snake case and the server reads includeMerged as written. 1.2.0 sends query keys as you write them, so feedback.list({ includeMerged: true }) now returns merged requests too. Request bodies are still sent in snake case, as before. Apart from new tests, that is the whole of 1.2.0, and 2.1.0 keeps it.
A feedback list for the web
feedbackkit-js/ui is a second entry point. Importing feedbackkit-js alone does not load it, so a server or a headless integration pays nothing for it. mountFeedbackList renders the same Showcase list as the native SDKs: each card has a status line with a glyph, the whole title and description, the category and comment count, and a vote capsule that fills once you have voted and turns to plain text on Completed and Rejected requests. A Sort & Filter menu holds the sort order and status and category filters, a "Filtered by" row with Clear appears while a filter is on, and a filter with no results shows No Matching Requests.
import { FeedbackKit } from 'feedbackkit-js';
import { mountFeedbackList } from 'feedbackkit-js/ui';
const client = new FeedbackKit({ apiKey: 'sf_your_api_key' });
const list = mountFeedbackList(document.getElementById('trail-ideas'), {
client,
title: 'Trailmate ideas',
onSelect: (feedback) => router.push(`/ideas/${feedback.id}`)
});
// later
list.update({ colorScheme: 'dark' });
list.destroy();
With a client, the list loads requests and sends votes for you. Votes show at once and roll back if the server refuses them. Cards respond to clicks when you pass onSelect. Without a client, pass your own items and handle onVote. listStyle: 'classic' draws compact rows with sort chips instead of cards.
The list has no framework dependency, so it works the same in React, Vue, Svelte or a plain page. It renders into a shadow root, so your page's CSS does not leak in, and you style it with theme (colours, font and a separate set for dark mode) or the card, vote, menu-button and menu parts. It follows the system's light or dark setting unless you pass colorScheme. Sizes are in rem, so it reflows at 200% text, the vote control is a toggle button for screen readers, the menu supports arrow keys and typeahead, and Arabic lays out right to left. Its text comes in eleven languages, picked from the browser, with locale and per-key strings to override.
Without a bundler, the global build exposes the client and the list on window.FeedbackKitUI:
<div id="trail-ideas"></div>
<script src="/js/feedbackkit-ui.global.js"></script>
<script>
const { FeedbackKit, mountFeedbackList } = window.FeedbackKitUI;
mountFeedbackList(document.getElementById('trail-ideas'), {
client: new FeedbackKit({ apiKey: 'sf_your_api_key' })
});
</script>
User ids you no longer invent
Up to 1.2, feedback.create(), votes.vote(), votes.unvote() and comments.create() each required a userId. In 2.1 it is optional. When you set none, the client generates local_ followed by a UUID, stores it in localStorage under feedbackkit_user_id where storage exists, and keeps it in memory where it does not, so Node and server rendering do not throw. An id you pass to the constructor or to setUserId replaces it and is the one stored. A userId on a single call still wins for that call.
const feedbackKit = new FeedbackKit({ apiKey: 'sf_your_api_key' });
await feedbackKit.votes.vote(feedbackId); // votes as the stored local_ id
// once the hiker signs in
feedbackKit.setUserId('trailmate_user_4821');
Paging
feedback.list() and comments.list() take page and perPage. Pass page to get that page alone. Leave it out and the client reads every page and joins them, so existing calls return the whole list as before. The server keeps perPage between 1 and 100.
Breaking changes and how to migrate
TrackedEvent.createdAt is optional
The type of createdAt on the TrackedEvent that events.track() returns is now string | undefined, because the server does not always send it. Nothing changes at runtime, but TypeScript code that treats it as a string stops compiling. Handle the missing case, for example event.createdAt ?? ''.
https is required
The constructor throws a ValidationError for a baseUrl that uses http://, unless the host is localhost, 127.0.0.1 or ::1, so local tests keep working. Requests also refuse to follow redirects, so a 3xx cannot carry your API key to another host. A missing apiKey now throws a ValidationError too, where it used to throw a plain Error.
Admin replies need a bearer token
In 1.2, comments.create() with isAdmin: true sent the flag with the project API key alone, so any app could mark its own reply as the team's. In 2.1 it throws before sending unless the client was created with bearerToken, a server-side credential. Never put that token in code you ship to browsers or phones. Reply as the team from the dashboard or the admin apps, or from your own server. Ordinary comments are unchanged.
Event properties are flat values
events.track() now sends each property as a string. Numbers and booleans are converted, and an object, an array, null or a non-finite number throws a ValidationError and nothing is sent. Property keys are no longer case converted in either direction, so step_count stays step_count and planTier is stored as planTier, where 1.2 stored plan_tier. If you read stored properties back by their snake case names, update those keys.
Errors you can tell apart
A 429 is now a RateLimitError, with retryAfterSeconds set from the server's Retry-After header. A 5xx is a ServerError, and a 422 is a ValidationError with status 422. All of them still extend FeedbackKitError, so a broad catch keeps working. Code that compared error.code to 'SERVER_ERROR' to spot a 429 should check the class instead. When the server sends a machine-readable code, it now lands in error.code with the server's message.
import { RateLimitError, ServerError } from 'feedbackkit-js';
try {
await feedbackKit.votes.vote(feedbackId);
} catch (error) {
if (error instanceof RateLimitError) {
retryIn(error.retryAfterSeconds ?? 30);
} else if (error instanceof ServerError) {
showMessage('Trailmate ideas are offline. Try again soon.');
} else {
throw error;
}
}
How FeedbackKit fits in
The new list puts voting on your own site with a few lines of code. Each vote raises the request on your board, and the person who filed it, plus voters who opted in on Team, get an email when its status changes. Here is how voting looks in FeedbackKit, with sample data from a fictional hiking app:
Votes from your own page
Upgrading
From 1.2: build 2.1.0 from the tag as shown above. Then handle a missing TrackedEvent.createdAt, check that any custom baseUrl uses https, move admin replies off the client, flatten event properties, and switch rate-limit and outage handling to RateLimitError and ServerError. Your userId arguments can stay.
From 1.1: do the same, and check any code that passes includeMerged: true, which now returns merged requests where 1.1 ignored it.
The reference is in the JavaScript SDK docs. If you are starting today, follow the current JavaScript tutorial. For where the SDK started, see What's New in JavaScript SDK 1.0. The React Native side of this release is in What's New in React Native SDK 2.1.
Every plan includes the JavaScript SDK. Start free and put the board on your site.