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
}
}
}| Field | Description |
|---|---|
page | Current page number |
limit | Items per page |
total | Total number of matching items |
pages | Total 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
| Code | HTTP | When |
|---|---|---|
unauthorized | 401 | Missing or invalid API key |
forbidden | 403 | API key lacks required scopes |
not_found | 404 | Resource does not exist |
conflict | 409 | Slug already taken |
validation_error | 400 | Request body failed validation |
invalid_json | 400 | Body is not valid JSON |
has_questions | 400 | Category has questions (cannot delete) |
rate_limit_exceeded | 429 | Rate 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')"
fiinterface 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 Value | Description |
|---|---|
createdAt | When the question was created |
updatedAt | When last modified |
sortOrder | Manual sort order field |
question | Alphabetical 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
| Header | Description |
|---|---|
X-RateLimit-Limit | Total requests allowed in current window |
X-RateLimit-Remaining | Requests remaining |
X-RateLimit-Reset | Unix timestamp when the window resets |
Retry-After | Seconds 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.