You need an API token (
Bearer qw-api-..., Growth plan or above) and your brand ID. See Authentication to create a token and List Brands to find your brand ID.Make your first request
Get your brand’s performance scores in one command:curl -s -H "Authorization: Bearer $QWAIRY_API_TOKEN" \
"https://www.qwairy.co/api/v1/brands/YOUR_BRAND_ID/performance?period=30" \
| jq '.scores'
const response = await fetch(
'https://www.qwairy.co/api/v1/brands/YOUR_BRAND_ID/performance?period=30',
{ headers: { 'Authorization': `Bearer ${process.env.QWAIRY_API_TOKEN}` } }
);
const { scores } = await response.json();
console.log(`Mention Rate: ${scores.mentionRate}% | Share of Voice: ${scores.shareOfVoice}%`);
import requests
import os
response = requests.get(
'https://www.qwairy.co/api/v1/brands/YOUR_BRAND_ID/performance?period=30',
headers={'Authorization': f'Bearer {os.environ["QWAIRY_API_TOKEN"]}'}
)
scores = response.json()['scores']
print(f"Mention Rate: {scores['mentionRate']}% | Share of Voice: {scores['shareOfVoice']}%")
{
"mentionRate": 45.2,
"mentionCount": 104,
"mentionTotal": 230,
"coverage": 33.33,
"sourceRate": 23.9,
"sourceCount": 44,
"sourceTotal": 184,
"sourcePages": 27,
"sentiment": 78.1,
"shareOfVoice": 8.13
}
Guides
Worked, copy-pasteable integrations built on the shared client below.Custom Dashboard
Build a branded GEO dashboard with KPIs and competitor rankings
Weekly Reports
Automate week-over-week performance reports
Competitive Analysis
Track your position against competitors
Data Export
Export data for BI tools (BigQuery, Snowflake, CSV)
API client
Reusable client with error handling for all guides on this site.// qwairy-client.js
class QwairyClient {
constructor(apiToken) {
this.baseUrl = 'https://www.qwairy.co/api/v1';
this.headers = {
'Authorization': `Bearer ${apiToken}`,
'Content-Type': 'application/json',
};
}
async request(endpoint, params = {}) {
const url = new URL(`${this.baseUrl}${endpoint}`);
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
url.searchParams.append(key, value);
}
});
const response = await fetch(url.toString(), { headers: this.headers });
if (!response.ok) {
const body = await response.json().catch(() => ({}));
if (response.status === 429) {
const retryAfter = Number(response.headers.get('Retry-After') ?? '60');
throw new Error(`Rate limited. Retry after ${retryAfter} second(s).`);
}
const nested = typeof body.error === 'object' ? body.error : null;
const code = nested?.code ?? `HTTP_${response.status}`;
const message = nested?.message ?? body.message ?? body.error ?? `HTTP ${response.status}`;
throw new Error(`[${code}] ${message}`);
}
return response.json();
}
async getPerformance(brandId, params = {}) {
return this.request(`/brands/${brandId}/performance`, params);
}
async getCompetitors(brandId, params = {}) {
return this.request(`/brands/${brandId}/competitors`, params);
}
async getSourceDomains(brandId, params = {}) {
return this.request(`/brands/${brandId}/source-domains`, params);
}
async getSourceUrls(brandId, params = {}) {
return this.request(`/brands/${brandId}/source-urls`, params);
}
async getAnswers(brandId, params = {}) {
return this.request(`/brands/${brandId}/answers`, params);
}
async getSearch(brandId, params = {}) {
return this.request(`/brands/${brandId}/search`, params);
}
async getPerception(brandId, params = {}) {
return this.request(`/brands/${brandId}/perception`, params);
}
async getContent(brandId, params = {}) {
return this.request(`/brands/${brandId}/content`, params);
}
async getTechnicalAnalysis(brandId, params = {}) {
return this.request(`/brands/${brandId}/technical-analysis`, params);
}
async getCompetitorEvolution(brandId, competitorId, params = {}) {
return this.request(`/brands/${brandId}/competitors/${competitorId}/evolution`, params);
}
}
// Usage
const client = new QwairyClient(process.env.QWAIRY_API_TOKEN);
# qwairy_client.py
import requests
import os
from typing import Optional, Dict, Any
class QwairyError(Exception):
"""Custom exception for Qwairy API errors."""
def __init__(self, code: str, message: str, status_code: int):
self.code = code
self.message = message
self.status_code = status_code
super().__init__(f"[{code}] {message}")
class QwairyClient:
def __init__(self, api_token: Optional[str] = None):
self.base_url = 'https://www.qwairy.co/api/v1'
self.api_token = api_token or os.environ.get('QWAIRY_API_TOKEN')
if not self.api_token:
raise ValueError("API token required. Set QWAIRY_API_TOKEN or pass to constructor.")
self.session = requests.Session()
self.session.headers.update({
'Authorization': f'Bearer {self.api_token}',
'Content-Type': 'application/json',
})
def _request(self, endpoint: str, params: Optional[Dict] = None) -> Dict[str, Any]:
url = f"{self.base_url}{endpoint}"
clean_params = {k: v for k, v in (params or {}).items() if v is not None}
response = self.session.get(url, params=clean_params)
if not response.ok:
try:
error_data = response.json() if response.content else {}
except ValueError:
error_data = {}
if response.status_code == 429:
retry_after = int(response.headers.get('Retry-After', '60'))
raise QwairyError(
'RATE_LIMITED',
f'Retry after {retry_after} second(s).',
429,
)
error = error_data.get('error')
nested = error if isinstance(error, dict) else {}
code = nested.get('code', f'HTTP_{response.status_code}')
message = nested.get('message') or error_data.get('message') or (
error if isinstance(error, str) else f'HTTP {response.status_code}'
)
raise QwairyError(code, message, response.status_code)
return response.json()
def get_performance(self, brand_id: str, period: Optional[int] = None,
start_date: Optional[str] = None, end_date: Optional[str] = None) -> Dict:
return self._request(f'/brands/{brand_id}/performance', {
'period': period, 'startDate': start_date, 'endDate': end_date,
})
def get_competitors(self, brand_id: str, period: Optional[int] = None,
limit: int = 50, offset: int = 0,
sort: str = 'mentions', order: str = 'desc') -> Dict:
return self._request(f'/brands/{brand_id}/competitors', {
'period': period, 'limit': limit, 'offset': offset, 'sort': sort, 'order': order,
})
def get_source_domains(self, brand_id: str, period: Optional[int] = None,
limit: int = 50, offset: int = 0,
sort: str = 'mentions', order: str = 'desc') -> Dict:
return self._request(f'/brands/{brand_id}/source-domains', {
'period': period, 'limit': limit, 'offset': offset, 'sort': sort, 'order': order,
})
def get_source_urls(self, brand_id: str, period: Optional[int] = None,
limit: int = 50, offset: int = 0,
sort: str = 'mentions', order: str = 'desc') -> Dict:
return self._request(f'/brands/{brand_id}/source-urls', {
'period': period, 'limit': limit, 'offset': offset, 'sort': sort, 'order': order,
})
def get_answers(self, brand_id: str, period: Optional[int] = None,
limit: int = 50, offset: int = 0,
provider: Optional[str] = None,
has_self_mention: Optional[bool] = None,
has_self_source: Optional[bool] = None) -> Dict:
return self._request(f'/brands/{brand_id}/answers', {
'period': period, 'limit': limit, 'offset': offset,
'provider': provider, 'hasSelfMention': has_self_mention, 'hasSelfSource': has_self_source,
})
def get_search(self, brand_id: str, period: Optional[int] = None,
limit: int = 50, offset: int = 0,
provider: Optional[str] = None) -> Dict:
return self._request(f'/brands/{brand_id}/search', {
'period': period, 'limit': limit, 'offset': offset, 'provider': provider,
})
def get_perception(self, brand_id: str, months: Optional[int] = None) -> Dict:
return self._request(f'/brands/{brand_id}/perception', {
'months': months,
})
def get_content(self, brand_id: str, status: Optional[str] = None,
limit: int = 50, offset: int = 0) -> Dict:
return self._request(f'/brands/{brand_id}/content', {
'status': status, 'limit': limit, 'offset': offset,
})
def get_technical_analysis(self, brand_id: str) -> Dict:
return self._request(f'/brands/{brand_id}/technical-analysis', {})
def get_competitor_evolution(self, brand_id: str, competitor_id: str,
period: Optional[int] = None) -> Dict:
return self._request(f'/brands/{brand_id}/competitors/{competitor_id}/evolution', {
'period': period,
})
# Usage
client = QwairyClient()
All guides on this site use this client. Copy it to your project or adapt it to your needs.
TypeScript types
Type definitions for the API client.// qwairy-types.ts
interface QwairyScores {
mentionRate: number;
mentionCount: number;
mentionTotal: number;
coverage: number;
sourceRate: number;
sourceCount: number;
sourceTotal: number;
sourcePages: number;
sentiment: number;
shareOfVoice: number;
}
interface QwairyMethodology {
promptsCount: number;
providersCount: number;
providers: string[];
responsesTotal: number;
responsesWithMentions: number;
responsesWithSources: number;
}
interface QwairyTopicBreakdown {
id: string;
topic: string;
score: number;
mentionRate: number;
sourceRate: number;
shareOfVoice: number;
avgSentiment: number | null;
promptsCount: number;
}
interface QwairyTagBreakdown {
id: string;
name: string;
score: number;
mentionRate: number;
sourceRate: number;
shareOfVoice: number;
avgSentiment: number | null;
promptsCount: number;
}
interface QwairyPerformance {
success: boolean;
brand: { id: string; name: string; domain: string | null };
period: { start: string | null; end: string | null };
scores: QwairyScores;
methodology: QwairyMethodology;
topCompetitors: QwairyCompetitor[];
topSources: QwairySource[];
byTopic?: QwairyTopicBreakdown[];
byTag?: QwairyTagBreakdown[];
}
interface QwairyCompetitor {
id: string;
name: string;
domain: string | null;
relationship: 'SELF' | 'DIRECT' | 'INDIRECT';
totalMentions: number;
shareOfVoice: number;
avgPosition: number | null;
avgSentiment: number | null;
}
interface QwairySource {
id: string;
domain: string;
type: 'INSTITUTIONAL' | 'COMMERCIAL' | 'MEDIA' | 'BLOG' | 'SOCIAL' | 'OTHER';
isSelf: boolean;
totalMentions: number;
rate: number;
avgPosition: number | null;
aiSourceAuthorityScore: number | null;
}
interface QwairyPagination {
total: number;
count: number;
limit: number;
offset: number;
}
interface QwairyCompetitorsResponse {
success: boolean;
pagination: QwairyPagination;
competitors: QwairyCompetitor[];
}
interface QwairySourcesResponse {
success: boolean;
pagination: QwairyPagination;
sources: QwairySource[];
}
interface QwairyAnswer {
id: string;
promptId: string;
promptText: string;
provider: string;
model: string;
textPreview: string;
hasSelfMention: boolean;
selfMentionPosition: number | null;
hasSelfSource: boolean;
competitorsCount: number;
sourcesCount: number;
sentiment: number | null;
createdAt: string;
}
interface QwairySearchQuery {
query: string;
occurrences: number;
priority: 'very-high' | 'high' | 'medium' | 'low';
brandPresence: number;
competitorPresence: number;
competitors: string[];
uniquePrompts: number;
prompts: Array<{ id: string; text: string }>;
uniqueAnswers: number;
uniqueModels: number;
models: string[];
topics: string[];
tags: string[];
firstSeen: string | null;
lastSeen: string | null;
}
interface QwairyAnswersResponse {
success: boolean;
pagination: QwairyPagination;
answers: QwairyAnswer[];
}
interface QwairySearchResponse {
success: boolean;
pagination: QwairyPagination;
searches: QwairySearchQuery[];
}
interface QwairyPerceptionScores {
sentiment: number | null;
alignment: number | null;
consistency: number | null;
factualAlignment: number | null;
}
interface QwairyPerceptionResponse {
success: boolean;
current: { snapshotId: string; month: number; year: number; scores: QwairyPerceptionScores } | null;
previous: { snapshotId: string; month: number; year: number; scores: QwairyPerceptionScores } | null;
trends: QwairyPerceptionScores;
averages: QwairyPerceptionScores;
}
interface QwairyArticle {
id: string;
title: string;
slug: string | null;
articleType: string;
status: 'DRAFT' | 'GENERATING' | 'LIVE' | 'ARCHIVED';
wordCount: number;
finalUrl: string | null;
publishedAt: string | null;
}
interface QwairyContentResponse {
success: boolean;
pagination: QwairyPagination;
articles: QwairyArticle[];
}
interface QwairyTechnicalAnalysisResponse {
success: boolean;
analyzed: boolean;
aiReadiness: { score: number; issuesCount: number; optimizationsCount: number } | null;
robotsAnalysis: Record<string, unknown> | null;
llmsAnalysis: Record<string, unknown> | null;
sitemapAnalysis: Record<string, unknown> | null;
lastAnalyzedAt: string | null;
}
Pagination
Fetch all results when you have more than 100 items.async function fetchAllCompetitors(client, brandId, period = 30) {
const allCompetitors = [];
let offset = 0;
const limit = 100;
while (true) {
const response = await client.getCompetitors(brandId, { period, limit, offset });
allCompetitors.push(...response.competitors);
if (response.competitors.length < limit || allCompetitors.length >= response.pagination.total) {
break;
}
offset += limit;
}
return allCompetitors;
}
// Usage
const allCompetitors = await fetchAllCompetitors(client, 'your-brand-id', 30);
console.log(`Total competitors: ${allCompetitors.length}`);
def fetch_all_competitors(client, brand_id: str, period: int = 30) -> list:
"""Fetch all competitors with pagination."""
all_competitors = []
offset = 0
limit = 100
while True:
response = client.get_competitors(brand_id, period=period, limit=limit, offset=offset)
all_competitors.extend(response['competitors'])
if len(response['competitors']) < limit or len(all_competitors) >= response['pagination']['total']:
break
offset += limit
return all_competitors
# Usage
all_competitors = fetch_all_competitors(client, 'your-brand-id', period=30)
print(f"Total competitors: {len(all_competitors)}")
Field reference
Mapping between business metrics and API fields:| Business metric | Endpoint | Field | Description |
|---|---|---|---|
| Mention Rate | /performance | scores.mentionRate | Responses with a SELF mention divided by responses with a SELF or DIRECT mention |
| Citation Rate | /performance | scores.sourceRate | Responses with a SELF citation divided by responses with a SELF or DIRECT citation |
| Coverage | /performance | scores.coverage | Responses with a SELF mention divided by all responses in the selected scope |
| Share of Voice | /performance | scores.shareOfVoice | SELF mention occurrences divided by SELF and DIRECT occurrences |
| Share of Voice (per competitor) | /competitors | shareOfVoice | Competitor occurrence share in the endpoint’s selected relationship set |
| Share of Citations | /source-domains | rate | Domain citation occurrences divided by all citation occurrences |
| Sentiment | /performance | scores.sentiment | Average SELF sentiment score on the 0 to 100 scale |
| Average Position | /competitors | avgPosition | Average rank in AI responses (1 = first) |
| Mentioned but not cited | /answers | hasSelfMention + hasSelfSource | Request hasSelfMention=true, then client-filter rows where hasSelfSource is false |
| Query fan-out | /search | query | Retained web queries associated with stored answers |
| Perception scores | /perception | current.scores | Sentiment, alignment, consistency, factual alignment (0-100) |
| Content articles | /content | articles | Content Studio articles (Markdown via /content/{id}) |
| Site Readiness Score | /technical-analysis | aiReadiness.score | Stored robots.txt, llms.txt, and sitemap.xml readiness (0-100) |
Use
/performance for aggregated KPIs. Use /competitors and /source-domains for detailed breakdowns. Use /answers and /search for LLM-level diagnostics. Use /perception, /content and /technical-analysis for reputation, content and technical readiness.
