> ## Documentation Index
> Fetch the complete documentation index at: https://docs.weaveos.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Ingest API

> Send OTLP telemetry from AI agents to Weave — endpoints, payload schemas, and authentication

Send OTLP telemetry from your AI agents to Weave. Currently supports Hermes (token usage and cost), OpenClaw (tokens, cost, and prompts), [Pi / Oh My Pi](/ai-tools/oh-my-pi) (tokens and sessions from OTLP traces, aggregate cost from OTLP metrics), and AI Gateway (tokens and cost for custom model gateways). One API key per tool — attributed to individuals by email.

## How it works

```text theme={null}
Your telemetry service → POST /ingest/v1/logs (or /v1/metrics) → Weave dashboards
```

After the ingest worker processes a payload, data is available for the next reporting refresh. New data typically appears within seconds, but queue backlogs can delay processing; dashboards refresh on an hourly cycle (up to \~60 minutes after processing before new data appears in reports).

## Setup

<Steps>
  <Step title="Generate an API key">
    Go to **Data** in Weave, find **Hermes** (or **OpenClaw**, **Pi**, or **AI Gateway**) under the AI section, and create a new API key. Hermes keys start with `whrmkey_`, OpenClaw keys start with `wockey_`, Pi keys start with `wpikey_`, AI Gateway keys start with `wagkey_`.

    <Note>
      One org-level key covers all users. Per-person attribution happens via the `user.email` attribute on each record.
    </Note>
  </Step>

  <Step title="POST to the ingest endpoint">
    **Hermes** only needs logs:

    ```bash theme={null}
    POST https://app.workweave.ai/ingest/v1/logs
    Content-Type: application/json
    X-Weave-Token: whrmkey_xxxx
    ```

    **OpenClaw** uses both metrics and optional logs:

    ```bash theme={null}
    # Metrics (required — tokens and cost)
    POST https://app.workweave.ai/ingest/v1/metrics
    Content-Type: application/json
    X-Weave-Token: wockey_xxxx

    # Logs (optional — user prompts, if captureContent is enabled)
    POST https://app.workweave.ai/ingest/v1/logs
    Content-Type: application/json
    X-Weave-Token: wockey_xxxx
    ```

    Both `application/json` and `application/x-protobuf` are accepted.
  </Step>

  <Step title="Validate your payloads">
    The endpoint returns a `partialSuccess` block when records are missing required attributes. A successful payload returns either no `partialSuccess` key at all, or one with all zeros:

    ```json theme={null}
    { "partialSuccess": {} }
    ```

    If records are missing fields, you'll see something like:

    ```json theme={null}
    {
      "partialSuccess": {
        "rejectedLogRecords": 2,
        "errorMessage": "2 of 5 log records are missing... 1: missing user.account_id. 1: hermes.api_request missing required fields: cost_usd."
      }
    }
    ```

    All records are still ingested — `partialSuccess` is advisory, flagging records that won't populate dashboards.
  </Step>

  <Step title="Verify data">
    Run a few requests, then check your [AI report](https://app.workweave.ai/reports/ai). Data appears after the next hourly refresh (up to \~60 minutes). Log records with valid attributes populate the token usage and cost charts.
  </Step>
</Steps>

## Payload reference

### Hermes (logs only)

Endpoint: `POST /ingest/v1/logs`

| Field | Type | Required | Description |
| - | - | - | - |
| Body (event name) | string | yes | `hermes.api_request` or `hermes.user_prompt` |
| `user.account_id` | string | yes | Unique identifier per user or agent |
| `user.email` | string | recommended | Maps to a Weave person for attribution |
| `model` | string | yes | e.g. `claude-sonnet-4`, `gpt-4o` |
| `cost_usd` | float | yes | Per-request cost in USD |
| `duration_ms` | int | yes | Request duration in milliseconds |
| `input_tokens` | int | no | |
| `output_tokens` | int | no | |
| `cache_read_tokens` | int | no | |
| `cache_creation_tokens` | int | no | |
| `session.id` | string | no | Groups related requests |

**Example — `hermes.api_request`:**

```json theme={null}
{
  "resourceLogs": [{
    "resource": {
      "attributes": [{"key": "weave.token", "value": {"stringValue": "whrmkey_xxxx"}}]
    },
    "scopeLogs": [{
      "logRecords": [{
        "timeUnixNano": "1780420343000000000",
        "body": {"stringValue": "hermes.api_request"},
        "attributes": [
          {"key": "user.account_id", "value": {"stringValue": "dev@company.com"}},
          {"key": "user.email", "value": {"stringValue": "dev@company.com"}},
          {"key": "model", "value": {"stringValue": "claude-sonnet-4"}},
          {"key": "cost_usd", "value": {"doubleValue": 0.05}},
          {"key": "duration_ms", "value": {"intValue": "1234"}},
          {"key": "input_tokens", "value": {"intValue": "1500"}},
          {"key": "output_tokens", "value": {"intValue": "800"}},
          {"key": "session.id", "value": {"stringValue": "sess_123"}}
        ]
      }]
    }]
  }]
}
```

**Example — `hermes.user_prompt` (optional):**

```json theme={null}
{
  "resourceLogs": [{
    "resource": {
      "attributes": [{"key": "weave.token", "value": {"stringValue": "whrmkey_xxxx"}}]
    },
    "scopeLogs": [{
      "logRecords": [{
        "timeUnixNano": "1780420343000000000",
        "body": {"stringValue": "hermes.user_prompt"},
        "attributes": [
          {"key": "user.account_id", "value": {"stringValue": "dev@company.com"}},
          {"key": "prompt", "value": {"stringValue": "Refactor this function"}},
          {"key": "prompt_length", "value": {"intValue": "23"}},
          {"key": "session.id", "value": {"stringValue": "sess_123"}}
        ]
      }]
    }]
  }]
}
```

### Pi / Oh My Pi (traces only)

Endpoint: `POST /ingest/v1/traces`

Oh My Pi exports OTLP/HTTP protobuf traces. Weave persists completed `chat {model}` spans (or any span with `gen_ai.operation.name = "chat"`). Metrics and logs are accepted and discarded.

| Field | Type | Required | Description |
| - | - | - | - |
| Span name | string | yes | `chat {requested-model-id}` |
| `user.email` | string | yes\* | Maps to a Weave person for attribution |
| `user.account_id` or `user.id` | string | yes\* | Stable identity; otherwise Weave hashes `user.email` |
| `gen_ai.request.model` | string | yes unless span name includes the model | Requested model ID |
| `gen_ai.response.model` | string | no | Served model; preferred over the request model |
| `gen_ai.conversation.id` | string | no | Session ID |
| `gen_ai.response.id` | string | no | Dedup key when present |
| `gen_ai.usage.input_tokens` | int | no | Uncached + cache-read + cache-write (upstream contract) |
| `gen_ai.usage.output_tokens` | int | no | |
| `gen_ai.usage.cache_read.input_tokens` | int | no | |
| `gen_ai.usage.cache_creation.input_tokens` | int | no | |
| `pi.gen_ai.cost.estimated_usd` | float | no | Cost reported by Pi; when routed through OpenRouter, this is OpenRouter's reported request cost when available |

`gen_ai.usage.input_tokens` includes cache buckets. Weave stores uncached input as `input_tokens - cache_read - cache_creation`.

\* One identity attribute is required. Upstream Oh My Pi emits none of them on its own — its chat spans carry model, token, and cost data only — so supply `user.email` through the OTel resource:

```bash theme={null}
export OTEL_RESOURCE_ATTRIBUTES='user.email=dev@company.com'
```

Spans without any identity attribute are authenticated and accepted, then dropped at attribution, so the usage never appears in Weave. The API key is organization-wide, but this value identifies one person: every developer sharing a key sets their own address. The **Data → Pi** configure page generates this line pre-filled for the signed-in user.

Because `OTEL_RESOURCE_ATTRIBUTES` is W3C Baggage, `,` and `=` act as separators and cannot appear in the address.

Authenticate with `X-Weave-Token: wpikey_...`. Point Oh My Pi at `/ingest` with `OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf`.

### AI Gateway (logs only)

Endpoint: `POST /ingest/v1/logs`

Use this integration to report telemetry from a custom AI model gateway (e.g., an AWS Bedrock proxy, a self-hosted LiteLLM gateway, or any OpenAI-compatible intermediary). The gateway sends one log record per model inference request.

| Field | Type | Required | Description |
| - | - | - | - |
| Body (event name) | string | yes | `ai_gateway.api_request` |
| `user.account_id` | string | yes | Unique identifier per user or agent |
| `user.email` | string | recommended | Maps to a Weave person for attribution |
| `model` | string | yes | e.g. `claude-sonnet-4`, `gpt-4o` |
| `cost_usd` | float | yes | Per-request cost in USD |
| `duration_ms` | int | yes | Request duration in milliseconds |
| `input_tokens` | int | no | |
| `output_tokens` | int | no | |
| `cache_read_tokens` | int | no | |
| `cache_creation_tokens` | int | no | |
| `session.id` | string | no | Groups related requests |
| `request_id` | string | no | Gateway-side request ID for correlation |

<Note>
  `weave.token` (sent as `X-Weave-Token`) authenticates to Weave only — it has no relationship to
  your gateway's upstream provider credentials. If your gateway sits in front of AWS Bedrock
  (rotating STS/temporary credentials) or another provider with its own key-rotation schedule,
  rotating those upstream credentials has no effect on this token, and rotating this token has no
  effect on your provider auth. The only way to rotate `weave.token` is to generate a new AI
  Gateway API key in Weave.
</Note>

<Note>
  Requests are deduplicated on `(organization_id, external_account_id, model, timestamp)`. If your
  gateway retries or replays a request with the same user, model, and timestamp, the later write
  overwrites the earlier one. Include a high-resolution `timestamp` (e.g., include sub-second
  precision or a unique `request_id`) to avoid collisions when a single user sends multiple requests
  to the same model at the same second.
</Note>

**Example — `ai_gateway.api_request`:**

```json theme={null}
{
  "resourceLogs": [{
    "resource": {
      "attributes": [{"key": "weave.token", "value": {"stringValue": "wagkey_xxxx"}}]
    },
    "scopeLogs": [{
      "logRecords": [{
        "timeUnixNano": "1780420343000000000",
        "body": {"stringValue": "ai_gateway.api_request"},
        "attributes": [
          {"key": "user.account_id", "value": {"stringValue": "dev@company.com"}},
          {"key": "user.email", "value": {"stringValue": "dev@company.com"}},
          {"key": "model", "value": {"stringValue": "claude-sonnet-4"}},
          {"key": "cost_usd", "value": {"doubleValue": 0.05}},
          {"key": "duration_ms", "value": {"intValue": "1234"}},
          {"key": "input_tokens", "value": {"intValue": "1500"}},
          {"key": "output_tokens", "value": {"intValue": "800"}},
          {"key": "session.id", "value": {"stringValue": "sess_123"}},
          {"key": "request_id", "value": {"stringValue": "req_abc456"}}
        ]
      }]
    }]
  }]
}
```

### OpenClaw

**Metrics** (required — `POST /ingest/v1/metrics`):

| Metric | Metric type | Required attributes | Description |
| - | - | - | - |
| `openclaw.tokens` | Sum | `openclaw.token`, `openclaw.model`, `openclaw.agent` or `openclaw.channel`, `session.id`, `user.email` | Token counts per type |
| `openclaw.cost.usd` | Sum | `openclaw.model`, `openclaw.agent` or `openclaw.channel`, `session.id`, `user.email` | Cost per request |

`openclaw.token` values: `input`, `output`, `cache_read`, `cache_write`, `cache_creation`.

**Example — metrics payload:**

```json theme={null}
{
  "resourceMetrics": [{
    "resource": {
      "attributes": [{"key": "weave.token", "value": {"stringValue": "wockey_xxxx"}}]
    },
    "scopeMetrics": [{
      "metrics": [{
        "name": "openclaw.tokens",
        "sum": {
          "dataPoints": [{
            "timeUnixNano": "1780420343000000000",
            "startTimeUnixNano": "1780420343000000000",
            "asInt": "1500",
            "attributes": [
              {"key": "openclaw.token", "value": {"stringValue": "input"}},
              {"key": "openclaw.agent", "value": {"stringValue": "my-agent"}},
              {"key": "openclaw.model", "value": {"stringValue": "claude-sonnet-4"}},
              {"key": "user.email", "value": {"stringValue": "dev@company.com"}},
              {"key": "session.id", "value": {"stringValue": "sess_123"}}
            ]
          }]
        }
      },
      {
        "name": "openclaw.cost.usd",
        "sum": {
          "dataPoints": [{
            "timeUnixNano": "1780420343000000000",
            "startTimeUnixNano": "1780420343000000000",
            "asDouble": 0.05,
            "attributes": [
              {"key": "openclaw.agent", "value": {"stringValue": "my-agent"}},
              {"key": "openclaw.model", "value": {"stringValue": "claude-sonnet-4"}},
              {"key": "user.email", "value": {"stringValue": "dev@company.com"}},
              {"key": "session.id", "value": {"stringValue": "sess_123"}}
            ]
          }]
        }
      }]
    }]
  }]
}
```

**Logs** (optional — `POST /ingest/v1/logs`):

| Field | Type | Required | Description |
| - | - | - | - |
| Body (event name) | string | yes | `openclaw.user_prompt` |
| `openclaw.agent` or `openclaw.channel` | string | yes | Identity for attribution |
| `prompt` | string | no | The user prompt text |
| `prompt_length` | int | no | Length of the prompt |
| `session.id` | string | no | Groups related requests |

**Example — `openclaw.user_prompt`:**

```json theme={null}
{
  "resourceLogs": [{
    "resource": {
      "attributes": [{"key": "weave.token", "value": {"stringValue": "wockey_xxxx"}}]
    },
    "scopeLogs": [{
      "logRecords": [{
        "timeUnixNano": "1780420343000000000",
        "body": {"stringValue": "openclaw.user_prompt"},
        "attributes": [
          {"key": "openclaw.agent", "value": {"stringValue": "my-agent"}},
          {"key": "openclaw.channel", "value": {"stringValue": "my-channel"}},
          {"key": "prompt", "value": {"stringValue": "Refactor this function"}},
          {"key": "prompt_length", "value": {"intValue": "23"}},
          {"key": "session.id", "value": {"stringValue": "sess_123"}}
        ]
      }]
    }]
  }]
}
```

## Multiple tenants in one batch

Each `ResourceLogs` (or `ResourceMetrics`) can carry its own `weave.token` resource attribute. Weave routes and validates per-resource, so a single batch can mix Hermes, AI Gateway, and OpenClaw data — or even multiple organizations — as long as each resource has its own token.

We validate each distinct resource token once before queueing the request; repeated resources using the same token do not trigger repeated key lookups. Mixed-tenant requests are accepted above 32 distinct tokens. The decompressed request body is limited to 6 MiB and each request can contain up to 4,096 resource groups. Requests over either limit receive HTTP 413 and are not queued, so collectors should keep exports within both limits and split oversized requests before sending them. An invalid or deleted `X-Weave-Token` header returns HTTP 401. Invalid resource-level `weave.token` values reject only their associated resource groups and are reported through OTLP partial success; valid groups can still be queued with HTTP 200. Temporary authentication-store or queue failures return retryable HTTP 503.

## Troubleshooting

<AccordionGroup>
  <Accordion icon="plug" title="200 OK but no data in Weave">
    1. Check the `partial_success` (or `partialSuccess` in JSON) field — if records are rejected, the message tells you why (missing `user.account_id`, `cost_usd`, etc.)
    2. Data shows up on the next hourly dashboard refresh — it may take up to \~60 minutes to appear
    3. Verify the API key prefix matches the tool: `whrmkey_` for Hermes, `wockey_` for OpenClaw, `wagkey_` for AI Gateway. Wrong prefix = dropped
    4. Ensure `user.email` matches an email in your Weave organization so data attributes to the right person
  </Accordion>

  <Accordion icon="ban" title="401 Unauthenticated">
    The `X-Weave-Token` header is invalid, unrecognized, or deleted. Rotating a key in Weave invalidates the old one immediately, so clients still sending it in the header get 401. An invalid resource-level `weave.token` is handled separately: its resource groups are reported as rejected through OTLP partial success, while valid groups can still be accepted. Update every place the key is configured: collector headers, `managed-settings.json`, and `OTEL_RESOURCE_ATTRIBUTES`.
  </Accordion>

  <Accordion icon="x" title="Records rejected with 'missing user.account_id, model, cost_usd...'">
    Hermes `api_request` events **must** include `user.account_id`, `model`, `cost_usd`, and `duration_ms`. Records missing any of these are flagged but still ingested — they just won't populate dashboards until the fields are included.
  </Accordion>

  <Accordion icon="alert-triangle" title="200 OK but data appears on wrong day or not at all">
    Check the `timeUnixNano` field — it's nanoseconds since Unix epoch. Missing or zero timestamps can cause data to land on the wrong day or be skipped.
  </Accordion>
</AccordionGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.