Ingest HTTP API
The SDKs are thin clients over three public JSON endpoints. Use this reference to report from any other language, from shell scripts, or from CI.
Base URL: https://linearjeph.cc
| Endpoint | Purpose |
|---|---|
POST /ingest/v1/events | Errors, messages and performance transactions |
POST /ingest/v1/heartbeat | Cron and worker check-ins |
POST /ingest/v1/packages | Installed packages for the dependency audit |
Authentication and common rules
Send your application's ingest key (Dashboard > Maintenance > your application > Reveal key) in the X-LJ-Key header and a JSON body with Content-Type: application/json. Only POST is accepted.
- Body size: at most 256 KB (262,144 bytes). Larger bodies get
413. - A successful request answers
202 Acceptedwith a small JSON body. The request has been validated and stored; nothing in the payload is ever executed. - Rate limits are per application key (heartbeats also per IP address). Over the limit you get
429, sometimes withRetry-After(seconds). Retry with exponential backoff. - A wrong, rotated or inactive key gets
401. Do not retry a 401. - The edge proxy also limits very high request rates per key and answers
503or413with a non-JSON body; treat 5xx as retryable. - Error responses are JSON
{"error": {"code": "...", "message": "..."}}; rely on the HTTP status, becausecodevalues differ between endpoints.
POST /ingest/v1/events
curl -sS -X POST https://linearjeph.cc/ingest/v1/events \
-H "X-LJ-Key: $LINEARJEPH_KEY" -H "Content-Type: application/json" \
-d '{
"events": [
{
"type": "error",
"timestamp": "2026-03-01T12:00:00Z",
"release": "1.4.2",
"environment": "production",
"exception": {
"type": "KeyError",
"value": "missing",
"stacktrace": [
{"filename": "/srv/app/views.py", "function": "index", "lineno": 10, "in_app": true},
{"filename": "/srv/app/util.py", "function": "parse", "lineno": 77, "in_app": true}
]
},
"tags": {"region": "eu"},
"user": {"id": "u-42"}
},
{
"type": "transaction",
"timestamp": "2026-03-01T12:00:01Z",
"release": "1.4.2",
"environment": "production",
"transaction": {"name": "GET /items/<id>", "duration_ms": 12.5, "status": "ok"}
}
]
}'Response: 202 {"accepted": 2}.
Event fields
| Field | Type | Rules |
|---|---|---|
type | string | Required: "error" or "transaction". Other events are skipped. |
timestamp | string or number | ISO 8601 or Unix seconds. Missing, invalid, more than 10 minutes in the future or older than 90 days is replaced by the receive time. |
release | string | Up to 100 characters. |
environment | string | Up to 40 characters. |
tags | object | At most 20 entries; key up to 50, value up to 200 characters. Values are stored as strings. |
user.id | string | Up to 100 characters. |
exception.type | string | Required for errors, up to 200 characters. An error without a type is skipped. |
exception.value | string | Up to 500 characters. |
exception.stacktrace | array | Up to 50 frames, oldest first (the innermost call last). |
frame filename / function | string | Up to 300 / 200 characters. |
frame lineno | integer | 0 to 10,000,000. |
frame in_app | boolean | true for your own code. Grouping uses the last three in-app frames. |
transaction.name | string | Required for transactions, up to 200 characters. |
transaction.duration_ms | number | Required, 0 to 86,400,000. |
transaction.status | string | Up to 20 characters, default "ok". Use "error" for failures. |
What accepted means
At most 100 events per request are read; extra events are ignored. accepted counts the events stored. It is lower than the number you sent (possibly 0, still with status 202) when:
- an event failed validation,
- the plan is not active,
- the organisation used its monthly allowance (Essential 50,000, Professional 500,000, Business 2,000,000, Enterprise 10,000,000 error and transaction events; further events are dropped and counted), or
- the plan does not include performance monitoring (transactions need Professional or above).
Grouping
Errors are grouped into issues by a fingerprint of the exception type and the last three in-app frames (file and function, numbers in file names normalised). Without in-app frames the fingerprint uses the exception type and message, numbers normalised. Set release consistently to get release tracking and regression alerts.
Limits
- Rate: 600 requests per minute per key. Batch events instead of sending one per request.
- Errors:
401invalid key,400body is not JSON or lacks aneventsarray,413over 256 KB,429rate limit (withRetry-After).
POST /ingest/v1/heartbeat
# at the end of a cron job
curl -fsS -X POST https://linearjeph.cc/ingest/v1/heartbeat \
-H "X-LJ-Key: $LINEARJEPH_KEY" -H "Content-Type: application/json" \
-d '{"name": "nightly-backup", "status": "ok", "interval_s": 86400}'Response: 202 {"accepted": 1}.
| Field | Type | Rules |
|---|---|---|
name | string | Required, [A-Za-z0-9._-], 1 to 64 characters. Identifies the job. |
status | string | "ok" (default) or "error". |
interval_s | integer | Expected seconds between check-ins, 30 to 2,592,000. Default 3600 when a monitor is first created; sending it later updates the interval. |
The first check-in with a new name creates a heartbeat monitor (up to 20 created automatically per application). An error check-in, or no check-in within interval_s plus a 5 minute grace period, opens an incident; the next ok closes it.
Limits: 60 requests per minute per key; 600 per minute per IP address. Errors: 401 invalid key (unauthorized), 400 invalid name, status or interval, 403 no active maintenance plan (plan_required) or too many heartbeat monitors (limit_reached), 413 too large, 429 (rate_limited).
A shell wrapper that reports failure too:
./backup.sh && S=ok || S=error
curl -fsS -X POST https://linearjeph.cc/ingest/v1/heartbeat -H "X-LJ-Key: $LINEARJEPH_KEY" \
-H "Content-Type: application/json" -d "{\"name\":\"nightly-backup\",\"status\":\"$S\",\"interval_s\":86400}"POST /ingest/v1/packages
curl -sS -X POST https://linearjeph.cc/ingest/v1/packages \
-H "X-LJ-Key: $LINEARJEPH_KEY" -H "Content-Type: application/json" \
-d '{"ecosystem": "PyPI", "packages": [{"name": "django", "version": "5.2.1"}, {"name": "requests", "version": "2.32.3"}]}'Response: 202 {"accepted": 2}.
| Field | Type | Rules |
|---|---|---|
ecosystem | string | Required: "PyPI", "npm" or "Go". |
packages | array | Required, up to 5,000 entries of {"name", "version"}. |
name | string | 1 to 200 characters from A-Za-z0-9_ @ . / + ~ : -. PyPI names are lowercased. |
version | string | 1 to 100 characters from A-Za-z0-9_ . + ~ ! : -. |
Entries that fail the name or version rules are skipped; accepted is the number stored. Each request replaces the previously reported list for that ecosystem from SDKs, so send the complete list in one request (under 256 KB) rather than in pieces. The dependency audit checks the list against the OSV database daily.
Limits: 30 requests per minute per key. Errors: 401 (unauthorized), 400 (invalid), 403 dependency audits not included in the plan (not_included, Essential does not include them), 413 over 256 KB or more than 5,000 packages (too_large), 429 (rate_limited).
Retry guidance
| Status | Action |
|---|---|
| 202 | Done. |
| 400, 403, 404 | Fix the request; do not retry. |
| 401 | Check the key; do not retry. |
| 413 | Split the batch (events) or reduce the payload. |
| 429 | Wait Retry-After seconds if present, otherwise back off exponentially. |
| 5xx, network error | Retry with exponential backoff and jitter, up to a few attempts. |