Il vous faut un jeton d’API (
Bearer qw-api-..., sur une offre éligible à l’API) et l’identifiant de votre marque. Consultez Authentification pour créer un jeton et Lister les marques pour trouver l’identifiant de votre marque.Faire votre première requête
Obtenez les scores de performance de votre marque en une commande :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
Des intégrations détaillées, prêtes à copier, construites sur le client partagé ci-dessous.Tableau de bord sur mesure
Construisez un tableau de bord GEO à vos couleurs, avec KPI et classement des concurrents
Rapports hebdomadaires
Automatisez des rapports de performance d’une semaine sur l’autre
Analyse concurrentielle
Suivez votre position face aux concurrents
Export de données
Exportez les données vers vos outils BI (BigQuery, Snowflake, CSV)
API client
Client réutilisable, avec gestion des erreurs, pour tous les guides de ce 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()
Tous les guides de ce site utilisent ce client. Copiez-le dans votre projet ou adaptez-le à vos besoins.
Types TypeScript
Définitions de types pour le client de l’API.// 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 | null; isComplete: boolean; issuesCount: number; optimizationsCount: number } | null;
robotsAnalysis: Record<string, unknown> | null;
llmsAnalysis: Record<string, unknown> | null;
sitemapAnalysis: Record<string, unknown> | null;
lastAnalyzedAt: string | null;
}
Pagination
Récupérez tous les résultats lorsque vous avez plus de 100 éléments.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)}")
Référence des champs
Correspondance entre indicateurs métier et champs de l’API :| Indicateur métier | Endpoint | Champ | Description |
|---|---|---|---|
| Taux de mention | /performance | scores.mentionRate | Réponses avec une mention SELF divisées par les réponses avec une mention SELF ou DIRECT |
| Taux de citation | /performance | scores.sourceRate | Réponses avec une citation SELF divisées par les réponses avec une citation SELF ou DIRECT |
| Couverture | /performance | scores.coverage | Réponses avec une mention SELF divisées par l’ensemble des réponses du périmètre sélectionné |
| Part de voix | /performance | scores.shareOfVoice | Occurrences de mentions SELF divisées par les occurrences SELF et DIRECT |
| Part de voix (par concurrent) | /competitors | shareOfVoice | Part des occurrences du concurrent dans l’ensemble de relations sélectionné par l’endpoint |
| Part des citations | /source-domains | rate | Occurrences de citation du domaine divisées par l’ensemble des occurrences de citation |
| Sentiment | /performance | scores.sentiment | Score de sentiment SELF moyen sur l’échelle de 0 à 100 |
| Position moyenne | /competitors | avgPosition | Rang moyen dans les réponses IA (1 = premier) |
| Mentionné mais non cité | /answers | hasSelfMention + hasSelfSource | Demandez hasSelfMention=true, puis filtrez côté client les lignes où hasSelfSource vaut false |
| Query Fan-Out | /search | query | Requêtes web conservées associées aux réponses enregistrées |
| Scores de perception | /perception | current.scores | Sentiment, alignement, cohérence, alignement factuel (0 à 100) |
| Articles de contenu | /content | articles | Articles Content Studio (Markdown via /content/{id}) |
| Score de préparation du site | /technical-analysis | aiReadiness.score | Préparation enregistrée de robots.txt, llms.txt et sitemap.xml (0-100, ou null si la vérification est incomplète) |
Utilisez
/performance pour les KPI agrégés, /competitors et /source-domains pour les ventilations détaillées, /answers et /search pour les diagnostics au niveau des LLM, et /perception, /content et /technical-analysis pour la réputation, le contenu et la préparation technique.
