Export Creator Analytics (CSV)
Download a CSV of the overview, traffic, or links endpoint. CSV-injection-safe via formula-character escaping. Returns text/csv with attachment Content-Disposition.
GET /api/v1/creators/analytics/export โ ๐ Bearer ยท Rate limit: 60 req / minute ยท Content-Type: text/csv
Returns a CSV file derived from one of three other endpoints โ pick via ?type=overview / ?type=traffic / ?type=links. The body is the raw CSV (not JSON-wrapped) and the response carries Content-Disposition: attachment; filename=biore-analytics-<type>-<YYYY-MM-DD>.csv so a browser will download it.
type is one of three. Only overview, traffic, links are supported. Anything else returns a CSV body of error\nInvalid export type. Use: overview, traffic, or links. (still 200, still text/csv). Validate client-side before calling.
Internally re-uses the same JSON endpoints. The export handler calls this.overview(userId, days), this.traffic(userId, days), or this.links(userId, days) and serializes the result. So the data is the same as the JSON endpoints โ only the wire format and the field flattening differ.
CSV injection is mitigated server-side. Every value runs through sanitizeCSVValue: leading =, +, -, @, \t, \r get prefixed with ' (single quote) so spreadsheet apps don't parse the cell as a formula. Double quotes are escaped (""); cells containing commas, quotes, or newlines are wrapped in double quotes. Do not double-sanitize on the client.
Filename has the current UTC date, not the window range. The suffix is new Date().toISOString().slice(0, 10) โ today's date, not "from-X-to-Y." If you save several exports the same day they'll collide; rename client-side if you need to disambiguate.
Request
Query parameters
| Param | Type | Required | Notes |
|---|---|---|---|
type | enum | โ | overview / traffic / links. Anything else โ error CSV body. |
days | integer | optional (default 30) | Lookback window. Capped at 365. |
Headers
| Header | Required | Notes |
|---|---|---|
Authorization: Bearer <accessToken> | โ | JwtAuthGuard. |
Response
200 OK โ text/csv
Important: this endpoint does NOT return the JSON envelope. The body is raw CSV bytes. Use res.text() (not res.json()).
| Header | Value |
|---|---|
Content-Type | text/csv |
Content-Disposition | attachment; filename=biore-analytics-<type>-<YYYY-MM-DD>.csv |
CSV shapes
?type=overview โ 5 rows, two columns:
metric,value
total_views,4200
unique_visitors,1800
total_clicks,320
ctr,7.62
days,30?type=traffic โ header + one row per traffic source:
source,visits,percentage
instagram.com,600,35
direct,280,16
youtube.com,150,9?type=links โ header + one row per link with clicks:
id,title,url,clicks,ctr
link-uuid-1,My Portfolio,https://example.com,120,4.8
link-uuid-deleted,Unknown,,6,0.24Invalid type โ single error row, still 200 OK:
error
Invalid export type. Use: overview, traffic, or links.Errors
| HTTP | Reason |
|---|---|
401 | Missing / invalid bearer token. |
404 | error.creator.not_found / error.creator.bio_not_found (raised inside the underlying overview/traffic/links calls). |
429 | Throttle. |
There is no 400 for invalid type โ instead the body becomes the error CSV described above.
Side effects
JwtAuthGuard+ThrottleGuard.- Compute
d = Math.min(parseInt(days ?? '30'), 365). - Switch on
type:overviewโ callthis.overview(userId, String(d))โ 5-row metric/value CSV.trafficโ callthis.traffic(userId, String(d))โ header + per-source rows.linksโ callthis.links(userId, String(d))โ header + per-link rows.- Anything else โ static
error\nInvalid export type. ...\nbody.
- Run every value through
sanitizeCSVValue(formula-prefix, quote-escape, comma/newline-wrap). res.setHeader('Content-Disposition', ...);res.send(csv)(raw text body).
Code samples
# Saves the file using the server-suggested filename
curl -OJ \
-H "Authorization: Bearer $ACCESS_TOKEN" \
'https://api.bio.re/api/v1/creators/analytics/export?type=overview&days=30'async function exportCsv(
accessToken: string,
type: 'overview' | 'traffic' | 'links',
days = 30,
): Promise<{ csv: string; filename: string }> {
const res = await fetch(
`https://api.bio.re/api/v1/creators/analytics/export?type=${type}&days=${days}`,
{ headers: { Authorization: `Bearer ${accessToken}` } },
);
if (!res.ok) throw new Error(`Export failed: ${res.status}`);
const csv = await res.text(); // NOT .json() โ body is raw CSV
const dispo = res.headers.get('Content-Disposition') ?? '';
const match = /filename=([^;]+)/.exec(dispo);
const filename = match?.[1]?.trim() ?? `biore-analytics-${type}.csv`;
return { csv, filename };
}async function downloadExport(
type: 'overview' | 'traffic' | 'links',
days = 30,
) {
const res = await fetch(
`/api/v1/creators/analytics/export?type=${type}&days=${days}`,
);
if (!res.ok) return;
const blob = await res.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `biore-analytics-${type}-${new Date().toISOString().slice(0, 10)}.csv`;
a.click();
URL.revokeObjectURL(url);
}Try it
Authorization
bearer In: header
Query Parameters
"overview" | "traffic" | "links"Response Body
text/csv
text/csv
curl -X GET "https://loading/api/v1/creators/analytics/export?type=overview"{
"success": false,
"error": {
"code": "AUTH_UNAUTHORIZED",
"message": "Invalid credentials",
"i18nKey": "auth.login.invalid_credentials",
"i18nVars": {
"field": "email"
},
"details": [
{
"message": "email must be an email"
}
],
"correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
}{
"success": false,
"error": {
"code": "AUTH_UNAUTHORIZED",
"message": "Invalid credentials",
"i18nKey": "auth.login.invalid_credentials",
"i18nVars": {
"field": "email"
},
"details": [
{
"message": "email must be an email"
}
],
"correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
}Source
| Source | Path | Lines |
|---|---|---|
| Controller | apps/api-core/src/modules/analytics/creator-analytics.controller.ts | 206โ246 (exportAnalytics โ type switch, sanitization, attachment header) |
| CSV sanitizer | apps/api-core/src/common/utils/sanitize-csv.ts | 6โ25 (sanitizeCSVValue โ formula prefix ', double-quote escaping, comma/newline wrap) |
| Re-used handlers | apps/api-core/src/modules/analytics/creator-analytics.controller.ts | 45โ62 (overview), 64โ80 (traffic), 165โ204 (links) |
Creator Link Performance
Per-link click counts and CTR for the creator's bio page. Cross-DB join โ clicks come from Analytics DB, link metadata (title + URL) is fetched from main DB by id.
Search Creators
Public creator search across username, displayName, and bio. PostgreSQL full-text search with ILIKE fallback. Cursor-based pagination, CDN-cached 5min/10min SWR.