List Help Articles
Public paginated list of published help articles. Filters by category slug, search term (title/content/excerpt ILIKE), featured-only, locale. Each item carries embedded category. CDN 5min/10min SWR.
GET /api/v1/public/help/articles โ ๐ Public ยท Rate limit: 60 req / minute
Paginated list of published help articles. Supports four filters: ?category=<slug>, ?search=<term> (case-insensitive substring match across title + content + excerpt), ?featured=true (featured articles only), ?locale=<code>. Each item is the lightweight projection (no full body) plus the embedded category ({ name, slug }).
Search is substring, not full-text. ?search=billing matches "Billing FAQ" and "How to handle billing disputes". The match runs as ILIKE over title, content, and excerpt (any of the three) with mode: 'insensitive'. No tokenization, no ranking โ substring contains.
?featured=true requires the literal string 'true'. Anything else (including '1', 'yes') falls back to "no featured filter". The check is featured === 'true'. Pass case-sensitive true.
Request
Query parameters
| Param | Type | Default | Validation | Notes |
|---|---|---|---|---|
page | number | 1 | ParseIntPipe, server-clamped to >= 1 | 1-based page index |
limit | number | 20 | ParseIntPipe, server-clamped to [1, 50] | Items per page |
category | string | โ | โ | Filter by HelpCategory.slug |
search | string | โ | โ | Case-insensitive substring across title/content/excerpt |
featured | string | โ | โ | Pass literal 'true' for featured-only; anything else = no filter |
locale | string | โ | โ | Filter by locale (e.g. en, tr) |
No headers required.
Response headers
| Header | Value |
|---|---|
Cache-Control | public, s-maxage=300, stale-while-revalidate=600 |
Response
200 OK โ ApiResponseOf<PublicHelpArticleListDataDto>
{
"success": true,
"data": {
"items": [
{
"slug": "how-to-withdraw",
"title": "How to Withdraw Funds",
"excerpt": "Step-by-step guide to withdrawing earnings.",
"icon": "wallet",
"readTimeMinutes": 5,
"isFeatured": true,
"publishedAt": "2026-04-29T20:00:00.000Z",
"category": { "name": "Payments & Billing", "slug": "payments" }
}
],
"total": 30,
"page": 1,
"limit": 20,
"totalPages": 2
}
}Item fields
| Field | Type | Notes |
|---|---|---|
slug | string | Pass to GET /public/help/articles/:slug for the body |
title | string | Display title |
excerpt | string | null | Short summary for cards |
icon | string | null | Icon identifier |
readTimeMinutes | number | null | Estimated read time |
isFeatured | boolean | Whether the article is in the "featured" set |
publishedAt | string (ISO 8601) | null | When published |
category | object | { name, slug } of the parent HelpCategory |
Top-level fields
| Field | Type | Notes |
|---|---|---|
items | array | Up to limit articles, ordered by publishedAt DESC |
total | number | Total matching the filter |
page / limit / totalPages | number | Echoed pagination metadata |
Errors
| HTTP | code / i18nKey | Reason |
|---|---|---|
429 | (throttle) | Rate limit exceeded (60 req/min) |
Side effects
- Clamp
page = max(page, 1),limit = min(max(limit, 1), 50). - Build
where:status: PUBLISHED(always).?categoryโ{ category: { slug: categorySlug } }.?featured === 'true'โ{ isFeatured: true }.?localeโ{ locale }.?searchโOR: [{ title: { contains: search, mode: 'insensitive' } }, { content: { contains } }, { excerpt: { contains } }].
- In parallel:
helpArticle.findMany({ where, orderBy: publishedAt desc, include: category, skip, take })+count({ where }). - Project category to
{ name, slug }. - Return paginated envelope. No mutations.
Code samples
# Featured articles in payments category
curl 'https://api.bio.re/api/v1/public/help/articles?category=payments&featured=true&limit=20'
# Search for "withdraw"
curl 'https://api.bio.re/api/v1/public/help/articles?search=withdraw'type HelpCategoryEmbed = { name: string; slug: string };
type HelpArticleListItem = {
slug: string;
title: string;
excerpt: string | null;
icon: string | null;
readTimeMinutes: number | null;
isFeatured: boolean;
publishedAt: string | null;
category: HelpCategoryEmbed;
};
type HelpArticleListData = {
items: HelpArticleListItem[];
total: number;
page: number;
limit: number;
totalPages: number;
};
async function listHelpArticles(filter: {
page?: number;
limit?: number;
category?: string;
search?: string;
featured?: boolean;
locale?: string;
} = {}): Promise<HelpArticleListData> {
const url = new URL('https://api.bio.re/api/v1/public/help/articles');
if (filter.page) url.searchParams.set('page', String(filter.page));
if (filter.limit) url.searchParams.set('limit', String(filter.limit));
if (filter.category) url.searchParams.set('category', filter.category);
if (filter.search) url.searchParams.set('search', filter.search);
if (filter.featured) url.searchParams.set('featured', 'true');
if (filter.locale) url.searchParams.set('locale', filter.locale);
const res = await fetch(url);
const json = await res.json();
if (!res.ok || !json.success) {
throw Object.assign(new Error(json?.error?.message ?? 'Help articles fetch failed'), {
code: json?.error?.code,
});
}
return json.data;
}import { useQuery, keepPreviousData } from '@tanstack/react-query';
export const contentKeys = {
helpArticles: (filter: Record<string, unknown>) =>
['content', 'help', 'articles', filter] as const,
};
export function useHelpArticles(filter: {
page?: number;
limit?: number;
category?: string;
search?: string;
featured?: boolean;
locale?: string;
} = {}) {
return useQuery({
queryKey: contentKeys.helpArticles(filter),
queryFn: async () => {
const url = new URL('/api/v1/public/help/articles', window.location.origin);
if (filter.page) url.searchParams.set('page', String(filter.page));
if (filter.limit) url.searchParams.set('limit', String(filter.limit));
if (filter.category) url.searchParams.set('category', filter.category);
if (filter.search) url.searchParams.set('search', filter.search);
if (filter.featured) url.searchParams.set('featured', 'true');
if (filter.locale) url.searchParams.set('locale', filter.locale);
const res = await fetch(url);
const json = await res.json();
if (!res.ok || !json.success) {
throw Object.assign(new Error(json?.error?.message ?? 'Help articles fetch failed'), {
code: json?.error?.code,
i18nKey: json?.error?.i18nKey,
});
}
return json.data as HelpArticleListData;
},
placeholderData: keepPreviousData,
staleTime: 5 * 60_000,
});
}Try it
Query Parameters
Filter by category slug
Search in title/content/excerpt
Only featured articles
Response Body
application/json
curl -X GET "https://loading/api/v1/public/help/articles"{
"success": true,
"data": {
"items": [
{
"slug": "how-to-withdraw",
"title": "How to Withdraw Funds",
"excerpt": "string",
"icon": "string",
"readTimeMinutes": 0,
"isFeatured": false,
"publishedAt": "2019-08-24T14:15:22Z",
"category": {
"slug": "payments",
"name": "Payments & Billing"
}
}
],
"total": 30,
"page": 1,
"limit": 20,
"totalPages": 2
}
}Source
| Source | Path | Lines |
|---|---|---|
| Controller | apps/api-core/src/modules/content/public-content.controller.ts | 310โ350 (listPublishedHelpArticles) |
| DTO (response) | apps/api-core/src/modules/content/dto/content-public-response.dto.ts | 239โ254 (PublicHelpArticleListDataDto), 210โ234 (PublicHelpArticleListItemDto + PublicHelpCategoryEmbedDto) |
| Service | apps/api-core/src/modules/content/content.service.ts | 751โ776 (listPublishedHelpArticles) |
| Prisma models | packages/prisma/prisma/schema.prisma | HelpArticle (status = PUBLISHED, optional isFeatured filter), HelpCategory (joined) |
List Help Categories
Public list of published help center categories with article counts. Optional locale filter. Ordered by admin-set sortOrder. CDN 5min/10min SWR.
Get Help Article (by slug)
Public read of a single help article by slug. PUBLISHED-only. HTML body server-side sanitized. Embedded category. 404 on missing/draft.