Contrat d'API
PostShip apprend la forme d'une réponse JSON sur 3 passages, puis alerte dès qu'elle dérive.
Mis à jour le 25 septembre 2026
Sur cette page
- Ce que PostShip vérifie
- Ce que le schéma retient
- Cassant ou informatif
- Régler
- Ce qui déclenche une alerte
- Limites et plans
- Dépannage
- Pourquoi une propriété optionnelle est signalée « n'est plus toujours présent » ?
- Pourquoi le passage est en erreur plutôt qu'en échec ?
- L'API répond du JSON mais avec Content-Type: text/html
- Tâche planifiée (battement)
- Ce que PostShip fait
- Régler
- Ce qui déclenche une alerte
- Limites et plans
- Dépannage
Une API qui répond 200 peut casser vos clients : price devient une chaîne, items disparaît, une date n'en est plus une. Rien ne le signale, jusqu'à ce que l'application mobile plante ou que l'intégration d'un partenaire s'arrête. Le contrat d'API demande une URL qui rend du JSON, apprend sa forme, puis compare chaque passage à ce qu'il a appris. Vous ne rédigez aucun schéma.
Ce que PostShip vérifie
À chaque passage, PostShip fait un GET avec Accept: application/json (12 secondes, 5 redirections, en-tête privé possible) et procède dans l'ordre :
- Le statut. Il doit être exactement le statut attendu (200 par défaut). Sinon, échec franc, avant même de lire le corps : le contrat commence par « répondre ».
- Le JSON. Le corps doit être du JSON valide et tenir en 512 Ko — au-delà, PostShip considère qu'il lit un export plutôt qu'un contrat, et le passage est en erreur.
- L'apprentissage. Les 3 premiers passages nourrissent le schéma sans jamais juger. Un seul échantillon ne dit pas ce qui est optionnel ni ce qui est une énumération ; 2, à peine plus.
- La comparaison. À partir du quatrième passage, la réponse est comparée au schéma appris. Chaque écart est une dérive, cassante ou informative.
Ce que le schéma retient
Pour chaque valeur : son type (null, booléen, nombre — entier ou non —, chaîne, tableau, objet), les types alternatifs vus au même endroit (string | null), et pour les chaînes un format reconnu (date, datetime, url, email, uuid) ou les valeurs distinctes vues tant qu'elles sont peu nombreuses (8 au plus, de 64 caractères au plus) — c'est ainsi qu'un status devient une énumération paid | pending. Une propriété présente dans tous les échantillons est requise ; présente dans certains, optionnelle. Au-delà de 12 niveaux d'imbrication ou de 200 clés dans un objet, le conteneur est tronqué : c'est un dictionnaire indexé, pas une structure.
Cassant ou informatif
| Dérive | Gravité | Phrase |
|---|---|---|
type_change | Cassante | $.price : number → string |
prop_disparue | Cassante | $.items a disparu |
requis_absent | Cassante | $.items[].sku n'est plus toujours présent |
format_perdu | Cassante | $.created_at n'est plus une date-heure |
prop_nouvelle | Informative | $.currency : nouvelle propriété |
enum_nouvelle | Informative | $.status : nouvelle valeur "refunded" |
tableau_vide | Informative | $.items est vide |
Cassant : un client typé ne s'en relève pas. Informatif : rien ne casse, mais le contrat bouge. Les dérives informatives sont apprises en silence — la propriété nouvelle fait désormais partie du contrat et ne sera plus signalée. Le schéma continue donc de grandir avec l'API, et « cassant » garde le sens que vous attendez : quelque chose que votre code n'aurait pas prévu.
Régler
Projet → URLs → « Surveiller… » → « Une API ». L'écran « Ce qu'on attend de l'API » demande le statut attendu et, pour une API privée, un en-tête d'accès (Authorization, X-Api-Key, X-Monitoring-Key ou X-Health-Token) envoyé à l'hôte d'origine seulement.
Sur le détail de l'URL, le panneau « Contrat d'API » montre : pendant l'apprentissage, « Apprentissage en cours : 2 passages sur 3. Rien n'est jugé avant. » ; ensuite, le schéma appris en clair (6 niveaux affichés au plus), les dérives cassantes en rouge, les informatives en gris avec la mention « appris », et le nombre de passages d'apprentissage.
Le bouton « Réapprendre le contrat » efface le schéma. Après un changement d'API volontaire, c'est ce qu'on veut : 3 passages plus tard, le nouveau contrat est la référence, sans avoir à le décrire.
Ce qui déclenche une alerte
| Code | Phrase |
|---|---|
statut:200:503 | L'API répond 503 au lieu de 200. |
json_invalide | La réponse n'est plus du JSON valide. |
contrat:type_change:$.price | Contrat d'API : $.price a changé de type. |
contrat:prop_disparue:$.items | Contrat d'API : $.items a disparu. |
contrat:requis_absent:$.items[].sku | Contrat d'API : $.items[].sku n'est plus toujours présent. |
contrat:format_perdu:$.created_at | Contrat d'API : $.created_at n'a plus le format attendu. |
L'alerte cite les 2 premières dérives cassantes et compte les autres : « 3 changements cassants : $.price : number → string, $.items a disparu et 1 autre ». Un code par dérive, stable d'un passage à l'autre : la même dérive n'alerte pas à chaque cycle, une dérive supplémentaire réalerte. Les dérives informatives n'alertent jamais. Un contrat en échec coûte 10 points au Ship Score.
Limites et plans
Disponible sur tous les plans ; une cible compte pour une URL du quota (Découverte 5, Pro 25, Pro Plus 100). Le rythme est celui du cycle — 5 minutes sur Découverte, 2 sur Pro, 1 sur Pro Plus (sa sonde de 30 secondes ne regarde que les pages) — plus chaque déploiement, et l'apprentissage demande donc de quelques minutes à un quart d'heure avant le premier verdict.
Dépannage
Pourquoi une propriété optionnelle est signalée « n'est plus toujours présent » ?
Parce qu'elle était présente dans les 3 échantillons d'apprentissage : PostShip l'a apprise requise. Cliquez sur « Réapprendre le contrat » à un moment où la réponse la contient parfois, ou acceptez le verdict comme le signal qu'un client typé aurait eu.
Pourquoi le passage est en erreur plutôt qu'en échec ?
Une erreur veut dire que PostShip n'a pas pu juger : délai dépassé, réponse de plus de 512 Ko, boucle de redirection. Un échec est un verdict sur le contrat. Les deux ouvrent un incident, mais seule l'erreur dit « La réponse dépasse 512 Ko : trop grosse pour un contrat. ».
L'API répond du JSON mais avec Content-Type: text/html
Le type annoncé n'est pas vérifié : seul le corps compte. Si JSON.parse réussit, le contrat s'apprend.
Tâche planifiée (battement)
Surveiller ce qui tourne sans page — un cron, un worker, une sauvegarde — par l'absence d'appel.
Tout ce qui tourne sans page : un cron de sauvegarde, un worker qui vide une file, un script de facturation nocturne. Aucune URL à vérifier — et quand ça s'arrête, rien ne le dit. Le site répond 200, la sauvegarde n'a pas tourné depuis 3 semaines. Le battement de cœur inverse la logique : c'est votre script qui appelle PostShip à la fin de chaque passage, et c'est l'absence d'appel qui déclenche l'alerte.
Ce que PostShip fait
Un battement est une cible sans URL à demander. PostShip fabrique une adresse secrète de la forme https://postship.fr/api/heartbeat/<jeton>, et attend qu'elle soit appelée :
- en
GETou enPOST, indifféremment — uncurlen fin de script est unGET, un webhook d'outil est unPOST; - le corps est ignoré ; tout ce qui compte est « on nous a appelés, et quand » ;
- la réponse est
OK, en texte, statut 200. Un jeton inconnu ou désactivé répond 404 « Jeton inconnu. » — les deux se répondent pareil, pour qu'un jeton désactivé ne révèle pas qu'il a existé.
À chaque cycle du projet (toutes les 5 minutes sur Découverte, 2 sur Pro, chaque minute sur Pro Plus ; la sonde de 30 secondes de Pro Plus ne regarde que les pages), PostShip relit la date du dernier appel et la compare à l'intervalle promis, avec une marge d'un quart d'intervalle, plafonnée à une heure : un cron horaire qui appelle à 10:00:00 puis 11:00:04 ne doit pas alerter à 11:00:01. Assez pour absorber un démarrage lent, pas assez pour laisser passer une exécution manquée.
Le premier passage. Un battement qui n'a encore jamais été appelé n'est pas en panne — le script n'est peut-être pas encore déployé. Il passe pendant un intervalle plein (plus sa marge) à compter de sa création, puis échoue : au bout de ce délai, « jamais appelé » est un vrai silence.
Le jeton est le seul secret, et il vaut ce qu'il vaut : quelqu'un qui l'a peut faire croire qu'un cron tourne. C'est le modèle de Healthchecks.io et Cronitor, et c'est acceptable parce que le pire effet est un silence — pas un accès à quoi que ce soit.
Régler
Projet → URLs → « Surveiller… » → « Une tâche planifiée » :
- Comment s'appelle-t-elle ? « sauvegarde nocturne », 80 caractères au plus.
- À quel rythme doit-elle tourner ? Toutes les 5 minutes, 15 minutes, toutes les heures, toutes les 6 heures, tous les jours (par défaut), toutes les semaines. Une liste et non un champ libre : ces 6 valeurs couvrent les crons réels, et un intervalle de 37 minutes est presque toujours une faute de frappe.
- « Créer le battement ». L'adresse à appeler est sur la page de la cible, panneau « Battement de cœur », avec le délai attendu, le dernier appel, et 2 boutons « Copier » — l'adresse seule, ou la ligne
curlprête à coller.
Puis, à la fin de votre script :
curl -fsS https://postship.fr/api/heartbeat/VOTRE_JETON > /dev/null-fsS : un échec de l'appel lui-même se voit dans les journaux du cron, sans bruit quand tout va bien. Appelez après le travail, pas avant : un appel en début de script dirait « ça tourne » d'une sauvegarde qui échoue à la deuxième ligne.
Désactiver un battement (menu de la cible) fait ignorer les appels et n'envoie aucune alerte — pour une maintenance, ou un cron retiré.
Ce qui déclenche une alerte
| Code | Phrase de l'écran | Phrase de l'alerte |
|---|---|---|
heartbeat_late | Le script n'a pas appelé PostShip dans le délai prévu. | Le battement de cœur n'est plus reçu — sauvegarde nocturne |
heartbeat_never | Le script n'a jamais appelé PostShip depuis la création de ce battement. | Le battement de cœur n'a jamais été reçu — sauvegarde nocturne |
Le détail du passage donne le retard en minutes. L'alerte part au franchissement du délai — après la confirmation du projet, qui compte ici en cycles — et une seule fois par silence ; « Rétabli » suit le prochain appel reçu. Un battement en retard ouvre un incident comme n'importe quelle cible.
Limites et plans
Disponible sur tous les plans. Un battement compte pour une URL du quota (Découverte 5, Pro 25, Pro Plus 100) : c'est une cible que vous demandez. Le verdict est relu à chaque cycle du projet : un silence est donc constaté avec au plus 5 minutes de retard sur Découverte, 2 minutes sur Pro, 1 sur Pro Plus — la sonde de 30 secondes de Pro Plus ne regarde que les pages, pas les battements.
| Intervalle | Marge | Alerte au plus tôt après le dernier appel |
|---|---|---|
| 5 min | 1 min 15 | 6 min 15 |
| 1 h | 15 min | 1 h 15 |
| 6 h | 1 h | 7 h |
| 1 jour | 1 h | 25 h |
| 1 semaine | 1 h | 7 jours et 1 h |
Dépannage
Pourquoi l'alerte n'est pas partie alors que le cron a sauté une exécution ?
Regardez la marge : un cron toutes les 5 minutes a 1 min 15 de marge, et le verdict n'est relu qu'au cycle du projet — 2 minutes sur Pro, 1 sur Pro Plus, 5 sur Découverte ; la sonde de 30 secondes de Pro Plus n'y change rien. Un retard de 3 minutes peut donc passer entre 2 cycles, et sur Découverte un battement de 5 minutes n'est relu qu'une fois par période : choisissez plutôt un intervalle d'au moins 15 minutes.
Mon script appelle l'adresse mais le panneau dit « jamais »
L'appel n'a pas atteint PostShip : curl sans -f masque un 404, une variable d'environnement vide donne une adresse tronquée, un pare-feu sortant bloque postship.fr. Lancez la ligne à la main et lisez la réponse : elle doit être OK.
Puis-je changer l'intervalle ?
Supprimez le battement et recréez-le avec le bon intervalle : le jeton change, mettez à jour votre script. L'ancien jeton répond 404 immédiatement.