Skip to content

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.

Swift 6.2+ Vapor 4.89+ Linux GitHub

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.

Package.swift
// 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
.localhttp://localhost:8080
.developmenthttps://api.dev.getfeedbackkit.com
.testflighthttps://api.testflight.getfeedbackkit.com
.productionhttps://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.

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.

func commentsWithUserHeader(_ req: Request) async throws {
    let feedbackId = UUID()
    let commentId = UUID()
    let comments = try await req.feedbackKit.comments.list(
        feedbackId: feedbackId,
        userId: "user-123"
    )
    let comment = try await req.feedbackKit.comments.create(
        feedbackId: feedbackId,
        request: CreateCommentRequest(
            content: "Acknowledged by server",
            userId: "user-123",
            isAdmin: true
        )
    )
    try await req.feedbackKit.comments.delete(
        feedbackId: feedbackId,
        commentId: commentId,
        userId: "user-123"
    )
    _ = (comments, comment)
}
func deleteCommentAsOwner(_ req: Request) async throws {
    let feedbackId = UUID()
    let commentId = UUID()
    let bearerToken = "token"
    try await req.feedbackKit.comments.delete(
        feedbackId: feedbackId,
        commentId: commentId,
        bearerToken: bearerToken
    )
}

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
badRequest400
unauthorized, invalidApiKey401
paymentRequired402, boolean body
forbidden403, role or suspended project
notFound404
conflict409
rateLimited429
serverErrorother, including 422
decodingErrorthrown locally after a 2xx body fails to decode
notConfiguredthrown locally
operationNotSupportedthrown 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.