---
title: "Ingest OTLP traces for a run (Bearer-authenticated)"
method: POST
path: "/v1/traces"
tags: ["Traces"]
---

# Ingest OTLP traces for a run (Bearer-authenticated)

`POST /v1/traces`

## Request

- Base URL: `https://ping.lastping.dev`
- URL: `https://ping.lastping.dev/v1/traces`
- Auth: HTTP bearer

Accepts one OTLP `ExportTraceServiceRequest` (protobuf or its JSON
mapping) and enqueues its spans to be stored. Model
and provider names are stored up to 200 bytes. This is
the documented path: a project API key with `ingest`,
`write` or `admin` scope in the `Authorization: Bearer` header names
the project; the `lastping.monitor_id` resource attribute names
which check (required unless the key is bound to a monitor).

**Monitor-bound keys**: an `ingest` key minted with a `check_id` is
bound to that monitor. Its spans always land on that monitor, so
`lastping.monitor_id` may be omitted; a payload that names a
different monitor is 400.

**Secrets are redacted before anything is stored.** Every string
attribute value, span name and status message is scanned at ingest
and each secret-shaped value is replaced by `[REDACTED:<class>]`
(see the check's `trace_content`). By default
(`trace_content: dropped`) prompt and command content is removed
entirely; a monitor set to `redacted` stores it, redacted.

**Auth**: `Authorization: Bearer lp_…`. The key must be unrevoked,
unexpired, and scoped `ingest`, `write` or `admin`. This endpoint enforces
scope unconditionally, regardless of how the management API
rolls out scope enforcement: a new endpoint starts strict. No `X-Api-Key` form; the monitor-URL alternative below is
this route's only unauthenticated option.

**Correlation**: `lastping.monitor_id` (uuid) names
the check and is required on this form (400 when missing). It is
read from the resource attributes, or from a span attribute when no
resource carries one (for exporters with no resource attributes);
the resource wins when both are present. `lastping.run_id` is
optional. With it, every span of the request joins that run (the
rid the run's pings already carry). Without it, each trace in the
request becomes its own run, with rid `trace-` followed by the hex
trace id, listed by `GET /api/v1/runs` with `traced: true`. Spans
are never guessed onto a run by time. A run made from a bare trace
never opens an incident and never changes the monitor's status.

**Content-Type**: `application/x-protobuf` or `application/json`
(415 otherwise). `Content-Encoding: gzip` is accepted and
decompressed under the same size cap as an uncompressed body.

**Caps** (enforced before enqueue): body ≤ 1 MiB
decompressed (413 over); ≤ 500 spans per request (400 over); span
name ≤ 256 bytes (truncated, not rejected); attributes ≤ 32 KiB per
span (excess dropped, `truncated` set on the span, not rejected); ≤
2,000 spans per run across every request (further spans dropped and
counted in `spans_dropped`, not rejected — this cap is enforced when
the spans are stored, after this endpoint answers). A span
that starts more than an hour after ingest received it is dropped
and counted in `dropped`; the rest of the request is kept. Rate
limit: 3,600 requests/hour/project, a bucket entirely separate from
the ping rate limits, so a chatty tracer cannot starve heartbeats
(429 with `Retry-After: 60`). LastPing reads a few OTLP
exports at once; past that an export is refused 429 with `code:
"OTLP_BUSY"` and `Retry-After: 5` before its body is read. Pings
are never refused this way.

**Durability**: identical rule to pings — an accepted request is
202 only after the enqueue to the spans queue succeeds; a queue
failure is 503, never a silent ack. Every span field is
third-party text: render it as text, never interpret it as markup.

## Request body

- object: The OTLP JSON mapping of `ExportTraceServiceRequest`.

## Response `202`

Accepted and enqueued.

- object
  - `accepted` integer, required: Spans enqueued from this request.
  - `dropped` integer, required: Spans of this request that were discarded because they start more than an hour after ingest received the request. An over-cap request is rejected outright (400/413), never partially admitted. The per-run 2,000-span storage cap drops further spans, counted in the run's `spans_dropped`, after this answer.

## Other responses

- `400`: Malformed OTLP payload, too many spans, or a missing `lastping.monitor_id`. A missing `lastping.run_id` is not an error: each trace becomes its own run.
- `401`: Missing, unknown, revoked or expired API key.
- `403`: The API key's scope is `read`.
- `404`: `lastping.monitor_id` does not name a check in the key's project. No existence oracle: identical to an unowned monitor id.
- `413`: Decompressed body exceeds 1 MiB.
- `415`: Content-Type is neither `application/x-protobuf` nor `application/json`.
- `429`: Trace rate cap exceeded for this project (3,600 requests/hour), independent of the ping rate limit, with `Retry-After: 60`. Or the project's daily span budget is used up (default 2,000,000 spans per UTC day): the problem body carries `code: "SPAN_BUDGET_EXCEEDED"` and `Retry-After` runs to 00:00 UTC. Or LastPing is already reading as many OTLP exports as it takes at once: `code: "OTLP_BUSY"`, `Retry-After: 5`.
- `503`: Tracing is unavailable on this deployment, the spans queue could not accept the message, or the span budget could not be read (refused rather than admitted unmetered). Retry.

---

[API](https://skmtc.dev/lastping/apis/lastping-management-api.md) · [All operations](https://skmtc.dev/lastping/apis/lastping-management-api/llms.txt) · [OpenAPI document](https://skmtc.dev/lastping/apis/lastping-management-api/revisions/bbc6eda91c46?raw)
