Ingestion API reference

This is the platform-neutral HTTP API that every client integrates against. If you are building or maintaining a client package, implement against this contract. All routes are JSON over HTTPS.

Base path and versioning

The ingestion endpoints live under /api/v1. Send and accept application/json (the backup upload is the one multipart/form-data exception).

Authentication

Every request authenticates with a project API key (lm_...). Two header styles are accepted; the bearer form is preferred:

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

The key is matched by its SHA-256 hash and resolves to a single project. Authentication failures return 401:

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

Rate limits

  • Ingestion (/ingest, /logs, /dependencies, /heartbeats/*): 300 requests per minute, keyed by project token.
  • Backups (/encryption-key, /backups*): 60 requests per minute.

Exceeding a limit returns 429 Too Many Requests.

When ingestion is suspended

While a payment is outstanding on the team's subscription, /ingest, /logs and /dependencies answer 402 Payment Required and store nothing:

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

Heartbeat pings keep being recorded during that window, so a suspended team's scheduled tasks do not start looking broken. Treat a 402 as a state to log and surface, not as an error to retry: it clears on its own when the payment goes through.

One vault endpoint answers 402 in the same window, with its own message: POST /backups. Reading stays open, the key material included, so a restore of what you have already paid for keeps working.

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

Listing, downloading and deleting a backup stay open, so an archive already stored can still be fetched. The key endpoint is the exception to notice: it also serves the material monitor:restore needs, so decrypting from the client waits for the payment while the blob itself does not.


POST /api/v1/ingest, report an exception

Reports a single exception occurrence.

Request body

json
{
  "exception": {
    "class": "RuntimeException",
    "message": "Undefined array key \"id\"",
    "code": 0,
    "file": "/app/Http/Controllers/OrderController.php",
    "line": 88,
    "trace": [ { "file": "...", "line": 12, "function": "handle" } ]
  },
  "context": {
    "environment": "production",
    "release": "9f2c1ab3d4e5",
    "occurred_at": "2026-06-28T10:15:00+00:00"
  }
}

Field rules

FieldRequiredType / limit
exception.classyesstring, max 255
exception.messagenostring, max 8192
exception.codenoany
exception.filenostring, max 1024
exception.linenointeger ≥ 0
exception.tracenoarray
context.environmentnostring, max 100
context.releasenostring, max 255 (hex SHA enables commit correlation)
context.occurred_atnostring, max 64 (parseable date)

Responses

  • 202 Accepted: stored:

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

  • 202 Accepted: over quota, acknowledged but not stored:

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

  • 422 Unprocessable Entity: validation failed.

issue_id is the issue's public identifier, a ULID. It is the same value the dashboard URL carries, so you can log it and paste it into the address bar. It is not a row number, and there is no event counter in the response: those would have told every project how many exceptions the whole platform had stored.


POST /api/v1/logs, report application logs (batch)

Stores a batch of log entries. Gated by the logs plan feature.

Request body

json
{
  "logs": [
    {
      "level": "error",
      "message": "Payment gateway timed out",
      "context": { "order_id": 1234 },
      "channel": "payments",
      "environment": "production",
      "release": "9f2c1ab",
      "logged_at": "2026-06-28T10:15:00+00:00"
    }
  ]
}

Field rules

FieldRequiredType / limit
logsyesarray, 1–500 entries
logs.*.levelnostring, max 20
logs.*.messageyesstring, max 8192
logs.*.contextnoarray
logs.*.channelnostring, max 100
logs.*.environmentnostring, max 100
logs.*.releasenostring, max 255
logs.*.logged_atnostring, max 64

Responses

  • 202 Accepted:

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

stored is how many entries were actually saved (a partial batch is stored when the quota is nearly full).

  • 403 Forbidden: plan does not include logs:

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


POST /api/v1/dependencies, report composer.lock

Replaces the project's dependency snapshot and triggers a background vulnerability scan.

Request body

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

Field rules

FieldRequiredType / limit
packagesyesarray, 1–5000 entries
packages.*.nameyesstring, max 255
packages.*.versionyesstring, max 100
packages.*.is_devnoboolean

Response

  • 202 Accepted:

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


POST /api/v1/heartbeats/{slug}, ping a scheduled task

Signals that a scheduled task has just run. The request carries no body.

{slug} is normalised to lowercase letters, digits and dashes. The first ping on an unknown slug registers the heartbeat unarmed: it is recorded and never alerts until an expected period is set in the dashboard. See Heartbeats.

Responses

  • 202 Accepted:

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

armed says whether an expected period has been set, so a client can tell a recorded ping from a watched one.

  • 422 Unprocessable Entity: the slug normalises to an empty string, or the project already holds the maximum number of heartbeats.

Backup vault endpoints

These power the encrypted backup vault. All require the backups plan feature. While a payment is outstanding, POST /backups answers 402 Payment Required; fetching the key material, listing, downloading and deleting stay open, so monitor:restore keeps working on the archives you already have.

GET /api/v1/encryption-key

Serves the team's key material so the client can seal and (with the passphrase, locally) unwrap.

json
{
  "public_key": "base64...",
  "private_key_wrapped": "base64...",
  "kdf_salt": "base64..."
}
  • 403 if the plan does not include backups.
  • 404 if the team has not configured encryption keys.

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

multipart/form-data upload of an already-encrypted blob.

FieldRequiredNotes
typeyesdatabase or files
namenostring, max 255
fileyesthe encrypted archive
  • 201 Created returns the stored backup's metadata (same shape as a list entry).
  • 403 if the plan does not include backups.
  • 413 Request Entity Too Large if the upload would exceed the storage quota:

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

GET /api/v1/backups/{uuid}

Streams the encrypted blob as a download (<name>.enc).

DELETE /api/v1/backups/{uuid}

Deletes the backup. Returns 204 No Content.


Error summary

StatusMeaning
202 AcceptedIngested (check stored to see whether/how much was kept).
201 CreatedBackup uploaded.
204 No ContentBackup deleted.
401 UnauthorizedMissing or invalid API key.
402 Payment RequiredIngestion, key material and backup uploads suspended while a payment is outstanding.
403 ForbiddenFeature not on the team's plan.
404 Not FoundEncryption keys not configured (key endpoint).
413 Payload Too LargeBackup storage quota exceeded.
422 Unprocessable EntityValidation failed.
429 Too Many RequestsRate limit exceeded.

You are reading the Quiet Guard v1.0 documentation.