Vapor SDK
The FeedbackKit Vapor SDK is a server-side Swift client for Vapor 4 apps: submit feedback, votes and comments and call the admin API from your own backend. Configure it with your project API key and use it from your routes.
Installation
Swift 6.2 or later. The package builds on Linux. macOS 13 is the minimum listed in Package.swift when you build for Apple platforms. Vapor 4.89 or later. The product name is SwiftlyFeedbackKitVapor. For a path dependency the package argument is the directory name SwiftlyFeedbackKit-Vapor. For the git URL it is SwiftlyFeedbackKit-Vapor-SDK.
The published pin is a URL dependency on https://github.com/Swiftly-Developed/SwiftlyFeedbackKit-Vapor-SDK.git with from 3.0.0 and the package argument SwiftlyFeedbackKit-Vapor-SDK. The samples on this page do not compile against tags 1.1.1 or 2.0.0. The Package.swift below is the compiled example inside the package, so it uses a path dependency.
// swift-tools-version:6.2
import PackageDescription
// Compiled by `swift build` in this directory. `../..` is the
// SwiftlyFeedbackKit-Vapor package that contains this example.
// A host app that sits next to a checkout of the package uses
// `.package(path: "../SwiftlyFeedbackKit-Vapor")` instead. The product
// name stays `SwiftlyFeedbackKitVapor`. The `package:` argument is the
// path's directory name for a path dependency, and
// `SwiftlyFeedbackKit-Vapor-SDK` for the git URL.
let package = Package(
name: "GettingStarted",
platforms: [
.macOS(.v13)
],
dependencies: [
.package(path: "../.."),
.package(url: "https://github.com/vapor/vapor.git", from: "4.89.0"),
],
targets: [
.executableTarget(
name: "GettingStarted",
dependencies: [
.product(name: "Vapor", package: "vapor"),
.product(name: "SwiftlyFeedbackKitVapor", package: "SwiftlyFeedbackKit-Vapor"),
]
),
]
)
Configure
Call configure once, from configure.swift, before any request uses the client. A request that runs before that throws FeedbackKitError.notConfigured. The client appends api/v1 itself. Pass the origin, not a URL that already ends in /api/v1.
| Environment | Base URL |
|---|---|
| .local | http://localhost:8080 |
| .development | https://api.dev.getfeedbackkit.com |
| .testflight | https://api.testflight.getfeedbackkit.com |
| .production | https://api.prod.getfeedbackkit.com |
| .custom(URL) | The URL you pass |
func configureFeedbackKit(_ app: Application) {
app.feedbackKit.configure(
apiKey: Environment.get("FEEDBACKKIT_API_KEY") ?? "",
environment: .production
)
}
req.feedbackKit is the X-API-Key client. req.feedbackKitAdmin(token:) is the Bearer client. Outside a request, use app.feedbackKit.client() and app.feedbackKit.adminClient(token:). Those two log with app.logger. Inside a request the client uses req.logger.
Feedback, votes, users, events
create sends userId as X-User-Id and in the body. list and get send the header only when you pass userId. The server uses it for hasVoted and for showing the creator an item hidden over the Free-tier cap. feedback.delete(id:) is deprecated. It throws FeedbackKitError.operationNotSupported and does not send a request. Delete with the admin client's feedback.delete(id:).
func feedbackRoutes(_ app: Application) throws {
app.get("feedback") { req async throws -> [FeedbackResponse] in
let sdk = try req.feedbackKit
return try await sdk.feedback.list(status: .approved, userId: "user-123")
}
app.get("feedback", ":id") { req async throws -> FeedbackResponse in
let id = try req.parameters.require("id", as: UUID.self)
return try await req.feedbackKit.feedback.get(id: id, userId: "user-123")
}
app.post("feedback") { req async throws -> FeedbackResponse in
let sdk = try req.feedbackKit
return try await sdk.feedback.create(CreateFeedbackRequest(
title: "Server-detected issue",
description: "Automatically reported by monitoring",
category: .bugReport,
userId: "system-monitor"
))
}
}
FeedbackResponse.syncStates decodes the optional sync_states array. Each row is provider, state, and providerRefUrl. Older payloads omit the key and the property stays nil.
func readSyncStates(_ req: Request) async throws {
let feedbackId = UUID()
let item = try await req.feedbackKit.feedback.get(id: feedbackId, userId: "user-123")
for state in item.syncStates ?? [] {
let ref = state.providerRefUrl ?? ""
print(state.provider, state.state, ref)
}
}
The X-API-Key client also exposes votes.vote, votes.unvote, users.register, and events.track.
Change a password
auth.changePassword sends PUT /auth/password with current_password and new_password. It returns nothing on success. Login is unauthenticated, so the token you pass to feedbackKitAdmin can be empty for that one call. auth.deleteAccount sends DELETE /auth/account. auth.resendVerification sends POST /auth/resend-verification.
func loginAndChangePassword(_ req: Request) async throws {
let auth = try await req.feedbackKitAdmin(token: "").auth.login(
LoginRequest(email: "admin@example.com", password: "password")
)
let token = auth.token
let admin = try req.feedbackKitAdmin(token: token)
let me = try await admin.auth.me()
try await admin.auth.changePassword(
ChangePasswordRequest(currentPassword: "password", newPassword: "a-new-password")
)
_ = me
}
The Bearer client also covers projects, members, dashboard, subscriptions, devices, admin feedback writes, users, events, and ten integration clients (GitHub, ClickUp, Notion, Monday, Linear, Trello, Airtable, Asana, Basecamp, Email Campaign). Slack settings are projects.updateSlack. HubSpot and Salesforce are not on this client. subscriptions.portal(returnUrl:) percent-encodes the query. The key stays returnUrl.
Errors and logging
Every API method throws FeedbackKitError, which conforms to AbortError. Which error body arrived is decided by the JSON type of error, not by the status. 402 and 403 arrive in both shapes.
| Case | Status |
|---|---|
| badRequest | 400 |
| unauthorized, invalidApiKey | 401 |
| paymentRequired | 402, boolean body |
| forbidden | 403, role or suspended project |
| notFound | 404 |
| conflict | 409 |
| rateLimited | 429 |
| serverError | other, including 422 |
| decodingError | thrown locally after a 2xx body fails to decode |
| notConfigured | thrown locally |
| operationNotSupported | thrown locally, before a request |
func handleFeedbackKitError(_ req: Request) async throws {
let request = CreateFeedbackRequest(
title: "Server-detected issue",
description: "Automatically reported by monitoring",
category: .bugReport,
userId: "system-monitor"
)
do {
_ = try await req.feedbackKit.feedback.create(request)
} catch let error as FeedbackKitError {
switch error {
case .badRequest:
break
case .paymentRequired:
break
case .unauthorized, .invalidApiKey:
break
case .forbidden:
break
case .rateLimited:
break
case .conflict:
break
case .notFound:
break
case .serverError:
break
case .decodingError:
break
case .notConfigured:
break
case .operationNotSupported:
break
default:
break
}
}
}
The only log line is at error, and only when a 2xx body fails to decode. The message is FeedbackKit failed to decode plus the type name. The decode error's description is not logged, because it can include response field values. The thrown FeedbackKitError.decodingError still carries that detail. The package uses swift-log via Vapor.
Guides
What's New in Vapor SDK 2.0 and 3.0
Vapor SDK 2.0 and 3.0 fix decoding, comment and query bugs, add list paging and a rate-limit error, and rename a few APIs. Here is how to migrate.
6 min read
Introducing the FeedbackKit Vapor SDK
Vapor SDK 1.0 lets a server-side Swift app talk to FeedbackKit: file and list requests, vote, comment, pass MRR, and manage projects and statuses.
5 min read
What Is a Feedback SDK? A Practical Guide for App Teams
A feedback SDK lets users send, vote on and track feature requests inside your app. How it differs from surveys, support desks and analytics.
9 min read
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: install, user ids, list, vote, submit, and what the browser API key can do.
9 min read
Comments and X-User-Id
comments.list(feedbackId:userId:) sends X-User-Id when userId is set. comments.create always sends request.userId as that header and in the body. The hidden-over-limit guard reads the header, not the body.
isAdmin true on this X-API-Key call is accepted only while the server's COMMENT_ADMIN_REPLY_REQUIRE_USER flag is off (the default). When that flag is on, an admin reply needs a Bearer token for a project owner, admin, or member.
comments.delete needs userId or bearerToken. userId deletes that SDK user's own non-admin comment (X-API-Key and X-User-Id). bearerToken deletes any comment as a project owner or admin (Authorization Bearer, no API key). A token wins when both are set. Calling with neither throws FeedbackKitError.operationNotSupported before a request is sent.