AI Reference (LLMs)

Complete Feedhog reference for AI assistants. Copy this entire page into Claude, ChatGPT, or other AI tools to get implementation help.

Feedhog Complete Reference

This page contains everything an AI assistant needs to help you implement Feedhog. Copy the entire contents into your AI tool for full context.


Overview

Feedhog is a customer feedback collection platform. It provides:

  • JavaScript SDK (@feedhog/js) - Programmatic feedback submission
  • Widget (@feedhog/widget) - Drop-in feedback UI component
  • REST API - Server-to-server integration

All methods require a public API key (format: fhpk_xxxxxxxxxxxx).


Installation

JavaScript SDK

npm install @feedhog/js

Widget

npm install @feedhog/widget

CDN (Widget only)

<script async src="https://cdn.feedhog.com/widget.js"></script>

TypeScript Type Definitions

// Feedback type enum
type FeedbackType = "bug" | "idea" | "question" | "other";

// Feedback status enum
type FeedbackStatus =
  | "new"
  | "under-review"
  | "planned"
  | "in-progress"
  | "completed"
  | "closed";

// Sort options for listing feedback
type SortBy = "newest" | "oldest" | "votes" | "comments";

// SDK Configuration
interface FeedhogConfig {
  /** Public API key (starts with fhpk_) */
  apiKey: string;
  /** Base URL for the API (defaults to https://feedhog.com) */
  baseUrl?: string;
}

// User identification data
interface UserIdentity {
  /** Your system's unique user ID (required) */
  externalId: string;
  /** User's email address */
  email?: string;
  /** User's display name */
  name?: string;
  /** URL to user's avatar image */
  avatarUrl?: string;
  /** Additional metadata to associate with the user */
  metadata?: Record<string, unknown>;
}

// Feedback submission input
interface SubmitFeedbackInput {
  /** Feedback title (required) */
  title: string;
  /** Detailed description */
  description?: string;
  /** Type of feedback (defaults to "idea") */
  type?: FeedbackType;
  /** Additional metadata */
  metadata?: Record<string, unknown>;
}

// Options for listing feedback
interface ListFeedbackOptions {
  /** Filter by status(es) */
  status?: FeedbackStatus | FeedbackStatus[];
  /** Filter by type(s) */
  type?: FeedbackType | FeedbackType[];
  /** Search query */
  search?: string;
  /** Sort order */
  sortBy?: SortBy;
  /** Page number (1-indexed) */
  page?: number;
  /** Items per page (max 100) */
  limit?: number;
}

// Feedback author info (public)
interface FeedbackAuthor {
  name: string | null;
  avatarUrl: string | null;
}

// Comment author info (public)
interface CommentAuthor {
  name: string | null;
  avatarUrl?: string | null;
  isTeam?: boolean;
}

// Public comment on feedback
interface FeedbackComment {
  id: string;
  content: string;
  createdAt: string;
  author: CommentAuthor | null;
}

// Feedback item from list endpoint
interface FeedbackListItem {
  id: string;
  title: string;
  description: string | null;
  type: FeedbackType;
  status: FeedbackStatus;
  voteCount: number;
  commentCount: number;
  createdAt: string;
  endUser: FeedbackAuthor | null;
}

// Feedback item with full details
interface FeedbackDetail extends FeedbackListItem {
  userHasVoted: boolean;
  comments: FeedbackComment[];
}

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

// Vote result
interface VoteResult {
  voted: boolean;
  voteCount: number;
}

// Identified user response
interface IdentifiedUser {
  id: string;
  email: string | null;
  name: string | null;
  avatarUrl: string | null;
  createdAt: string;
}

// API Error response
interface ApiError {
  error: string;
  details?: {
    formErrors: string[];
    fieldErrors: Record<string, string[]>;
  };
}

// Widget settings
interface WidgetSettings {
  /** Public API key (starts with fhpk_) */
  apiKey: string;
  /** Base URL for the API */
  baseUrl?: string;
  /** Widget position on the page */
  position?: "bottom-right" | "bottom-left" | "top-right" | "top-left";
  /** Primary brand color (hex) */
  primaryColor?: string;
  /** Trigger button text */
  triggerText?: string;
  /** Show/hide the trigger button */
  showTrigger?: boolean;
  /** Modal title */
  title?: string;
  /** Modal subtitle */
  subtitle?: string;
  /** User identity (for pre-identified users) */
  user?: UserIdentity;
}

// React Widget Props
interface FeedhogWidgetProps {
  /** Public API key (starts with fhpk_) */
  apiKey: string;
  /** Base URL for the API */
  baseUrl?: string;
  /** Widget position on the page */
  position?: "bottom-right" | "bottom-left" | "top-right" | "top-left";
  /** Primary brand color (hex) */
  primaryColor?: string;
  /** Trigger button text */
  triggerText?: string;
  /** Show/hide the trigger button */
  showTrigger?: boolean;
  /** Modal title */
  title?: string;
  /** Modal subtitle */
  subtitle?: string;
  /** User identity (for pre-identified users) */
  user?: UserIdentity;
}

JavaScript SDK

Initialization

import Feedhog from '@feedhog/js';

const feedhog = new Feedhog({
  apiKey: 'fhpk_your_public_key',
  baseUrl: 'https://feedhog.com' // optional, defaults to https://feedhog.com
});

identify(user: UserIdentity): Promise<IdentifiedUser>

Associates feedback and votes with a specific user. User data is persisted in localStorage.

const identifiedUser = await feedhog.identify({
  externalId: 'user-123',
  email: 'user@example.com',
  name: 'John Doe',
  avatarUrl: 'https://example.com/avatar.jpg',
  metadata: {
    plan: 'pro',
    company: 'Acme Inc'
  }
});

console.log('User identified:', identifiedUser.id);

submit(input: SubmitFeedbackInput): Promise<FeedbackListItem>

Submits new feedback. If a user has been identified, feedback is associated with them.

const feedback = await feedhog.submit({
  title: 'Add dark mode',
  description: 'Would love a dark theme option for better night-time usage',
  type: 'idea',
  metadata: {
    page: '/settings',
    browser: 'Chrome'
  }
});

console.log('Feedback submitted:', feedback.id);
console.log('Vote count:', feedback.voteCount);

list(options?: ListFeedbackOptions): Promise<PaginatedResponse<FeedbackListItem>>

Returns a paginated list of feedback for the project.

// Get all ideas in "planned" status, sorted by votes
const { items, total, totalPages } = await feedhog.list({
  status: ['planned', 'in-progress'],
  type: 'idea',
  sortBy: 'votes',
  page: 1,
  limit: 10
});

console.log(`Found ${total} items across ${totalPages} pages`);
items.forEach(item => {
  console.log(`${item.title} - ${item.voteCount} votes`);
});

get(feedbackId: string): Promise<FeedbackDetail>

Gets a single feedback item with full details including comments.

const feedback = await feedhog.get('abc123');

console.log('Title:', feedback.title);
console.log('Status:', feedback.status);
console.log('Has user voted:', feedback.userHasVoted);
console.log('Comments:', feedback.comments.length);

feedback.comments.forEach(comment => {
  console.log(`${comment.author?.name}: ${comment.content}`);
});

vote(feedbackId: string): Promise<VoteResult>

Toggles vote on a feedback item. Adds vote if not voted, removes if already voted.

const { voted, voteCount } = await feedhog.vote('abc123');

if (voted) {
  console.log('Vote added! Total votes:', voteCount);
} else {
  console.log('Vote removed. Total votes:', voteCount);
}

hasVoted(feedbackId: string): Promise<VoteResult>

Checks if the current user has voted on a feedback item.

const { voted, voteCount } = await feedhog.hasVoted('abc123');

console.log('Has voted:', voted);
console.log('Total votes:', voteCount);

reset(): void

Clears the current user and removes stored data. Use when a user logs out.

feedhog.reset();
console.log('User data cleared');

user: CurrentUser | null

Property to access the currently identified user.

if (feedhog.user) {
  console.log('Current user:', feedhog.user.externalId);
  console.log('Is identified:', feedhog.user.identified);
} else {
  console.log('No user identified');
}

Event Listeners

// Listen for successful identification
feedhog.on('identify', (user) => {
  console.log('User identified:', user.id);
});

// Listen for feedback submission
feedhog.on('submit', (feedback) => {
  console.log('Feedback submitted:', feedback.id);
});

// Listen for votes
feedhog.on('vote', ({ feedbackId, result }) => {
  console.log(`Vote on ${feedbackId}:`, result.voted ? 'added' : 'removed');
});

// Listen for errors
feedhog.on('error', (error) => {
  console.error('Feedhog error:', error.message);
});

// Listen for reset
feedhog.on('reset', () => {
  console.log('User data cleared');
});

// Remove listener
feedhog.off('identify', myHandler);

Error Handling

import Feedhog, { FeedhogApiError } from '@feedhog/js';

const feedhog = new Feedhog({ apiKey: 'fhpk_your_public_key' });

try {
  await feedhog.submit({ title: 'My feedback' });
} catch (error) {
  if (error instanceof FeedhogApiError) {
    console.log('Status:', error.status);
    console.log('Message:', error.message);
    if (error.details) {
      console.log('Field errors:', error.details.fieldErrors);
    }
  }
}

Widget

Script Tag Setup

<!DOCTYPE html>
<html>
<head>
  <title>My App</title>
</head>
<body>
  <!-- Your app content -->

  <script>
    window.feedhogSettings = {
      apiKey: 'fhpk_your_public_key',
      position: 'bottom-right',
      primaryColor: '#3b82f6',
      triggerText: 'Feedback',
      showTrigger: true,
      title: 'Send us feedback',
      subtitle: 'We\'d love to hear from you',
      user: {
        externalId: 'user-123',
        email: 'user@example.com',
        name: 'John Doe'
      }
    };
  </script>
  <script async src="https://cdn.feedhog.com/widget.js"></script>
</body>
</html>

Widget JavaScript API

// Open the widget modal
window.FeedhogWidget.open();

// Close the widget modal
window.FeedhogWidget.close();

// Identify a user (after page load or login)
window.FeedhogWidget.identify({
  externalId: 'user-123',
  email: 'user@example.com',
  name: 'John Doe'
});

// Clear user data (on logout)
window.FeedhogWidget.reset();

React Component

import { FeedhogWidget } from '@feedhog/widget/react';

function App() {
  const currentUser = useAuth(); // Your auth hook

  return (
    <>
      {/* Your app content */}

      <FeedhogWidget
        apiKey="fhpk_your_public_key"
        position="bottom-right"
        primaryColor="#3b82f6"
        triggerText="Feedback"
        showTrigger={true}
        title="Send us feedback"
        subtitle="We'd love to hear from you"
        user={currentUser ? {
          externalId: currentUser.id,
          email: currentUser.email,
          name: currentUser.name,
          avatarUrl: currentUser.avatarUrl
        } : undefined}
      />
    </>
  );
}

Next.js Integration

// app/layout.tsx or app/providers.tsx
'use client';

import { FeedhogWidget } from '@feedhog/widget/react';
import { useAuth } from '@/hooks/use-auth';

export function FeedbackProvider({ children }: { children: React.ReactNode }) {
  const { user } = useAuth();

  return (
    <>
      {children}
      <FeedhogWidget
        apiKey={process.env.NEXT_PUBLIC_FEEDHOG_API_KEY!}
        position="bottom-right"
        user={user ? {
          externalId: user.id,
          email: user.email,
          name: user.name
        } : undefined}
      />
    </>
  );
}

Custom Trigger Button

'use client';

import { FeedhogWidget } from '@feedhog/widget/react';
import { Button } from '@/components/ui/button';

export function FeedbackButton() {
  return (
    <>
      <FeedhogWidget
        apiKey="fhpk_your_public_key"
        showTrigger={false} // Hide default trigger
      />

      <Button onClick={() => window.FeedhogWidget?.open()}>
        Share Feedback
      </Button>
    </>
  );
}

REST API

Base URL: https://feedhog.com/api/v1

All requests require the x-api-key header with your public API key.

POST /api/v1/identify

Identify or create an end user.

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"
    }
  }'

Response (200):

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

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",
    "type": "idea",
    "metadata": {
      "page": "/settings"
    },
    "endUser": {
      "externalId": "user-123",
      "email": "user@example.com",
      "name": "John Doe"
    }
  }'

Response (201):

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

GET /api/v1/feedback

List feedback with optional filters.

Query Parameters:

  • status - Filter by status (comma-separated for multiple)
  • type - Filter by type (comma-separated for multiple)
  • search - Search query
  • sortBy - Sort order: newest, oldest, votes, comments
  • page - Page number (1-indexed)
  • limit - Items per page (max 100)

Request:

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

Response (200):

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

GET /api/v1/feedback/:id

Get a single feedback item with comments.

Query Parameters:

  • endUserId - External user ID to check vote status

Request:

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

Response (200):

{
  "feedback": {
    "id": "fb_abc123",
    "title": "Add dark mode",
    "description": "Would love a dark theme option",
    "type": "idea",
    "status": "planned",
    "voteCount": 42,
    "commentCount": 2,
    "createdAt": "2024-01-15T10:30:00Z",
    "userHasVoted": true,
    "endUser": {
      "name": "John Doe",
      "avatarUrl": null
    },
    "comments": [
      {
        "id": "cmt_xyz789",
        "content": "Great idea! We're working on this.",
        "createdAt": "2024-01-16T09:00:00Z",
        "author": {
          "name": "Support Team",
          "isTeam": true
        }
      }
    ]
  }
}

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

Toggle vote on feedback.

Request:

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

Response (200):

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

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

Check if user has voted.

Query Parameters:

  • endUserId - External user ID

Request:

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

Response (200):

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

Error Responses

401 Unauthorized:

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

400 Bad Request:

{
  "error": "Invalid input",
  "details": {
    "formErrors": [],
    "fieldErrors": {
      "title": ["Title is required"]
    }
  }
}

404 Not Found:

{
  "error": "Feedback not found"
}

Common Integration Patterns

Next.js App Router with Auth

// app/providers.tsx
'use client';

import { FeedhogWidget } from '@feedhog/widget/react';
import { useSession } from 'next-auth/react';

export function Providers({ children }: { children: React.ReactNode }) {
  const { data: session } = useSession();

  return (
    <>
      {children}
      <FeedhogWidget
        apiKey={process.env.NEXT_PUBLIC_FEEDHOG_API_KEY!}
        position="bottom-right"
        user={session?.user ? {
          externalId: session.user.id,
          email: session.user.email!,
          name: session.user.name!,
          avatarUrl: session.user.image || undefined
        } : undefined}
      />
    </>
  );
}

Custom Feedback Form

'use client';

import { useState } from 'react';
import Feedhog from '@feedhog/js';
import type { FeedbackType } from '@feedhog/js';

const feedhog = new Feedhog({
  apiKey: process.env.NEXT_PUBLIC_FEEDHOG_API_KEY!
});

export function FeedbackForm({ userId, userEmail }: { userId: string; userEmail: string }) {
  const [title, setTitle] = useState('');
  const [description, setDescription] = useState('');
  const [type, setType] = useState<FeedbackType>('idea');
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [isSuccess, setIsSuccess] = useState(false);

  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    setIsSubmitting(true);

    try {
      // Identify user first
      await feedhog.identify({
        externalId: userId,
        email: userEmail
      });

      // Submit feedback
      await feedhog.submit({
        title,
        description,
        type
      });

      setIsSuccess(true);
      setTitle('');
      setDescription('');
    } catch (error) {
      console.error('Failed to submit feedback:', error);
    } finally {
      setIsSubmitting(false);
    }
  }

  if (isSuccess) {
    return <div>Thank you for your feedback!</div>;
  }

  return (
    <form onSubmit={handleSubmit}>
      <select value={type} onChange={(e) => setType(e.target.value as FeedbackType)}>
        <option value="idea">Feature Request</option>
        <option value="bug">Bug Report</option>
        <option value="question">Question</option>
        <option value="other">Other</option>
      </select>

      <input
        type="text"
        value={title}
        onChange={(e) => setTitle(e.target.value)}
        placeholder="Summary"
        required
      />

      <textarea
        value={description}
        onChange={(e) => setDescription(e.target.value)}
        placeholder="Details (optional)"
      />

      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? 'Submitting...' : 'Submit Feedback'}
      </button>
    </form>
  );
}

Roadmap/Changelog Display

'use client';

import { useEffect, useState } from 'react';
import Feedhog from '@feedhog/js';
import type { FeedbackListItem, PaginatedResponse } from '@feedhog/js';

const feedhog = new Feedhog({
  apiKey: process.env.NEXT_PUBLIC_FEEDHOG_API_KEY!
});

export function PublicRoadmap() {
  const [feedback, setFeedback] = useState<FeedbackListItem[]>([]);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    async function loadRoadmap() {
      try {
        const { items } = await feedhog.list({
          status: ['planned', 'in-progress'],
          type: 'idea',
          sortBy: 'votes',
          limit: 20
        });
        setFeedback(items);
      } catch (error) {
        console.error('Failed to load roadmap:', error);
      } finally {
        setIsLoading(false);
      }
    }

    loadRoadmap();
  }, []);

  if (isLoading) return <div>Loading...</div>;

  const planned = feedback.filter(f => f.status === 'planned');
  const inProgress = feedback.filter(f => f.status === 'in-progress');

  return (
    <div className="grid grid-cols-2 gap-8">
      <div>
        <h2>Planned</h2>
        {planned.map(item => (
          <div key={item.id}>
            <h3>{item.title}</h3>
            <p>{item.voteCount} votes</p>
          </div>
        ))}
      </div>
      <div>
        <h2>In Progress</h2>
        {inProgress.map(item => (
          <div key={item.id}>
            <h3>{item.title}</h3>
            <p>{item.voteCount} votes</p>
          </div>
        ))}
      </div>
    </div>
  );
}

Voting Component

'use client';

import { useState } from 'react';
import Feedhog from '@feedhog/js';

const feedhog = new Feedhog({
  apiKey: process.env.NEXT_PUBLIC_FEEDHOG_API_KEY!
});

interface VoteButtonProps {
  feedbackId: string;
  initialVoteCount: number;
  initialHasVoted: boolean;
  userId: string;
}

export function VoteButton({
  feedbackId,
  initialVoteCount,
  initialHasVoted,
  userId
}: VoteButtonProps) {
  const [voteCount, setVoteCount] = useState(initialVoteCount);
  const [hasVoted, setHasVoted] = useState(initialHasVoted);
  const [isLoading, setIsLoading] = useState(false);

  async function handleVote() {
    setIsLoading(true);

    try {
      // Ensure user is identified
      await feedhog.identify({ externalId: userId });

      // Toggle vote
      const { voted, voteCount: newCount } = await feedhog.vote(feedbackId);

      setHasVoted(voted);
      setVoteCount(newCount);
    } catch (error) {
      console.error('Failed to vote:', error);
    } finally {
      setIsLoading(false);
    }
  }

  return (
    <button
      onClick={handleVote}
      disabled={isLoading}
      className={hasVoted ? 'voted' : ''}
    >
      {hasVoted ? '▲' : '△'} {voteCount}
    </button>
  );
}

Environment Variables

# Public API key (safe for client-side)
NEXT_PUBLIC_FEEDHOG_API_KEY=fhpk_your_public_key

Package Exports

@feedhog/js

// Default export
import Feedhog from '@feedhog/js';

// Named exports
import {
  Feedhog,
  FeedhogApiError,
  // Types
  type FeedhogConfig,
  type UserIdentity,
  type SubmitFeedbackInput,
  type ListFeedbackOptions,
  type FeedbackListItem,
  type FeedbackDetail,
  type PaginatedResponse,
  type VoteResult,
  type IdentifiedUser,
  type CurrentUser,
  type FeedbackType,
  type FeedbackStatus,
  type SortBy
} from '@feedhog/js';

@feedhog/widget

// React component
import { FeedhogWidget } from '@feedhog/widget/react';

// Types
import type {
  FeedhogWidgetProps,
  WidgetSettings,
  UserIdentity
} from '@feedhog/widget/react';