Aller au contenu

Référence de l'API d'ingestion

Voici l'API HTTP neutre que chaque client intègre. Si vous construisez ou maintenez un package client, implémentez-le contre ce contrat. Toutes les routes sont en JSON sur HTTPS.

Chemin de base et versionnage

Les points d'entrée d'ingestion se trouvent sous /api/v1. Envoyez et acceptez application/json (le téléversement de sauvegarde est la seule exception en multipart/form-data).

Authentification

Chaque requête s'authentifie avec une clé d'API de projet (lm_...). Deux styles d'en-tête sont acceptés ; la forme bearer est recommandée :

http
Authorization: Bearer lm_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
http
X-Monitor-Key: lm_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

La clé est mise en correspondance par son hachage SHA-256 et se résout vers un seul projet. Les échecs d'authentification renvoient 401 :

json
{ "message": "Missing API key." }
json
{ "message": "Invalid API key." }

Limites de débit

  • Ingestion (/ingest, /logs, /dependencies) : 300 requêtes par minute, indexées par jeton de projet.
  • Heartbeats (/heartbeats/*) : 300 requêtes par minute, indexées par jeton de projet, sur un budget séparé de celui de l'ingestion pour qu'une tempête d'exceptions ne retarde pas les heartbeats.
  • Sauvegardes (/encryption-key, /backups*) : 60 requêtes par minute.

Dépasser une limite renvoie 429 Too Many Requests.

Quand l'ingestion est suspendue

Tant qu'un paiement reste dû sur l'abonnement de l'équipe, /ingest, /logs et /dependencies répondent 402 Payment Required et ne stockent rien :

json
{ "message": "Ingestion is suspended while a payment is outstanding. Your existing data stays readable in the dashboard." }

Les pings de heartbeat continuent d'être enregistrés pendant cette période, pour qu'une équipe suspendue ne voie pas ses tâches planifiées passer pour cassées. Traitez un 402 comme un état à journaliser et à afficher, non comme une erreur à réessayer : il se lève de lui-même quand le paiement passe.

Un point d'entrée du coffre répond 402 dans la même période, avec son propre message : POST /backups. La lecture reste ouverte, matériel de clé compris, pour qu'une restauration de ce que vous avez déjà payé continue de fonctionner.

json
{ "message": "Backup uploads are suspended while a payment is outstanding. Existing backups stay downloadable." }

Lister, télécharger et supprimer une sauvegarde restent ouverts, comme le point d'entrée de clé, qui sert le matériel dont monitor:restore a besoin : une archive déjà stockée peut toujours être récupérée, et déchiffrée.


POST /api/v1/ingest, rapporter une exception

Rapporte une seule occurrence d'exception.

Corps de la requête

json
{
  "exception": {
    "class": "RuntimeException",
    "message": "Undefined array key \"id\"",
    "code": 0,
    "file": "/app/Http/Controllers/OrderController.php",
    "line": 88,
    "trace": [
      {
        "file": "/app/Http/Controllers/OrderController.php",
        "line": 88,
        "function": null,
        "class": null,
        "type": null,
        "code": { "start": 83, "lines": ["    public function show(int $id)", "    {", "        $order = $this->orders[$id];", "", "        return view('orders.show', [", "            'order' => $order,", "        ]);", "    }", "", "    public function store(Request $request)", "    {"] }
      },
      { "file": "/vendor/laravel/framework/src/Illuminate/Routing/Controller.php", "line": 54, "function": "show", "class": "App\\Http\\Controllers\\OrderController", "type": "->" }
    ]
  },
  "context": {
    "environment": "production",
    "release": "9f2c1ab3d4e5",
    "occurred_at": "2026-06-28T10:15:00+00:00"
  }
}

Règles des champs

ChampRequisType / limite
exception.classouichaîne, max 255
exception.messagenonchaîne, max 8192
exception.codenonquelconque
exception.filenonchaîne, max 1024
exception.linenonentier ≥ 0
exception.tracenontableau
exception.trace[].code.startavec codeentier ≥ 1, numéro de la première ligne de l'extrait
exception.trace[].code.linesavec codetableau de chaînes, max 40 lignes de max 500 caractères
context.environmentnonchaîne, max 100
context.releasenonchaîne, max 255 (un SHA hex active la corrélation de commit)
context.occurred_atnonchaîne, max 64 (date analysable)

Le contexte. Le tableau ne nomme que les trois clés du contexte que le serveur valide et range dans des colonnes à part. Le contexte est pourtant stocké entier, avec toutes les clés que le client envoie (url, method, request, user...), et il n'entre pas en base tel quel :

  1. il est d'abord masqué à l'arrivée, avec la liste propre de Quiet Guard, qui reprend les secrets de la liste par défaut du SDK Laravel, et par la règle des clients : sous-chaîne sans casse, un tiret valant un souligné, un nom cherché aussi sans ses séparateurs intérieurs (api_key masque apikey et apiKey), chaîne JSON ouverte, ou masquée entière quand elle ne se décode pas et qu'un nom entre guillemets suivi d'un : y correspond, paramètres de requête et segments de chemin en forme de jeton d'une URL, et mot de passe d'un utilisateur:mot-de-passe@, celui d'un DSN (mysql://...) compris (voir le masquage du cœur). Un secret de cette liste qu'un client a retiré de la sienne y est masqué quand même, et ce qu'un client installé avant la version 0.3.1 ne savait pas masquer sous ces noms l'est aussi : il envoie encore l'URL sans la masquer, un instantané Livewire fermé et des en-têtes à tirets. Les en-têtes de cette liste qui portent l'adresse IP du visiteur (x-forwarded-for, x-real-ip...) ne sont pas repris : ce sont des données personnelles et non des secrets, et les garder ou les masquer est une décision du responsable de traitement. Le serveur les stocke tels qu'ils arrivent ;
  2. il est ensuite borné à 65 535 octets de JSON encodé et à 30 niveaux d'imbrication, parce que MariaDB refuse un document JSON de 32 niveaux dans une colonne json : au-delà de l'une ou l'autre borne, ou s'il ne peut pas être encodé, le contexte stocké est {"_truncated": true}.

environment, release et occurred_at sont lus avant cette borne et gardent leurs colonnes : un contexte trop gros ne les emporte pas. Chiffré ou en clair, c'est ce contexte masqué et borné qui est écrit, et une équipe dont le stockage chiffré est actif ne scelle jamais la valeur brute.

Les trames. Le serveur ne garde de chaque trame que file (coupé à 1024 caractères), line, function et class (255 caractères chacun), type (8), is_site et, de code, start et lines seulement. Une valeur d'un autre type que celui attendu est écartée plutôt que convertie, sauf pour line, qui accepte tout nombre, même écrit en chaîne ("88"), et le garde en entier, et pour code.start, qui accepte un entier écrit en chaîne ("40", la validation refusant tout autre nombre) et le garde en entier aussi ; toute autre clé, les arguments d'un appel en premier lieu, n'entre jamais en base.

Trame zéro et extrait de code. À partir de la version 0.3, les clients placent le site de levée en tête de trace : une trame réduite au file et à la line de l'exception elle-même, puisque la trace de PHP commence à l'appelant et n'a jamais contenu la ligne fautive. Une trace_limit compte cette trame (1 = le site seul). Chaque trame peut porter un objet code optionnel : start, le numéro de sa première ligne, et lines, les lignes de source qui entourent la ligne de la trame. Les clients envoient les cinq lignes de part et d'autre (onze au plus), chacune coupée à 500 caractères, sur au plus dix trames par rapport, jamais pour un chemin contenant /vendor/, et remplacent entière par [scrubbed] toute ligne qui nomme une clé de leur liste de masquage et lui donne une valeur. Le serveur ne repasse pas sur ces lignes, seul le contexte est masqué à l'arrivée ; il refuse un extrait au-delà de 40 lignes ou de 500 caractères par ligne, et scelle les extraits au repos avec le reste de la trace quand le stockage chiffré de l'équipe est actif.

Réponses

  • 202 Accepted: stockée :

``json { "message": "accepted", "stored": true, "issue_id": "01kw6tkrd8wjbfv2pktr7ssw43" } ``

  • 202 Accepted: quota dépassé, accusée mais non stockée :

``json { "message": "accepted", "stored": false } ``

  • 422 Unprocessable Entity: échec de validation.

issue_id est l'identifiant public de l'issue, un ULID. C'est la valeur que porte l'URL du tableau de bord, vous pouvez donc la journaliser et la coller dans la barre d'adresse. Ce n'est pas un numéro de ligne, et la réponse ne contient aucun compteur d'occurrences : ces nombres auraient indiqué à chaque projet combien d'exceptions la plateforme entière avait stockées.


POST /api/v1/logs, rapporter des logs applicatifs (lot)

Stocke un lot d'entrées de log. Conditionné à la fonctionnalité de plan logs.

Corps de la requête

json
{
  "logs": [
    {
      "level": "error",
      "message": "La passerelle de paiement a expiré",
      "context": { "order_id": 1234 },
      "channel": "payments",
      "environment": "production",
      "release": "9f2c1ab",
      "logged_at": "2026-06-28T10:15:00+00:00"
    }
  ]
}

Règles des champs

ChampRequisType / limite
logsouitableau, 1 à 500 entrées
logs.*.levelnonchaîne, max 20
logs.*.messageouichaîne, max 8192
logs.*.contextnontableau
logs.*.channelnonchaîne, max 100
logs.*.environmentnonchaîne, max 100
logs.*.releasenonchaîne, max 255
logs.*.logged_atnonchaîne, max 64

Le contexte d'un log. Le serveur ne le masque pas : c'est ce que votre application a choisi d'écrire dans son propre log, et c'est la liste de masquage de votre client qui s'y applique, avant l'envoi. Il le borne par la même règle que celui d'une exception : au-delà de 65 535 octets de JSON encodé, il est remplacé par {"_truncated": true}, et l'entrée est stockée avec le reste du lot.

Réponses

  • 202 Accepted :

``json { "message": "accepted", "stored": 137 } ``

stored indique combien d'entrées ont réellement été enregistrées (un lot partiel est stocké lorsque le quota est presque plein).

  • 403 Forbidden: le plan n'inclut pas les logs :

``json { "message": "Application logs are not available on your current plan." } ``


POST /api/v1/dependencies, rapporter le composer.lock

Remplace l'instantané de dépendances du projet et déclenche un scan de vulnérabilités en arrière-plan.

Corps de la requête

json
{
  "packages": [
    { "name": "laravel/framework", "version": "11.9.0", "is_dev": false },
    { "name": "phpunit/phpunit", "version": "11.2.0", "is_dev": true }
  ]
}

Règles des champs

ChampRequisType / limite
packagesouitableau, 1 à 5000 entrées
packages.*.nameouichaîne, max 255
packages.*.versionouichaîne, max 100
packages.*.is_devnonbooléen

Réponse

  • 202 Accepted :

``json { "message": "accepted", "dependencies": 142 } ``


POST /api/v1/heartbeats/{slug}, pinguer une tâche planifiée

Signale qu'une tâche planifiée vient de s'exécuter. Le corps est facultatif : sans corps, le ping se comporte comme il l'a toujours fait. Le SDK Laravel envoie, pour ses battements automatiques :

json
{ "auto": true, "kind": "queue", "period": 1, "grace": 9, "queue": "redis/emails", "lag_ms": 1840 }

auto doit valoir true pour être pris en compte, tout autre valeur laisse le ping se comporter comme s'il n'y avait pas de corps. kind vaut scheduler ou queue. queue porte jusqu'à 100 caractères. Un heartbeat créé par un ping qui porte auto, kind, period (1 à 1440) et grace (0 à 1440) naît armé ; un heartbeat existant n'est jamais réarmé. lag_ms (0 ou plus ; au-delà de 86 400 000, une journée, il est conservé comme une journée) n'est enregistré que pour un heartbeat de file, et un ping de file sans lag_ms efface la valeur précédente. Un champ illisible est ignoré, jamais refusé : le ping est toujours enregistré.

{slug} est normalisé en minuscules, chiffres et tirets. Le premier ping sur un slug inconnu enregistre le heartbeat, armé quand le corps ci-dessus suffit à l'armer, non armé sinon : consigné dans les deux cas, il n'alerte jamais tant qu'une période attendue n'a pas été définie. Voir Heartbeats.

Réponses

  • 202 Accepted :

``json { "message": "pong", "armed": true } ``

armed indique si une période attendue est définie, pour qu'un client distingue un ping consigné d'un ping surveillé.

  • 422 Unprocessable Entity : le slug se normalise en chaîne vide, ou le projet détient déjà le nombre maximal de heartbeats.

Points d'entrée du coffre de sauvegardes

Ceux-ci alimentent le coffre de sauvegardes chiffrées. Tous requièrent la fonctionnalité de plan backups. Tant qu'un paiement reste dû, POST /backups répond 402 Payment Required ; récupérer le matériel de clé, lister, télécharger et supprimer restent ouverts, pour que monitor:restore continue de fonctionner sur les archives déjà en place.

GET /api/v1/encryption-key

Sert le matériel de clé de l'équipe pour que le client puisse sceller et (avec la phrase secrète, en local) désenvelopper.

json
{
  "public_key": "base64...",
  "private_key_wrapped": "base64...",
  "kdf_salt": "base64..."
}
  • 403 si le plan n'inclut pas les sauvegardes.
  • 404 si l'équipe n'a pas configuré de clés de chiffrement.

GET /api/v1/backups

json
{
  "backups": [
    {
      "id": "9b1c...uuid",
      "type": "database",
      "name": "nightly",
      "size_bytes": 10485760,
      "checksum": "sha256...",
      "created_at": "2026-06-28T03:00:00+00:00"
    }
  ]
}

POST /api/v1/backups

Téléversement multipart/form-data d'un blob déjà chiffré.

ChampRequisRemarques
typeouidatabase ou files
namenonchaîne, max 255
fileouil'archive chiffrée
  • 201 Created renvoie les métadonnées de la sauvegarde stockée (même forme qu'une entrée de liste).
  • 403 si le plan n'inclut pas les sauvegardes.
  • 413 Request Entity Too Large si le téléversement dépasserait le quota de stockage :

``json { "message": "Backup storage quota exceeded.", "used_bytes": 5012345678, "limit_bytes": 5000000000 } ``

GET /api/v1/backups/{uuid}

Diffuse le blob chiffré en téléchargement (<nom>.enc).

DELETE /api/v1/backups/{uuid}

Supprime la sauvegarde. Renvoie 204 No Content.


Récapitulatif des erreurs

StatutSignification
202 AcceptedIngéré (consultez stored pour savoir ce qui a été conservé).
201 CreatedSauvegarde téléversée.
204 No ContentSauvegarde supprimée.
401 UnauthorizedClé d'API manquante ou invalide.
402 Payment RequiredIngestion et téléversements de sauvegarde suspendus tant qu'un paiement reste dû.
403 ForbiddenFonctionnalité absente du plan de l'équipe.
404 Not FoundClés de chiffrement non configurées (point d'entrée de clé).
413 Payload Too LargeQuota de stockage des sauvegardes dépassé.
422 Unprocessable EntityÉchec de validation.
429 Too Many RequestsLimite de débit dépassée.

Vous lisez la documentation Quiet Guard v1.0.