L'API REST permet de piloter Page Observer depuis vos propres outils : créer des monitors, les ranger, suivre leur état de santé, récupérer les changements déjà qualifiés et relire le contenu d'une page, sans jamais ouvrir l'interface. Elle est pensée pour un usage machine à machine — un script, un worker, une automatisation — et non pour un navigateur.
Adresse de base : https://app.pageobserver.com/api/v1.
Tout est en JSON, les dates sont en ISO 8601 UTC, et l'accès est
réservé aux comptes Pro.
Créer une clé API
Rendez-vous sur la page Compte, section « Clés API ». Donnez un nom à la clé (par exemple le nom de l'outil qui va s'en servir) et validez.
La clé complète, de la forme po_live_…, s'affiche une seule
fois. Copiez-la immédiatement : nous n'en conservons que l'empreinte
cryptographique, elle ne peut donc jamais être réaffichée ni récupérée. Si vous la perdez,
révoquez-la et créez-en une nouvelle. Seul le préfixe reste visible par la suite, pour vous
permettre d'identifier vos clés.
La section n'apparaît que pour les comptes Pro, et uniquement pour le titulaire du compte : un membre invité dans une organisation ne peut pas émettre de clé, car c'est le compte propriétaire qui porte les crédits qu'elle consomme.
S'authentifier
Chaque requête porte la clé dans un en-tête Authorization :
curl -H "Authorization: Bearer po_live_VotreCleIci" \
https://app.pageobserver.com/api/v1/quota
Il n'y a ni cookie, ni session, ni jeton CSRF à gérer : l'API vit délibérément en dehors du circuit d'authentification du navigateur.
Les portées
Chaque clé porte des portées qui décrivent ce qu'elle a le droit de faire. Une clé créée depuis l'interface les reçoit toutes les quatre :
monitors.read— lire les monitors et les dossiers ;monitors.write— créer, modifier et supprimer monitors et dossiers ;changes.read— récupérer les changements détectés ;content.read— lire le contenu d'une page surveillée.
Une requête dont la clé n'a pas la portée requise reçoit un 403 forbidden
nommant la portée manquante.
content.read est une portée à part, et c'est voulu. Lire le
contenu entier d'une page est d'une autre nature que lire son adresse et son état de
santé : une clé ne gagne pas ce droit sans qu'on l'ait décidé. Conséquence pratique —
une clé émise avant l'ouverture de cette portée ne la possède pas et
recevra un 403 sur la lecture de contenu. Émettez-en une nouvelle.
Limites de débit
Deux plafonds s'appliquent : 600 requêtes par quart d'heure et par adresse IP
(avant authentification), puis 300 requêtes par tranche de 5 minutes et par clé.
Au-delà, la réponse est un 429 rate_limited. Les en-têtes standards
RateLimit-* indiquent où vous en êtes.
Le corps d'une requête est limité à 32 ko.
Forme des erreurs
Toute erreur suit la même enveloppe, sans jamais exposer de détail interne :
{ "error": { "code": "invalid_request", "message": "Invalid or forbidden url" } }
| Code HTTP | code | Signification |
|---|---|---|
| 400 | invalid_request | Un paramètre est absent, malformé ou hors des valeurs admises. |
| 401 | unauthorized | En-tête absent, clé malformée, inconnue ou révoquée. |
| 402 | subscription_required | Le compte porteur de la clé n'est pas (ou plus) Pro. |
| 403 | forbidden | Portée manquante, ou réglage réservé au Pro (fréquence horaire). |
| 404 | not_found | La ressource n'existe pas, ou n'appartient pas à ce compte. |
| 409 | credit_quota_exceeded | Crédits insuffisants. La réponse porte quota, used et cost. |
| 409 | folder_quota_exceeded | Nombre maximal de dossiers atteint. |
| 429 | rate_limited | Plafond de débit dépassé. |
| 500 | server_error | Erreur interne. Réessayez plus tard. |
Le 409 credit_quota_exceeded mérite un traitement à part : ce n'est pas une
requête invalide mais un plafond de capacité. Votre outil doit le distinguer d'un 400 et
alerter un humain, plutôt que de réessayer en boucle.
Les monitors
Créer un monitor
POST /api/v1/monitors — portée monitors.write
| Champ | Obligatoire | Description |
|---|---|---|
url | oui | Page à surveiller, en http(s). Les adresses internes ou privées sont refusées. |
check_frequency | oui | En minutes : 10080 (hebdomadaire), 1440 (quotidien) ou 60 (horaire). |
intent | oui | Phrase décrivant ce que vous suivez. 500 caractères maximum. |
external_ref | oui | Votre propre identifiant pour ce monitor. 200 caractères, A-Z a-z 0-9 _ . : @ + / -. |
css_selector | non | Restreint le monitor à une zone de la page. |
folder_ids | non | Tableau d'identifiants de dossiers, qui doivent vous appartenir. |
curl -X POST https://app.pageobserver.com/api/v1/monitors \
-H "Authorization: Bearer po_live_VotreCleIci" \
-H "Content-Type: application/json" \
-d '{
"url": "https://exemple.fr/politique-de-confidentialite",
"check_frequency": 10080,
"intent": "Signaler tout changement de durée de conservation ou de transfert hors UE.",
"external_ref": "doc-4217"
}'
Réponse : 201 avec l'objet créé.
L'intent n'est pas décoratif — contrairement à l'interface, où
il est facultatif, il est obligatoire ici. C'est lui qui pilote toute la
qualification par l'IA : le résumé de chaque changement, son niveau d'importance et le fait
qu'il soit jugé dans le périmètre ou hors sujet. Un monitor créé sans intention
utile remonterait des changements non qualifiés, sans qu'aucune erreur ne vous le signale.
Il n'est pas modifiable après coup.
L'appel est idempotent. Rejouer la même requête avec le même
external_ref ne crée pas de doublon : vous recevez un 200 avec
le monitor existant. Cette garantie est assurée par la base de données, elle tient
donc même si deux appels partent exactement en même temps. C'est ce qui rend une file de
travail rejouable sans précaution particulière.
Lister les monitors
GET /api/v1/monitors — portée monitors.read
| Paramètre | Valeurs | Effet |
|---|---|---|
health | broken · ok | Ne renvoie que les monitors en panne, ou que ceux dont le dernier passage s'est bien déroulé. |
is_active | true · false | Actives, ou en pause. |
external_ref | chaîne | Retrouve un monitor par votre propre identifiant. |
limit | 1 à 200 | Taille de page. 100 par défaut. |
cursor | opaque | Voir la pagination ci-dessous. |
C'est l'endpoint de supervision, et le seul moyen d'apprendre qu'un
monitor s'est arrêté. Une page en panne cesse de produire des changements : elle
disparaît donc du flux /changes au lieu d'y signaler quoi que ce soit. Sondez
?health=broken régulièrement.
Les filtres sont stricts : une valeur non reconnue est refusée par un 400, jamais ignorée. Une faute de frappe ne peut donc pas vous renvoyer toute la flotte en la faisant passer pour le sous-ensemble demandé.
{
"monitors": [
{
"id": "8f1c…",
"url": "https://exemple.fr/politique-de-confidentialite",
"external_ref": "doc-4217",
"css_selector": null,
"check_frequency": 10080,
"is_active": true,
"monitor_type": "diff",
"intent": "Signaler tout changement de durée…",
"change_type": ["policy_change"],
"last_checked_at": "2026-09-19T04:12:07.000Z",
"last_outcome": "ok",
"last_http_status": 200,
"consecutive_failures": 0,
"failing_since": null,
"credits_reserved": 1,
"folder_ids": ["2ab9…"],
"created_at": "2026-09-01T10:00:00.000Z"
}
],
"next_cursor": "MjAyNi0wOS0wMV…",
"has_more": false
}
Lire un monitor
GET /api/v1/monitors/:id — portée monitors.read. Renvoie le même
objet que la liste, pour un seul monitor. Un identifiant qui ne vous appartient pas
renvoie 404, exactement comme un identifiant inexistant.
Lire le contenu d'une page
GET /api/v1/monitors/:id/content — portée content.read
Renvoie la page telle que Page Observer l'a vue au dernier relevé. Là où
/changes vous dit ce qui a bougé, cet appel vous dit ce qui est
là : c'est ce qu'il faut pour reconstituer un inventaire — une liste de
sous-traitants, un tableau de tarifs, un catalogue — plutôt que d'essayer de le
rebâtir à partir d'une succession de différences.
curl https://app.pageobserver.com/api/v1/monitors/<id>/content \
-H "Authorization: Bearer po_live_…"
{
"snapshot": {
"monitor_id": "3f2a…",
"check_id": "91c4…",
"checked_at": "2026-09-18T03:12:00Z",
"hash": "8d1e…",
"format": "cleaned_html",
"bytes": 184233,
"truncated": false,
"content": "<table>…</table>"
}
}
formatvaut toujourscleaned_html. Ce n'est pas le HTML d'origine : scripts, styles, attributs de mise en forme et paramètres d'URL en ont été retirés, et seule la zone visée par votre sélecteur CSS est conservée. Les balises de structure — tableaux, listes, titres — sont intactes. Y chercher une classe ou undata-*serait vain : ils n'y sont plus.checked_atethashidentifient le relevé. Unhashinchangé signifie que la page n'a pas bougé : inutile de retraiter.truncatedpasse àtrueau-delà de 512 ko, et le contenu est alors coupé. Ne l'ignorez pas : sur une liste, une coupe se lit comme une disparition massive d'entrées. Resserrez plutôt le sélecteur CSS.snapshotvautnull, avec un 200, pour un monitor qui n'a pas encore été relevé une seule fois. C'est un état normal, pas une erreur — un monitor fraîchement créé y passe jusqu'à son premier passage.
Un identifiant qui ne vous appartient pas renvoie 404, exactement comme un
identifiant inexistant.
Un seul relevé est disponible : le dernier. L'appel ne donne pas accès à l'historique des versions d'une page. Pour suivre l'évolution, c'est le fil des changements qui fait foi.
La lecture ne consomme aucun crédit : elle ne déclenche pas de relevé et ne fait que restituer ce qui est déjà stocké.
Modifier un monitor
PATCH /api/v1/monitors/:id — portée monitors.write
Quatre champs seulement sont modifiables :
check_frequency— soumis au contrôle de crédits, et l'horaire reste réservé au Pro ;css_selector— voir l'avertissement ci-dessous ;is_active— booléen JSON strict : la chaîne"true"est refusée par un 400, plutôt que d'être interprétée à l'envers ;folder_ids— remplace l'intégralité du rangement.
Changer le sélecteur efface l'historique. Diffs, captures et relevés antérieurs sont supprimés, et le monitor repart d'une base neuve au prochain passage. C'est volontaire : comparer un ancien périmètre à un nouveau produirait un faux changement massif. Ne modifiez donc pas un sélecteur à la légère sur un monitor de longue date.
L'url, le type et l'intent sont figés. Les
changer suppose de supprimer le monitor et d'en créer un autre.
Supprimer un monitor
DELETE /api/v1/monitors/:id — portée monitors.write. Réponse
204 sans corps. L'appel est idempotent : un identifiant
inconnu renvoie lui aussi 204, ce qui permet de rejouer une file de suppressions sans cas
particulier. Les crédits sont libérés immédiatement.
Les dossiers
GET /api/v1/folders (portée monitors.read) liste vos dossiers.
POST /api/v1/folders en crée un à partir d'un champ name, et
DELETE /api/v1/folders/:id le supprime — les deux sous la portée
monitors.write.
Supprimer un dossier ne supprime pas les monitors qu'il contenait : ils deviennent simplement « sans dossier ».
Deux points sur folder_ids, à la création comme à la modification. D'abord, un
dossier inconnu ou appartenant à un autre compte fait échouer la requête
(400 Unknown folder_ids) au lieu d'être silencieusement ignoré : un outil qui
croit avoir rangé un monitor doit l'apprendre. Ensuite, omettre le champ n'est
pas la même chose que l'envoyer vide — absent, le rangement n'est pas touché ;
à [], le monitor sort de tous ses dossiers.
Récupérer les changements
GET /api/v1/changes — portée changes.read
C'est le cœur de l'API. Elle renvoie les changements déjà qualifiés par l'IA : vous n'avez aucun traitement à refaire de votre côté.
{
"items": [
{
"id": "c31f…",
"monitor_id": "8f1c…",
"external_ref": "doc-4217",
"url": "https://exemple.fr/politique-de-confidentialite",
"detected_at": "2026-09-19T04:12:09.000Z",
"ai_summary": "Ajout de trois sous-traitants hébergés en Inde.",
"ai_importance": "critical",
"ai_in_scope": true,
"ai_metadata": { "…": "…" },
"change_type": ["third_party_change"],
"diff": { "tokens": [ { "op": "add", "text": "…" } ], "truncated": false }
}
],
"next_cursor": "MjAyNi0wOS0xO…",
"has_more": true
}
ai_summary— résumé en langage naturel, rédigé dans la langue du compte ;ai_importance—minor,significantoucritical;ai_in_scope—falsequand le changement est jugé hors de l'intention déclarée (mise en forme, date de révision…).nullsignifie que l'IA n'a pas encore statué ;diff.tokens— le détail mot à mot, chaque entrée portantop(eqinchangé,addajouté,delsupprimé) ettext.truncatedvauttruequand le changement était trop volumineux pour être renvoyé en entier.
Le HTML brut des pages, avant et après, n'est jamais renvoyé : c'est la charge la plus lourde, et le résumé accompagné des jetons de diff couvre les usages réels.
Pagination et rattrapage
Le paramètre since accepte une date ISO 8601 et borne le début de la période.
Il ne peut pas remonter au-delà de la durée de conservation de votre formule.
La pagination se fait par curseur : reprenez le next_cursor de
la réponse précédente tant que has_more vaut true. Le curseur est
une valeur opaque, à renvoyer telle quelle sans chercher à l'interpréter.
GET /api/v1/changes?since=2026-09-01T00:00:00Z&limit=100
GET /api/v1/changes?cursor=MjAyNi0wOS0xO…&limit=100
Ce choix n'est pas cosmétique : contrairement à une pagination par numéro de page, un curseur ne saute aucune entrée même si de nouveaux changements sont détectés pendant que vous parcourez les pages. Les entrées arrivent par date croissante, ce qui permet d'enregistrer le curseur après chaque lot traité et de reprendre exactement là où vous vous étiez arrêté après une panne.
Surveiller son quota
GET /api/v1/quota — aucune portée requise
{ "quota_credits": 20, "credits_used": 14, "credits_available": 6, "monitors_active": 9 }
Interrogez-le avant une création en série. Les crédits sont un
plafond dur : une fois atteint, toute nouvelle création est refusée par un
409. Mieux vaut alerter à 80 % que découvrir le mur.
Voir Crédits, formules et quotas.
Ce que l'API ne fait pas encore
- Seuls les monitors de type « Page diff » sont créables par API ; les types à éléments (RSS Feed, Flux RSS) restent à créer depuis l'interface.
- Les captures d'écran ne sont pas exposées.
- L'historique des relevés non plus : la lecture de contenu ne rend que le dernier état d'une page, jamais ses versions antérieures.
- Il n'y a pas de création en lot : un monitor par requête.
- Aucune notification sortante : l'API fonctionne par interrogation. Pour être averti par email, passez par les alertes depuis l'interface.
- Les clés ne se gèrent pas par API : une clé capable d'en émettre d'autres serait une porte dérobée.