Appearance
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/ingestThe 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:
| Where | Example |
|---|---|
Authorization header | Authorization: Bearer lit_... |
X-Latency-Key header | X-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:
| Field | Meaning | Also accepted |
|---|---|---|
name | Event name | event, event_name |
timestamp | When it happened (ISO 8601, or Unix s, ms, µs or ns) | ts, time, @timestamp |
duration_ms | How long it took | duration, duration_s, duration_ns |
service | Which app sent it | service.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"}'| Field | Meaning | Also accepted |
|---|---|---|
message | The log line | msg, body, log |
level | Severity: debug, info, warn, error, fatal | severity, status, numeric pino and syslog levels |
service | Which app sent it | service.name, app |
trace_id, span_id | Link the log to a trace | traceId, 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.logMetrics
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.
| Type | Use it for | Example |
|---|---|---|
gauge | A current value | {"metric": "queue.depth", "value": 12} |
count | How many since you last sent | {"metric": "orders.placed", "type": "count", "value": 3} |
counter | A running total that only goes up | {"metric": "jobs.done", "type": "counter", "value": 18234} |
rate | Per second, over an interval | {"metric": "cpu.busy", "type": "rate", "value": 0.4, "interval": 10} |
histogram | A spread of values, for percentiles | {"metric": "checkout.duration", "type": "histogram", "values": [182, 240, 95]} |
summary | Percentiles you already computed | {"metric": "db.latency", "type": "summary", "quantiles": {"0.5": 3.1, "0.99": 18}} |
set | How 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.1and the value is scaled up to match. - StatsD letters work as types:
c,g,ms,h,dands.
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.ndjsonResponses
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.