Programmatic policy management
The programmatic management API lets you create, update, publish, and retire policies from outside the InPolicy web app — from a CI/CD pipeline, a Terraform run, or an AI coding assistant. The same operations available in the editor are available over REST, keyed by an API key you control.
Before you start
Section titled “Before you start”Create an API key with management scopes
Section titled “Create an API key with management scopes”Management scopes are not included on governance keys by default. You need a separate key.
- Go to Settings → API Keys and click New API Key.
- Give it a name (e.g.
ci-policy-sync). - Under Scopes, select the scopes you need:
| Scope | What it allows |
|---|---|
policies:read | List and fetch policies |
policies:write | Create and update policies |
policies:publish | Publish drafts; required for publish=true in bulk upsert |
policies:delete | Soft-delete policies |
documents:write | Upload files for AI extraction; register public URL sources |
webhooks:write | Register and manage webhook subscriptions |
- Copy the key immediately — it’s shown once. Store it in your secrets manager or CI environment as
INPOLICY_API_KEY.
The Admin bundle (all six scopes above) is the recommended choice for a CI/CD key. Keep it separate from the runtime governance keys used by the browser extension and Mac app — if a management key leaks, it can’t be used for governance checks, and vice versa.
The @inpolicy/cli quickstart
Section titled “The @inpolicy/cli quickstart”The CLI is the fastest way to sync a directory of Markdown policies.
npm install -g @inpolicy/cli
# One-time auth setupexport INPOLICY_API_KEY=inp_live_...
# Sync all .md files in ./policies/ (creates or updates by externalId)inpolicy policies sync ./policies/
# Preview without sending anythinginpolicy policies sync ./policies/ --dry-run
# Sync and immediately publishinpolicy policies sync ./policies/ --publishEach Markdown file is upserted by externalId. The externalId defaults to the filename (without .md) sanitized to [A-Za-z0-9._/:-]. You can override it in frontmatter:
---externalId: data-classification/v2title: Data Classification Policyseverity: 4enforcement: warningconfidenceRequired: 0.8tags: [data, privacy]policyAreaId: <uuid from Settings → Policy Areas>publish: false---
Employees must classify all files containing customer PII before sharing externally...Supported frontmatter fields: externalId, title, severity (1–5), enforcement (fix | warning | audit), confidenceRequired (0.0–1.0), tags, policyAreaId, rationale, effectiveDate, expiryDate, publish.
Other CLI commands
Section titled “Other CLI commands”inpolicy policies list # tabular listinpolicy policies get <id> # fetch one policyinpolicy policies publish <id> # publish a draftinpolicy policies upload ./policy.pdf # upload a document for AI extractionThe @inpolicy/policy-admin-mcp server
Section titled “The @inpolicy/policy-admin-mcp server”The policy admin MCP server lets Claude Desktop, Claude Code, or Cursor manage your policy catalog through natural language. It is distinct from the governance MCP server (which only reads policies to check content).
Claude Desktop
Section titled “Claude Desktop”Add to ~/Library/Application Support/Claude/claude_desktop_config.json:
{ "mcpServers": { "inpolicy-policy-admin": { "command": "npx", "args": ["-y", "@inpolicy/policy-admin-mcp"], "env": { "INPOLICY_API_KEY": "inp_live_..." } } }}Restart Claude Desktop. You can then ask: “List my published data policies”, “Update the data classification policy to severity 4”, or “Publish the draft ‘AI Usage Policy’”.
Claude Code / Cursor
Section titled “Claude Code / Cursor”# Add to your project's MCP configINPOLICY_API_KEY=inp_live_... npx @inpolicy/policy-admin-mcpOr in .claude/settings.json:
{ "mcpServers": { "inpolicy-policy-admin": { "command": "npx", "args": ["-y", "@inpolicy/policy-admin-mcp"], "env": { "INPOLICY_API_KEY": "inp_live_..." } } }}Required scopes: policies:read, policies:write, policies:publish, policies:delete, documents:write.
REST API reference
Section titled “REST API reference”Base URL: https://api.inpolicy.ai/api/v1/agent. Authenticate with Authorization: Bearer <api-key>.
All endpoints in this section are mounted under /api/v1/agent (the public Agent Governance namespace). Use the full paths as listed in each section.
Policies
Section titled “Policies”GET /api/v1/agent/policies
Section titled “GET /api/v1/agent/policies”Paginated list. Requires policies:read.
Query params: status (DRAFT | PUBLISHED | DEPRECATED), externalId, tag, source, cursor, limit (max 100).
GET /api/v1/agent/policies?status=PUBLISHED&limit=50Returns { data: PolicyV1[], nextCursor: string | null }.
GET /api/v1/agent/policies/:id
Section titled “GET /api/v1/agent/policies/:id”Fetch one policy. Accepts an InPolicy UUID or external:<externalId> (e.g. /api/v1/agent/policies/external:data-classification/v2). Requires policies:read.
POST /api/v1/agent/policies/bulk
Section titled “POST /api/v1/agent/policies/bulk”Idempotent bulk upsert. Up to 100 entries per call. Requires policies:write. Requires policies:publish if any entry has publish: true.
POST /api/v1/agent/policies/bulk{ "policies": [ { "externalId": "data-classification/v2", "title": "Data Classification Policy", "text": "Employees must classify...", "severity": 4, "enforcement": "warning", "publish": false } ]}Response: { results: UpsertResult[], created, updated, unchanged, published, errors }. Each UpsertResult has action: created | updated | unchanged | published | error. Partial failures are non-fatal — one bad row does not abort the rest.
PATCH /api/v1/agent/policies/:id
Section titled “PATCH /api/v1/agent/policies/:id”Partial update. Accepts InPolicy UUID or external:<externalId>. Requires policies:write. All fields are optional; omitted fields are unchanged.
DELETE /api/v1/agent/policies/:id
Section titled “DELETE /api/v1/agent/policies/:id”Soft-delete. Sets status=DEPRECATED and isActive=false. Requires policies:delete. Hard delete is only available from the admin UI.
POST /api/v1/agent/policies/:id/publish
Section titled “POST /api/v1/agent/policies/:id/publish”Publishes a DRAFT or SUGGESTION policy. Triggers embedding regeneration via the AI service (same as clicking Save & publish in the editor). Requires policies:publish. Republishing an already-PUBLISHED policy returns 400.
Documents
Section titled “Documents”Upload a file and let the AI pipeline extract policies from it.
POST /api/v1/agent/documents
Section titled “POST /api/v1/agent/documents”Async — returns 202 immediately. Requires documents:write.
curl -X POST https://api.inpolicy.ai/api/v1/agent/documents \ -H "Authorization: Bearer $INPOLICY_API_KEY" \ -F "file=@./security-handbook.pdf" \ -F "mode=stage" \ -F "policyAreaId=<uuid>"| Field | Required | Values | Default |
|---|---|---|---|
file | yes | PDF, DOCX, TXT, MD (max 25 MB) | — |
title | no | string | filename |
policyAreaId | no | UUID | — |
mode | no | stage | draft | publish | stage |
externalDocumentId | no | string | — |
Modes:
stage— extracted policies go to the Policy Inbox for human review before any action is taken.draft— extracted policies are created as DRAFT; reviewable in the web app.publish— runs the full publish pipeline immediately. Requirespolicies:publishon the key.
Response: { documentId: string, checkUrl: string, status: "pending" }.
GET /api/v1/agent/documents/:id
Section titled “GET /api/v1/agent/documents/:id”Poll for status. Requires documents:write.
{ "id": "doc_...", "status": "completed", "extractedPolicies": [ { "id": "pol_...", "title": "Data Retention", "status": "DRAFT" } ]}Status values: pending → processing → completed | failed. Poll until terminal. Typical completion time: 10–60 seconds depending on file size.
Sources
Section titled “Sources”Register a public URL for recurring sync. Useful for policies hosted in Confluence, Notion, or a public handbook URL. InPolicy fetches the URL, extracts policies, and re-syncs on a daily schedule.
POST /api/v1/agent/sources
Section titled “POST /api/v1/agent/sources”Returns 202. Requires documents:write.
POST /api/v1/agent/sources{ "url": "https://handbook.acme.com/security-policy", "title": "Security Handbook", "mode": "stage", "schedule": "daily", "policyAreaId": "<uuid>"}The initial fetch is enqueued immediately. Subsequent fetches run on the configured schedule.
Note: v1 supports public URLs only — no authentication headers, no Confluence OAuth, no Notion tokens. For authenticated sources, upload directly with POST /api/v1/agent/documents.
GET /api/v1/agent/sources
Section titled “GET /api/v1/agent/sources”List registered sources. Requires documents:write.
GET /api/v1/agent/sources/:id
Section titled “GET /api/v1/agent/sources/:id”Fetch one source. Requires documents:write.
DELETE /api/v1/agent/sources/:id
Section titled “DELETE /api/v1/agent/sources/:id”Stop syncing. The daily refresh is disabled; previously extracted policies remain in their current state. Requires documents:write.
Webhooks
Section titled “Webhooks”Receive real-time events when policies or documents change.
POST /api/v1/agent/webhooks
Section titled “POST /api/v1/agent/webhooks”Register a webhook. Requires webhooks:write.
POST /api/v1/agent/webhooks{ "url": "https://your-server.example.com/inpolicy-hook", "events": ["policy.published", "document.processing.completed"], "description": "Notify CI when policies publish"}Response: { id, url, events, secret, createdAt }. The secret is returned once — store it immediately to verify the X-InPolicy-Signature header on incoming deliveries.
Event types
Section titled “Event types”| Event | When it fires |
|---|---|
policy.created | New policy saved (any status) |
policy.updated | Policy body or metadata changed |
policy.published | Status set to PUBLISHED |
policy.retired | Status set to DEPRECATED |
document.processing.completed | Document extraction finished |
document.processing.failed | Document extraction failed |
source.synced | Source URL re-fetched successfully |
source.failed | Source URL fetch failed |
Verifying the signature
Section titled “Verifying the signature”InPolicy signs every delivery with HMAC-SHA256 using the secret returned at registration. Verify it on the receiver before processing:
import { createHmac, timingSafeEqual } from 'crypto';
function verifyInPolicySignature( body: Buffer, signatureHeader: string, secret: string,): boolean { const expected = createHmac('sha256', secret) .update(body) .digest('hex'); const expected_buf = Buffer.from(expected, 'utf8'); const received_buf = Buffer.from(signatureHeader, 'utf8'); if (expected_buf.length !== received_buf.length) return false; return timingSafeEqual(expected_buf, received_buf);}The X-InPolicy-Signature header value is the raw hex digest (no sha256= prefix).
Webhook delivery semantics
Section titled “Webhook delivery semantics”- InPolicy retries failed deliveries with exponential back-off. Your endpoint should return 2xx within 30 seconds.
- Each delivery has a unique
deliveryId— safe to use as an idempotency key. - Use
GET /api/v1/agent/webhooks/:id/deliveriesto inspect recent delivery attempts.
Other webhook endpoints
Section titled “Other webhook endpoints”GET /api/v1/agent/webhooks — list subscriptionsGET /api/v1/agent/webhooks/:id — fetch one subscriptionPATCH /api/v1/agent/webhooks/:id — update url, events, or descriptionDELETE /api/v1/agent/webhooks/:id — delete subscriptionGET /api/v1/agent/webhooks/:id/deliveries — delivery history (last 50)Rate limits and metering
Section titled “Rate limits and metering”All management endpoints share the same rate limit as the governance API. The default limit is 300 requests per minute per API key. Bulk upsert counts as one request regardless of how many policies it contains.
API usage is metered per tenant. You can view usage in Settings → API Keys.
Gotchas
Section titled “Gotchas”externalIdis your primary key, not a label. Changing anexternalIdin frontmatter creates a new policy rather than updating the old one. If you need to rename, usePATCH /api/v1/agent/policies/:idto updateexternalIdexplicitly, then delete the old entry.publish=truein bulk upsert requirespolicies:publishon the API key, not justpolicies:write. The request will fail with 403 if the key lacks that scope.mode=publishon document upload also requirespolicies:publish. The same scope check applies.DELETEis soft. Deleted policies stay in the database as DEPRECATED. Use the admin UI for a hard delete.- Embeddings regenerate on publish. If you publish 50 policies in one bulk call, 50 embedding jobs are queued. There may be a brief window (30–60 seconds) where the new policies aren’t yet enforced by the extension and Mac app.
POST /api/v1/agent/policies/:id/publishis idempotent on DRAFT → PUBLISHED but not on PUBLISHED → PUBLISHED. Calling it on an already-published policy returns 400. Check status first if needed.- Webhook secrets are stored and returned once. There is no
GET /api/v1/agent/webhooks/:id/secretendpoint. If you lose the secret, delete the subscription and re-register.