API Reference

Complete REST API reference for Feedhog with request/response examples for all endpoints.

API Reference

Complete reference for all Feedhog REST API endpoints.

Authentication

All endpoints require the x-api-key header:

x-api-key: fhpk_your_public_key

POST /api/v1/identify

Identify or create an end user. If a user with the same externalId exists, their data is updated.

Request

curl -X POST https://feedhog.com/api/v1/identify \
  -H "Content-Type: application/json" \
  -H "x-api-key: fhpk_your_public_key" \
  -d '{
    "externalId": "user-123",
    "email": "user@example.com",
    "name": "John Doe",
    "avatarUrl": "https://example.com/avatar.jpg",
    "metadata": {
      "plan": "pro",
      "company": "Acme Inc"
    }
  }'

Request Body

FieldTypeRequiredDescription
externalIdstringYesYour system's unique user ID
emailstringNoUser's email address
namestringNoUser's display name
avatarUrlstringNoURL to user's avatar
metadataobjectNoAdditional key-value data

Response (200 OK)

{
  "user": {
    "id": "user-123",
    "email": "user@example.com",
    "name": "John Doe",
    "avatarUrl": "https://example.com/avatar.jpg",
    "createdAt": "2024-01-15T10:30:00.000Z"
  }
}

POST /api/v1/feedback

Submit new feedback.

Request

curl -X POST https://feedhog.com/api/v1/feedback \
  -H "Content-Type: application/json" \
  -H "x-api-key: fhpk_your_public_key" \
  -d '{
    "title": "Add dark mode",
    "description": "Would love a dark theme option for better night-time usage",
    "type": "idea",
    "metadata": {
      "page": "/settings",
      "browser": "Chrome"
    },
    "endUser": {
      "externalId": "user-123",
      "email": "user@example.com",
      "name": "John Doe"
    }
  }'

Request Body

FieldTypeRequiredDescription
titlestringYesFeedback title (max 200 chars)
descriptionstringNoDetailed description
typestringNobug, idea, question, or other (default: idea)
metadataobjectNoAdditional key-value data
endUserobjectNoUser submitting feedback
endUser.externalIdstringYes*User's ID (*required if endUser provided)
endUser.emailstringNoUser's email
endUser.namestringNoUser's name
endUser.avatarUrlstringNoUser's avatar URL
endUser.metadataobjectNoUser metadata

Response (201 Created)

{
  "feedback": {
    "id": "fb_abc123xyz",
    "title": "Add dark mode",
    "description": "Would love a dark theme option for better night-time usage",
    "type": "idea",
    "status": "new",
    "voteCount": 1,
    "commentCount": 0,
    "createdAt": "2024-01-15T10:30:00.000Z",
    "endUser": {
      "name": "John Doe",
      "avatarUrl": null
    }
  }
}

GET /api/v1/feedback

List feedback with optional filters and pagination.

Request

curl -X GET "https://feedhog.com/api/v1/feedback?status=planned,in-progress&type=idea&sortBy=votes&page=1&limit=10" \
  -H "x-api-key: fhpk_your_public_key"

Query Parameters

ParameterTypeDefaultDescription
statusstring-Filter by status (comma-separated for multiple)
typestring-Filter by type (comma-separated for multiple)
searchstring-Search in title and description
sortBystringnewestSort order: newest, oldest, votes, comments
pagenumber1Page number (1-indexed)
limitnumber20Items per page (max 100)

Status Values

  • new - Newly submitted
  • under-review - Being reviewed
  • planned - Planned for implementation
  • in-progress - Currently being worked on
  • completed - Implemented
  • closed - Closed without implementing

Type Values

  • bug - Bug report
  • idea - Feature request / idea
  • question - Question
  • other - Other feedback

Response (200 OK)

{
  "items": [
    {
      "id": "fb_abc123xyz",
      "title": "Add dark mode",
      "description": "Would love a dark theme option",
      "type": "idea",
      "status": "planned",
      "voteCount": 42,
      "commentCount": 5,
      "createdAt": "2024-01-15T10:30:00.000Z",
      "endUser": {
        "name": "John Doe",
        "avatarUrl": null
      }
    }
  ],
  "total": 42,
  "page": 1,
  "limit": 10,
  "totalPages": 5
}

GET /api/v1/feedback/:id

Get a single feedback item with full details including comments.

Request

curl -X GET "https://feedhog.com/api/v1/feedback/fb_abc123xyz?endUserId=user-123" \
  -H "x-api-key: fhpk_your_public_key"

Query Parameters

ParameterTypeDescription
endUserIdstringExternal user ID to check vote status

Response (200 OK)

{
  "feedback": {
    "id": "fb_abc123xyz",
    "title": "Add dark mode",
    "description": "Would love a dark theme option",
    "type": "idea",
    "status": "planned",
    "voteCount": 42,
    "commentCount": 2,
    "createdAt": "2024-01-15T10:30:00.000Z",
    "userHasVoted": true,
    "endUser": {
      "name": "John Doe",
      "avatarUrl": null
    },
    "comments": [
      {
        "id": "cmt_xyz789",
        "content": "Great idea! We're planning to add this in Q2.",
        "createdAt": "2024-01-16T09:00:00.000Z",
        "author": {
          "name": "Product Team",
          "isTeam": true
        }
      },
      {
        "id": "cmt_abc456",
        "content": "Can't wait for this feature!",
        "createdAt": "2024-01-16T10:30:00.000Z",
        "author": {
          "name": "Jane Smith",
          "avatarUrl": "https://example.com/jane.jpg"
        }
      }
    ]
  }
}

POST /api/v1/feedback/:id/vote

Toggle vote on a feedback item. If the user has voted, removes their vote. If not, adds a vote.

Request (Identified User)

curl -X POST https://feedhog.com/api/v1/feedback/fb_abc123xyz/vote \
  -H "Content-Type: application/json" \
  -H "x-api-key: fhpk_your_public_key" \
  -d '{
    "endUser": {
      "externalId": "user-123",
      "email": "user@example.com"
    }
  }'

Request (Anonymous)

curl -X POST https://feedhog.com/api/v1/feedback/fb_abc123xyz/vote \
  -H "Content-Type: application/json" \
  -H "x-api-key: fhpk_your_public_key" \
  -d '{}'

Request Body

FieldTypeRequiredDescription
endUserobjectNoUser voting (omit for anonymous)
endUser.externalIdstringYes*User's ID (*required if endUser provided)
endUser.emailstringNoUser's email
endUser.namestringNoUser's name

Response (200 OK)

{
  "voted": true,
  "voteCount": 43
}

The voted field indicates the new state:

  • true - Vote was added
  • false - Vote was removed

GET /api/v1/feedback/:id/vote

Check if a user has voted on a feedback item.

Request

curl -X GET "https://feedhog.com/api/v1/feedback/fb_abc123xyz/vote?endUserId=user-123" \
  -H "x-api-key: fhpk_your_public_key"

Query Parameters

ParameterTypeDescription
endUserIdstringExternal user ID to check

If endUserId is not provided, checks by IP address.

Response (200 OK)

{
  "voted": true,
  "voteCount": 43
}

Error Responses

400 Bad Request

Validation error:

{
  "error": "Invalid input",
  "details": {
    "formErrors": [],
    "fieldErrors": {
      "title": ["Title is required"],
      "type": ["Invalid type. Must be one of: bug, idea, question, other"]
    }
  }
}

401 Unauthorized

Missing or invalid API key:

{
  "error": "Missing x-api-key header"
}
{
  "error": "Invalid API key"
}

404 Not Found

Resource not found:

{
  "error": "Feedback not found"
}

429 Too Many Requests

Rate limited:

{
  "error": "Too many requests, please slow down"
}

500 Internal Server Error

Server error:

{
  "error": "Internal server error"
}

Type Definitions

For reference, here are the TypeScript types used in responses:

type FeedbackType = "bug" | "idea" | "question" | "other";

type FeedbackStatus =
  | "new"
  | "under-review"
  | "planned"
  | "in-progress"
  | "completed"
  | "closed";

interface FeedbackListItem {
  id: string;
  title: string;
  description: string | null;
  type: FeedbackType;
  status: FeedbackStatus;
  voteCount: number;
  commentCount: number;
  createdAt: string;
  endUser: {
    name: string | null;
    avatarUrl: string | null;
  } | null;
}

interface FeedbackDetail extends FeedbackListItem {
  userHasVoted: boolean;
  comments: {
    id: string;
    content: string;
    createdAt: string;
    author: {
      name: string | null;
      avatarUrl?: string | null;
      isTeam?: boolean;
    } | null;
  }[];
}

interface IdentifiedUser {
  id: string;
  email: string | null;
  name: string | null;
  avatarUrl: string | null;
  createdAt: string;
}

interface VoteResult {
  voted: boolean;
  voteCount: number;
}

interface PaginatedResponse<T> {
  items: T[];
  total: number;
  page: number;
  limit: number;
  totalPages: number;
}