OGuardAI
API Reference

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.

This page is a hands-on catalog of the OGuardAI HTTP API. Every endpoint below exists in the running server. Each entry gives the method, its purpose, the auth scope it requires, and a curl example that matches the real request body. Copy a call, set your key, and run it against a live server.

For field-by-field request and response definitions, see the API Reference. This page is the fast path: find the endpoint, run the curl, read the response.

Before you start

  • Base URL. The server listens on http://localhost:3000 by default (configurable via server.host / server.port). The Docker quick start (deploy/docker/docker-compose.yml) also publishes it on 3000; the two-instance compose stack (deploy/docker/docker-compose.ha.yml) fronts it with nginx on 8080 instead. Replace the host and port in the examples with your deployment.
  • Content type. JSON endpoints expect and return application/json. The transform-file and image endpoints use multipart/form-data. /v1/rehydrate/file is the exception: it is a JSON alias of /v1/rehydrate and takes a RehydrateRequest body, not an upload.
  • Tokens. OGuardAI replaces detected values with semantic tokens. The emitted form is always three segments, {{type:id:cap}} (for example {{email:e_001:5873cc5722d8ff00ee11dd22cc33bb44}}): a lowercase type, an id stable within a session, and a per-token capability cap. Every transform emits the cap, RAG or not; it is matched exactly at rehydrate and fails closed, so a capless {{type:id}} token or a guessed id never resolves. RAG calls that pass a corpus_id derive a deterministic corpus-scoped id and cap (a 24-hex id suffix and a 32-hex cap) so the same value maps to the same token across documents and queries; other calls use a per-request prefix_NNN id with a 32-hex random cap. The curl examples below show the full form, because only that form round-trips.
  • Session state. transform returns a sealed session_state blob. Carry it into rehydrate (and into the RAG context and answer calls) so restoration is deterministic. It holds the token map, not raw text you need to persist elsewhere.
  • Retries. The mutating, non-streaming endpoints (the full list is in the reference) accept an optional Idempotency-Key header so a network retry never executes the same mutation twice. See Idempotent retries in the reference.

Authentication

When auth is enabled, send either an API key or a JWT. When auth is disabled (local dev), the header is optional.

# API key
curl -H "X-API-Key: $GUARDAI_API_KEY" http://localhost:3000/v1/health

# JWT bearer token
curl -H "Authorization: Bearer eyJhbG..." http://localhost:3000/v1/health

Each protected endpoint requires a specific scope (transform, rehydrate, detect, policy, batch, rag, revoke, global_revoke, session, admin), and the detect_values scope additionally authorizes returning raw detected values from /v1/detect and /v1/batch/detect. revoke erases within the caller's tenant; an untenanted caller needs global_revoke (which admin implies) for a cross-tenant revocation. The scope column in the catalog below tells you which one. The admin scope implies full access. See Authentication for how scopes bind to keys and tokens.

Is there an OpenAPI spec to load into Swagger UI?

Yes. The repository ships a single OpenAPI 3.1 document at schemas/openapi.yaml. It is the canonical description of every route the server mounts, and apps/server/tests/openapi_contract.rs enforces that the paths it lists and the mounted router never drift. Point Swagger UI, Redoc, or any OpenAPI import at that file.

The repository also ships per-endpoint JSON Schema (draft 2020-12) files under schemas/api/ for validating request and response bodies in your own client, or for driving a schema-aware editor:

schemas/api/transform-request.json
schemas/api/transform-response.json
schemas/api/rehydrate-request.json
schemas/api/rehydrate-response.json
schemas/api/detect-request.json
schemas/api/detect-response.json
schemas/api/session-status-request.json
schemas/api/session-status-response.json
schemas/api/session-delete-request.json
schemas/api/session-delete-response.json
schemas/api/capabilities-response.json
schemas/api/health-response.json
schemas/api/error-response.json

For endpoints without a dedicated JSON Schema file (batch, RAG, revocation, evaluate-policy, streaming, file, image), schemas/openapi.yaml plus the curl examples on this page and the API Reference are the contract. Everything below is the interactive reference: the curl call is the "try it" button.

Endpoint catalog

MethodPathPurposeScope
POST/v1/transformReplace detected entities with semantic tokenstransform
POST/v1/rehydrateRestore tokens in LLM output to real valuesrehydrate
POST/v1/rehydrate/metadataSame restore, returning only counts and unresolved ids (never the restored text)rehydrate
POST/v1/detectDetect entities without transformingdetect
POST/v1/evaluate-policyDry-run a policy against a list of entitiespolicy
POST/v1/transform/streamStreaming transform (SSE)transform
POST/v1/rehydrate/streamStreaming rehydrate (SSE)rehydrate
POST/v1/batch/transformTransform many inputs in one requestbatch
POST/v1/batch/detectDetect entities across many inputsbatch
POST/v1/rag/ingestChunk and tokenize a document for a vector storerag
POST/v1/rag/ingest/batchIngest multiple documents in one requestrag
POST/v1/rag/queryTokenize a user query for retrievalrag
POST/v1/rag/contextTokenize retrieved chunks for the promptrag
POST/v1/rag/answerRehydrate an LLM answer built from RAG contextrag
POST/v1/rag/answer/metadataSame restore, returning only counts and unresolved ids (never the restored answer)rag
POST/v1/rag/deleteRevoke every value in a RAG session (erasure)revoke
POST/v1/revokeRevoke one entity value (future rehydrate returns [DELETED])revoke
POST/v1/revoke/bulkRevoke many entity values at oncerevoke
GET/v1/revocations/countScope-local count of revoked entitiesadmin
POST/v1/sessions/statusReport a session's lifecycle statesession
DELETE/v1/sessionsInvalidate a server-held sessionsession
POST/v1/transform/fileTransform an uploaded file (multipart)transform
POST/v1/rehydrate/fileRestore a file transform's tokenized output; a JSON alias of /v1/rehydrate (no file upload)rehydrate
POST/v1/transform/imageTokenize text detected in an image (multipart, OCR)transform
POST/v1/redact/imageReturn the image with PII regions blacked out (multipart, OCR)transform
POST/v1/entity-typesValidate a custom entity-type name (nothing persisted)transform
POST/v1/patternsValidate a custom regex detection pattern (nothing persisted)transform
GET/v1/healthHealth status, component breakdown, and the resolved licenceany authed
GET/v1/capabilitiesSupported entity types, languages, detectors, restore modesany authed
GET/v1/diagnosticsRuntime diagnostics (detector mode, policies, config)admin
GET/metricsPrometheus metricsadmin
POST/v1/admin/policy/validateValidate policy YAML in a directoryadmin
POST/v1/admin/policy/reloadHot-reload the policy directory (atomic, fail-closed)admin
GET/livezLiveness probepublic
GET/readyzReadiness probepublic

Quick round trip

The core loop is protect, then generate, then restore. Two calls of yours wrap one call to your LLM.

# 1. Protect: raw text in, tokenized safe_text plus session_state out
curl -sS http://localhost:3000/v1/transform \
  -H "X-API-Key: $GUARDAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "input": "Bitte antworten Sie Frau Erika Mustermann unter erika.mustermann@firma.example.",
    "language": "de",
    "policy": "german-support",
    "context": { "destination": "external_llm" }
  }'

# The response carries safe_text (send this to your LLM) and session_state.
# 2. Your LLM generates a reply that still contains the tokens.
# 3. Restore: LLM output plus session_state in, restored_text out
curl -sS http://localhost:3000/v1/rehydrate \
  -H "X-API-Key: $GUARDAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "output": "Sehr geehrte {{person:p_001:9f2c4a7b1e8d00112233445566778899}}, wir melden uns unter {{email:e_001:5873cc5722d8ff00ee11dd22cc33bb44}}.",
    "session_state": "<paste session_state from step 1>",
    "restore_mode": "full",
    "output_channel": "customer_email"
  }'

The request field is input on transform but output on rehydrate, because rehydrate's input is the LLM's output. The tokenized field is safe_text after transform and restored_text after rehydrate.

Core endpoints

POST /v1/transform

Detects entities in the input and replaces them with semantic tokens. Accepts plain text, structured JSON (input_type: "json" with input_json), or OpenAI-style chat arrays (input_type: "chat_messages" with input_messages).

curl -sS http://localhost:3000/v1/transform \
  -H "X-API-Key: $GUARDAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "input": "Contact Erika Mustermann at erika.mustermann@firma.example or +49 30 12345678.",
    "input_type": "text",
    "language": "de",
    "policy": "german-support",
    "detectors": ["builtin_regex", "python_ner"],
    "protection_overrides": { "phone": "masked" },
    "context": {
      "destination": "external_llm",
      "caller_role": "support_agent",
      "caller_purpose": "support"
    }
  }'

Response highlights: safe_text (send this to the LLM), session_state (carry into rehydrate), entity_context (type-only metadata safe for a system prompt, never raw values), entities (full diagnostic detail for your side), stats, and detector_mode.

Notes:

  • detectors accepts builtin_regex and python_ner. Omit it to use the server's configured default. python_ner requires the NER sidecar to be configured.
  • protection_overrides and restore modes are one of full, partial, masked, formatted, abstract, none.
  • context.destination is one of the built-ins external_llm, local_llm, vector_store, tool, agent_memory, or any custom [a-z0-9_-]+ label.
  • In non-dev auth, caller_role and caller_purpose are taken from the credential, not from the request body, so a caller cannot self-assert a privileged role.

POST /v1/rehydrate

Restores tokens in LLM output back to real values, governed by the restore mode and the output channel. session_state is required.

curl -sS http://localhost:3000/v1/rehydrate \
  -H "X-API-Key: $GUARDAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "output": "Sehr geehrte {{person:p_001:9f2c4a7b1e8d00112233445566778899}}, Ihre Anfrage zu {{email:e_001:5873cc5722d8ff00ee11dd22cc33bb44}} ist erledigt.",
    "session_state": "<session_state from transform>",
    "restore_mode": "full",
    "restore_overrides": { "e_001": "masked" },
    "output_channel": "customer_email"
  }'

output_channel selects which restore rules apply. Built-in channels are user_output, internal_summary, customer_email, tool_payload, export, log_safe. Any other [a-z0-9_-]+ name is a custom channel and is fail-closed. Restoring anything on one takes two independent grants: the policy must define its channel_rules, and the deployment must name it in GUARDAI_TRUSTED_OUTPUT_CHANNELS. Without the second, the channel is clamped to none and restores nothing however the policy is written. tool_payload is clamped the same way, because a caller naming a machine sink is not authority to send it raw values.

POST /v1/detect

Detects entities and returns spans, types, and confidence. No transformation, no session state.

curl -sS http://localhost:3000/v1/detect \
  -H "X-API-Key: $GUARDAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "input": "Reach me at erika.mustermann@firma.example or IBAN DE97 9900 0000 0123 4567 89.",
    "language": "de",
    "detectors": ["builtin_regex"],
    "entity_types": ["email", "iban"],
    "threshold": 0.5
  }'

threshold is the minimum confidence in [0.0, 1.0] (default 0.5). entity_types filters the output to specific types.

POST /v1/evaluate-policy

Dry-runs a policy against a list of entities and returns the decision, protection level, matched rule, and restore mode per entity. Nothing is stored. Omit value on an entity to evaluate by type only and keep raw PII out of the request; the decision then falls back to the type-based rule, which is at least as restrictive.

curl -sS http://localhost:3000/v1/evaluate-policy \
  -H "X-API-Key: $GUARDAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "policy": "strict-pii",
    "entities": [
      { "type": "email", "value": "erika.mustermann@firma.example" },
      { "type": "iban" }
    ],
    "context": { "destination": "external_llm", "caller_role": "analyst" },
    "output_channel": "internal_summary"
  }'

Note the entity field is type, not entity_type, on this endpoint.

Streaming endpoints

Both streaming endpoints take the same JSON body as their non-streaming counterparts and return a text/event-stream (SSE) response. Use curl -N to keep the stream open.

POST /v1/transform/stream

curl -N http://localhost:3000/v1/transform/stream \
  -H "X-API-Key: $GUARDAI_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: text/event-stream" \
  -d '{
    "input": "Contact Erika Mustermann at erika.mustermann@firma.example.",
    "language": "de",
    "policy": "german-support"
  }'

POST /v1/rehydrate/stream

curl -N http://localhost:3000/v1/rehydrate/stream \
  -H "X-API-Key: $GUARDAI_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: text/event-stream" \
  -d '{
    "output": "Sehr geehrte {{person:p_001:9f2c4a7b1e8d00112233445566778899}}, Danke.",
    "session_state": "<session_state from transform>",
    "restore_mode": "full",
    "output_channel": "user_output"
  }'

Batch endpoints

POST /v1/batch/transform

Transforms an array of items in one request. Each item can be text, JSON, or chat. session_mode is independent (each item gets its own session) or shared (the token map carries across all items). Batch size is capped by the server's limits.max_batch_size (see /v1/capabilities).

In shared session mode the credential's policy entitlement is decided before any item executes, against every policy the batch can run under: each item's own policy (or the batch-level policy) and, for any item that omits both, the deployment default. An item that relies on inheriting the shared session's policy must therefore also be entitled to the default; a batch that should run entirely under one named policy sets it at the batch level.

curl -sS http://localhost:3000/v1/batch/transform \
  -H "X-API-Key: $GUARDAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "session_mode": "independent",
    "policy": "german-support",
    "items": [
      { "text": "Frau Erika Mustermann, erika.mustermann@firma.example", "language": "de" },
      { "text": "Herr Max Mustermann, max@firma.example", "language": "de" }
    ]
  }'

POST /v1/batch/detect

Detects entities across many items. Set include_values: false so results omit the raw detected value and return only types and spans, useful when the caller must not receive raw PII back.

curl -sS http://localhost:3000/v1/batch/detect \
  -H "X-API-Key: $GUARDAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "include_values": false,
    "policy": "strict-pii",
    "items": [
      { "text": "erika.mustermann@firma.example", "language": "de" },
      { "text": "+49 30 12345678", "language": "de" }
    ]
  }'

RAG endpoints

The RAG flow is: ingest a document into safe chunks, query to tokenize the user question, context to tokenize the retrieved chunks, then answer to restore the model's reply. Use a shared corpus_id across ingest, query, and context so the same value tokenizes to the same id.

POST /v1/rag/ingest

Chunks a document and tokenizes each chunk for vector-store ingestion. Returns per-chunk safe_text, a chunk_session_state per chunk, and a document-level session_state.

curl -sS http://localhost:3000/v1/rag/ingest \
  -H "X-API-Key: $GUARDAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "text": "Kunde Erika Mustermann (erika.mustermann@firma.example) meldete am 3. Mai einen Defekt.",
    "chunking_strategy": "paragraph",
    "policy": "german-support",
    "language": "de",
    "corpus_id": "support-2026"
  }'

chunk_size (used by fixed and sliding-window strategies) must be at least 100 when supplied.

POST /v1/rag/ingest/batch

Ingests several documents in one call. Each item is a full ingest request body and yields its own chunks and sealed session; a failed item carries a per-item error and never fails the batch. Capped by limits.max_batch_size.

curl -sS http://localhost:3000/v1/rag/ingest/batch \
  -H "X-API-Key: $GUARDAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "items": [
      { "text": "Kunde Erika Mustermann meldete einen Defekt.", "policy": "german-support", "language": "de", "corpus_id": "support-2026" },
      { "text": "Rueckruf an erika.mustermann@firma.example vereinbart.", "policy": "german-support", "language": "de", "corpus_id": "support-2026" }
    ]
  }'

POST /v1/rag/query

Tokenizes the user query so it can hit the vector store without leaking PII.

curl -sS http://localhost:3000/v1/rag/query \
  -H "X-API-Key: $GUARDAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "Was hat Erika Mustermann gemeldet?",
    "policy": "german-support",
    "language": "de",
    "corpus_id": "support-2026"
  }'

POST /v1/rag/context

Tokenizes the chunks you retrieved from the vector store and merges their tokens into the session. Pass the session_state from the query call. Optional access_level plus chunk_classifications filter out chunks above the requested level (fail-closed).

curl -sS http://localhost:3000/v1/rag/context \
  -H "X-API-Key: $GUARDAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "chunks": ["Erika Mustermann meldete einen Defekt an erika.mustermann@firma.example."],
    "session_state": "<session_state from /v1/rag/query>",
    "policy": "german-support",
    "language": "de",
    "access_level": "internal",
    "chunk_classifications": ["internal"],
    "corpus_id": "support-2026"
  }'

POST /v1/rag/answer

Rehydrates the LLM's answer using the accumulated session. Pass the session_state from the context call and the model's tokenized answer.

curl -sS http://localhost:3000/v1/rag/answer \
  -H "X-API-Key: $GUARDAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "answer": "{{person:p_3e7a1c5f9b2d8e4a6c0f2b8d:7c1e9a3f5d2b8c4e6a0d1f3b5e7c9a2f}} meldete einen Defekt und ist unter {{email:e_6b2d8f4a0c9e1b7d3f5a2c8e:2a8c4e6f0b1d3a5c7e9f2b4d6a8c0e1f}} erreichbar.",
    "session_state": "<session_state from /v1/rag/context>",
    "output_channel": "user_output"
  }'

POST /v1/rag/delete

Erasure: revokes every value carried in the given session so any later rehydrate resolves them to [DELETED]. To forget one document, pass that document's ingest session_state. Requires the revoke scope.

curl -sS http://localhost:3000/v1/rag/delete \
  -H "X-API-Key: $GUARDAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "session_state": "<ingest session_state to forget>" }'

Revocation endpoints

POST /v1/revoke

Revokes one entity value so future rehydration returns [DELETED] instead of the original. Only an HMAC digest of the value is stored, never the raw value.

curl -sS http://localhost:3000/v1/revoke \
  -H "X-API-Key: $GUARDAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "entity_type": "email", "value": "erika.mustermann@firma.example" }'

POST /v1/revoke/bulk

Revokes several values in one call.

curl -sS http://localhost:3000/v1/revoke/bulk \
  -H "X-API-Key: $GUARDAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "entities": [
      { "entity_type": "email", "value": "erika.mustermann@firma.example" },
      { "entity_type": "person", "value": "Erika Mustermann" },
      { "entity_type": "phone", "value": "+49 30 12345678" }
    ]
  }'

GET /v1/revocations/count

Returns the count of revoked entities in the caller's own scope: a tenant-authenticated caller sees only its own tenant's revocations, and an unscoped platform-admin caller sees only the unscoped revocations. Requires the admin scope. The store partitions digests by tenant, so one tenant can never observe another tenant's revocation volume.

curl -sS http://localhost:3000/v1/revocations/count \
  -H "X-API-Key: $GUARDAI_ADMIN_KEY"

File and image endpoints

The transform-file and image endpoints below use multipart/form-data rather than JSON. /v1/rehydrate/file is grouped here for the file workflow but is a JSON alias of /v1/rehydrate, not an upload.

Each multipart form is strict: send exactly the documented fields, each at most once. An unknown field, a nameless part, a repeated field, or a control field over 255 bytes is rejected with 400 GUARDAI_BAD_REQUEST before entitlement, extraction, or OCR runs. policy and language follow the JSON body's semantics (an empty policy is a named policy, a blank language falls back to the default).

POST /v1/transform/file

Uploads a file, extracts its text, and tokenizes it. Form fields: file (required), policy (optional), language (optional).

curl -sS http://localhost:3000/v1/transform/file \
  -H "X-API-Key: $GUARDAI_API_KEY" \
  -F "file=@complaint.pdf" \
  -F "policy=german-support" \
  -F "language=de"

A failed extraction is classified, so do not read every non-200 as a bad file: 400 means the format is unsupported or the document is malformed, 413 means a configured safety ceiling refused it (the document may be perfectly valid), and 503 means the runtime could not perform the extraction at all and the call is worth retrying. See Document failures.

POST /v1/rehydrate/file

Restores the tokenized text a file transform produced, using its prior session_state. Despite the name it uploads no file: it is a JSON alias of /v1/rehydrate that accepts a RehydrateRequest body. Requires the rehydrate scope.

POST /v1/transform/image

Runs OCR on an uploaded image and tokenizes the detected text. Form field: image. Requires Tesseract OCR installed on the server, otherwise the endpoint returns 503 GUARDAI_OCR_UNAVAILABLE. An image whose text cannot be read (no readable region, or any region below the confidence floor) is refused with 422 GUARDAI_OCR_UNVERIFIABLE rather than returned as clean.

curl -sS http://localhost:3000/v1/transform/image \
  -H "X-API-Key: $GUARDAI_API_KEY" \
  -F "image=@scan.png"

POST /v1/redact/image

Returns the uploaded image as image/png with detected PII regions blacked out. Form field: image. Also requires Tesseract OCR, and refuses an unreadable image with 422 GUARDAI_OCR_UNVERIFIABLE for the same reason as /v1/transform/image.

curl -sS http://localhost:3000/v1/redact/image \
  -H "X-API-Key: $GUARDAI_API_KEY" \
  -F "image=@scan.png" \
  --output redacted.png

Health and introspection

GET /v1/health

Returns status (healthy, degraded, or unhealthy), version, uptime_seconds, a per-component breakdown, and a license block describing the resolved licence.

curl -sS http://localhost:3000/v1/health \
  -H "X-API-Key: $GUARDAI_API_KEY"

The license block reports status (absent, valid, version_cutoff, malformed), the edition in effect, this binary's compiled-in release_date, and, when a licence is present, its identity, updates_until, declared_edition, and recorded contract terms. It never carries the signature, key material, or entitlement flags, and nothing is gated by it: no capability is withheld and no request takes a different path because of it. See the licence block in the reference and the Licensing guide.

GET /v1/capabilities

Lists the supported entity_types (with protection level and source), languages, detectors (type builtin, python, or custom), restore_modes, ner_active, max_batch_size, and the per-session budget max_session_entities and max_session_bytes (an optional gender_inference object appears only when a name-gender overlay is installed). Use it to discover what the deployment can do before you send work.

curl -sS http://localhost:3000/v1/capabilities \
  -H "X-API-Key: $GUARDAI_API_KEY"

GET /v1/diagnostics

Runtime diagnostics: detector mode, session backend, loaded policies, default policy, and feature toggles (rate limiting, prompt security, output guard). Requires the admin scope.

curl -sS http://localhost:3000/v1/diagnostics \
  -H "X-API-Key: $GUARDAI_ADMIN_KEY"

GET /metrics

Prometheus exposition format. Requires the admin scope.

curl -sS http://localhost:3000/metrics \
  -H "X-API-Key: $GUARDAI_ADMIN_KEY"

GET /livez and GET /readyz

Kubernetes liveness and readiness probes. Public: they sit in front of the auth layer and never require a key.

curl -sS http://localhost:3000/livez
curl -sS http://localhost:3000/readyz

Admin: policy management

POST /v1/admin/policy/validate

Validates the policy YAML files in a directory and previews the changes against the currently loaded set, without swapping them in. Requires the admin scope.

curl -sS http://localhost:3000/v1/admin/policy/validate \
  -H "X-API-Key: $GUARDAI_ADMIN_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "directory": "policies/" }'

POST /v1/admin/policy/reload

Hot-reloads the policy directory with an atomic, fail-closed swap: if the new set does not parse, the running policies stay in place. No request body. Requires the admin scope.

curl -sS -X POST http://localhost:3000/v1/admin/policy/reload \
  -H "X-API-Key: $GUARDAI_ADMIN_KEY"

Runnable example: raw HTTP walkthrough

This section embeds the examples/raw-http/ example verbatim. It is a complete curl walkthrough of the core endpoints, each call paired with the exact response the example documents, so you can run the flow end to end and check what comes back. Source file: examples/raw-http/README.md.

The example uses the variable MY_API_KEY for the key and curl -X POST for the mutating calls, which is why the commands here differ in style from the catalog above. Where a call returns a {{person:...}} token it needs the NER detector sidecar; those calls are marked as needs-NER below. Everything else runs in builtin-only mode.

Walkthrough prerequisites

A running OGuardAI server on localhost:3000. Start it one of two ways:

# Option 1: Docker. The stack requires four secrets, and its client key ships least-privilege, so
# widen it to the policies and channels this walkthrough uses (strict-pii, plus the customer_email
# and internal_summary restores). Set MY_API_KEY to the GUARDAI_API_KEY_1 value below.
export GUARDAI_SESSION_SECRET=$(openssl rand -base64 32)
export GUARDAI_API_KEY_1=$(openssl rand -hex 32)
export GUARDAI_DETECTOR_API_KEY=$(openssl rand -hex 32)
export GUARDAI_REDIS_PASSWORD=$(openssl rand -hex 32)
export GUARDAI_API_KEY_1_ALLOWED_POLICIES=default,default-ner-required,german-support,strict-pii
export GUARDAI_API_KEY_1_ALLOWED_OUTPUT_CHANNELS=user_output,customer_email,internal_summary
docker compose -f deploy/docker/docker-compose.yml up

# Option 2: the signed release binary
oguardai run

Auth: The Docker Compose setup enables API key auth. Set MY_API_KEY to the value of GUARDAI_API_KEY_1 and add -H "X-API-Key: $MY_API_KEY" to every /v1/* request below.

Detection: Examples showing {{person:...}} tokens require the NER sidecar (docker compose up starts both server and detector). Builtin-only mode detects emails, phones, IPs, etc. but not names.

Data: All names, emails, phone numbers, IDs, and other values shown in the examples below are fictional test data.

Liveness and health check

curl http://localhost:3000/livez

Expected response: {"status":"ok"}

The health check requires auth:

curl -H "X-API-Key: $MY_API_KEY" http://localhost:3000/v1/health

Expected response:

{
  "status": "healthy",
  "version": "0.1.0",
  "uptime_seconds": 42.5,
  "components": {
    "detector": { "status": "healthy" },
    "session": { "status": "healthy" }
  }
}

Capabilities lookup

curl -H "X-API-Key: $MY_API_KEY" http://localhost:3000/v1/capabilities

Expected response:

{
  "entity_types": [
    { "name": "person", "protection_level": "2", "source": "ner", "id_prefix": "p_", "description": "Full person name" },
    { "name": "email", "protection_level": "2", "source": "builtin", "id_prefix": "e_", "description": "Email address" },
    { "name": "phone", "protection_level": "2", "source": "builtin", "id_prefix": "ph_", "description": "Phone number" },
    { "name": "iban", "protection_level": "1", "source": "builtin", "id_prefix": "ib_", "description": "International bank account number" },
    { "name": "ssn", "protection_level": "1", "source": "builtin", "id_prefix": "ss_", "description": "Social security number" }
  ],
  "languages": [
    { "code": "de", "name": "German", "detection_support": "full", "rehydration_support": "full" },
    { "code": "en", "name": "English", "detection_support": "full", "rehydration_support": "full" }
  ],
  "detectors": [
    { "name": "builtin_regex", "type": "builtin", "entity_types": ["email", "phone", "iban", "ssn", "ip", "url"] }
  ],
  "restore_modes": ["full", "partial", "masked", "formatted", "abstract", "none"],
  "ner_active": true,
  "max_batch_size": 100
}

Transform plain text

Detect an email and phone in plain text and tokenize them. Builtin-only, no NER needed.

curl -X POST http://localhost:3000/v1/transform \
  -H "Content-Type: application/json" \
  -H "X-API-Key: $MY_API_KEY" \
  -d '{
    "input": "Contact erika.mustermann@example.com or call +49 30 12345678",
    "policy": "default"
  }'

Expected response:

{
  "safe_text": "Contact {{email:e_001:7b1068662ab5}} or call {{phone:ph_001:d82a05c1f4e6}}",
  "session_id": "01916a3e-7b2c-7000-8000-000000000001",
  "session_state": "eyJ2IjoxLCJzaWQiOiIwMTkx...<sealed blob>",
  "entity_context": [
    { "token": "{{email:e_001:7b1068662ab5}}", "type": "email" },
    { "token": "{{phone:ph_001:d82a05c1f4e6}}", "type": "phone" }
  ],
  "entities": [
    {
      "token": "{{email:e_001:7b1068662ab5}}",
      "type": "email",
      "protection_level": "2",
      "action_applied": "tokenize",
      "confidence": 1.0,
      "detector": "builtin_regex",
      "span": { "start": 8, "end": 22 }
    },
    {
      "token": "{{phone:ph_001:d82a05c1f4e6}}",
      "type": "phone",
      "protection_level": "2",
      "action_applied": "tokenize",
      "confidence": 1.0,
      "detector": "builtin_regex",
      "span": { "start": 31, "end": 48 }
    }
  ],
  "stats": {
    "entities_detected": 2,
    "entities_transformed": 2,
    "entities_blocked": 0,
    "detection_time_ms": 1.2,
    "transform_time_ms": 0.3
  }
}

The third token segment (7b1068662ab5 above) is a random per-token capability suffix. Rehydrate resolves a token only on an exact {{type:id:cap}} match, so echo the tokens back verbatim. The suffix shown here is illustrative; each run returns its own.

Transform chat messages (needs-NER)

Tokenize an OpenAI-style chat array field by field. The person name is detected by NER, so this call needs the detector sidecar.

curl -X POST http://localhost:3000/v1/transform \
  -H "Content-Type: application/json" \
  -H "X-API-Key: $MY_API_KEY" \
  -d '{
    "input_type": "chat_messages",
    "input_messages": [
      { "role": "system", "content": "You are a helpful support agent." },
      { "role": "user", "content": "I am Erika Mustermann, my email is erika.mustermann@example.com" }
    ],
    "policy": "german-support"
  }'

Expected response (each message is tokenized field by field and returned as safe_messages; safe_text is the serialized array):

{
  "safe_messages": [
    { "role": "system", "content": "You are a helpful support agent." },
    { "role": "user", "content": "I am {{person:p_001:ad4f97591c16}}, my email is {{email:e_001:7b1068662ab5}}" }
  ],
  "safe_text": "[{\"role\":\"system\",\"content\":\"You are a helpful support agent.\"},{\"role\":\"user\",\"content\":\"I am {{person:p_001:ad4f97591c16}}, my email is {{email:e_001:7b1068662ab5}}\"}]",
  "session_id": "01916a3e-7b2c-7000-8000-000000000002",
  "session_state": "eyJ2IjoxLCJzaWQiOiIwMTkx...<sealed blob>",
  "entity_context": [
    { "token": "{{person:p_001:ad4f97591c16}}", "type": "person", "gender": "female" },
    { "token": "{{email:e_001:7b1068662ab5}}", "type": "email" }
  ],
  "entities": [
    {
      "token": "{{person:p_001:ad4f97591c16}}",
      "type": "person",
      "protection_level": "2",
      "action_applied": "tokenize",
      "confidence": 0.92,
      "detector": "python_ner",
      "span": { "start": 5, "end": 21 }
    },
    {
      "token": "{{email:e_001:7b1068662ab5}}",
      "type": "email",
      "protection_level": "2",
      "action_applied": "tokenize",
      "confidence": 1.0,
      "detector": "builtin_regex",
      "span": { "start": 33, "end": 49 }
    }
  ],
  "stats": {
    "entities_detected": 2,
    "entities_transformed": 2,
    "detection_time_ms": 1.5,
    "transform_time_ms": 0.4
  }
}

Rehydrate

Restore the tokens using the session_state from a prior transform response:

curl -X POST http://localhost:3000/v1/rehydrate \
  -H "Content-Type: application/json" \
  -H "X-API-Key: $MY_API_KEY" \
  -d '{
    "output": "Dear {{person:p_001:ad4f97591c16}}, we have received your request.",
    "session_state": "<session_state from transform response>",
    "output_channel": "customer_email",
    "restore_mode": "formatted"
  }'

Expected response:

{
  "restored_text": "Dear Frau Erika Mustermann, we have received your request.",
  "tokens_resolved": 1,
  "tokens_unresolved": [],
  "stats": {
    "rehydrate_time_ms": 0.8
  }
}

Detect only

Detect entities without transforming. The endpoint is default-secure: under API-key auth, omitting include_values suppresses the raw detected values, so each entity's value comes back as an empty string. Types, spans, and confidences are still returned:

curl -X POST http://localhost:3000/v1/detect \
  -H "Content-Type: application/json" \
  -H "X-API-Key: $MY_API_KEY" \
  -d '{
    "input": "Email: test@example.com, SSN: 123-45-6789",
    "threshold": 0.5
  }'

Expected response (values suppressed):

{
  "entities": [
    {
      "type": "email",
      "value": "",
      "span": { "start": 7, "end": 23 },
      "confidence": 1.0,
      "detector": "builtin_regex"
    },
    {
      "type": "ssn",
      "value": "",
      "span": { "start": 30, "end": 41 },
      "confidence": 1.0,
      "detector": "builtin_regex"
    }
  ],
  "stats": {
    "detection_time_ms": 0.9,
    "detectors_used": ["builtin_regex"]
  }
}

To receive the raw detected values, set "include_values": true explicitly:

curl -X POST http://localhost:3000/v1/detect \
  -H "Content-Type: application/json" \
  -H "X-API-Key: $MY_API_KEY" \
  -d '{
    "input": "Email: test@example.com, SSN: 123-45-6789",
    "threshold": 0.5,
    "include_values": true
  }'

With authorization granted, each value field carries the raw detected value (for example "value": "test@example.com").

Scope: include_values: true requires an API key with the detect_values scope; without it the request fails with 403. When include_values is omitted, the server resolves it from the detect.include_values_default config key, which defaults to true only under dev auth. Even a resolved default of true still requires the detect_values scope, otherwise values stay suppressed.

Full round trip: transform, LLM, rehydrate (needs-NER)

A complete transform-then-rehydrate cycle demonstrating the core OGuardAI flow. Save it as a script and run it with bash. It needs the NER sidecar because it extracts the person token. It also relies on jq.

#!/bin/bash
# Full round-trip: transform -> (simulate LLM) -> rehydrate

# Step 1: Transform the input
TRANSFORM_RESPONSE=$(curl -s -X POST http://localhost:3000/v1/transform \
  -H "Content-Type: application/json" \
  -H "X-API-Key: $MY_API_KEY" \
  -d '{
    "input": "Hallo, ich bin Erika Mustermann. Meine Kundennummer ist 948221. Ich warte seit 5 Tagen auf meine Rueckzahlung.",
    "policy": "german-support",
    "language": "de"
  }')

echo "=== Transform Response ==="
echo "$TRANSFORM_RESPONSE" | jq .

# Step 2: Extract session_state and safe_text
SESSION_STATE=$(echo "$TRANSFORM_RESPONSE" | jq -r '.session_state')
SAFE_TEXT=$(echo "$TRANSFORM_RESPONSE" | jq -r '.safe_text')

echo ""
echo "=== Safe text sent to LLM ==="
echo "$SAFE_TEXT"

# Step 3: Simulate the LLM response using the EXACT tokens transform returned. Production tokens
# carry a capability suffix ({{type:id:cap}}); a capless {{type:id}} resolves to nothing, so the
# draft must echo the real 3-segment tokens. Pull them from .entities[].token the way a model
# preserves them verbatim (in production you send $SAFE_TEXT to your LLM and get these back).
P_CUST=$(echo "$TRANSFORM_RESPONSE" | jq -r '[.entities[] | select(.type=="person") | .token][0]')
CID=$(echo "$TRANSFORM_RESPONSE" | jq -r '[.entities[] | select(.type=="customer_id") | .token][0]')

LLM_OUTPUT="Sehr geehrte ${P_CUST},

vielen Dank fuer Ihre Nachricht. Es tut uns leid, dass Sie bereits seit 5 Tagen auf Ihre Rueckzahlung warten. Wir haben Ihren Fall unter der Kundennummer ${CID} ueberprueft und die Rueckzahlung wurde heute angewiesen.

Mit freundlichen Gruessen,
Ihr Support-Team"

echo ""
echo "=== LLM output (with tokens) ==="
echo "$LLM_OUTPUT"

# Step 4: Rehydrate for customer email
REHYDRATE_RESPONSE=$(curl -s -X POST http://localhost:3000/v1/rehydrate \
  -H "Content-Type: application/json" \
  -H "X-API-Key: $MY_API_KEY" \
  -d "$(jq -n \
    --arg output "$LLM_OUTPUT" \
    --arg session_state "$SESSION_STATE" \
    '{
      "output": $output,
      "session_state": $session_state,
      "output_channel": "customer_email",
      "restore_mode": "formatted"
    }')")

echo ""
echo "=== Rehydrated output (customer email) ==="
echo "$REHYDRATE_RESPONSE" | jq -r '.restored_text'

Expected final output:

Sehr geehrte Frau Erika Mustermann,

vielen Dank fuer Ihre Nachricht. Es tut uns leid, dass Sie bereits seit
5 Tagen auf Ihre Rueckzahlung warten. Wir haben Ihren Fall unter der
Kundennummer 948221 ueberprueft und die Rueckzahlung wurde heute angewiesen.

Mit freundlichen Gruessen,
Ihr Support-Team

Walkthrough error responses

The same example documents these error bodies.

Invalid request (400):

curl -s -X POST http://localhost:3000/v1/transform \
  -H "Content-Type: application/json" \
  -H "X-API-Key: $MY_API_KEY" \
  -d '{}'
{
  "error": "invalid input: input field required when input_type is text",
  "code": "GUARDAI_INVALID_INPUT",
  "request_id": "9a888ef4-cbc9-4d4d-aaa1-bbd420caeea8"
}

Entity blocked by policy (403). The reason is deliberately generic: it never names the entity type or value that tripped the policy, so an error body cannot be used to probe what a document contained.

{
  "error": "policy denied: blocked entities detected",
  "code": "GUARDAI_POLICY_DENIED",
  "request_id": "f2288a8e-bc4b-4227-909d-b38757d30cb5"
}

Session expired (410):

{
  "error": "Session has expired. Create a new session by calling /v1/transform.",
  "code": "GUARDAI_SESSION_EXPIRED"
}

Errors

Failures return a JSON error body with a stable code of the form GUARDAI_{CATEGORY}_{SPECIFIC} (for example GUARDAI_SESSION_EXPIRED, GUARDAI_POLICY_DENIED, GUARDAI_OCR_UNAVAILABLE), plus a safe human-readable message. Error messages never contain raw PII or internal detail. The shape is defined in schemas/api/error-response.json. See Error Codes in the reference for the full code list.

Two codes on the restore path are easy to confuse and must not be retried the same way. 422 GUARDAI_OUTPUT_BLOCKED means the output guard judged the content and refused it, so retrying the same content will not help. 503 GUARDAI_OUTPUT_GUARD_UNAVAILABLE means the scan could not run at all, so the content was never judged and the same request may succeed once the deployment is fixed.