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, ... ] }
| Field | Type | Required | Description |
|---|---|---|---|
batch | array of GuardEvent | required | 1–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" }
] }
| Field | Type | Description |
|---|---|---|
results | array | Order-preserving: results[i] answers batch[i] |
results[].id | string | null | The event's event_id, or null if it could not be read at all |
results[].status | integer | Per-event status: 201 recorded, 200 duplicate, 400 invalid |
results[].error | string | Present 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:
| Status | Body | Notes |
|---|---|---|
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 |