thefaqapp
Guides

Patterns & Recipes

Common API patterns -- pagination, error handling, filtering, and more

Pagination

All list endpoints return paginated results using page and limit query parameters.

Reading Pagination Metadata

Every paginated response includes a meta.pagination object:

{
  "data": [...],
  "meta": {
    "pagination": {
      "page": 1,
      "limit": 25,
      "total": 142,
      "pages": 6
    }
  }
}
FieldDescription
pageCurrent page number
limitItems per page
totalTotal number of matching items
pagesTotal number of pages

Iterating All Pages

# Fetch page 1
curl "https://app.thefaq.app/api/v1/your-org/questions?page=1&limit=100" \
  -H "Authorization: Bearer faq_your-api-key"

# Check meta.pagination.pages, then fetch remaining pages
curl "https://app.thefaq.app/api/v1/your-org/questions?page=2&limit=100" \
  -H "Authorization: Bearer faq_your-api-key"
async function fetchAllQuestions(apiKey: string, orgSlug: string) {
  const all = [];
  let page = 1;
  let totalPages = 1;

  do {
    const response = await fetch(
      `https://app.thefaq.app/api/v1/${orgSlug}/questions?page=${page}&limit=100`,
      { headers: { 'Authorization': `Bearer ${apiKey}` } }
    );
    const { data, meta } = await response.json();
    all.push(...data);
    totalPages = meta.pagination.pages;
    page++;
  } while (page <= totalPages);

  return all;
}

Error Handling

All API errors return a consistent envelope. Build your error handling around this structure.

Error Envelope

{
  "error": {
    "code": "validation_error",
    "message": "Invalid request body",
    "details": {
      "fieldErrors": { "question": ["String must contain at least 1 character(s)"] },
      "formErrors": []
    }
  }
}

Common Error Codes

CodeHTTPWhen
unauthorized401Missing or invalid API key
forbidden403API key lacks required scopes
not_found404Resource does not exist
conflict409Slug already taken
validation_error400Request body failed validation
invalid_json400Body is not valid JSON
has_questions400Category has questions (cannot delete)
rate_limit_exceeded429Rate limit exceeded

Robust Error Handler

# Check HTTP status and parse error
response=$(curl -s -w "\n%{http_code}" \
  https://app.thefaq.app/api/v1/your-org/questions \
  -H "Authorization: Bearer faq_your-api-key")

http_code=$(echo "$response" | tail -1)
body=$(echo "$response" | head -n -1)

if [ "$http_code" -ge 400 ]; then
  echo "Error $http_code: $(echo "$body" | jq -r '.error.message')"
fi
interface ApiError {
  error: {
    code: string;
    message: string;
    details?: Record<string, unknown>;
  };
}

async function apiRequest<T>(url: string, options?: RequestInit): Promise<T> {
  const response = await fetch(url, {
    ...options,
    headers: {
      'Authorization': `Bearer ${process.env.FAQ_API_KEY}`,
      'Content-Type': 'application/json',
      ...options?.headers,
    },
  });

  if (!response.ok) {
    const error: ApiError = await response.json();

    switch (error.error.code) {
      case 'unauthorized':
        throw new Error('Invalid API key. Check your credentials.');
      case 'rate_limit_exceeded':
        const retryAfter = response.headers.get('Retry-After');
        throw new Error(`Rate limited. Retry after ${retryAfter}s.`);
      case 'validation_error':
        throw new Error(`Validation: ${JSON.stringify(error.error.details)}`);
      default:
        throw new Error(`API error: ${error.error.message}`);
    }
  }

  return response.json();
}

See Errors Reference for the complete error code list.

Filtering and Sorting

The questions endpoint supports rich filtering via query parameters.

Filter Examples

# Published questions in "billing" category
curl "https://app.thefaq.app/api/v1/your-org/questions?category=billing&status=published" \
  -H "Authorization: Bearer faq_your-api-key"

# Featured questions, newest first
curl "https://app.thefaq.app/api/v1/your-org/questions?featured=true&sortBy=createdAt&sortOrder=desc" \
  -H "Authorization: Bearer faq_your-api-key"

# Search across all content
curl "https://app.thefaq.app/api/v1/your-org/questions?search=password+reset" \
  -H "Authorization: Bearer faq_your-api-key"

# Filter by tags
curl "https://app.thefaq.app/api/v1/your-org/questions?tags=important,onboarding" \
  -H "Authorization: Bearer faq_your-api-key"
// Build query params dynamically
function buildQueryParams(filters: {
  category?: string;
  status?: 'published' | 'draft';
  featured?: boolean;
  search?: string;
  tags?: string[];
  sortBy?: string;
  sortOrder?: 'asc' | 'desc';
  page?: number;
  limit?: number;
}) {
  const params = new URLSearchParams();

  if (filters.category) params.set('category', filters.category);
  if (filters.status) params.set('status', filters.status);
  if (filters.featured !== undefined) params.set('featured', String(filters.featured));
  if (filters.search) params.set('search', filters.search);
  if (filters.tags?.length) params.set('tags', filters.tags.join(','));
  if (filters.sortBy) params.set('sortBy', filters.sortBy);
  if (filters.sortOrder) params.set('sortOrder', filters.sortOrder);
  if (filters.page) params.set('page', String(filters.page));
  if (filters.limit) params.set('limit', String(filters.limit));

  return params.toString();
}

const query = buildQueryParams({
  category: 'billing',
  status: 'published',
  sortBy: 'createdAt',
  sortOrder: 'desc',
  limit: 10,
});

const response = await fetch(
  `https://app.thefaq.app/api/v1/your-org/questions?${query}`,
  { headers: { 'Authorization': 'Bearer faq_your-api-key' } }
);

Sort Options

sortBy ValueDescription
createdAtWhen the question was created
updatedAtWhen last modified
sortOrderManual sort order field
questionAlphabetical by title

Use sortOrder=asc (default) or sortOrder=desc to control direction.

Rate Limits

Every API response includes rate limit headers so you can monitor your usage.

Rate Limit Headers

HeaderDescription
X-RateLimit-LimitTotal requests allowed in current window
X-RateLimit-RemainingRequests remaining
X-RateLimit-ResetUnix timestamp when the window resets
Retry-AfterSeconds until you can retry (only on 429)

Handling 429 Responses

# Check rate limit headers
curl -v https://app.thefaq.app/api/v1/your-org/questions \
  -H "Authorization: Bearer faq_your-api-key" 2>&1 \
  | grep -i "x-ratelimit"
async function fetchWithRetry(
  url: string,
  options: RequestInit,
  maxRetries = 3
): Promise<Response> {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    const response = await fetch(url, options);

    if (response.status !== 429) return response;

    const retryAfter = Number(response.headers.get('Retry-After') || '60');
    console.log(`Rate limited. Retrying in ${retryAfter}s...`);
    await new Promise((resolve) => setTimeout(resolve, retryAfter * 1000));
  }

  throw new Error('Max retries exceeded');
}

See Rate Limits Reference for plan-specific limits and best practices.

Bulk Operations

When you need to modify multiple items, use the bulk endpoint instead of individual requests.

Batch Patterns

# Bulk publish
curl -X POST https://app.thefaq.app/api/v1/your-org/questions/bulk \
  -H "Authorization: Bearer faq_your-api-key" \
  -H "Content-Type: application/json" \
  -d '{ "action": "publish", "ids": ["id1", "id2", "id3"] }'

# Bulk delete
curl -X POST https://app.thefaq.app/api/v1/your-org/questions/bulk \
  -H "Authorization: Bearer faq_your-api-key" \
  -H "Content-Type: application/json" \
  -d '{ "action": "delete", "ids": ["id1", "id2"] }'

# Bulk categorize
curl -X POST https://app.thefaq.app/api/v1/your-org/questions/bulk \
  -H "Authorization: Bearer faq_your-api-key" \
  -H "Content-Type: application/json" \
  -d '{ "action": "categorize", "ids": ["id1", "id2"], "categoryId": "cat_id" }'
type BulkAction = 'publish' | 'unpublish' | 'delete' | 'categorize';

async function bulkOperation(
  action: BulkAction,
  ids: string[],
  categoryId?: string
) {
  const body: Record<string, unknown> = { action, ids };
  if (action === 'categorize' && categoryId) {
    body.categoryId = categoryId;
  }

  const response = await fetch(
    'https://app.thefaq.app/api/v1/your-org/questions/bulk',
    {
      method: 'POST',
      headers: {
        'Authorization': 'Bearer faq_your-api-key',
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(body),
    }
  );

  const { data } = await response.json();
  return data; // { count: number }
}

Export

Export all questions as JSON or CSV for backup, migration, or analytics.

# Export as JSON (default)
curl "https://app.thefaq.app/api/v1/your-org/questions/export" \
  -H "Authorization: Bearer faq_your-api-key" \
  -o questions.json

# Export as CSV
curl "https://app.thefaq.app/api/v1/your-org/questions/export?format=csv" \
  -H "Authorization: Bearer faq_your-api-key" \
  -o questions.csv
// Export and save to file
const response = await fetch(
  'https://app.thefaq.app/api/v1/your-org/questions/export?format=json',
  {
    headers: { 'Authorization': 'Bearer faq_your-api-key' },
  }
);

const { data } = await response.json();
console.log(`Exported ${data.length} questions`);

See Questions API Reference for full export options.

On this page