API Reference
Complete API reference for all OGuardAI v1 endpoints including transform, rehydrate, detect, policy evaluation, health, and capabilities
All endpoints are served under the /v1 prefix. Request and response bodies are JSON (Content-Type: application/json).
Detected values are replaced with semantic tokens. The emitted form is always three segments, {{type:id:cap}} (for example {{email:e_001:5873cc5722d8}}): a lowercase type label, an id stable within a session, and a per-token capability cap. Every transform emits the cap, RAG or not. The :cap segment is matched exactly at rehydrate and fails closed: a capless {{type:id}} token, like a wrong cap, never resolves, so a guessed id cannot restore a value. In the default per-request scheme the id is a prefix_NNN counter (e_001, p_001) and the cap is 12 random hex characters; RAG calls that pass a corpus_id instead derive a deterministic corpus-scoped identity (a 24-hex id suffix and a 32-hex cap), so the same value maps to the same token across documents and queries. Token examples in this reference show the full emitted form, because only that form round-trips.
Endpoint Table
| Method | Path | Description | Auth Required |
|---|---|---|---|
POST | /v1/transform | Transform text, replacing entities with semantic tokens | Yes (if enabled) |
POST | /v1/transform/stream | Streaming transform (SSE) | Yes (if enabled) |
POST | /v1/transform/file | Transform an uploaded file (multipart) | Yes (if enabled) |
POST | /v1/transform/image | Transform text detected in an image | Yes (if enabled) |
POST | /v1/rehydrate | Restore semantic tokens in LLM output to original values | Yes (if enabled) |
POST | /v1/rehydrate/stream | Streaming rehydrate (SSE) | Yes (if enabled) |
POST | /v1/rehydrate/file | Rehydrate an uploaded file (multipart) | Yes (if enabled) |
POST | /v1/detect | Detect entities without transforming | Yes (if enabled) |
POST | /v1/batch/transform | Batch transform multiple inputs in one request | Yes (if enabled) |
POST | /v1/batch/detect | Batch detect entities in multiple inputs | Yes (if enabled) |
POST | /v1/evaluate-policy | Evaluate policy against entities (dry run) | Yes (if enabled) |
POST | /v1/rag/ingest | Ingest a document for RAG with PII protection | Yes (if enabled) |
POST | /v1/rag/ingest/batch | Ingest multiple documents in one request | Yes (if enabled) |
POST | /v1/rag/query | Query RAG index with PII-safe context | Yes (if enabled) |
POST | /v1/rag/context | Retrieve RAG context chunks (tokenized) | Yes (if enabled) |
POST | /v1/rag/answer | Generate a RAG answer with automatic rehydration | Yes (if enabled) |
POST | /v1/rag/delete | Delete ingested RAG documents | Yes (if enabled) |
POST | /v1/revoke | Revoke a single entity (makes it non-restorable) | Yes (if enabled) |
POST | /v1/revoke/bulk | Revoke multiple entities in one request | Yes (if enabled) |
GET | /v1/revocations/count | Get the count of revoked entities | Yes (if enabled) |
POST | /v1/sessions/status | Report a session's lifecycle state (active/stateless) | Yes (session scope) |
DELETE | /v1/sessions | Invalidate a server-held session | Yes (session scope) |
POST | /v1/redact/image | Redact PII regions in an image | Yes (if enabled) |
POST | /v1/admin/policy/validate | Validate policy configuration files | Yes (admin scope) |
POST | /v1/admin/policy/reload | Hot-reload the policy directory (atomic swap, fail-closed) | Yes (admin scope) |
POST | /v1/entity-types | Validate a custom entity type (validate-only, no persistence) | Yes (transform scope) |
POST | /v1/patterns | Validate a custom regex pattern (validate-only, no persistence) | Yes (transform scope) |
GET | /v1/health | Health check | Yes (requires auth when enabled) |
GET | /v1/capabilities | List entity types, languages, detectors | Yes (requires auth when enabled) |
GET | /livez | Liveness probe (always public, no auth) | No |
GET | /readyz | Readiness probe (always public, no auth) | No |
GET | /v1/diagnostics | Runtime diagnostics (detector mode, config, versions) | Yes (Admin scope required) |
GET | /metrics | Prometheus metrics endpoint | Yes (Admin scope required) |
Port note: Examples below use port 3000 (the server default). The Docker quick start (
deploy/docker/docker-compose.yml) also publishes port 3000; the HA compose stack (deploy/docker/docker-compose.ha.yml) fronts the server with nginx on port 8080. For other deployments, substitute the appropriate host and port.
Round-trip field flow
The protect then restore round trip uses a different field name at each step, because the text changes role as it moves through the pipeline:
| Step | Endpoint | You send | You get back |
|---|---|---|---|
| 1. Protect | POST /v1/transform | input (your raw text) | safe_text (tokenized) plus session_state |
| 2. Generate | your LLM | safe_text | the model reply, still tokenized |
| 3. Restore | POST /v1/rehydrate | output (the model reply) plus session_state | restored_text |
The request field is input on transform but output on rehydrate because rehydrate's input is the LLM's output. The text field is safe_text after transform and restored_text after rehydrate. Carry the session_state from step 1 into step 3: it holds the token map that makes restoration deterministic.
Authentication
API Key
Send the key in the X-API-Key header:
curl -H "X-API-Key: your-key-here" http://localhost:3000/v1/transform ...JWT Bearer Token
Send a JWT in the Authorization header:
curl -H "Authorization: Bearer eyJhbG..." http://localhost:3000/v1/transform ...In the jwt auth mode, tokens are validated with a configured shared secret (HMAC: HS256, HS384, or HS512). Asymmetric and JWKS verification are not part of this mode; use the oidc mode below for those.
OIDC (JWKS)
For asymmetric tokens issued by an OIDC provider, set auth.mode: oidc and configure [auth.oidc] with issuer, jwks_url, audience, tenant_claim, and the allowlisted asymmetric algorithms (RS*, ES*, or PS* families). The server fetches and caches the provider's JWKS, verifies the token signature against it, pins the algorithm allowlist (rejecting alg: none and HMAC-in-asymmetric confusion), and requires the tenant claim on every token. Send the token the same way as a JWT:
curl -H "Authorization: Bearer eyJhbG..." http://localhost:3000/v1/transform ...Scopes
The runtime supports 11 scopes:
| Scope | Grants Access To |
|---|---|
transform | /v1/transform, /v1/transform/stream, /v1/transform/file, /v1/transform/image, /v1/redact/image, /v1/entity-types, /v1/patterns |
rehydrate | /v1/rehydrate, /v1/rehydrate/stream, /v1/rehydrate/file |
detect | /v1/detect |
policy | /v1/evaluate-policy |
batch | /v1/batch/transform, /v1/batch/detect |
rag | /v1/rag/ingest, /v1/rag/ingest/batch, /v1/rag/query, /v1/rag/context, /v1/rag/answer |
revoke | /v1/revoke, /v1/revoke/bulk, /v1/rag/delete, scoped to the caller's own tenant |
global_revoke | Elevation required in addition to revoke when the caller has no tenant binding, to write an unscoped revocation that suppresses a value at rehydration for every tenant. admin implies it |
session | /v1/sessions/status, DELETE /v1/sessions |
detect_values | Permission to receive raw detected values from /v1/detect and /v1/batch/detect. Without it, detection returns type, span, and confidence only. |
admin | All endpoints including /v1/admin/policy/validate, /v1/revocations/count, and config management |
admin implies every other scope, so an admin key reaches all endpoints. /v1/revocations/count
stays admin-only; it returns a scope-local count (the caller's own tenant, or the unscoped partition
for a platform admin), never a cross-tenant total.
Migration (granular route scopes): the batch, RAG, and revoke endpoints now require their own scope instead of reusing
transform. A key that previously relied ontransformto reach/v1/batch/*or/v1/rag/*, or onadminto revoke, must be grantedbatch,rag, orrevoke. This is a deliberate least-privilege change: a key now gets exactly the route classes it is granted.
When auth is disabled (default for development), all endpoints are accessible without credentials.
Idempotent retries (Idempotency-Key)
The mutating, non-streaming endpoints accept an optional Idempotency-Key request header so a
network retry cannot execute the same mutation twice: POST /v1/transform, POST /v1/batch/transform,
POST /v1/rag/ingest, POST /v1/rag/ingest/batch, POST /v1/rag/delete, POST /v1/revoke,
POST /v1/revoke/bulk, and DELETE /v1/sessions. Requests without the header, and every read,
detect, or streaming route, are never deduplicated.
curl -X POST http://localhost:3000/v1/revoke \
-H "X-API-Key: your-key" \
-H "Idempotency-Key: 7f3d2c1e-revoke-julia" \
-H "Content-Type: application/json" \
-d '{ "entity_type": "email", "value": "julia@example.com" }'The contract:
- The key must be 1 to 255 printable ASCII characters; anything else is rejected with
400 GUARDAI_IDEMPOTENCY_KEY_INVALID. - The first request with a key executes normally. A retry with the same key, method, path, and body
within the retention window replays the stored response byte-identically, stamped with the response
header
Idempotency-Replayed: true. - Reusing a key with a different method, path, or body is a client bug and returns
409 GUARDAI_IDEMPOTENCY_KEY_REUSE; the request is never silently re-run. - A concurrent duplicate while the first attempt is still running returns
409 GUARDAI_IDEMPOTENCY_IN_PROGRESS. - Only 2xx responses are recorded. A failed attempt releases the key, so an identical retry may re-execute.
- If the mutation succeeded but its response was too large to cache for replay, a retry gets
409 GUARDAI_IDEMPOTENCY_RESULT_UNAVAILABLE: the operation completed, the result is not replayable, do not retry blindly. - If the store is at capacity the request is rejected with
503 GUARDAI_IDEMPOTENCY_STORE_FULLbefore the mutation runs, so it is safe to retry once the store has room.
Keys are partitioned by the authenticated tenant, so one caller's key can never collide with
another's. The store is configured under idempotency: backend (memory, the default, per
replica; or redis, shared across replicas via session.redis_url), ttl_seconds (how long a
completed response is replayed, default 86400), and in_progress_ttl_seconds (how long an abandoned
in-flight claim is held before it becomes reclaimable, default 60).
POST /v1/transform
Transform input text, replacing detected entities with semantic tokens.
Request
{
"input": "Contact Julia Schneider at julia@example.com",
"input_type": "text",
"policy": "default",
"language": "de",
"session_state": "(optional sealed blob from previous transform)",
"detectors": ["builtin_regex"],
"context": {
"destination": "external_llm",
"provider": "openai",
"caller_role": "support_agent",
"caller_purpose": "customer inquiry response"
},
"trace_id": "550e8400-e29b-41d4-a716-446655440000"
}| Field | Type | Required | Description |
|---|---|---|---|
input | string | Yes (for text) | Raw input text |
input_type | string | No | "text" (default), "json", or "chat_messages" |
input_json | any JSON value | For JSON mode | Structured JSON with field-level transformation. Accepts any JSON value: object, array, string, number, boolean, or null. |
input_messages | array | For chat mode | OpenAI-format message array; each message carries role plus content and optional tool fields (tool_calls, tool_call_id, name). See Chat input mode. |
language | string | No | ISO 639-1 hint (defaults to detector.default_language, English if unset; not auto-detected) |
policy | string | No | Policy name (uses server default if omitted) |
session_id | string | No | Existing session ID for multi-turn continuity |
session_state | string | No | Sealed session blob from previous transform |
detectors | string[] | No | Select detectors: ["builtin_regex"], ["python_ner"], or both. Defaults to server config. |
custom_patterns | array | No | Per-request custom regex detectors, each {entity_type, pattern, confidence?, context_words?}. Validated on use and not persisted; additive (they only add detections). |
protection_overrides | object | No | Per-entity-type restore-mode override: maps an entity type to one of full, partial, masked, formatted, abstract, none |
context | object | No | destination, provider, caller_role, caller_purpose. This object rejects unknown fields, so there is no model or max_tokens key. |
trace_id | string | No | Client-supplied correlation ID. The server generates a UUID v4 if omitted; pass the same value into the rehydrate call to correlate the full transform to LLM to rehydrate lifecycle in logs and audit. |
Exactly one input source is permitted: input (text), input_json, or input_messages.
Response
{
"safe_text": "Contact Julia Schneider at {{email:e_001:5873cc5722d8}}",
"session_id": "01916a3e-7b2c-7000-8000-000000000001",
"session_state": "eyJ2IjoxLCJzaWQiOi...",
"entity_context": [
{
"token": "{{email:e_001:5873cc5722d8}}",
"type": "email"
}
],
"entities": [
{
"token": "{{email:e_001:5873cc5722d8}}",
"type": "email",
"protection_level": "2",
"action_applied": "tokenize",
"confidence": 0.99,
"detector": "builtin_regex",
"span": { "start": 27, "end": 42 }
}
],
"stats": {
"entities_detected": 1,
"entities_transformed": 1,
"entities_blocked": 0,
"detection_time_ms": 1.2,
"transform_time_ms": 0.3
}
}| Field | Description |
|---|---|
safe_text | Transformed text with semantic tokens. Send this to the LLM. |
safe_json | Present only for JSON input: the tokenized document with the same shape as input_json. See JSON input mode. |
safe_messages | Present only for chat input: the tokenized message array with the same shape as input_messages. See Chat input mode. |
session_id | Session identifier |
session_state | Sealed session blob. Pass this to rehydrate. |
entity_context | Safe metadata for the LLM system prompt: token, type, and any policy-exposed gender, formality, language, role (for example "role": "patient"), or belongs_to. Never contains raw values. |
entities | Full diagnostic metadata. For debugging, NOT for the LLM. |
entity_summary | Optional map of entity type to count for this request. Omitted when not populated. |
stats | Performance metrics |
detector_mode | The detector mode used for this request: builtin_regex, builtin_and_ner, or python_ner. Explains why a given entity type may or may not have been detected. |
warnings | Advisories about degraded detection coverage, for example "NER sidecar not active". This is the honest signal that the "raw PII never reaches the model" property is scoped to detected entities: with the NER sidecar off, person, company, and location names are not detected and can pass through. |
policy_version | Version of the policy applied during this transform, for correlating results with a specific policy version. |
trace_id | UUID correlating this transform with its rehydrate call in logs and audit. Echoes the request trace_id when supplied, otherwise a server-generated value. |
Fields are omitted when not set. safe_json and safe_messages are covered under the JSON and chat
input modes below.
Entity metadata fields (gender, formality, language, role, belongs_to) appear only when the
policy exposes them (metadata_policy.expose_*) and a value was inferred. role is the policy-defined
entity role, for example labelling a person as the patient or the doctor (see the
extensibility guide); belongs_to links an entity to the person or entity
it belongs to (the relational side of the role taxonomy). Both are always declared labels, never raw values.
JSON input mode
Set input_type to "json" and pass the document as input_json. Any JSON value is accepted (an
object, an array, or a bare string, number, boolean, or null). Each string value and each
object key is detected and tokenized independently, and the response adds safe_json: the same
structure with PII replaced by {{type:id:cap}} tokens. safe_text is the serialized safe_json, so
you can send either to the LLM. The same value in two fields receives the same token id, and one
sealed session_state rehydrates the whole document.
Request:
{
"input_type": "json",
"input_json": {
"customer": "Julia Schneider",
"email": "julia@example.com",
"card": 4539148803436467,
"notes": "VIP account"
}
}Response:
{
"safe_json": {
"customer": "{{person:p_001:9f2c4a7b1e8d}}",
"email": "{{email:e_001:5873cc5722d8}}",
"card": "{{credit_card:cc_001:7d1a3f5b9c2e}}",
"notes": "VIP account"
},
"safe_text": "{\"customer\":\"{{person:p_001:9f2c4a7b1e8d}}\",\"email\":\"{{email:e_001:5873cc5722d8}}\",\"card\":\"{{credit_card:cc_001:7d1a3f5b9c2e}}\",\"notes\":\"VIP account\"}",
"session_id": "01916a3e-7b2c-7000-8000-000000000001",
"session_state": "eyJ2IjoxLCJzaWQiOi..."
}Notes:
- The structure is always preserved: same keys, same array lengths, same nesting. A structural mismatch fails closed (the request errors) rather than returning a partially tokenized object.
- A numeric leaf that is itself PII (for example a credit-card number sent as a JSON number) is
tokenized to a string token, so its type changes from number to string in
safe_json. A number with no detected PII is returned unchanged as a number. Booleans andnullalways pass through. - Object keys are detected too: a key that is itself PII (for example an object keyed by an email address) is tokenized or redacted; non-PII keys pass through unchanged.
- Limits: a JSON document may not exceed 64 levels of nesting, 10,000 scannable slots, or 1 MB of total scanned text. Exceeding any limit fails closed.
Chat input mode
Set input_type to "chat_messages" and pass an OpenAI-format message array as input_messages.
The whole conversation is protected in one call: every string-bearing field of every message
(content, multimodal text parts, tool-call arguments) is detected and tokenized, and the response
adds safe_messages with the same shape. The same value across messages receives the same token id,
and one sealed session_state rehydrates the whole conversation. safe_text is the serialized
safe_messages.
Request:
{
"input_type": "chat_messages",
"input_messages": [
{ "role": "system", "content": "You are a helpful assistant." },
{ "role": "user", "content": "My email is julia@example.com" }
]
}Response:
{
"safe_messages": [
{ "role": "system", "content": "You are a helpful assistant." },
{ "role": "user", "content": "My email is {{email:e_001:5873cc5722d8}}" }
],
"session_id": "01916a3e-7b2c-7000-8000-000000000001",
"session_state": "eyJ2IjoxLCJzaWQiOi..."
}Notes:
- Each message must be an object with a non-empty string
roleand acontentthat is a string, a content-part array, or null. Onlyrole,content,name,tool_calls, andtool_call_idare supported; a message carrying any other text-bearing field is rejected so raw data cannot ride through an unscanned field. - Tool-call
argumentsis a JSON string and is protected structurally: it is parsed, its values are tokenized, and it is re-serialized as valid JSON. A non-JSONargumentsstring is rejected. - Non-PII fields pass through unchanged, so a static system prompt is returned as sent.
POST /v1/rehydrate
Restore semantic tokens in LLM output to their original values.
Request
{
"output": "Dear {{person:p_001:9f2c4a7b1e8d}}, your order {{order:o_001:4b8e2d6f0a1c}} has shipped.",
"session_state": "eyJ2IjoxLCJzaWQiOi...",
"output_channel": "customer_email",
"restore_mode": "formatted",
"restore_overrides": {
"e_001": "none"
}
}| Field | Type | Required | Description |
|---|---|---|---|
output | string | Yes | LLM output containing {{type:id:cap}} tokens as emitted by transform (a capless token does not resolve) |
session_id | string | No | Session ID from transform response |
session_state | string | Yes | Sealed session blob from transform response |
restore_mode | string | No | Default restore mode: full, partial, masked, formatted, abstract, none |
restore_overrides | object | No | Per-token overrides keyed by token ID |
output_channel | string | No | user_output, internal_summary, customer_email, tool_payload, export, log_safe |
trace_id | string | No | Correlation ID. Pass the trace_id from the transform response to correlate the full transform to LLM to rehydrate lifecycle in logs and audit. |
Response
{
"restored_text": "Dear Frau Julia Schneider, your order ORD-2026-4892 has shipped.",
"tokens_resolved": 2,
"tokens_unresolved": [],
"stats": {
"rehydrate_time_ms": 0.8
},
"trace_id": "550e8400-e29b-41d4-a716-446655440000"
}Restore Modes
| Mode | Behavior | Person Example | Email Example |
|---|---|---|---|
full | Complete original value | Julia Schneider | julia@example.com |
partial | Deterministic subset | J. Schneider | j***@example.com |
masked | Character masking | ******* ********* | j*******************e |
formatted | Original + contextual formatting | Frau Julia Schneider | julia@example.com (email) |
abstract | Semantic description | (female customer) | (email on file) |
none | Removed / redacted | [REDACTED] | [REDACTED] |
POST /v1/detect
Detect entities in text without performing transformation.
Request
{
"input": "Email: test@example.com, SSN: 123-45-6789",
"language": "en",
"detectors": ["builtin_regex", "python_ner"],
"entity_types": ["email", "ssn"],
"threshold": 0.5,
"policy": "healthcare",
"include_values": true
}| Field | Type | Required | Description |
|---|---|---|---|
input | string | Yes | Text to scan |
language | string | No | ISO 639-1 hint (defaults to detector.default_language, English if unset; not auto-detected) |
detectors | string[] | No | Select detectors: builtin_regex, python_ner, or both. Defaults to server config. |
custom_patterns | array | No | Per-request custom regex detectors ({entity_type, pattern, ...}), validated on use and not persisted. |
entity_types | string[] | No | Restrict the result to these entity types |
threshold | number | No | Minimum confidence to include an entity |
policy | string | No | When set, the policy's custom_patterns and detection control also apply, so detect-only sees the same domain entities a transform would (including per-policy detection.ner_labels, and required_for fail-closed). Omitted means raw built-in detection. The same field exists on POST /v1/batch/detect. |
include_values | boolean | No | Whether each entity carries its raw value. Returning raw values requires the detect_values scope. false always suppresses them (empty string). true requires the detect_values scope, else the request fails with 403. Omitted resolves to the server's detect.include_values_default, which itself defaults to true only under dev auth; in production the raw value stays suppressed unless the caller holds detect_values. The same flag and gating apply to POST /v1/batch/detect. |
Response
{
"entities": [
{
"type": "email",
"value": "test@example.com",
"span": { "start": 7, "end": 23 },
"confidence": 1.0,
"detector": "builtin_regex"
},
{
"type": "ssn",
"value": "123-45-6789",
"span": { "start": 30, "end": 41 },
"confidence": 1.0,
"detector": "builtin_regex"
}
],
"stats": {
"detection_time_ms": 0.9,
"detectors_used": ["builtin_regex"],
"detector_mode": "builtin_regex"
},
"warnings": ["NER sidecar not active"]
}Each detected entity may also carry metadata (gender, formality, language, role, belongs_to)
when a policy exposes it. stats.detector_mode reports the mode used (builtin_regex, builtin_and_ner,
or python_ner). Top-level warnings carries the same honest degraded-coverage advisory as transform,
for example when the NER sidecar is not active. All three are omitted when empty.
POST /v1/evaluate-policy
Evaluate a policy against entities without performing transformation or creating a session. Useful for previewing what actions would be taken. The optional output_channel (default user_output) previews the channel-resolved restore_mode each entity would get, clamped by the safe-sink ceiling exactly as rehydrate does.
Request
{
"policy": "strict-pii",
"entities": [
{ "type": "person", "value": "Julia Schneider" },
{ "type": "ssn", "value": "123-45-6789" }
],
"output_channel": "customer_email"
}Response
{
"decisions": [
{
"entity_type": "person",
"action": "tokenize",
"protection_level": "1",
"rule_matched": "strict-pii/person",
"restore_mode": "masked",
"reason": "Level 1 entity, masked restore for customer_email channel"
},
{
"entity_type": "ssn",
"action": "redact",
"protection_level": "1",
"rule_matched": "strict-pii/ssn",
"restore_mode": "none"
}
],
"policy_version": "1.0.0",
"warnings": ["output_channel filtering previewed; restore_mode is channel-resolved"]
}Each decision may include an optional reason explaining the resolved action or restore mode. The
response may also carry top-level warnings advising about unsupported or unimplemented request fields.
Both are omitted when empty.
POST /v1/batch/transform
Transform many inputs in one request. Requires the batch scope. Accepts up to max_batch_size items
(see capabilities).
Request
{
"session_mode": "independent",
"policy": "default",
"items": [
{ "text": "Hello Julia Schneider", "language": "de" },
{ "text": "Contact julia@example.com", "policy": "gdpr-strict" },
{ "input_type": "json", "input_json": { "email": "julia@example.com" } }
]
}| Field | Type | Required | Description |
|---|---|---|---|
items | array | Yes | The inputs to transform. Each item carries its own input source and optional overrides. |
session_mode | string | No | independent (default): each item gets its own session. shared: all items share one session, so the token map carries forward across items. |
policy | string | No | Policy applied to every item; an item's own policy overrides it. |
Each item (BatchTransformItem) supports:
| Field | Type | Required | Description |
|---|---|---|---|
text | string | For text items | Plain-text input |
input_type | string | No | text (default), json, or chat_messages; a batch can mix item types |
input_json | any JSON value | For JSON items | Structured JSON input (object, array, string, number, boolean, or null) |
input_messages | array | For chat items | OpenAI-format message array |
policy | string | No | Per-item policy override |
language | string | No | Per-item language hint (ISO 639-1). Language is set per item, not once for the whole batch. |
Response
{
"results": [
{
"index": 0,
"safe_text": "Hello {{person:p_001:a1b2c3d4e5f6}}",
"session_state": "eyJ2IjoxLCJ...",
"entities_detected": 1,
"entities_transformed": 1
},
{
"index": 2,
"safe_text": "{\"email\":\"{{email:e_001:0f9e8d7c6b5a}}\"}",
"safe_json": { "email": "{{email:e_001:0f9e8d7c6b5a}}" },
"entities_detected": 1,
"entities_transformed": 1
}
],
"stats": { "total_items": 3, "total_entities": 2, "total_time_ms": 15.5 }
}Each result carries its index (matching the request order), safe_text, and entities_detected /
entities_transformed. A JSON item also returns safe_json; a chat item returns safe_messages. If a
single item fails, its result carries an error string and an empty safe_text while the rest of the
batch still processes. stats aggregates total_items, total_entities, and total_time_ms.
Batch works on every session backend. With the default sealed backend each result carries a
session_state blob; with a server-side backend (memory or redis) each result instead carries a
session_id, and the token map lives server-side. In shared mode only the last successful result
carries the combined session_state or session_id for the whole batch.
POST /v1/batch/detect
Detect entities across many inputs in one request. Requires the batch scope.
Request
{
"policy": "default",
"include_values": false,
"items": [
{ "text": "Julia Schneider works at Firma GmbH", "language": "de", "policy": "german-support" },
{ "text": "Contact julia@example.com" }
]
}| Field | Type | Required | Description |
|---|---|---|---|
items | array | Yes | Each item has text (required), optional language, and optional per-item policy. |
include_values | boolean | No | Whether each result carries the raw detected value. Raw values require the detect_values scope: false always suppresses them, true requires the scope (403 without it), and an omitted value resolves to detect.include_values_default (which defaults to true only under dev auth). In production the raw value stays suppressed unless the caller holds detect_values. |
policy | string | No | Policy applied to every item; an item's own policy overrides it. |
Response
{
"results": [
{
"index": 0,
"entities": [
{ "type": "person", "value": "", "span": { "start": 0, "end": 15 }, "confidence": 0.95, "detector": "python_ner" }
]
}
],
"stats": { "total_items": 2, "total_entities": 1, "total_time_ms": 3.2 }
}The value fields are empty strings because the request set include_values: false. That is also
what an api-key caller sees by default in production: the raw detected value comes back only when the
resolved include_values is true, which requires the detect_values scope. Each result carries its
index and its detected entities (the same shape as /v1/detect). Per-item warnings mirror the
single-item detect advisory (for example, the NER sidecar did not run), so a batch never hides degraded
coverage. A failed item carries an error string and empty entities.
POST /v1/rag/ingest
Chunk a document and tokenize each chunk for safe vector-store ingestion. Requires the rag scope.
The five RAG endpoints share one identity concept: corpus_id. When you pass the same corpus_id on
ingest, query, and context, the same raw value tokenizes to the same token id across every document and
query in that corpus, which is what lets a query entity align with an ingested document entity. Absent
corpus_id, ids are session-scoped counters with random caps and cross-document alignment does not hold.
Request
{
"text": "James Rodriguez opened case CR-4821 about his account.",
"chunking_strategy": "paragraph",
"chunk_size": 1000,
"chunk_overlap": 100,
"policy": "default",
"language": "en",
"corpus_id": "support-kb-2026",
"document_metadata": { "document_id": "doc-001", "classification": "internal", "owner": "admin", "tags": ["support"] }
}| Field | Type | Required | Description |
|---|---|---|---|
text | string | Yes | Document text to chunk and transform |
chunking_strategy | string | No | paragraph (default), sentence, fixed, or sliding_window |
chunk_size | number | No | Max chunk size in characters (for fixed / sliding_window) |
chunk_overlap | number | No | Overlap in characters (for sliding_window) |
policy | string | No | Policy for transformation |
language | string | No | ISO 639-1 detection hint |
document_metadata | object | No | document_id, classification, owner, tags for access control and filtering |
corpus_id | string | No | Deterministic cross-document token identity (see above) |
Response
{
"chunks": [
{
"chunk_index": 0,
"safe_text": "{{person:p_8a4f2c9d1e6b3a7f5c0d2e8b:1f3e5d7c9b2a4e6f8d0c1b3a5e7f9d2c}} opened case CR-4821 about his account.",
"entity_count": 1,
"start_offset": 0,
"end_offset": 54,
"chunk_session_state": "eyJ2Ijox...chunk0"
}
],
"session_state": "eyJ2IjoxLCJz...",
"total_entities": 1
}Because this request passed a corpus_id, the token id and cap are corpus-derived (a 24-hex id suffix
and a 32-hex cap) rather than the shorter p_001 per-request form; the same person yields the same
token in every document and query of this corpus. CR-4821 stays raw here: a domain reference like a
case number is tokenized only when the policy defines a matching custom_patterns entry (custom types
render as {{custom:<name>:x_...:cap}} tokens), and detecting the person name requires the NER
detector. Each chunk returns safe_text (store this in the vector DB) plus a per-chunk
chunk_session_state, the sealed blob that holds only that chunk's tokens. Carry those back later as
document_sessions on /v1/rag/context. The top-level session_state is shared across chunks, and
warnings may advise that the NER sidecar did not run for a chunk (so undetected
person/company/location may remain).
POST /v1/rag/ingest/batch
Ingest multiple documents in one request. Requires the rag scope. Each item is a full
/v1/rag/ingest request body and is processed exactly like a single ingest, yielding its own chunks
and its own sealed session, so documents never share a token map. The batch accepts up to
max_batch_size items, and the rate limiter charges one token per document, not one per request. One
item's failure never fails the batch.
Request
{
"items": [
{ "text": "First support case ...", "chunking_strategy": "paragraph", "corpus_id": "support-kb-2026" },
{ "text": "Second support case ...", "corpus_id": "support-kb-2026" }
]
}Response
{
"results": [
{ "index": 0, "result": { "chunks": ["..."], "session_state": "eyJ2Ijox...", "total_entities": 2 } },
{ "index": 1, "error": "chunk_size must be at least 100" }
]
}Each result carries its index (matching the request order) and exactly one of result (the same
shape as a single /v1/rag/ingest response for that document) or error (a per-item failure message;
the other items are unaffected).
POST /v1/rag/query
Tokenize a user query before vector search. Requires the rag scope.
Request
{
"query": "What is James Rodriguez's account status?",
"policy": "default",
"language": "en",
"corpus_id": "support-kb-2026"
}Pass the same corpus_id used at ingest so the query's person token derives the same corpus-scoped id
and cap as the document's.
Response
{
"safe_query": "What is {{person:p_8a4f2c9d1e6b3a7f5c0d2e8b:1f3e5d7c9b2a4e6f8d0c1b3a5e7f9d2c}}'s account status?",
"session_state": "eyJ2IjoxLCJxdWVye...",
"entities_detected": 1
}Send safe_query to your vector store; carry session_state into the following /v1/rag/context call.
POST /v1/rag/context
Tokenize the retrieved chunks and merge them into the query session. Requires the rag scope.
Request
{
"chunks": ["Raw retrieved chunk about James Rodriguez ..."],
"session_state": "eyJ2IjoxLCJxdWVye...",
"policy": "default",
"language": "en",
"access_level": "internal",
"chunk_classifications": ["internal"],
"document_sessions": ["eyJ2Ijox...chunk0"],
"corpus_id": "support-kb-2026"
}| Field | Type | Required | Description |
|---|---|---|---|
chunks | string[] | Yes | Retrieved chunks (raw text) |
session_state | string | Yes | Session from the preceding /v1/rag/query |
policy | string | No | Policy for transformation |
language | string | No | ISO 639-1 detection hint |
access_level | string | No | Max classification to admit. Chunks above this level are dropped. Levels are the deployment's rag.classification_levels lattice (default public < internal < confidential < restricted); an unknown label ranks most-restrictive (fail closed). |
chunk_classifications | string[] | No | Per-chunk classification labels, parallel to chunks. A missing entry is treated as most-restrictive when filtering is active. |
document_sessions | string[] | No | Per-chunk chunk_session_state blobs from ingest, parallel to chunks. When set, its length must equal chunks; each admitted chunk's tokens merge into the context. |
corpus_id | string | No | Must match the query session's corpus, or the request is rejected (fail closed), so a caller cannot mix identity domains across query and context. |
Response
{
"safe_chunks": ["{{person:p_8a4f2c9d1e6b3a7f5c0d2e8b:1f3e5d7c9b2a4e6f8d0c1b3a5e7f9d2c}} ..."],
"session_state": "eyJ2IjoxLCJjdHh0...",
"entities_detected": 3,
"chunks_filtered": 0
}safe_chunks are the tokenized chunks for the LLM prompt (filtered chunks are omitted).
chunks_filtered counts how many were dropped by access_level (omitted when zero).
POST /v1/rag/answer
Rehydrate the LLM's answer using the accumulated RAG session. Requires the rag scope.
Request
{
"answer": "The account for {{person:p_8a4f2c9d1e6b3a7f5c0d2e8b:1f3e5d7c9b2a4e6f8d0c1b3a5e7f9d2c}} is active.",
"session_state": "eyJ2IjoxLCJjdHh0...",
"output_channel": "user_output"
}output_channel (default user_output) selects the restore rules, exactly as /v1/rehydrate.
Response
{
"restored_answer": "The account for James Rodriguez is active.",
"tokens_resolved": 1,
"tokens_unresolved": []
}POST /v1/rag/delete
Forget every value in a session, so any later rehydrate resolves it to [DELETED]. Requires the revoke
scope. To erase one document, pass that document's ingest session_state.
Request
{ "session_state": "eyJ2IjoxLCJp..." }Response
{
"deleted": true,
"session_invalidated": true,
"entities_purged": 5,
"message": "Session a1b2c3d4e5f60718 invalidated (5 entities). Application should delete corresponding vector store chunks.",
"manifest": {
"session_fingerprint": "a1b2c3d4e5f60718",
"entities_revoked": 5,
"timestamp_unix": 1700000000,
"status": "completed"
}
}The manifest is a PII-free deletion receipt for a GDPR / audit trail. It carries no raw session id,
token, or value: the erased corpus/document is identified only by the one-way session_fingerprint (the
same fingerprint the audit log records), entities_revoked is the number of values revoked from the store
(0 for an empty corpus, which is still a completed success), and timestamp_unix is when the deletion
was recorded. status is completed.
POST /v1/transform/image
Extract text from an uploaded image with OCR, then tokenize any detected PII. Requires the transform
scope. The official server Docker image bundles Tesseract with English, German, and Arabic language data,
so this endpoint works out of the box in that image. From-source builds need Tesseract installed on the
host. When Tesseract is absent, the endpoint fails closed with 503 GUARDAI_OCR_UNAVAILABLE while the
text and JSON endpoints keep working.
Request
Multipart form (Content-Type: multipart/form-data):
| Field | Required | Description |
|---|---|---|
image | Yes | Image file (PNG, JPEG, TIFF, BMP) |
language | No | OCR language hint |
curl -H "X-API-Key: your-key" \
-F "image=@scan.png" -F "language=de" \
http://localhost:3000/v1/transform/imageResponse
{
"safe_text": "Rechnung an {{person:p_001:9f2c4a7b1e8d}}, {{email:e_001:5873cc5722d8}}",
"entities": [
{
"entity_type": "person",
"token": "{{person:p_001:9f2c4a7b1e8d}}",
"bounding_box": { "left": 120, "top": 48, "width": 180, "height": 22 },
"confidence": 0.94
}
],
"bounding_boxes": [ { "left": 120, "top": 48, "width": 180, "height": 22 } ],
"session_id": "01916a3e-7b2c-7000-8000-000000000001",
"session_state": "eyJ2IjoxLCJ..."
}safe_text is the OCR text with tokens in place. Each entities item maps a detected token to its
bounding_box (pixel coordinates) in the image; bounding_boxes is the flat list of those regions.
warnings carries the same NER-sidecar advisory as text transform.
POST /v1/redact/image
Return the uploaded image with PII regions blacked out. Requires the transform scope. Redaction blacks
out every detected PII region regardless of policy action (a policy allow is still visually redacted),
and it drops the policy's min_confidence floor so it does not under-redact. It honors the tenant policy's
custom_patterns and required_for (fail closed if a required NER detector is down). Same
503 GUARDAI_OCR_UNAVAILABLE behavior when Tesseract is absent.
Request
Multipart form with an image file (PNG, JPEG, TIFF, BMP).
curl -H "X-API-Key: your-key" \
-F "image=@scan.png" \
http://localhost:3000/v1/redact/image --output redacted.pngResponse
200 OK with Content-Type: image/png: the raw redacted image bytes (not JSON).
POST /v1/transform/stream and POST /v1/rehydrate/stream
Stream the result as Server-Sent Events. transform/stream requires the transform scope,
rehydrate/stream the rehydrate scope. Both run the full pipeline first, then stream the finished text
in token-boundary-aware chunks: transform streams safe_text, rehydrate streams restored_text. Request
bodies are the same JSON as /v1/transform and /v1/rehydrate.
Each SSE data: frame is a JSON object. Content frames carry { "chunk": "...", "complete": false }. The
final frame carries "complete": true plus the session metadata: transform adds session_state,
session_id, and stats; rehydrate adds tokens_resolved, tokens_unresolved, and stats. If a
detection error occurs mid-stream, a single frame { "error": "...", "complete": true } is emitted and the
connection closes; partial results are not recoverable.
POST /v1/transform/file and POST /v1/rehydrate/file
Process an uploaded document. transform/file requires the transform scope, rehydrate/file the
rehydrate scope.
transform/file takes a multipart form with file (required), plus optional policy and language
fields. Supported extensions are pdf, txt, csv, html, md, and docx. It extracts the text and
runs the standard transform, returning JSON:
{
"safe_text": "...tokenized document text...",
"entities": [ /* diagnostic entity list */ ],
"session_id": "01916a3e-...",
"session_state": "eyJ2IjoxLCJ...",
"original_format": "Pdf",
"original_size": 84213,
"stats": { "entities_detected": 3, "entities_transformed": 3 }
}rehydrate/file delegates to the standard text rehydrate; send the RehydrateRequest JSON body (it does
not take a file). session_state is required.
POST /v1/revoke, POST /v1/revoke/bulk, GET /v1/revocations/count
Revoke entity values so any later rehydrate resolves them to [DELETED]. Only an HMAC-SHA-256 digest of
each value is stored; no raw PII is persisted. /v1/revoke and /v1/revoke/bulk require the revoke
scope; /v1/revocations/count requires the admin scope and returns a scope-local count (the caller's
own tenant, or the unscoped partition for a platform admin), never a cross-tenant total.
POST /v1/revoke
{ "entity_type": "email", "value": "julia@example.com" }Response:
{
"revoked": true,
"entity_type": "email",
"total_revoked": 1,
"message": "Entity value revoked. Future rehydration will return [DELETED] for this value."
}total_revoked (the global total) is present only for unscoped platform admins; it is omitted for a
tenant-scoped caller so the cross-tenant revocation volume is not disclosed.
POST /v1/revoke/bulk
{
"entities": [
{ "entity_type": "email", "value": "julia@example.com" },
{ "entity_type": "person", "value": "Julia Schneider" }
]
}Response: { "revoked_count": 2, "total_revoked": 5 } (again, total_revoked is admin-only).
GET /v1/revocations/count
Returns { "total_revoked": 42 }, where the value is the number of revocations in the caller's own
scope. A tenant-authenticated caller sees only its own tenant's revocations; an unscoped platform-admin
caller sees only the unscoped (global-scope) revocations. The store partitions digests by tenant, so one
tenant can never observe another tenant's revocation volume through this count. Requires the admin scope.
POST /v1/sessions/status, DELETE /v1/sessions
Inspect or invalidate a session by its lifecycle. Both require the session scope. The session is
identified in the JSON body (never the URL): pass exactly one of session_id or the sealed
session_state blob, so a bearer credential never lands in an access log. A missing or cross-tenant
session is a non-disclosing 404 GUARDAI_SESSION_NOT_FOUND, so a caller cannot probe another tenant's
session ids.
POST /v1/sessions/status
Reports whether a session is live and, when it is, its backend, expiry, and entity count.
{ "session_state": "eyJ2IjoxLCJz..." }Response:
{
"status": "active",
"backend": "sealed",
"expires_at": 1700003600,
"entity_count": 3
}status is one of active, stateless, deleted, or not_found. backend is the active session
backend (sealed, memory, or redis). expires_at (Unix seconds) and entity_count are present
only for a live session. On the sealed backend, where the caller holds the only copy of the state, the
status is stateless.
DELETE /v1/sessions
Removes a server-held session (memory or redis backend). Mapped entity values are not revoked here;
use /v1/revoke or /v1/rag/delete for value erasure.
{ "session_id": "01916a3e-7b2c-7000-8000-000000000001" }Response:
{ "deleted": true, "status": "deleted" }deleted is true only when a server-held session was actually removed. The sealed backend has no
server-side state to remove, so it returns { "deleted": false, "status": "stateless" }.
GET /v1/health
Returns runtime health status. Requires auth when auth is enabled. Use /livez or /readyz for unauthenticated probes.
{
"status": "healthy",
"version": "0.1.0",
"uptime_seconds": 3600.5,
"components": {
"detector": { "status": "healthy" },
"session": { "status": "healthy" },
"policies": { "status": "healthy" }
}
}Status values: healthy, degraded, unhealthy.
GET /v1/capabilities
Returns supported entity types, languages, available detectors, restore modes, and the batch size limit. Requires auth when auth is enabled.
{
"entity_types": [
{ "name": "person", "protection_level": "2", "source": "ner", "id_prefix": "p_" },
{ "name": "email", "protection_level": "2", "source": "builtin", "id_prefix": "e_" },
{ "name": "ssn", "protection_level": "1", "source": "builtin", "id_prefix": "ss_" }
],
"languages": [
{ "code": "de", "name": "German", "detection_support": "full", "rehydration_support": "full" }
],
"detectors": [
{ "name": "builtin_regex", "type": "builtin", "entity_types": ["email", "phone", "ssn"] }
],
"restore_modes": ["full", "partial", "masked", "formatted", "abstract", "none"],
"ner_active": true,
"max_batch_size": 100
}max_batch_size is the maximum number of items accepted by /v1/batch/transform and
/v1/batch/detect in a single request (a ceiling of 100, lowerable via limits.max_batch_size).
Each entity_types entry also carries a source field that tells the fixed built-in floor apart from
the policy or config-extensible surface: builtin (a universal-PII-format regex detector), builtin_custom
(a built-in custom detector such as money or de_tax_id), ner (detected by the NER model, for example
person, company, location), config_custom (added by this deployment's detector config), or policy_custom
(defined by a loaded policy's custom_patterns). An optional description may accompany a type, and each
language may report rehydration_support alongside detection_support. ner_active reflects the configured
mode, not live sidecar reachability; monitor GUARDAI_DETECTION_FAILED for sidecar outages.
Error Codes
All error responses follow this format:
{
"error": "Human-readable description",
"code": "ERROR_CODE",
"details": "optional additional context",
"request_id": "550e8400-e29b-41d4-a716-446655440000"
}Every error response carries a request_id (a fresh UUID generated per error), so a caller can correlate
a failure with the server logs. details and request_id are omitted when null.
| Code | HTTP Status | Description |
|---|---|---|
GUARDAI_INVALID_INPUT | 400 | Malformed request, missing fields, type mismatch |
GUARDAI_BAD_REQUEST | 400 | Generic bad request (e.g., missing field) |
GUARDAI_UNAUTHORIZED | 401 | Missing or invalid credentials |
GUARDAI_FORBIDDEN | 403 | Credentials valid but insufficient scope |
GUARDAI_AUTH_FAILED | 401 | Authentication is enabled but no credentials provided |
GUARDAI_INVALID_TOKEN | 401 | JWT or session token is malformed or expired |
GUARDAI_POLICY_DENIED | 403 | A redacted entity's rule sets on_redact: reject. Routine redaction returns 200 with the value redacted; this code is the opt-in hard-fail. |
GUARDAI_SESSION_EXPIRED | 410 | Session TTL has elapsed, sealed blob failed integrity check, or envelope version unsupported |
GUARDAI_SESSION_REPLAY | 409 | A transform continuation was replayed: its sealed request counter did not advance past the highest seen for that session |
GUARDAI_IDEMPOTENCY_KEY_INVALID | 400 | The Idempotency-Key header is not 1 to 255 printable ASCII characters |
GUARDAI_IDEMPOTENCY_KEY_REUSE | 409 | An Idempotency-Key was reused with a different method, path, or body |
GUARDAI_IDEMPOTENCY_IN_PROGRESS | 409 | A request with this Idempotency-Key is still in progress |
GUARDAI_IDEMPOTENCY_RESULT_UNAVAILABLE | 409 | The idempotent operation completed but its response was too large to cache for replay; do not retry blindly |
GUARDAI_IDEMPOTENCY_STORE_FULL | 503 | The idempotency store is at capacity; the mutation did not run and may be retried |
GUARDAI_REPLAY_STORE_UNAVAILABLE | 503 | The replay store could not be reached; the continuation is rejected fail-closed rather than risk accepting a replay |
GUARDAI_OUTPUT_BLOCKED | 422 | Output guard blocked the response due to hallucinated PII |
GUARDAI_TOKEN_REPAIR_FAILED | 422 | Token repair could not resolve tokens in LLM output |
GUARDAI_DETECTION_FAILED | 500 | Detector service is down or detection failed |
GUARDAI_RATE_LIMITED | 429 | Request rate exceeds configured limit |
GUARDAI_PAYLOAD_TOO_LARGE | 413 | Request payload exceeds configured size limit |
GUARDAI_AUDIT_UNAVAILABLE | 503 | Audit log unavailable; request rejected fail-closed (applies when audit.strict is on) |
GUARDAI_OCR_UNAVAILABLE | 503 | Tesseract OCR is not installed |
GUARDAI_OCR_FAILED | 500 | OCR processing failed |
GUARDAI_REDACTION_FAILED | 500 | Image redaction failed |
GUARDAI_REVOCATION_LOCK_POISONED | 500 | Revocation lock failure |
GUARDAI_REVOCATION_PERSIST_FAILED | 500 | Revocation persistence failed (rolled back) |
GUARDAI_TENANT_SCOPED | 403 | Operation requires tenant scope |
GUARDAI_INTERNAL_ERROR | 500 | Unexpected server error |
For full JSON schemas, see the product specification.
Multi-Tenant SaaS
How a SaaS platform serves three customers with different data protection policies using a single OGuardAI deployment
API Explorer
An interactive-style overview of every OGuardAI v1 endpoint, with method, purpose, required scope, and a copy-paste curl example built from the real request shape.