---
title: "Get inferred audience age + gender distributions"
method: GET
path: "/v1/tracking/creators/{id}/audience-demographics"
tags: ["Tracking"]
---

# Get inferred audience age + gender distributions

`GET /v1/tracking/creators/{id}/audience-demographics`

## Request

- Base URL: `https://api.virlo.ai`
- URL: `https://api.virlo.ai/v1/tracking/creators/{id}/audience-demographics`
- Auth: HTTP bearer

Returns the latest cached audience snapshot for the tracked creator. Snapshot may be older than freshness_days; check is_stale. Use POST /v1/tracking/creators/:id/audience-refresh to trigger regeneration. Returns { snapshot: null } when no snapshot has ever been generated for this creator.

## Path parameters

- `id` string, required

## Query parameters

- `freshness_days` number

## Response `200`

Audience demographics snapshot

- AudienceSnapshotResponseDto
  - `data` AudienceSnapshotDataDto, required
    - `platform` 'tiktok' | 'youtube' | 'instagram', required
    - `handle` string, required
    - `snapshot` AudienceSnapshotDto
      - `snapshot_id` string, required
      - `snapshot_at` string, required
      - `sample_size` number, required: Number of commenter samples resolved for this snapshot
      - `age_distribution` object, nullable: Age band → share of resolved sample (0–1). Keys: "13-17", "18-24", "25-34", "35-44", "45+". Null when confidence is below internal floor.
      - `gender_distribution` object, nullable: Gender → share of resolved sample (0–1). Keys: "female", "male", "unknown".
      - `country_distribution` AudienceGeoBucketDto[], nullable: Top countries, ordered by share descending. Low-volume tail entries (those backed by fewer than 2 underlying commenters) are consolidated into a single trailing `{ "name": "Other (N countries)", "pct": <sum> }` bucket so the distribution head stays readable on small samples.
        - `code` string: ISO 3166-1 alpha-2 code (countries) or two-letter country code the city belongs to. Omitted on the trailing "Other" bucket that aggregates the long tail.
        - `name` string, required: Display name. The trailing bucket may be returned as "Other (N countries)" / "Other (N cities)" when low-volume tail entries are consolidated.
        - `pct` number, required: Share of the resolved sample (0–1)
        - `confidence` number: Aggregate confidence the pipeline has in this bucket assignment (0–1). Combines per-commenter inference confidence and corroborating signals. Omitted on the trailing "Other" bucket.
      - `city_distribution` AudienceGeoBucketDto[], nullable: Top cities, ordered by share descending. Long-tail consolidation matches `country_distribution` — singletons are rolled into a trailing `{ "name": "Other (N cities)", "pct": <sum> }` bucket. May be null when city signal is too sparse.
        - `code` string: ISO 3166-1 alpha-2 code (countries) or two-letter country code the city belongs to. Omitted on the trailing "Other" bucket that aggregates the long tail.
        - `name` string, required: Display name. The trailing bucket may be returned as "Other (N countries)" / "Other (N cities)" when low-volume tail entries are consolidated.
        - `pct` number, required: Share of the resolved sample (0–1)
        - `confidence` number: Aggregate confidence the pipeline has in this bucket assignment (0–1). Combines per-commenter inference confidence and corroborating signals. Omitted on the trailing "Other" bucket.
      - `language_distribution` object, nullable: Language code → share of resolved sample (0–1). ISO 639-1 lowercase codes.
      - `confidence_per_signal` AudienceConfidenceDto, required
        - `age` number
        - `gender` number
        - `country` number
        - `city` number
        - `language` number
      - `confidence_level` 'low' | 'medium' | 'high', required: Coarse confidence band. "high" = ≥200 samples with strong signals; "medium" = ≥100 or "high" downgraded (weak signals OR followers-only sample); "low" = ≥30 samples. Quick-take on snapshot reliability before reading `confidence_per_signal`.
      - `data_source` 'comments' | 'mixed' | 'followers' | 'comments_extended', required: How the sample was assembled. "comments" — engagement-only sample (default); "mixed" — comments + follower list blend (TikTok Tier 1 fallback when comments are sparse); "followers" — follower-list only (TikTok Tier 2 fallback, indicates audience composition without active engagement); "comments_extended" — comments re-collected over an extended post window (IG/YT fallback). Use to weight signals: comment-derived samples reflect active engagement; follower-derived samples reflect who chose to subscribe.
      - `signal_breakdown` object, required: Per-source contribution to `sample_size`. Always sums to `sample_size`. For `data_source = "followers"` snapshots, `comments` is 0; for `data_source = "comments"` snapshots, `followers` is 0; for `mixed` both are non-zero.
        - `comments` number
        - `followers` number
      - `evidence_summary` string, nullable: Human-readable summary of how the inferences were derived. Calls out when the sample is follower-sourced rather than commenter-sourced.
      - `model_version` string, required: Audience inference model version.
    - `is_stale` boolean, required: True when the snapshot exists but is older than the requested freshness_days window. False when no freshness_days was supplied or the snapshot is within the window.
    - `finalized` boolean: True when no audience-snapshot job is currently in flight for this creator and `pending_jobs` is empty (snapshot is either present and final, or genuinely missing). False when an audience-refresh job is mid-flight — see `pending_jobs[0]` for the live job_id and poll URL.
    - `pending_jobs` PendingJobDto[]: Empty when the resource is finalized. Contains a single `audience_demographics` / `audience_geography` entry while a refresh job is in flight, so an agent that lost the job_id from its earlier `audience-refresh` call can still poll `/v1/audience/snapshot/:job_id` instead of triggering a duplicate refresh.
      - `type` 'viral_analysis' | 'audience_demographics' | 'audience_geography' | 'tracking_report', required: Stable identifier for the kind of secondary job. Use this to switch on the field where the result will appear (see `result_path`).
      - `status` 'pending' | 'processing', required: Current non-terminal state of the job. Once the job reaches a terminal state (ready / failed / skipped) the entry is removed from `pending_jobs[]`.
      - `job_id` string: Optional opaque identifier for the underlying job, when one exists. Useful with `poll_url` to fetch the result directly.
      - `poll_url` string: Path (relative to api.virlo.ai) you can GET to fetch the result of this job once it is ready. Free to call.
      - `result_path` string: Dot-path inside the parent resource's response body where the completed result will appear. Useful for clients that re-fetch the resource instead of using `poll_url`.
      - `webhook_event` string: Name of an event from the existing `SUPPORTED_EVENTS` list that fires when this job reaches a terminal state. Subscribe to it via POST /v1/webhooks to avoid polling. Guaranteed to be one of the already-supported event names so existing webhook subscriptions keep working — no new event types were introduced.
      - `started_at` string: When the job entered its current state (ISO 8601 UTC).
      - `retry_after_seconds` number: Suggested minimum delay (seconds) before polling again. Respect this to avoid hitting rate limits.

## Other responses

- `401`: Unauthorized - missing or invalid API key
- `404`: Tracked creator not found
- `429`: Rate limit exceeded (`rate_limit_exceeded`); see the Retry-After header

---

[API](https://skmtc.dev/virlo/apis/virlo-public-api.md) · [All operations](https://skmtc.dev/virlo/apis/virlo-public-api/llms.txt) · [OpenAPI document](https://skmtc.dev/virlo/apis/virlo-public-api/revisions/3b69c6145087?raw)
