DocsIntegrationsAgent API Integration

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_access feature. Community and Professional plans get 403 Forbidden.
  • API key with correct scopes -- Create a key at Settings > API Keys with statement:read, glossary:read, and assembly: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

MethodPathPurpose
GET/api/agent/statementsSearch statements with agent-guidance fields
GET/api/agent/glossarySearch glossary terms with ontology metadata
GET/api/agent/ontologyExport 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/mcpMCP 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>;
}
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):

PlanRequests / minute
Business100
Enterprise500

Every response includes:

  • X-RateLimit-Limit -- window cap
  • X-RateLimit-Remaining -- how many are left
  • X-RateLimit-Reset -- Unix timestamp when the window resets
  • X-RateLimit-Window -- 60

MCP requests are metered separately from browser API requests. Check usage at Settings > Billing > API & MCP Usage.

Error Handling

StatusMeaningWhat to do
401Missing, invalid, or revoked API keyRegenerate the key at Settings > API Keys
403 with reason: "mcp_access_required"Tenant is on Community or ProfessionalUpgrade plan
403 with missing scopeKey lacks a required scopeEdit key scopes or create a new key
404Entity does not exist (bundle for non-existent assembly, etc.)Verify the ID
429Rate limit exceededBack off until X-RateLimit-Reset; batch requests where possible
5xxTransient server errorRetry 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-Id on 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