Importing your data
Bring your feedback with you: boards, posts with their statuses and tags, votes, comments, and the customers behind them.
Owners and admins import under Settings → Import: from Canny, from Nolt, or from a CSV file, which almost any other tool can export. An import writes nothing until you've seen its dry run, keeps the original dates, and notifies no one.
Get your data out#
Every tool exports its data its own way: some as CSV files, some through an API, some both. Each comparison page has a guide to its tool's exports, what they contain and where to find them:
- Moving from Canny
- Moving from UserJot
- Moving from Featurebase
- Moving from Productboard
- Moving from UserVoice
- Moving from Nolt
- Moving from Frill
- Moving from Upvoty
- Moving from Sleekplan
- Moving from FeatureOS
Choose the source#
Canny#
Paste Canny's secret API key (in Canny: Settings → API & Webhooks). The importer reads everything through Canny's API: boards, posts with their tags, votes, comments and status history, then the changelog. Canny's private comments become internal notes. The key is kept sealed until the import ends, then deleted.
Nolt#
Nolt's export API is part of its Pro plan and the plans above. Create an API key in the board's settings (in Nolt:
APIs), then enter the board (acme for acme.nolt.io) and the key. The key is used once, to fetch the board's
export, and never kept. The importer brings the board's posts, without their votes or comments. On another Nolt plan,
ask Nolt for the board's CSV export, and import that file as a CSV file.
A CSV file#
Save the other tool's export as CSV: UTF-8 text whose first line names the columns, separated by commas, semicolons or tabs, up to 10 MB, 100 columns and 20,000 rows. Then match the columns to the fields. One must be the title; the others can be the text, the status, the board, the tags, the author's email, name or id, the date, and the voters' emails. The importer matches the columns by their names; where the AI is on, Suggest a mapping has it map them all instead (one of the month's AI drafts). You check every choice. A CSV brings no comments, and its votes only as voters' emails.
Dry-run it#
Before the import writes anything, choose which of your statuses each of the source's becomes, and which board takes the posts that name none. Then dry-run it: it reads the source and reports what the run would create, find already there, or refuse, and writes nothing. Change the options and dry-run it again as often as you like.
Run it#
The run writes what the source holds, keeping its dates, and notifies no one. It goes on if you leave the page; a workspace runs one import at a time. Cancel stops it within a few records, and what it wrote stays.
Import the same source again later, on the day you switch for instance: it adds only what's new since, and what you deleted in the meantime stays deleted.
The AI doesn't read imported posts as they arrive. To tag them and look for duplicates, start a backfill under Settings → AI: it reads them at your plan's pace, counted in the month's AI reads.
How things map#
Most feedback tools use the same ideas under different names.
| In the other tool | In Swingback |
|---|---|
| Board, category, forum, project | Board |
| Post, idea, request, suggestion | Post |
| Status | Status: Open, Under review, Planned, In progress, Completed or Closed |
| Tag, label, topic | Tag |
| Vote, upvote, supporter, requester | Vote |
| Comment | Comment, or an internal note if only your team saw it |
| User, member, voter | Customer |
| Company, account, organization | Company |
| Changelog entry, announcement, update | Changelog entry |
A status the other tool has and Swingback doesn't, such as "Declined", usually maps to Closed.
Import with the REST API#
For data the importer doesn't read, from your own systems for instance, a short script can create posts, votes and comments through the REST API.
Before you start#
Content made through the API is new to Swingback, so it notifies people the way new content does: a post's author hears of its votes, its followers hear of its comments, and your team hears of new posts. To keep the import quiet:
- Create your customers without their email first, and add their emails once the import is done. Emails only go to customers who have an address when they're sent, and a notification that couldn't be sent isn't sent later.
- Turn off the weekly digest under Settings → General until the import is done, so the next digest doesn't list the imported posts as news.
- Pause the AI features under Settings → AI: each imported post would use an AI read. Once the import is done, a backfill tags your posts and looks for duplicates in one go.
- Tell your team that new posts will arrive, or turn off "New posts" under Settings → Email notifications for the time of the import.
Posts, votes and comments made through the API are dated the day you create them.
Create an API key#
Under Settings → API keys, create a key with write access. It's shown once: keep it in an environment
variable, such as SWINGBACK_API_KEY, next to your workspace's id (ws_…, shown under Settings → Sign-in) in
SWINGBACK_WORKSPACE_ID.
Create boards and tags#
Create your boards under Boards and your tags under Tags, or through the API
(POST /v1/workspaces/{workspace_id}/boards and .../tags), and note their ids: posts name them.
Create posts, votes and comments#
Name each customer by an external_id: your own user id when the other tool has it, so the same person is
recognized when they sign in through your widget later, or else the other tool's. A customer who doesn't exist
yet is created on the way.
Send an Idempotency-Key with each request, made from the other tool's ids: if your script stops halfway, run
it again, and the API answers the requests it already handled (for 24 hours) instead of doing them twice.
// import.mjs: posts from another tool, with their votes and comments. Node.js 18 or later.
const api = `https://api.swingback.dev/v1/workspaces/${process.env.SWINGBACK_WORKSPACE_ID}`;
async function create(path, body, key) {
for (;;) {
const response = await fetch(api + path, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SWINGBACK_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": key,
},
body: JSON.stringify(body),
});
if (response.status === 429) {
// Rate limited: wait as long as the API asks.
const seconds = Number(response.headers.get("Retry-After") ?? 1);
await new Promise((resolve) => setTimeout(resolve, seconds * 1000));
continue;
}
if (!response.ok) throw new Error(`${path}: ${response.status} ${await response.text()}`);
return response.json();
}
}
// Your export, read into objects; boardIds, tagIds and statuses map the other tool's names to Swingback's.
for (const old of posts) {
const post = await create(
"/posts",
{
board_id: boardIds[old.board],
title: old.title,
body_md: old.body,
status: statuses[old.status], // "pending", "review", "planned", "in_progress", "completed" or "closed"
tag_ids: old.tags.map((tag) => tagIds[tag]),
author: { external_id: old.author.id, name: old.author.name },
},
`import-post-${old.id}`,
);
for (const voter of old.voters) {
await create(
`/posts/${post.id}/votes`,
{ end_user: { external_id: voter.id, name: voter.name } },
`import-vote-${old.id}-${voter.id}`,
);
}
for (const comment of old.comments) {
await create(
`/posts/${post.id}/comments`,
{ body_md: comment.body, author: { external_id: comment.author.id, name: comment.author.name } },
`import-comment-${comment.id}`,
);
}
}Add your customers' details#
Once everything is in, give your customers their emails, and their companies if you have them, with one request
each. A customer is found by external_id, and only the fields you send change.
curl https://api.swingback.dev/v1/workspaces/$SWINGBACK_WORKSPACE_ID/end-users \
-H "Authorization: Bearer $SWINGBACK_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "external_id": "user_8412", "email": "dana@example.com" }'Then turn the weekly digest and the AI features back on.
Switch over#
When your data is in:
- Replace the other tool's widget with Swingback's: see Widget.
- On Pro, move your feedback domain to your portal: see Custom domain and branding.
- Announce the move in your first changelog entry.
