Skip to main content

Policies API

Quick example

curl "$TRINITITE_BASE/v1/policies" \
-H "Authorization: Bearer $TRINITITE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "acme-pii-and-tone",
"source_documents": ["s3://acme-policies/handbook.pdf"],
"auto_generate_tests": true,
"auto_train": true
}'

See Policy Intelligence for the architecture, and Cookbook → Multi-tenant fleet for the end-to-end recipe.


Overview

Ingest regulatory and organisational policy documents into the platform's knowledge graph. Each document is decomposed into Policy Nodes — atomic, structured rule / definition / principle records — that are embedded for retrieval and RAG-injected into Guardian context at inference time.

The flow is:

  1. Create & ingest (async). Source can be inline content, a URL, or a third-party integration.
  2. Review & edit the AI-extracted nodes. Every edit is audit-logged.
  3. Finalize to lock the document. Optionally triggers downstream test-data generation, scenario densification, and Guardian retraining.
  4. Reingest when the underlying source changes; this creates a new version that starts back in draft.
  5. Pin a policy_version_id when running test suites or simulations to make governance behaviour deterministic across releases.

Inference-time RAG is fully transparent: the platform embeds every governed conversation, retrieves relevant nodes, and injects them into the Guardian's context. Callers never need to fetch or send nodes manually.

Authentication: Authorization: Bearer <session_token | api_key> with the relevant policies:* permission.


Endpoints

Document lifecycle

MethodPathPermission
POST/v1/policiespolicies:create
GET/v1/policiespolicies:read
GET/v1/policies/{document_id}policies:read
PATCH/v1/policies/{document_id}policies:update
DELETE/v1/policies/{document_id}policies:delete
GET/v1/policies/{document_id}/statuspolicies:read
POST/v1/policies/{document_id}/reingestpolicies:update
POST/v1/policies/{document_id}/finalizepolicies:finalize
GET/v1/policies/{document_id}/impactpolicies:read
GET/v1/policies/{document_id}/edit-historypolicies:read

Versions & nodes

MethodPathPermission
GET/v1/policies/{document_id}/versionspolicies:read
GET/v1/policies/{document_id}/nodespolicies:read
PATCH/v1/policies/{document_id}/nodes/{node_id}policies:update

Test-suite linking

MethodPathPermission
GET/v1/test-suites/{suite_id}/policiestest_suites:read
POST/v1/test-suites/{suite_id}/policiestest_suites:update
DELETE/v1/test-suites/{suite_id}/policies/{document_id}test_suites:update
GET/v1/policies/{document_id}/test-suitestest_suites:read

Lifecycle

Every document has two orthogonal lifecycles.

Ingestion lifecycle (status)

pending → ingesting → ready
↘ failed → (POST /reingest) → ingesting
StatusMeaning
pendingMetadata persisted, ingestion not yet started
ingestingExtraction + embedding pipeline running
readyAll nodes extracted; embeddings stored; available for governance
failedPipeline error; inspect ingestion_error on the detail response

Review lifecycle (finalized_at / is_editable)

draft (is_editable=true) → finalized (is_editable=false)
↘ POST /reingest → new version starting in draft

Finalisation is irreversible. A reingest creates a new version that starts in draft.


POST /v1/policies

Creates a policy document and queues ingestion. Returns 202 Accepted immediately.

{
"name": "GDPR Article 33 — Notification of a personal data breach",
"description": "Notification timing, content requirements, supervisory authority routing.",
"source": {
"type": "url",
"url": "https://gdpr-info.eu/art-33-gdpr/",
"format": "html"
},
"tags": ["gdpr", "breach-notification"],
"skip_extraction": false
}
FieldTypeRequiredDescription
namestringYesDisplay name
descriptionstringNoFree-form description
sourceobjectYesDiscriminated union — see below
tagsstring[]NoOrganizational tags
skip_extractionbooleanNoPersist the source without running node extraction. Default false

Source types

// Inline
{ "type": "inline", "content": "...raw text or base64...", "format": "pdf | docx | html | markdown | plain_text | csv | xlsx | json | xml | rtf", "filename": "..." }

// URL — backend fetches at ingestion time
{ "type": "url", "url": "https://example.com/policies/handbook.pdf", "authorization": "Bearer optional", "format": "optional, auto-detected" }

// Integration
{ "type": "integration", "provider": "confluence | sharepoint | google_drive | notion", "resource_id": "...", "params": { "workspace": "acme-corp" } }

Response — 202 Accepted

{
"document_id": "pol_01JF8RPDC1A2B3C4D5E6F7G8H9I",
"name": "GDPR Article 33 — Notification of a personal data breach",
"status": "pending",
"is_editable": true,
"finalized_at": null,
"created_at": "2026-05-01T22:14:00Z",
"poll_url": "/v1/policies/pol_01JF8RPDC1A2B3C4D5E6F7G8H9I/status"
}

Poll /status at 2 s intervals; most ingestions complete in 30 s – 5 min depending on document size.


GET /v1/policies

QueryTypeDescription
statusstringpending, ingesting, ready, failed
tagstringFilter by tag
is_editablebooleantrue for drafts, false for finalized
limit / cursorCursor pagination
{
"data": [
{
"document_id": "pol_01JF8RPDC1A2B3C4D5E6F7G8H9I",
"name": "GDPR Article 33 — Notification of a personal data breach",
"status": "ready",
"is_editable": false,
"finalized_at": "2026-05-01T22:42:00Z",
"current_version_id": "pver_01JF8RPVR1A2B3C4D5E6F7G8H9I",
"node_count": 47,
"tags": ["gdpr", "breach-notification"],
"created_at": "2026-05-01T22:14:00Z"
}
],
"page": { "next_cursor": null, "has_more": false }
}

GET /v1/policies/{document_id}

Full document detail including the raw markdown and current version metadata.

{
"document_id": "pol_01JF8RPDC1A2B3C4D5E6F7G8H9I",
"name": "GDPR Article 33 — Notification of a personal data breach",
"description": "Notification timing, content requirements, supervisory authority routing.",
"status": "ready",
"is_editable": false,
"finalized_at": "2026-05-01T22:42:00Z",
"current_version_id": "pver_01JF8RPVR1A2B3C4D5E6F7G8H9I",
"node_count": 47,
"markdown_content": "# Article 33\n\nIn the case of a personal data breach...",
"source": { "type": "url", "url": "https://gdpr-info.eu/art-33-gdpr/", "format": "html" },
"tags": ["gdpr", "breach-notification"],
"ingestion_error": null,
"created_at": "2026-05-01T22:14:00Z",
"updated_at": "2026-05-01T22:42:00Z"
}

GET /v1/policies/{document_id}/status

Lightweight status poll — progress is 0.01.0 with intermediate stage labels.

{
"document_id": "pol_01JF8RPDC1A2B3C4D5E6F7G8H9I",
"status": "ingesting",
"progress": 0.42,
"stage": "node_extraction",
"estimated_completion": "2026-05-01T22:35:00Z"
}

Stages: fetching, parsing, node_extraction, embedding, indexing.


PATCH /v1/policies/{document_id}

Update document-level fields while a document is draft. Returns 409 conflict if the document is finalized.

{
"name": "GDPR Art. 33 — Breach Notification (Q2 2026)",
"description": "Updated cross-references for Q2 2026.",
"tags": ["gdpr", "breach-notification", "q2-2026"]
}

POST /v1/policies/{document_id}/reingest

Re-run extraction against an updated source. Creates a new version that starts in draft. The previous version is preserved and remains pinnable.

{
"source": { "type": "url", "url": "https://gdpr-info.eu/art-33-gdpr/" },
"skip_extraction": false
}

Returns 202 Accepted with the new version ID and a fresh poll_url.


POST /v1/policies/{document_id}/finalize

Lock the document. After finalisation it cannot be edited; further changes require a reingest.

{
"automation": {
"regenerate_test_data": true,
"densify_scenarios": true,
"retrain_guardians": ["gov_01JF8R3M3X4N5Q6T7V8W9Y0Z1A"],
"pipeline_id": null
}
}
FieldTypeDescription
automation.regenerate_test_databooleanGenerate fresh adversarial scenarios from the new policy nodes
automation.densify_scenariosbooleanRun scenario densification
automation.retrain_guardiansstring[]Guardian IDs to retrain after finalisation
automation.pipeline_idstring | nullReuse an existing finalisation pipeline (else a new one is started)

Response — 200 OK:

{
"document_id": "pol_01JF8RPDC1A2B3C4D5E6F7G8H9I",
"finalized_at": "2026-05-01T22:42:00Z",
"is_editable": false,
"pipeline_id": "fpl_01JF8RFPL1A2B3C4D5E6F7G8H9I"
}

Track the downstream pipeline via GET /v1/finalization-pipelines/{pipeline_id} (see Test Suites).


GET /v1/policies/{document_id}/versions

Every successful ingest/reingest produces an immutable version snapshot.

{
"document_id": "pol_01JF8RPDC1A2B3C4D5E6F7G8H9I",
"versions": [
{
"version_id": "pver_01JF8RPVR1A2B3C4D5E6F7G8H9I",
"version_number": 2,
"content_hash": "sha256:e3b0c4...",
"node_count": 47,
"is_current": true,
"created_at": "2026-05-01T22:42:00Z"
},
{
"version_id": "pver_01JF8RPVR0A1B2C3D4E5F6G7H8",
"version_number": 1,
"content_hash": "sha256:9b5a17...",
"node_count": 45,
"is_current": false,
"created_at": "2026-04-15T11:08:00Z"
}
]
}

When running a Test Suite or Simulation, pin a specific version_id for deterministic, reproducible results.


Policy Nodes

GET /v1/policies/{document_id}/nodes

{
"document_id": "pol_01JF8RPDC1A2B3C4D5E6F7G8H9I",
"nodes": [
{
"node_id": "gdpr_art33_sec1_a",
"statement_type": "obligation",
"modality": "must",
"subject": "controller",
"action": "notify the supervisory authority",
"conditions": ["a personal data breach has occurred"],
"exceptions": ["unless the breach is unlikely to result in a risk to natural persons"],
"consequences": ["administrative fines under Article 83(4)(a)"],
"raw_excerpt": "In the case of a personal data breach, the controller shall...",
"tags": ["gdpr", "art33"]
}
]
}

PATCH /v1/policies/{document_id}/nodes/{node_id}

Edit a node while the document is in draft. Every change appends an entry to the document's edit-history (see below).

{
"modality": "must",
"conditions": ["a personal data breach has occurred", "and is likely to result in risk to natural persons"]
}

GET /v1/policies/{document_id}/edit-history

Append-only audit trail of every human edit applied to the document or its nodes.

{
"data": [
{
"edit_id": "pedit_01JF8RPED1A2B3C4D5E6F7G8H9I",
"actor": { "user_id": "usr_01JF8RUS1A2B3C4D5E6F7G8H9I", "display_name": "Jane Doe" },
"target": { "type": "node", "node_id": "gdpr_art33_sec1_a" },
"before": { "modality": "should" },
"after": { "modality": "must" },
"rationale": "Aligned with Art. 83(4)(a) penalty surface.",
"occurred_at": "2026-05-01T22:33:00Z"
}
],
"page": { "next_cursor": null, "has_more": false }
}

The edit log is append-only at the database layer — it cannot be modified or deleted via the API.


GET /v1/policies/{document_id}/impact

Before finalizing a policy update, use this endpoint to understand exactly which Guardians will be affected and whether a retrain is recommended. This prevents surprise coverage gaps in production.

Query parameters

ParamTypeDescription
next_version_idstringThe draft version to evaluate (defaults to current draft)
compare_to_version_idstringBaseline to diff against (defaults to the active finalized version)
{
"document_id": "pol_01JF8RPDC1A2B3C4D5E6F7G8H9I",
"next_version_id": "pver_01JF8RPVR2B3C4D5E6F7G8H9I0J",
"compare_to_version_id": "pver_01JF8RPVR1A2B3C4D5E6F7G8H9I",
"changed_nodes": 14,
"added_nodes": 3,
"removed_nodes": 1,
"affected_guardians": [
{
"guardian_id": "gov_01JF8R3M3X4N5Q6T7V8W9Y0Z1A",
"guardian_name": "PII-Redactor",
"impact": "high",
"changed_node_count": 11,
"reason": "11 policy nodes this Guardian enforces have changed",
"recommendation": "Retrain before finalizing this policy version",
"estimated_retrain_min": 45
},
{
"guardian_id": "gov_01JF8R3M4Y5N6Q7T8V9W0Y1Z2B",
"guardian_name": "SQL-Safety",
"impact": "low",
"changed_node_count": 3,
"reason": "3 ancillary policy nodes changed; core enforcement nodes unchanged",
"recommendation": "Optional retrain; current version remains valid"
}
],
"test_suites_to_rerun": [
"suite_01JF8RSTS1A2B3C4D5E6F7G8H9I",
"suite_01JF8RSTS2B3C4D5E6F7G8H9I0J"
]
}
Run impact analysis before finalizing

Always call this endpoint before POST /v1/policies/{id}/finalize on a high-value compliance document. It takes milliseconds and surfaces retrain candidates automatically — you can include the guardian_ids directly in the automation.retrain_guardians field of your finalize request.


DELETE /v1/policies/{document_id}

Soft-delete. Returns 204 No Content. The document is excluded from list responses and inference-time RAG, but the underlying versions, nodes, and edit history are retained for compliance.


Errors

{
"error": {
"code": "validation_error",
"message": "source.format is required for inline content.",
"details": { "field": "source.format" },
"request_id": "req_01J..."
}
}
HTTPerror.codeCause
400validation_errorBody or query failed schema validation
400bad_requestUnsupported format, conflicting source fields
401unauthenticatedMissing or invalid credential
403forbiddenCaller lacks the required policies:* permission
404not_foundDocument, node, or version not found in your organization
409conflictAttempted to PATCH a finalized document; document is currently ingesting
410resource_goneDocument was hard-deleted
422unprocessable_entitySource could not be fetched / parsed
429rate_limitedPer-organization rate limit exceeded

Next steps