REST API
The FeedbackKit REST API is the HTTP interface behind every SDK: submit feedback, read votes and manage projects with plain JSON requests. Use it from any language or platform that has no FeedbackKit SDK, or generate a client from the OpenAPI file.
Authentication
All API requests require authentication via the X-API-Key header. You can find your project API key in the FeedbackKit dashboard under Project Settings.
X-API-Key: your-project-api-key
X-User-Id: optional-user-id # For personalized has_voted state
Content-Type: application/json
Optional: X-User-Id Header
Include the X-User-Id header to get personalized has_voted state in feedback responses. Without it, has_voted will always be false.
Field naming
Response bodies are snake_case — every field below is spelled exactly as it arrives on the wire.
Request bodies accept either casing. Both {"userId": …} and {"user_id": …} decode to the same field, so the camelCase request examples below are correct and so is the snake_case every official SDK sends.
Query parameters are read verbatim — neither convention applies, so includeMerged is spelled exactly as documented.
Endpoints Overview
All endpoints are prefixed with /api/v1. The base URL for production is https://api.prod.getfeedbackkit.com.
| Method | Path | Description |
|---|---|---|
| GET | /feedbacks | List all feedback |
| POST | /feedbacks | Submit new feedback |
| GET | /feedbacks/:id | Get feedback by ID |
| POST | /feedbacks/:id/votes | Vote for feedback |
| DELETE | /feedbacks/:id/votes | Remove vote |
| GET | /feedbacks/:id/comments | List comments |
| POST | /feedbacks/:id/comments | Add comment |
| POST | /users/register | Register SDK user |
| POST | /events/track | Track analytics event |
| GET | /metrics/summary | Aggregate metrics for the key’s project |
| GET | /metrics/timeseries | Bucketed metrics for the key’s project |
| GET | /metrics/breakdown | Metrics grouped by one dimension |
| GET | /metrics/events | Raw event feed (Pro) |
| GET | /projects/:id/metrics/summary | Aggregate metrics, incl. revenue measures (admin token) |
| GET | /projects/:id/metrics/timeseries | Bucketed metrics (admin token) |
| GET | /projects/:id/metrics/breakdown | Grouped metrics (admin token) |
| GET | /projects/:id/metrics/events | Raw event feed (admin token, Pro) |
Feedback
/feedbacks
Retrieves feedback for the project, sorted by vote count (descending) unless sort says otherwise. Comment counts are totals; comment text is not included. Merged feedback items are excluded by default. The default body is a JSON array of every visible row. page and per_page do not change that array. Send paged=1 for { items, meta }. Then page defaults to 1 and per_page defaults to 20 (clamped to 1–100). A page past the end returns an empty items array and the real total. The array path has no row cap, because a cap would drop rows existing clients already receive.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
| status | string | Filter by status: pending, approved, in_progress, testflight, completed, rejected |
| category | string | Filter by category: feature_request, bug_report, improvement, other |
| includeMerged | boolean | Include merged feedback items. Default: false |
| sort | string | Sort order: votes, newest, oldest, comments. Default: votes |
| paged | string | 1 or true selects the page envelope. Omitted, the body stays a JSON array. |
| page | integer | 1-indexed page. Ignored unless paged=1. Default: 1 |
| per_page | integer | Page size, clamped to 1–100. Ignored unless paged=1. Default: 20 |
curl -X GET "https://api.prod.getfeedbackkit.com/api/v1/feedbacks?status=approved" \
-H "X-API-Key: your-api-key" \
-H "X-User-Id: user_12345"
Response when paged=1
Without paged=1 the same objects are returned as a JSON array, with no items or meta wrapper.
{
"items": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"title": "Add dark mode support",
"description": "It would be great to have a dark mode option.",
"status": "approved",
"category": "feature_request",
"user_id": "user_12345",
"vote_count": 42,
"has_voted": false,
"comment_count": 5,
"created_at": "2026-02-01T10:30:00Z",
"updated_at": "2026-02-05T14:20:00Z"
}
],
"meta": {
"total": 1,
"page": 1,
"per_page": 20,
"total_pages": 1
}
}
/feedbacks
Creates a new feedback item. The creator automatically receives a vote (vote_count starts at 1). Triggers notifications to project members and configured integrations.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| title | string | Yes | Feedback title (1-200 chars) |
| description | string | Yes | Detailed description (1-5000 chars) |
| category | string | Yes | feature_request, bug_report, improvement, other |
| userId | string | Yes | Unique identifier of the submitting user |
| userEmail | string | No | Email for status update notifications |
curl -X POST "https://api.prod.getfeedbackkit.com/api/v1/feedbacks" \
-H "X-API-Key: your-api-key" \
-H "Content-Type: application/json" \
-d '{
"title": "Add dark mode support",
"description": "It would be great to have a dark mode option.",
"category": "feature_request",
"userId": "user_12345",
"userEmail": "user@example.com"
}'
/feedbacks/:id
Retrieves a single feedback item by its UUID.
curl -X GET "https://api.prod.getfeedbackkit.com/api/v1/feedbacks/550e8400-e29b-41d4-a716-446655440000" \
-H "X-API-Key: your-api-key" \
-H "X-User-Id: user_12345"
Votes
/feedbacks/:id/votes
Adds a vote to a feedback item. Each user can only vote once per feedback.
Restrictions
- Cannot vote on archived projects (403)
- Cannot vote on completed or rejected feedback (403)
- Cannot vote twice on the same feedback (409)
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| userId | string | Yes | Unique identifier of the voting user |
| string | No | Email for status change notifications | |
| notifyStatusChange | boolean | No | Opt-in to email notifications on status changes. Default: false |
curl -X POST "https://api.prod.getfeedbackkit.com/api/v1/feedbacks/550e8400-.../votes" \
-H "X-API-Key: your-api-key" \
-H "Content-Type: application/json" \
-d '{
"userId": "user_12345",
"email": "user@example.com",
"notifyStatusChange": true
}'
Response
{
"feedback_id": "550e8400-e29b-41d4-a716-446655440000",
"vote_count": 43,
"has_voted": true
}
/feedbacks/:id/votes
Removes the user's vote from a feedback item.
curl -X DELETE "https://api.prod.getfeedbackkit.com/api/v1/feedbacks/550e8400-.../votes" \
-H "X-API-Key: your-api-key" \
-H "Content-Type: application/json" \
-d '{"userId": "user_12345"}'
Users
/users/register
Registers or updates an SDK user. Use this to track user activity and associate MRR (Monthly Recurring Revenue). If the user already exists, their last_seen_at timestamp and MRR will be updated.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| userId | string | Yes | Unique identifier of the SDK user |
| mrr | number | No | Monthly Recurring Revenue for this user |
curl -X POST "https://api.prod.getfeedbackkit.com/api/v1/users/register" \
-H "X-API-Key: your-api-key" \
-H "Content-Type: application/json" \
-d '{
"userId": "user_12345",
"mrr": 9.99
}'
Response
{
"id": "770e8400-e29b-41d4-a716-446655440000",
"user_id": "user_12345",
"mrr": 9.99,
"first_seen_at": "2026-01-15T08:00:00Z",
"last_seen_at": "2026-02-08T12:30:00Z"
}
Events
/events/track
Tracks a custom event for analytics. Use this to measure user engagement with the feedback system.
Common Events
| Event Name | Description |
|---|---|
| feedback_list | User viewed the feedback list |
| feedback_detail | User viewed feedback details |
| submit_feedback | User opened the submit form |
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| eventName | string | Yes | Name of the event (1-100 chars) |
| userId | string | Yes | Unique identifier of the user |
| properties | object | No | Key-value properties for the event |
curl -X POST "https://api.prod.getfeedbackkit.com/api/v1/events/track" \
-H "X-API-Key: your-api-key" \
-H "Content-Type: application/json" \
-d '{
"eventName": "feedback_list",
"userId": "user_12345",
"properties": {
"filter": "feature_request",
"sort": "votes"
}
}'
Metrics
Eight read-only operations return aggregated project metrics and the raw event feed. Four answer for the project behind an X-API-Key; four are nested under /projects/:id/metrics, take an admin bearer token, and additionally answer the SDK-user and revenue measures.
The machine-readable document is the authority for field names
This page deliberately does not restate the metrics response schemas. Every property name, type and example lives in the OpenAPI document at /openapi/openapi.yaml, which is generated from a single source and checked against the server’s own encoder by an automated drift test. A second, hand-maintained copy of those names on this page is exactly how the published contract came to declare field names the server had never sent.
A narrowed answer is a 200, not an error
Two subscription axes can narrow a metrics answer, and neither makes the feature unavailable. Every metrics response echoes what the server actually ran, so a low figure always arrives with a machine-readable reason attached — read the value, never infer it from an absence.
| Axis | Free | Pro | Team |
|---|---|---|---|
| Custom event names — events your app names itself | not readable | readable | readable |
| Metrics window — how far back a query may reach | 30 days | 90 days | 365 days |
FeedbackKit’s own three predefined events — feedback_list, feedback_detail and submit_feedback — are readable on every tier. Below Pro your own event names are still ingested and retained; they simply are not counted, and they become readable on upgrade with no re-tracking.
feedback_count, vote_count, and comment_count count the same rows a customer can list: not merged, and not hidden by the Free cap. Those rows stay stored. The response fields are unchanged, so an older client keeps decoding.
Authenticated project metrics reads allow 60 requests per minute per client IP and path. The IP is the connection peer, or X-Forwarded-For when that peer address is missing. A suspended project answers 403. POST /events/track is not on that budget, and older clients keep the same paths and bodies.
event_scope—all, orpredefined_onlywhen custom event names were excluded. It narrows only the two event measures,event_countandunique_event_users; no other measure is ever affected by it. The field is additive: a client that has never seen it keeps decoding the rest of the overview, and a current client treats a missingevent_scopeasallbecause servers that omit it do not narrow.POST /events/trackis unchanged, and the web Events page announcespredefined_onlywithout refusing the request or dropping stored rows.retention_clamped— true when your plan narrowed the window you asked for. The request still succeeds.retention_window_days— your plan’s allowance: 30 days on Free, 90 on Pro, 365 on Team. Together with the applieddate_fromanddate_tothis makes “showing 90 of the 180 days you asked for” computable from the response alone.
The one 402
METRICS_CUSTOM_EVENTS_REQUIRE_PRO is this family’s only payment-required refusal. It is raised on exactly three requests, each of which is exclusively about custom event names and so has no narrower honest answer: a breakdown by event name, a filter naming an event outside the three predefined ones, and any request to the raw event feed. Everything else a plan affects comes back as a narrower 200.
Two different error bodies
The metrics family is not uniformly enveloped, and a client that assumes an error code is always present will fail on a 401. Failures raised by the metrics API itself — its 400s, the 402, and the API-key surface’s measure 403 — carry error as an object with a stable code. Failures raised by the shared authentication, authorization and rate-limiting layers — 401, the suspended-project and no-access 403s, 404, 429 and 500 — use the server-wide shape below, where error is a boolean and there is no code. Same property name, incompatible types; the OpenAPI document declares both for every status that can carry either.
Models
FeedbackResponse
| Field | Type | Description |
|---|---|---|
| id | UUID | Unique identifier |
| title | string | Feedback title |
| description | string | Detailed description |
| status | FeedbackStatus | Current status of the feedback |
| category | FeedbackCategory | Feedback category |
| user_id | string | ID of the submitting user |
| user_email | string? | Email of the submitter (if provided) |
| vote_count | integer | Total number of votes |
| has_voted | boolean | Whether the current user has voted |
| comment_count | integer | Total number of comments |
| total_mrr | number? | Combined MRR of all voters |
| created_at | ISO 8601 | Creation timestamp |
| updated_at | ISO 8601 | Last update timestamp |
| rejection_reason | string? | Explanation for rejection (max 500 chars) |
| merged_into_id | UUID? | ID of feedback this was merged into |
| merged_at | ISO 8601? | When this feedback was merged |
| merged_feedback_ids | UUID[]? | IDs of feedback merged into this one |
FeedbackStatus
| Value | Description | Voting |
|---|---|---|
| pending | New, awaiting review | Allowed |
| approved | Accepted for consideration | Allowed |
| in_progress | Currently being worked on | Allowed |
| testflight | Available in beta/TestFlight | Allowed |
| completed | Shipped and available | Blocked |
| rejected | Won't be implemented | Blocked |
FeedbackCategory
| Value | Description |
|---|---|
| feature_request | New functionality request |
| bug_report | Issue or problem report |
| improvement | Enhancement to existing feature |
| other | General feedback |
VoteResponse
| Field | Type | Description |
|---|---|---|
| feedback_id | UUID | ID of the feedback item |
| vote_count | integer | Updated vote count |
| has_voted | boolean | Whether the user has voted after this action |
CommentResponse
| Field | Type | Description |
|---|---|---|
| id | UUID | Unique identifier |
| content | string | Comment text |
| user_id | string | ID of the commenting user |
| is_admin | boolean | Whether this is an admin comment |
| created_at | ISO 8601 | Creation timestamp |
Error Handling
All errors follow a consistent format with an error boolean and a human-readable reason string.
{
"error": true,
"reason": "Invalid API key"
}
HTTP Status Codes
| Code | Name | Description |
|---|---|---|
| 200 | OK | Request succeeded |
| 400 | Bad Request | Validation error (missing or invalid fields) |
| 401 | Unauthorized | Missing or invalid API key |
| 402 | Payment Required | Subscription tier limit exceeded |
| 403 | Forbidden | Action not allowed (archived project, completed feedback, etc.) |
| 404 | Not Found | Resource does not exist |
| 409 | Conflict | Duplicate action (e.g., already voted) |
| 429 | Too Many Requests | Rate limit exceeded |
Subscription Tiers
Some API features are limited by subscription tier. Exceeding limits returns a 402 Payment Required response.
| Feature | Free | Pro | Team |
|---|---|---|---|
| Projects | 1 | 2 | Unlimited |
| Feedback per project | 10 | Unlimited | Unlimited |
| Team members | - | - | Unlimited |
| Integrations | - | All | All |
| Voter notifications | - | - | Yes |
Guides
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: user ids, list, vote, submit, comments, MRR, and what the browser API key can do.
11 min read
Comments
/feedbacks/:id/commentsRetrieves comments for a feedback item, oldest first. The default body is a JSON array of every comment.
paged=1selects{ items, meta }, and only then dopageandper_pageapply, with the same clamps as the feedback list.Response when paged=1
Without
paged=1the same comments are returned as a JSON array./feedbacks/:id/commentsAdds a comment to a feedback item. Cannot comment on archived projects.
Request Body
false