POST /v1/ingest

The asynchronous observation path: record events that need no synchronous decision — transcript turns, telemetry, and the whole answer after a streamed partial judgment. Nothing is blocked on the response; the events feed session state, findings, and audit.

POST {base_url}/v1/ingest
Authorization: Bearer ogr_<key>
Content-Type: application/json

Request

A batch envelope of 1–100 GuardEvents:

{ "batch": [ GuardEvent, ... ] }
FieldTypeRequiredDescription
batcharray of GuardEventrequired1–100 events; each element is validated independently

One malformed event does not fail the batch — it fails its own slot in the results array. The documented extension fields (run_id, turn, authz) are accepted on each event.

Response — always 207

When the envelope itself is well-formed, the response is always HTTP 207 with one result per submitted event:

{ "results": [
  { "id": "evt_1", "status": 201 },
  { "id": "evt_2", "status": 400, "error": "timestamp: invalid datetime" }
] }
FieldTypeDescription
resultsarrayOrder-preserving: results[i] answers batch[i]
results[].idstring | nullThe event's event_id, or null if it could not be read at all
results[].statusintegerPer-event status: 201 recorded, 200 duplicate, 400 invalid
results[].errorstringPresent on failures; human-readable validation message

Idempotency

Ingest is idempotent on (workspace, event_id): retrying a batch never duplicates events. Safe to retry the whole request on a network error.

Attestation ceiling

Events arriving through ingest without a valid ogr-batch-signature are capped at the self_declared attestation ceiling — recorded, but their identity claims carry the unenrolled floor of trust. Sign the batch body with your enrolled key to raise it.

Example

curl -s $OGR_RUNTIME/v1/ingest \
  -H "Authorization: Bearer $OGR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "batch": [{
      "ogr_version": "0.4",
      "event_id": "evt_a1",
      "guard_id": "g_b2",
      "session_id": "sess_01HZX",
      "timestamp": "2026-08-11T09:31:04Z",
      "observation_point": "conversation",
      "sensor": {"id": "my-gateway", "class": "proxy"},
      "kind": "model_output",
      "subject": {"agent_id": "support-bot"},
      "payload": {"text": "Here is the summary you asked for..."}
    }]
  }'
{ "results": [ { "id": "evt_a1", "status": 201 } ] }

Python

from openguardrails import GuardEvent, RuntimeClient

client = RuntimeClient()

results = client.ingest([
    GuardEvent(
        kind="model_output",
        observation_point="conversation",
        sensor={"id": "my-gateway", "class": "proxy"},
        subject={"agent_id": "support-bot"},
        payload={"text": "Here is the summary you asked for..."},
        event_id="evt_a1",
        guard_id="g_b2",
        timestamp="2026-08-11T09:31:04Z",
        session_id="sess_01HZX",
    ),
])
for r in results:
    if r["status"] >= 300:
        log.warning("event %s rejected: %s", r["id"], r.get("error"))

The Python client chunks iterables longer than 100 into multiple requests and concatenates the results. For fire-and-forget background reporting, use BatchingIngestor instead of calling ingest inline.

JavaScript

import { RuntimeClient } from "@openguardrails/core"

const client = new RuntimeClient()

const results = await client.ingest([
  {
    kind: "model_output",
    observationPoint: "conversation",
    sensor: { id: "my-gateway", class: "proxy" },
    subject: { agent_id: "support-bot" },
    payload: { text: "Here is the summary you asked for..." },
    eventId: "evt_a1",
    guardId: "g_b2",
    timestamp: "2026-08-11T09:31:04Z",
    sessionId: "sess_01HZX",
  },
])
for (const r of results) {
  if (r.status >= 300) console.warn(`event ${r.id} rejected: ${r.error}`)
}

The JS client sends one request per call and throws a RangeError for arrays longer than INGEST_BATCH_MAX (100) — chunk long streams yourself.

Errors

Per-event failures come back inside the 207 results, not as HTTP errors. The request itself can still fail:

StatusBodyNotes
400{"error": "invalid_body"}Envelope malformed (no batch, or over 100 events)
401{"error": "unauthorized"}Bad or missing workspace key
429{"error": "rate_limited", "limit": n}Back off and retry — idempotency makes retries safe