Skip to content

HTTP API ​

No SDK required. If it can make an HTTP request, it can send telemetry: shell scripts, cron jobs, CI pipelines, serverless functions, browsers and webhooks.

The endpoint ​

POST https://ws.latency.app/v1/ingest

The key decides the Stream, so there's nothing else in the URL. (The older /v1/ingest/{stream} form still works, as long as it names the key's Stream.)

Authentication ​

Send your ingest key any one of these ways:

WhereExample
Authorization headerAuthorization: Bearer lit_...
X-Latency-Key headerX-Latency-Key: lit_...
key query parameter?key=lit_... (for browsers)

A disabled or revoked key gets 401.

Events ​

Send one JSON object:

bash
curl -X POST https://ws.latency.app/v1/ingest \
  -H "Authorization: Bearer $LATENCY_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "signup", "plan": "pro", "duration_ms": 412}'

Every field you send becomes something you can search and group by. Nested objects are flattened with dots, so {"user": {"id": 7}} becomes user.id.

A few fields are recognized:

FieldMeaningAlso accepted
nameEvent nameevent, event_name
timestampWhen it happened (ISO 8601, or Unix s, ms, µs or ns)ts, time, @timestamp
duration_msHow long it tookduration, duration_s, duration_ns
serviceWhich app sent itservice.name, app

No timestamp? We use the time we received it.

Sending many at once ​

Batch as much as you like into one request, up to 4 MB. Pick whichever shape is easiest:

json
[
  { "name": "pageview", "path": "/" },
  { "name": "pageview", "path": "/pricing" }
]
json
{
  "service": "web",
  "events": [
    { "name": "pageview", "path": "/" },
    { "name": "pageview", "path": "/pricing" }
  ]
}
txt
{"name": "pageview", "path": "/"}
{"name": "pageview", "path": "/pricing"}

In the wrapped form, top-level fields (like service above) apply to every record. The list can be called events, logs, records, data, items or batch.

For NDJSON, send Content-Type: application/x-ndjson.

Logs ​

Use a Logs stream. JSON logs work the same way as events:

bash
curl -X POST https://ws.latency.app/v1/ingest \
  -H "Authorization: Bearer $LATENCY_KEY" \
  -H "Content-Type: application/json" \
  -d '{"level": "error", "message": "payment failed", "service": "checkout", "order_id": "o-9"}'
FieldMeaningAlso accepted
messageThe log linemsg, body, log
levelSeverity: debug, info, warn, error, fatalseverity, status, numeric pino and syslog levels
serviceWhich app sent itservice.name, app
trace_id, span_idLink the log to a tracetraceId, spanId

Plain text ​

Already have log files? Send them as they are. Each line becomes a log entry, and lines that are JSON are parsed as JSON:

bash
curl -X POST https://ws.latency.app/v1/ingest \
  -H "Authorization: Bearer $LATENCY_KEY" \
  -H "Content-Type: text/plain" \
  --data-binary @app.log

Metrics ​

Use a Metrics stream. Each record is one measurement. type defaults to gauge:

bash
curl -X POST https://ws.latency.app/v1/ingest \
  -H "Authorization: Bearer $LATENCY_KEY" \
  -H "Content-Type: application/json" \
  -d '[
    {"metric": "queue.depth", "value": 12},
    {"metric": "orders.placed", "type": "count", "value": 3, "plan": "pro"},
    {"metric": "checkout.duration", "type": "histogram", "unit": "ms", "values": [182, 240, 95]}
  ]'

Extra fields (like plan above) become tags you can filter and group by.

TypeUse it forExample
gaugeA current value{"metric": "queue.depth", "value": 12}
countHow many since you last sent{"metric": "orders.placed", "type": "count", "value": 3}
counterA running total that only goes up{"metric": "jobs.done", "type": "counter", "value": 18234}
ratePer second, over an interval{"metric": "cpu.busy", "type": "rate", "value": 0.4, "interval": 10}
histogramA spread of values, for percentiles{"metric": "checkout.duration", "type": "histogram", "values": [182, 240, 95]}
summaryPercentiles you already computed{"metric": "db.latency", "type": "summary", "quantiles": {"0.5": 3.1, "0.99": 18}}
setHow many unique values{"metric": "users.active", "type": "set", "values": ["u1", "u2"]}

A few details:

  • Histograms also take pre-bucketed counts: "buckets": {"10": 5, "50": 12, "+Inf": 1}. Histograms from every host merge, so a p99 across 50 servers is computed from all of their data, not averaged.
  • Counters can reset (say, on a restart). Latency notices and keeps your rates correct.
  • Sampled counts: add "sample_rate": 0.1 and the value is scaled up to match.
  • StatsD letters work as types: c, g, ms, h, d and s.

OpenTelemetry payloads ​

This endpoint also takes OTLP. Send Content-Type: application/x-protobuf and the body is read as OTLP for the Stream's type (logs for Logs and Events Streams, traces, or metrics). An OTLP/JSON export sent as application/json is recognised too. SDKs should use the OpenTelemetry endpoints.

Compression ​

Compress large batches and send the matching Content-Encoding header: gzip, deflate, br or zstd.

bash
gzip -c events.ndjson | curl -X POST https://ws.latency.app/v1/ingest \
  -H "Authorization: Bearer $LATENCY_KEY" \
  -H "Content-Type: application/x-ndjson" \
  -H "Content-Encoding: gzip" \
  --data-binary @-

From a browser ​

navigator.sendBeacon can't set headers, so pass the key as ?key=. It also survives the page closing, which makes it perfect for analytics:

js
navigator.sendBeacon(
  'https://ws.latency.app/v1/ingest?key=lit_...',
  JSON.stringify({ name: 'pageview', path: location.pathname }),
);

Ingest keys can only write to their one stream, so shipping one in page code is safe. The endpoint accepts requests from any origin.

Cap browser keys

Give the key in your page code its own key limit, so a copied key can only ever send a little.

Retries ​

A 429 or 503 means "try again shortly": wait for the Retry-After header and resend. A 429 also names the limit you hit in its X-Latency-Limit header. The exception is 429 quota_exceeded, which lasts until the next month or an upgrade.

To make retries safe, send an Idempotency-Key with a value unique to that batch. If we already stored it within the last two minutes, the retry is acknowledged without storing a second copy:

bash
curl -X POST https://ws.latency.app/v1/ingest \
  -H "Authorization: Bearer $LATENCY_KEY" \
  -H "Idempotency-Key: batch-2026-09-24-0042" \
  -H "Content-Type: application/x-ndjson" \
  --data-binary @batch.ndjson

Responses ​

json
{ "accepted": true, "bytes": 54 }

202 Accepted means your data is stored. A retry that matched an earlier Idempotency-Key also gets "duplicate": true. For everything else, see Limits & Errors.

© 2026 Latency Labs LLC