Aller au contenu

API de lecture

Récupérez vos statistiques par programme : tableaux de bord internes, rapports, exports. Réservée aux plans Pro et Agence, à la bêta et à l'essai en cours.

Authentification

Créez un jeton dans Espace membre → Jetons API, puis passez-le en Bearer :

curl -H "Authorization: Bearer VOTRE_JETON" \
  "https://quietmetrics.dev/api/v1/stats/qm_pub_XXXXXXXXXXXXXXXXX/summary?period=30d"

Toutes les routes sont préfixées par /api/v1/stats/{clé_publique_du_site}. Un jeton ne donne accès qu'aux sites de votre compte ; un site qui ne vous appartient pas répond 404 (aucune fuite d'existence).

Un jeton couvre soit tous vos sites, actuels et futurs, soit les seuls sites choisis à sa création : un site de votre compte qu'il ne couvre pas répond 403. Il cesse de fonctionner à la date d'expiration affichée dans la liste des jetons.

Endpoints

Endpoint Contenu
GET /summary Indicateurs de la période, et ceux de la période précédente de même durée
GET /timeseries Série temporelle : un point par jour, ou par heure quand la période tient sur un jour
GET /breakdown Classement des valeurs d'une dimension ({label, value})
GET /realtime Visiteurs actifs sur les 5 dernières minutes ({"visitors": N})

Paramètres communs

period

Un préréglage du dashboard : today, yesterday, 7d, 30d (défaut), month (mois en cours), last_month, 12m. Les périodes glissantes se terminent aujourd'hui, jour inclus.

Ou une plage explicite, par exemple ?period=custom:2026-07-01..2026-07-31 :

  • custom:AAAA-MM-JJ..AAAA-MM-JJ : du premier au second jour, bornes incluses, sur 760 jours au plus. Des bornes inversées sont remises dans l'ordre. La plage peut finir aujourd'hui, pas après.

Les dates sont celles du fuseau du site.

Une période invalide ne produit pas d'erreur : elle est remplacée par 30d. C'est le cas d'un préréglage inconnu comme d'une plage mal formée (format autre que AAAA-MM-JJ, date qui n'existe pas), trop longue ou qui finit dans le futur. Vérifiez donc la période servie : chaque réponse, sauf realtime, porte period.from et period.to, et summary y ajoute period.preset, qui vaut custom pour une plage acceptée et 30d après un remplacement.

La période précédente (previous dans summary) compte le même nombre de jours et se termine la veille du premier jour de la période. Pour month, ce n'est donc pas le mois précédent entier, mais autant de jours que le mois en cours en a déjà.

filters

filters reprend les filtres des clics du dashboard, au format dimension:valeur, séparés par ; :

?filters=country:FR;device:mobile
?filters=page:/tarifs
?filters=channel:search;browser:Firefox
  • Les dimensions utilisables sont indiquées dans la table ci-dessous. Toute autre dimension, entry et exit comprises, répond 422 en la nommant : vous recevez une erreur plutôt que des chiffres non filtrés.
  • Une seule valeur par dimension : quand une dimension apparaît deux fois, la dernière l'emporte (?filters=device:mobile;device:desktop ne garde que desktop). Des dimensions différentes se cumulent : seuls comptent les envois qui les satisfont toutes.
  • filters n'accepte que des valeurs de 500 caractères au plus : au-delà, la valeur est écartée sans erreur, et elle n'apparaît alors pas dans le filters de la réponse.
  • Le filters de summary ne porte que les filtres RÉELLEMENT appliqués, et non la requête reçue : une dimension qui y figure a bien restreint les chiffres qui l'accompagnent.
  • Vocabulaire fermé : channel vaut direct, search, social, referral, email ou campaign ; device vaut desktop, mobile, tablet ou other.
  • Dès qu'un filtre s'applique, les indicateurs sont recalculés depuis les événements bruts, et bounce_rate comme avg_duration valent null.
  • Dans breakdown, un filtre sur la dimension classée est ignoré pour ce classement : ?dimension=country&filters=country:FR rend tous les pays, pour que le filtre ne réduise pas le classement à sa propre valeur.

dimension (breakdown)

dimension Ce que compte value Utilisable dans filters
page Pages vues oui
entry Entrées : sessions commencées sur cette page non
exit Sorties : sessions terminées sur cette page, calculées la nuit suivante (la journée en cours n'en compte aucune, sauf sous filtre) non
referrer Sessions, par domaine référent oui
channel Sessions, par canal d'acquisition oui
source Sessions, par valeur de utm_source oui
medium Sessions, par valeur de utm_medium oui
campaign Sessions, par valeur de utm_campaign oui
country Pages vues, par pays (code ISO à deux lettres) oui
region Pages vues, par région oui
browser Pages vues, par navigateur oui
os Pages vues, par système d'exploitation oui
device Pages vues, par type d'appareil oui
lang Pages vues, par langue oui
event Déclenchements, par nom d'événement oui

Une dimension absente de cette table répond 422. label vaut null pour les visites sans valeur, par exemple sans domaine référent.

limit (breakdown) : nombre de lignes, 10 par défaut, borné entre 1 et 1 000.

Pas de la série temporelle

GET /timeseries rend un point par jour en général, et un point par heure dès que la période tient sur une seule journée locale (today, yesterday, ou une plage custom: dont les deux bornes sont égales). Le champ date prend alors la forme AAAA-MM-JJ HH:MM, à l'heure locale du site, au lieu de AAAA-MM-JJ.

Une journée locale ne fait pas toujours 24 heures. Aux deux changements d'heure de l'année, la série porte donc 23, 24 ou 25 points : ne comptez pas sur un nombre fixe.

  • Nuit où l'heure recule : 25 points. L'heure locale rejouée (02:00 à Paris) donne deux points consécutifs portant le même date, chacun avec ses propres mesures. Ce format ne porte pas de décalage horaire : repérez ces points par leur rang dans la liste, jamais par leur libellé.
  • Nuit où l'heure avance : 23 points, l'heure locale sautée (02:00 à Paris) est absente. Aucun point ne déborde sur le lendemain.

Les visiteurs d'un point horaire sont ceux qui étaient actifs pendant cette heure : une personne présente sur deux heures apparaît dans les deux, et les points horaires ne s'additionnent donc pas en un total de journée.

Exemples de réponse

summary

GET /summary?period=30d :

{
    "site": "monsite.fr",
    "period": { "preset": "30d", "from": "2026-06-17", "to": "2026-07-16" },
    "filters": {},
    "kpis": { "pageviews": 9873, "visitors": 4210, "sessions": 4630, "events": 312, "bounce_rate": 47.2, "avg_duration": 74 },
    "previous": { "pageviews": 9145, "visitors": 3877, "sessions": 4288, "events": 290, "bounce_rate": 49.1, "avg_duration": 71 }
}
kpis Contenu
pageviews Pages vues
visitors Visiteurs uniques de chaque journée, additionnés sur la période : une personne revenue le lendemain est comptée deux fois
sessions Visites
events Événements mesurés autres que les pages vues
bounce_rate Taux de rebond en pourcentage, de 0 à 100, arrondi au dixième : part des sessions avec un seul envoi mesuré. null sans aucune session, ou dès qu'un filtre s'applique
avg_duration Durée moyenne d'une session, en secondes entières. null sans aucune session, ou dès qu'un filtre s'applique

previous porte les mêmes clés pour la période précédente. L'API ne calcule aucune variation : c'est à vous de la déduire des deux valeurs, en traitant à part une valeur précédente nulle ou null.

GET /summary?period=7d&filters=device:mobile :

{
    "site": "monsite.fr",
    "period": { "preset": "7d", "from": "2026-07-10", "to": "2026-07-16" },
    "filters": { "device": "mobile" },
    "kpis": { "pageviews": 1204, "visitors": 611, "sessions": 655, "events": 38, "bounce_rate": null, "avg_duration": null },
    "previous": { "pageviews": 1139, "visitors": 590, "sessions": 622, "events": 41, "bounce_rate": null, "avg_duration": null }
}

timeseries

Un point par jour, jours sans trafic compris (ils valent zéro).

GET /timeseries?period=custom:2026-07-14..2026-07-16 :

{
    "period": { "from": "2026-07-14", "to": "2026-07-16" },
    "data": [
        { "date": "2026-07-14", "pageviews": 341, "visitors": 142, "sessions": 158, "events": 9 },
        { "date": "2026-07-15", "pageviews": 298, "visitors": 131, "sessions": 144, "events": 12 },
        { "date": "2026-07-16", "pageviews": 0, "visitors": 0, "sessions": 0, "events": 0 }
    ]
}

Quand la période tient sur un jour (today, yesterday, ou une plage dont les deux bornes sont égales), la série passe à l'heure : un point par heure de la journée locale, de 00:00 à 23:00 à l'heure locale du site (23 ou 25 points les jours de changement d'heure, voir « Pas de la série temporelle »), et date prend la forme AAAA-MM-JJ HH:MM. Les visiteurs d'un point horaire sont ceux qui étaient actifs pendant cette heure : une personne présente sur deux heures apparaît dans les deux, et les points ne s'additionnent donc pas en un total de journée.

GET /timeseries?period=custom:2026-07-16..2026-07-16, les trois premiers des 24 points :

{
    "period": { "from": "2026-07-16", "to": "2026-07-16" },
    "data": [
        { "date": "2026-07-16 00:00", "pageviews": 4, "visitors": 3, "sessions": 3, "events": 0 },
        { "date": "2026-07-16 01:00", "pageviews": 1, "visitors": 1, "sessions": 1, "events": 0 },
        { "date": "2026-07-16 02:00", "pageviews": 0, "visitors": 0, "sessions": 0, "events": 0 }
    ]
}

breakdown

GET /breakdown?dimension=page&limit=3 :

{
    "dimension": "page",
    "period": { "from": "2026-06-17", "to": "2026-07-16" },
    "data": [
        { "label": "/", "value": 3120 },
        { "label": "/tarifs", "value": 1284 },
        { "label": "/docs", "value": 977 }
    ],
    "capped": {
        "any": true,
        "dimensions": [
            { "dimension": "page", "days": 2, "days_repaired": 1, "folded_values": 1406, "cap": 2000 }
        ]
    }
}

Sans plafonnement sur la période, capped vaut {"any": false, "dimensions": []}.

realtime

GET /realtime :

{ "visitors": 3 }

Plafond de cardinalité (capped)

Chaque site mesure un nombre borné de valeurs distinctes par jour et par dimension. Au-delà, les valeurs suivantes sont comptées ensemble sous un libellé unique, ~autres : le détail est regroupé, aucune visite n'est perdue et les totaux restent exacts. breakdown annonce cet état dans la clé capped :

  • any est toujours présent et dimensions toujours une liste : le schéma ne varie pas selon l'état du site, aucun branchement n'est nécessaire pour le lire.
  • dimension est la dimension plafonnée, pas celle que vous avez demandée. entry et exit sont regroupés par le plafond page ; source et medium sont regroupés par le plafond campaign.
  • days compte les journées locales plafonnées sur la période ; cap est le seuil le plus haut réellement appliqué (il peut varier si un relèvement est posé en cours de période).
  • folded_values: null ne signifie PAS zéro. Le nombre de valeurs regroupées n'est connu qu'après la reconstruction de nuit, qui ne traite une journée qu'à J+2. null veut dire « plafond atteint, nombre encore inconnu » ; 0 veut dire « journée reconstruite, aucune valeur regroupée ». Afficher 0 à la place de null annoncerait à vos utilisateurs qu'ils ne perdent aucun détail, ce qui est faux.
  • days_repaired dit sur combien des days journées le compte est connu. Si days_repaired < days, folded_values est une somme partielle, comme dans l'exemple ci-dessus.

capped ne décrit que la dimension demandée. Il vaut aussi {"any": false, "dimensions": []} dès qu'un filtre s'applique au classement : celui-ci est alors lu sur les événements bruts, où rien n'est regroupé.

summary et timeseries ne portent pas cette clé : elles ne lisent aucune dimension et sont insensibles au plafonnement.

L'export CSV du dashboard porte le même indicateur, mais dans l'en-tête HTTP X-Cardinality-Capped plutôt que dans le corps du fichier : {"capped":false}, ou {"capped":true,"dimension":"page","days":1,"days_repaired":1,"folded_values":2812,"cap":2000} cadré sur la seule dimension exportée. Le corps du CSV reste une donnée machine pure, sans ligne de métadonnée : aucune ligne synthétique ne peut ainsi fausser un total ni faire échouer un analyseur à nombre de colonnes fixe.

Ce plafond peut être relevé pour votre site : écrivez au support.

Exemple PHP

$response = json_decode(file_get_contents(
    'https://quietmetrics.dev/api/v1/stats/qm_pub_XXXX/summary?period=7d',
    false,
    stream_context_create(['http' => ['header' => "Authorization: Bearer VOTRE_JETON\r\nAccept: application/json"]])
), true);

echo $response['kpis']['visitors'];

Erreurs

Le corps d'une erreur est un objet JSON dont la clé message décrit la cause.

Code Signification
401 Jeton absent, invalide, révoqué ou expiré
403 Le site est à vous, mais le jeton ne le couvre pas, ou votre plan n'ouvre pas l'API (essai expiré compris)
403 Votre compte est suspendu ou en attente d'approbation : l'API reste fermée, quel que soit le plan
404 Site inconnu, ou appartenant à un autre compte
422 Dimension de breakdown inconnue, ou dimension de filters sur laquelle on ne peut pas filtrer
429 Trop de requêtes : au plus 120 par minute par jeton, et 300 par minute pour l'ensemble du compte, tous jetons confondus. L'en-tête Retry-After donne le délai à attendre, en secondes

Bonnes pratiques

  • Mettez en cache côté client : interroger toutes les heures suffit largement. Attention toutefois, une journée close n'est pas définitivement figée : la reconstruction de nuit repasse sur les journées plafonnées et peut en recalculer les agrégats jusqu'à deux jours après. Prévoyez donc, sur les deux derniers jours, un cache court ou une réinterrogation ; au-delà, les valeurs ne bougent plus.
  • Lisez period dans chaque réponse avant d'afficher un chiffre : une période invalide ne produit pas d'erreur, elle est remplacée par 30d.
  • Un jeton par usage (un pour le dashboard interne, un pour le rapport mensuel) : la révocation reste chirurgicale, et deux usages ne se disputent pas la limite du même jeton.
  • Pour un export ponctuel, chaque vue du dashboard s'exporte aussi en CSV depuis l'espace membre : pas besoin d'écrire du code.

Vous ne trouvez pas votre réponse ?

Le support répond en français, les jours ouvrés.