> ## Documentation Index
> Fetch the complete documentation index at: https://docs.qwairy.co/llms.txt
> Use this file to discover all available pages before exploring further.

# Custom Dashboard

> Build a branded GEO dashboard with KPIs, competitor rankings, and source analysis

Build a custom dashboard displaying your brand's GEO performance metrics, competitor rankings, and top sources.

<Note>
  This guide uses the API client from the [Guides index](/developers/guides/index#api-client). Copy it to your project first.
</Note>

## What You'll Build

A dashboard with:

* **KPI cards**: Mention Rate, Source Rate, Coverage, Share of Voice, Sentiment
* **Competitor ranking**: Share of Voice comparison with gap analysis
* **Source ranking**: Share of Citations by domain

***

## Fetch Dashboard Data

Fetch all data in parallel for optimal performance.

<CodeGroup>
  ```javascript JavaScript theme={null}
  async function fetchDashboardData(client, brandId, period = 30) {
    // Parallel API calls
    const [performance, competitors, sources] = await Promise.all([
      client.getPerformance(brandId, { period }),
      client.getCompetitors(brandId, { period, limit: 10, sort: 'shareOfVoice', order: 'desc' }),
      client.getSourceDomains(brandId, { period, limit: 10, sort: 'mentions', order: 'desc' }),
    ]);

    // Find your brand in competitors list
    const yourBrand = competitors.competitors.find(c => c.relationship === 'SELF');
    const directCompetitors = competitors.competitors.filter(c => c.relationship === 'DIRECT');

    return {
      kpis: {
        mentionRate: performance.scores.mentionRate,
        sourceRate: performance.scores.sourceRate,
        coverage: performance.scores.coverage,
        shareOfVoice: performance.scores.shareOfVoice,
        sentiment: performance.scores.sentiment,
      },

      yourBrand: yourBrand ? {
        name: yourBrand.name,
        mentions: yourBrand.totalMentions,
        shareOfVoice: yourBrand.shareOfVoice,
        avgPosition: yourBrand.avgPosition,
        sentiment: yourBrand.avgSentiment,
      } : null,

      competitorRanking: directCompetitors.map(c => ({
        name: c.name,
        shareOfVoice: c.shareOfVoice,
        mentions: c.totalMentions,
        gap: yourBrand ? (c.shareOfVoice - yourBrand.shareOfVoice).toFixed(2) : null,
      })),

      sourceRanking: sources.sources.map(s => ({
        domain: s.domain,
        shareOfCitations: s.rate,
        mentions: s.totalMentions,
        isSelf: s.isSelf,
      })),

      methodology: {
        period: `${period} days`,
        promptsAnalyzed: performance.methodology.promptsCount,
        responsesGenerated: performance.methodology.responsesTotal,
        providers: performance.methodology.providers,
      },
    };
  }
  ```

  ```python Python theme={null}
  from dataclasses import dataclass
  from typing import List, Optional
  from concurrent.futures import ThreadPoolExecutor


  @dataclass
  class DashboardData:
      kpis: dict
      your_brand: Optional[dict]
      competitor_ranking: List[dict]
      source_ranking: List[dict]
      methodology: dict


  def fetch_dashboard_data(client, brand_id: str, period: int = 30) -> DashboardData:
      """Fetch all dashboard data in parallel."""

      with ThreadPoolExecutor(max_workers=3) as executor:
          perf_future = executor.submit(client.get_performance, brand_id, period=period)
          comp_future = executor.submit(client.get_competitors, brand_id, period=period, limit=10, sort='shareOfVoice')
          src_future = executor.submit(client.get_source_domains, brand_id, period=period, limit=10, sort='mentions')

          performance = perf_future.result()
          competitors = comp_future.result()
          sources = src_future.result()

      your_brand = next((c for c in competitors['competitors'] if c['relationship'] == 'SELF'), None)
      direct_competitors = [c for c in competitors['competitors'] if c['relationship'] == 'DIRECT']

      return DashboardData(
          kpis={
              'mention_rate': performance['scores']['mentionRate'],
              'source_rate': performance['scores']['sourceRate'],
              'coverage': performance['scores']['coverage'],
              'share_of_voice': performance['scores']['shareOfVoice'],
              'sentiment': performance['scores']['sentiment'],
          },
          your_brand={
              'name': your_brand['name'],
              'mentions': your_brand['totalMentions'],
              'share_of_voice': your_brand['shareOfVoice'],
              'avg_position': your_brand['avgPosition'],
              'sentiment': your_brand['avgSentiment'],
          } if your_brand else None,
          competitor_ranking=[
              {
                  'name': c['name'],
                  'share_of_voice': c['shareOfVoice'],
                  'mentions': c['totalMentions'],
                  'gap': round(c['shareOfVoice'] - your_brand['shareOfVoice'], 2) if your_brand else None,
              }
              for c in direct_competitors
          ],
          source_ranking=[
              {
                  'domain': s['domain'],
                  'share_of_citations': s['rate'],
                  'mentions': s['totalMentions'],
                  'is_self': s['isSelf'],
              }
              for s in sources['sources']
          ],
          methodology={
              'period': f"{period} days",
              'prompts_analyzed': performance['methodology']['promptsCount'],
              'responses_generated': performance['methodology']['responsesTotal'],
              'providers': performance['methodology']['providers'],
          },
      )
  ```
</CodeGroup>

***

## Usage

<CodeGroup>
  ```javascript JavaScript theme={null}
  const client = new QwairyClient(process.env.QWAIRY_API_TOKEN);
  const dashboard = await fetchDashboardData(client, 'your-brand-id', 30);

  console.log('KPIs:', dashboard.kpis);
  console.log('Your position:', dashboard.yourBrand);
  console.log('Top competitors:', dashboard.competitorRanking);
  console.log('Top sources:', dashboard.sourceRanking);
  ```

  ```python Python theme={null}
  client = QwairyClient()
  dashboard = fetch_dashboard_data(client, 'your-brand-id', period=30)

  print(f"Mention Rate: {dashboard.kpis['mention_rate']}%")
  print(f"Your Share of Voice: {dashboard.your_brand['share_of_voice']}%")
  print(f"Top competitor gap: {dashboard.competitor_ranking[0]['gap']}%")
  ```
</CodeGroup>

***

## Example Output

```json theme={null}
{
  "kpis": {
    "mentionRate": 45.2,
    "sourceRate": 23.9,
    "coverage": 33.33,
    "shareOfVoice": 8.13,
    "sentiment": 78.1
  },
  "yourBrand": {
    "name": "My Brand",
    "mentions": 104,
    "shareOfVoice": 8.13,
    "avgPosition": 2.1,
    "sentiment": 78.1
  },
  "competitorRanking": [
    { "name": "Competitor A", "shareOfVoice": 12.5, "mentions": 156, "gap": "4.37" },
    { "name": "Competitor B", "shareOfVoice": 9.8, "mentions": 122, "gap": "1.67" }
  ],
  "sourceRanking": [
    { "domain": "industry-news.com", "shareOfCitations": 5.10, "mentions": 102, "isSelf": false },
    { "domain": "mybrand.com", "shareOfCitations": 2.25, "mentions": 45, "isSelf": true }
  ],
  "methodology": {
    "period": "30 days",
    "promptsAnalyzed": 156,
    "responsesGenerated": 312,
    "providers": ["chatgpt", "perplexity"]
  }
}
```

***

## Data Structure Reference

| Field                              | Type    | Description                                           |
| ---------------------------------- | ------- | ----------------------------------------------------- |
| `kpis.mentionRate`                 | number  | % of brand-mentioning AI responses that mention yours |
| `kpis.sourceRate`                  | number  | % of source-citing AI responses that cite your domain |
| `kpis.coverage`                    | number  | % of all monitored AI responses mentioning your brand |
| `kpis.shareOfVoice`                | number  | Your % of all brand mentions (SELF + DIRECT)          |
| `kpis.sentiment`                   | number  | Average sentiment score (0-100)                       |
| `competitorRanking[].gap`          | string  | Difference in Share of Voice vs your brand            |
| `sourceRanking[].shareOfCitations` | number  | Source's % of all citations (`rate` field)            |
| `sourceRanking[].isSelf`           | boolean | Whether this is your own domain                       |

***

## LLM Diagnostics (Optional)

Go beyond aggregate scores: identify responses where your brand is mentioned but not cited (missed link opportunities), and discover what web queries AI models trigger before answering.

<CodeGroup>
  ```javascript JavaScript theme={null}
  async function fetchDiagnostics(client, brandId, period = 30) {
    const [mentionedNotCited, searchQueries] = await Promise.all([
      client.getAnswers(brandId, {
        period,
        hasSelfMention: true,
        hasSelfSource: false,
        limit: 5,
        sort: 'createdAt',
        order: 'desc',
      }),
      client.getSearch(brandId, { period, limit: 5, sort: 'createdAt', order: 'desc' }),
    ]);

    return {
      // Responses where AI mentions you but doesn't link to your content
      missedCitations: mentionedNotCited.answers.map(a => ({
        prompt: a.promptText,
        provider: a.provider,
        position: a.selfMentionPosition,
        competitors: a.competitorsCount,
      })),
      // Web queries AI models run before generating a response
      aiSearchQueries: searchQueries.searches.map(s => ({
        query: s.query,
        prompt: s.prompt,
        provider: s.provider,
      })),
    };
  }
  ```

  ```python Python theme={null}
  def fetch_diagnostics(client, brand_id: str, period: int = 30) -> dict:
      """Fetch LLM diagnostic data."""
      from concurrent.futures import ThreadPoolExecutor

      with ThreadPoolExecutor(max_workers=2) as executor:
          mention_future = executor.submit(
              client.get_answers, brand_id, period=period,
              has_self_mention=True, has_self_source=False, limit=5,
          )
          search_future = executor.submit(
              client.get_search, brand_id, period=period, limit=5,
          )

          mentioned_not_cited = mention_future.result()
          search_queries = search_future.result()

      return {
          'missed_citations': [
              {
                  'prompt': a['promptText'],
                  'provider': a['provider'],
                  'position': a.get('selfMentionPosition'),
                  'competitors': a['competitorsCount'],
              }
              for a in mentioned_not_cited['answers']
          ],
          'ai_search_queries': [
              {
                  'query': s['query'],
                  'prompt': s['prompt'],
                  'provider': s.get('provider'),
              }
              for s in search_queries['searches']
          ],
      }
  ```
</CodeGroup>

<Tip>
  **Missed citations** reveal where AI talks about you but doesn't link to your content — your highest-ROI content opportunities. **AI search queries** show what AI models actually search for before responding, giving you direct content targeting signals.
</Tip>

***

## Next Steps

* Add [weekly reports](/developers/guides/weekly-reports) to track changes over time
* Use [competitive analysis](/developers/guides/competitive-analysis) for deeper insights
* [Export data](/developers/guides/data-export) to your BI tools
