---
description: Send native observations, LangSmith-compatible runs, or OpenTelemetry traces to WeaveScope.
---

# Ingestion API

Use the ingestion API to send trace data from custom clients, compatibility
bridges, and OpenTelemetry collectors.

{% hint style="info" %}
For Elixir applications, [BeamWeaver](beamweaver-integration.md) provides the
simplest integration and handles batching, retries, trace context, and
redaction.
{% endhint %}

Base URL:

```text
https://app.weavescope.com
```

## Authentication

Every request requires a project API key in one of these headers:

```http
Authorization: Bearer ws_...
```

```http
x-api-key: ws_...
```

The key must have `ingest:write`. A missing or invalid key receives
`401 Unauthorized`; a valid key without ingest permission receives
`403 Forbidden`.

## Endpoints

| Method | Path | Accepts |
| --- | --- | --- |
| `POST` | `/api/v1/observations/batch` | Native WeaveScope observations. |
| `POST` | `/api/v1/runs` | A LangSmith-compatible run creation. |
| `PATCH` | `/api/v1/runs/:id` | A LangSmith-compatible run update. |
| `POST` | `/api/v1/runs/batch` | LangSmith-compatible `post` and `patch` arrays. |
| `POST` | `/api/public/otel/v1/traces` | OpenTelemetry Protocol over HTTP (OTLP/HTTP) traces. |

## Native observations

Send native events in an `events` array:

```json
{
  "events": [
    {
      "observation_id": "018f5f2a-77a6-7bd4-bb58-df7b90f8e337",
      "trace_id": "018f5f2a-77a6-7bd4-bb58-df7b90f8e337",
      "operation": "finish",
      "name": "support.reply",
      "kind": "agent",
      "status": "success",
      "start_time": "2026-07-02T12:00:00Z",
      "end_time": "2026-07-02T12:00:03Z",
      "event_version": 17830032030000002,
      "inputs": {"messages": [{"role": "user", "content": "Hello"}]},
      "outputs": {"text": "Hi"},
      "usage": {"input_tokens": 12, "output_tokens": 7},
      "custom_fields": {"account_id": "acct_123"}
    }
  ]
}
```

Required fields:

| Field | Requirement |
| --- | --- |
| `observation_id` or `id` | A non-empty string. |
| `trace_id` | A non-empty string. |
| `operation` | `start`, `finish`, or `error`. |
| `kind` or `run_type` | One of the accepted observation kinds. |
| `status` | One of the accepted statuses. |

Validated optional fields:

| Field | Requirement |
| --- | --- |
| `start_time` | An ISO 8601 datetime. |
| `end_time` | An ISO 8601 datetime. |
| `event_version` | An integer from `0` through `18446744073709551615`. A string integer is also accepted. |

Accepted kinds:

```text
event, span, generation, agent, tool, chain, retriever, evaluator,
embedding, guardrail, graph, model, llm
```

Accepted statuses:

```text
pending, running, success, ok, error, interrupted, cancelled
```

### Example request

```bash
curl https://app.weavescope.com/api/v1/observations/batch \
  -H "authorization: Bearer $WEAVESCOPE_API_KEY" \
  -H "content-type: application/json" \
  -d '{
    "events": [
      {
        "observation_id": "018f5f2a-77a6-7bd4-bb58-df7b90f8e337",
        "trace_id": "018f5f2a-77a6-7bd4-bb58-df7b90f8e337",
        "operation": "finish",
        "name": "manual.test",
        "kind": "span",
        "status": "success"
      }
    ]
  }'
```

### Native responses

An accepted event returns `202 Accepted`:

```json
{
  "status": "queued",
  "accepted": 1,
  "rejected": 0,
  "results": [
    {
      "index": 0,
      "id": "018f5f2a-77a6-7bd4-bb58-df7b90f8e337",
      "status": "queued",
      "raw_event_id": "..."
    }
  ]
}
```

A partially accepted batch still includes an itemized `results` array.
Rejected items contain `status`, `code`, and `reason`.

| Status | Meaning |
| --- | --- |
| `202 Accepted` | At least one event was accepted and queued. |
| `400 Bad Request` | No event was accepted or the request shape was invalid. |
| `401 Unauthorized` | The API key is missing or invalid. |
| `402 Payment Required` | The accepted-event limit was reached. |
| `403 Forbidden` | The API key cannot ingest. |
| `413 Content Too Large` | The request exceeds a size or event-count limit. |
| `429 Too Many Requests` | An hourly ingest or edge rate/concurrency limit was reached. |
| `503 Service Unavailable` | WeaveScope could not safely accept the payload. |

An hourly ingest-limit response includes `hourly_ingest_reset_at`. An edge
`429` can use a different response body. Retry either case with backoff.

## LangSmith-compatible runs

Create one run:

```text
POST /api/v1/runs
```

Update one run:

```text
PATCH /api/v1/runs/:id
```

Send a batch:

```json
{
  "post": [{"id": "run-id", "name": "root", "run_type": "chain"}],
  "patch": [{"id": "run-id", "end_time": "2026-07-02T12:00:03Z"}]
}
```

Each run object must include `id` or `observation_id`.

## OpenTelemetry traces

Send OTLP/HTTP traces to:

```text
POST /api/public/otel/v1/traces
```

The standard OTLP/HTTP protobuf encoding uses:

```http
content-type: application/x-protobuf
```

Protobuf JSON is also supported:

```http
content-type: application/json
```

JSON requests must use the OTLP `resourceSpans` shape. Protobuf requests may
use `content-encoding: gzip`; other encodings receive
`415 Unsupported Media Type`.

Both the transferred protobuf body and its decompressed value are limited to
25 MiB. A proxy may reject an oversized transferred body before it reaches
WeaveScope, so that response body can differ from the OTLP response used for a
decompressed-size rejection.

WeaveScope converts every OTLP span to a native observation. Span and resource
attributes become metadata, with common fields such as `input`, `output`,
`llm.input_messages`, `llm.output_messages`, `error`, and `exception.message`
recognized automatically.

Original OTLP `TraceId`, `SpanId`, and `parentSpanId` values remain available as
external identifiers. Internal observation IDs include both the trace ID and
span ID, so equal span IDs in different traces remain distinct.

Status mapping:

- `STATUS_CODE_ERROR`, or numeric code `2`, becomes an error observation.
- `STATUS_CODE_OK` becomes a successful finished observation.
- `STATUS_CODE_UNSET`, or no status, becomes pending. It is treated as finished
  when the span has an end timestamp and started when it does not.

OTLP responses use OTLP shapes rather than the native JSON envelope:

| Case | HTTP status | Protobuf response | JSON response |
| --- | --- | --- | --- |
| All spans accepted, or the request is empty | `200 OK` | Empty `ExportTraceServiceResponse` | `{}` |
| Some or all spans rejected after a valid decode | `200 OK` | `ExportTraceServiceResponse.partial_success` | `{"partialSuccess": {"rejectedSpans": n, "errorMessage": "..."}}` |
| More than 1,000 spans | `413` | `google.rpc.Status` with `INVALID_ARGUMENT` | `{"code": 3, "message": "..."}` |
| Decompressed protobuf exceeds 25 MiB | `413` | `google.rpc.Status` | — |
| Invalid media type, encoding, or payload | `400` or `415` | `google.rpc.Status` | `{"code": 3, "message": "..."}` |
| Quota, idempotency, or persistence failure | A meaningful status such as `402`, `409`, `429`, or `503` | `google.rpc.Status` | `{"code": n, "message": "..."}` |

## Custom fields

`custom_fields` is optional. When present, it must be a JSON object or `null`.
You can provide it at:

- The event root
- `metadata.custom_fields`
- `extra.metadata.custom_fields`
- `context_metadata.custom_fields`

Missing and `null` values behave like an empty object. String, number, and
boolean values are converted to strings. Nested objects, arrays, blank values,
reserved keys, and secret-like keys are ignored.

Values merge in this order, with later values winning:

1. Context metadata
2. Effective metadata
3. Event-level custom fields

An array, string, number, or boolean in place of the object rejects that event
with `invalid_custom_fields`. The reason identifies the invalid path without
echoing its value. Other valid events in the request can still be accepted.
OTLP reports the rejected spans through `partial_success`.

## Payload size limits

Authenticated request bodies are limited to 25 MiB. Authentication happens
before JSON decoding. For gzip-compressed OTLP protobuf, both the transferred
and decompressed bodies must fit the limit.

Native batches, combined LangSmith batches, and OTLP trace requests can contain
at most 1,000 events or spans. A larger request is rejected atomically with
`413 Content Too Large`; none of its events are counted, stored, or queued.

Within each event or span, the JSON encoding of each field below can be at most
4 MiB:

- `inputs`
- `outputs`
- `events`
- `metadata`
- `context_metadata`

For LangSmith-compatible runs, `extra.metadata` counts as metadata. A field
that is exactly 4 MiB is accepted. A larger field rejects only that event with
`payload_too_large`; valid siblings can still be accepted. Accepted fields are
stored in full and are never silently truncated.

## When usage is counted

An event counts toward monthly usage after it passes validation and WeaveScope
safely accepts it for processing. Rejected events do not count and do not
consume hourly ingest capacity.

Processing happens after acceptance. If one accepted event later cannot be
normalized, valid siblings can still appear, but the accepted event remains
counted. Temporary processing failures are retried.

Hourly ingest usage counts accepted data in these fields:

- `inputs`
- `outputs`
- `metadata`
- `context_metadata`
- `events`
