Agent API Integration
Connect custom agents, automations, and internal tools to Dictiva's governance data using REST or MCP. Includes curl, TypeScript, and Python quickstarts with copy-paste examples.
When To Use This
The MCP Governance Server is the recommended path for most AI agents -- it provides structured tools, resources, and prompts over a standard protocol. Use the direct REST API instead when:
- You're building an internal automation, CLI, or web service that isn't a conversational agent
- Your framework doesn't speak MCP (custom RAG pipelines, data warehouses, workflow engines)
- You want to integrate Dictiva data into dashboards, reports, or scheduled jobs
- You're prototyping before deciding on MCP
Both surfaces share the same authentication, entitlement, rate limits, and tenant isolation. The MCP server is literally a thin proxy over these REST endpoints.
Prerequisites
- Business or Enterprise plan -- Agent endpoints require the
mcp_accessfeature. Community and Professional plans get403 Forbidden. - API key with correct scopes -- Create a key at Settings > API Keys with
statement:read,glossary:read, andassembly:read. Add write scopes only if your integration modifies data. - HTTP client that can set custom headers and handle JSON. Any modern language will do.
See the API Keys guide for key provisioning, rotation, and scope semantics.
Base URL
Production: https://app.dictiva.com
Local dev: http://localhost:3100
All agent endpoints live under /api/agent/ and expect Authorization: Bearer dv_live_... on every request.
Available Endpoints
| Method | Path | Purpose |
|---|---|---|
GET | /api/agent/statements | Search statements with agent-guidance fields |
GET | /api/agent/glossary | Search glossary terms with ontology metadata |
GET | /api/agent/ontology | Export the full glossary as a graph (JSON or JSON-LD) |
GET | /api/agent/bundles/{assemblyId} | Compile an assembly into a signed governance bundle (JSON or JSON-LD) |
POST | /api/mcp | MCP Streamable HTTP -- tool/resource/prompt access via JSON-RPC |
The interactive OpenAPI reference is at /api/docs (Scalar UI). The raw spec is at /api/openapi.json.
curl Quickstart
Copy-paste ready. Replace the Bearer token with your key.
List agent-applicable statements
curl -s "https://app.dictiva.com/api/agent/statements?applies_to=agent&per_page=5" \
-H "Authorization: Bearer dv_live_..." \
-H "Accept: application/json"
Returns paginated statements with full agentGuidance blocks (allowed/prohibited actions, required context, approvals, evidence requirements, escalation rules, failure mode).
Search glossary by term type
curl -s "https://app.dictiva.com/api/agent/glossary?term_type=constraint&per_page=10" \
-H "Authorization: Bearer dv_live_..."
Export the full ontology as JSON-LD
curl -s "https://app.dictiva.com/api/agent/ontology?format=json-ld" \
-H "Authorization: Bearer dv_live_..."
The response embeds "@context": "https://app.dictiva.com/ns/ontology/v1". A strict JSON-LD processor will dereference that URL to resolve term IRIs. You can fetch the context directly:
curl -s https://app.dictiva.com/ns/ontology/v1
Compile an assembly bundle with JSON-LD
curl -s "https://app.dictiva.com/api/agent/bundles/YOUR-ASSEMBLY-UUID?format=json-ld&actor=agent" \
-H "Authorization: Bearer dv_live_..."
Bundles include statements filtered by applicability, linked glossary terms, a signed SHA-256 content hash, and (when format=json-ld) linked-data references to https://app.dictiva.com/ns/governance/v1.
TypeScript
Minimal fetch wrapper
const BASE_URL = process.env.DICTIVA_BASE_URL ?? "https://app.dictiva.com";
const API_KEY = process.env.DICTIVA_API_KEY!;
async function agentApi<T>(
path: string,
params?: Record<string, string | number | undefined>,
): Promise<T> {
const url = new URL(`/api/agent${path}`, BASE_URL);
if (params) {
for (const [key, value] of Object.entries(params)) {
if (value !== undefined) url.searchParams.set(key, String(value));
}
}
const res = await fetch(url, {
headers: {
Authorization: `Bearer ${API_KEY}`,
Accept: "application/json",
},
});
if (!res.ok) {
throw new Error(`${res.status} ${res.statusText}: ${await res.text()}`);
}
return res.json() as Promise<T>;
}
Typed response with Zod (recommended)
import { z } from "zod";
const agentGuidance = z.object({
allowedActions: z.array(z.string()).nullable(),
prohibitedActions: z.array(z.string()).nullable(),
requiredContext: z.array(z.string()).nullable(),
requiredApprovals: z.unknown().nullable(),
evidenceRequirements: z.array(z.string()).nullable(),
escalationRules: z.record(z.string(), z.unknown()).nullable(),
failureMode: z.record(z.string(), z.unknown()).nullable(),
});
const agentStatement = z.object({
id: z.string().uuid(),
displayId: z.string(),
title: z.string().nullable(),
body: z.string().nullable(),
modality: z.string().nullable(),
lifecycleState: z.string(),
appliesTo: z.enum(["human", "agent", "both"]),
actorScope: z.array(z.string()).nullable(),
enforcementMode: z.string().nullable(),
domain: z.string().nullable(),
agentGuidance: agentGuidance.nullable(),
updatedAt: z.string(),
});
const statementsResponse = z.object({
data: z.array(agentStatement),
meta: z.object({
page: z.number().int(),
perPage: z.number().int(),
total: z.number().int(),
totalPages: z.number().int(),
}),
});
export async function searchStatements(params: {
query?: string;
appliesTo?: "human" | "agent" | "both";
perPage?: number;
}) {
const data = await agentApi("/statements", {
q: params.query,
applies_to: params.appliesTo,
per_page: params.perPage,
});
return statementsResponse.parse(data);
}
Pagination helper
export async function* paginate<T>(
fetchPage: (page: number) => Promise<{ data: T[]; meta: { totalPages: number } }>,
): AsyncGenerator<T, void, unknown> {
let page = 1;
while (true) {
const { data, meta } = await fetchPage(page);
for (const item of data) yield item;
if (page >= meta.totalPages) break;
page++;
}
}
// Usage:
for await (const stmt of paginate((page) =>
searchStatements({ appliesTo: "agent", perPage: 50 }),
)) {
console.log(stmt.displayId, stmt.title);
}
Python
Minimal requests wrapper
import os
from typing import Any
import requests
BASE_URL = os.environ.get("DICTIVA_BASE_URL", "https://app.dictiva.com")
API_KEY = os.environ["DICTIVA_API_KEY"]
def agent_api(path: str, params: dict[str, Any] | None = None) -> dict[str, Any]:
res = requests.get(
f"{BASE_URL}/api/agent{path}",
params={k: v for k, v in (params or {}).items() if v is not None},
headers={
"Authorization": f"Bearer {API_KEY}",
"Accept": "application/json",
},
timeout=30,
)
res.raise_for_status()
return res.json()
Typed with Pydantic
from typing import Literal
from pydantic import BaseModel, Field
class AgentGuidance(BaseModel):
allowed_actions: list[str] | None = Field(default=None, alias="allowedActions")
prohibited_actions: list[str] | None = Field(default=None, alias="prohibitedActions")
required_context: list[str] | None = Field(default=None, alias="requiredContext")
required_approvals: object | None = Field(default=None, alias="requiredApprovals")
evidence_requirements: list[str] | None = Field(default=None, alias="evidenceRequirements")
escalation_rules: dict | None = Field(default=None, alias="escalationRules")
failure_mode: dict | None = Field(default=None, alias="failureMode")
class AgentStatement(BaseModel):
id: str
display_id: str = Field(alias="displayId")
title: str | None
body: str | None
modality: str | None
lifecycle_state: str = Field(alias="lifecycleState")
applies_to: Literal["human", "agent", "both"] = Field(alias="appliesTo")
actor_scope: list[str] | None = Field(default=None, alias="actorScope")
enforcement_mode: str | None = Field(default=None, alias="enforcementMode")
domain: str | None
agent_guidance: AgentGuidance | None = Field(default=None, alias="agentGuidance")
updated_at: str = Field(alias="updatedAt")
def search_statements(
query: str | None = None,
applies_to: Literal["human", "agent", "both"] | None = None,
per_page: int = 20,
) -> list[AgentStatement]:
data = agent_api(
"/statements",
{"q": query, "applies_to": applies_to, "per_page": per_page},
)
return [AgentStatement.model_validate(s) for s in data["data"]]
Pagination
from collections.abc import Generator
def paginate_statements(**kwargs) -> Generator[AgentStatement, None, None]:
page = 1
while True:
data = agent_api(
"/statements", {**kwargs, "page": page, "per_page": 50}
)
for s in data["data"]:
yield AgentStatement.model_validate(s)
if page >= data["meta"]["totalPages"]:
break
page += 1
Rate Limits & Metering
Both REST and MCP requests count against your API key's rate limit (per-key, 60-second fixed window):
| Plan | Requests / minute |
|---|---|
| Business | 100 |
| Enterprise | 500 |
Every response includes:
X-RateLimit-Limit-- window capX-RateLimit-Remaining-- how many are leftX-RateLimit-Reset-- Unix timestamp when the window resetsX-RateLimit-Window--60
MCP requests are metered separately from browser API requests. Check usage at Settings > Billing > API & MCP Usage.
Error Handling
| Status | Meaning | What to do |
|---|---|---|
401 | Missing, invalid, or revoked API key | Regenerate the key at Settings > API Keys |
403 with reason: "mcp_access_required" | Tenant is on Community or Professional | Upgrade plan |
403 with missing scope | Key lacks a required scope | Edit key scopes or create a new key |
404 | Entity does not exist (bundle for non-existent assembly, etc.) | Verify the ID |
429 | Rate limit exceeded | Back off until X-RateLimit-Reset; batch requests where possible |
5xx | Transient server error | Retry with exponential backoff |
Always include auth errors in your telemetry. If 401 starts appearing mid-integration, the key was likely rotated or revoked.
JSON-LD Context URLs
When you request format=json-ld on /api/agent/bundles/{id} or /api/agent/ontology, the response embeds an @context URL. These resolve to public JSON-LD documents:
https://app.dictiva.com/ns/governance/v1-- bundle vocabulary (Statement, Assembly, taxonomy spans, agent guidance fields)https://app.dictiva.com/ns/ontology/v1-- glossary vocabulary (SKOS mappings for broader/narrower/related)
Both are cacheable (Cache-Control: public, max-age=3600) and CORS-open. Any JSON-LD 1.1 processor can dereference them.
Security Notes
- Never commit API keys to git. Use environment variables or a secrets manager.
- Tenant isolation is strict: a key scoped to tenant A cannot read tenant B's data. There is no cross-tenant read permission for regular keys.
- Platform keys (internal only) bypass tenant binding but require
X-Tenant-Idon every request. They also bypass MCP entitlement checks and credit consumption. - Scope minimally: production integrations should use read-only scopes unless they explicitly need write.
Next Steps
- Set up the MCP governance server for conversational agents (stdio or Streamable HTTP)
- Explore the interactive API docs to see every endpoint, parameter, and response schema
- Create an API key with minimum-viable scopes
- Review RBAC configuration to control who can mint keys