Skip to main content
Main content

Developer Portal

API reference

Every endpoint of the Vouli IQ Public API, generated from its OpenAPI 3.1.0 document — parameters, schemas, scopes and examples in curl, TypeScript and Python. Each read endpoint has a Try-it panel that calls the API from this page with a key you paste; use a vk_test_ sandbox key.

Version
2026-09-12
Base URL
https://www.vouliiq.com/api/v1
Operations
30

Getting started

Send the key as a bearer token and pin the dated version. A vk_test_ key reads a fixed set of synthetic rows and never touches tenant data.

Authorization: Bearer vk_test_XXXXXXXX_your_secret
Vouli-Version: 2026-09-12
  • Rate limits: per-key token bucket (capacity = per-minute limit, refill = limit/60 per second) plus a rolling 24h cap.
  • Pagination: cursor-based; limit 1–100 (default 25); pass next_cursor back as cursor.
  • Errors are always { error: { type, code, message, request_id } } — branch on code.

Command line

The TypeScript SDK includes a small vouli CLI. It reads the key from VOULI_API_KEY, never prints it, and knows the same resources as the SDK (compliance, gaps, incidents, inventory, kpis, milestones, obligations, risks, roadmap, scorecard, scores, signals, talent, vendor). Add --json for the raw envelope. Until registry publication, run it from the repository's sdk/vouli-iq directory after building it.

# The CLI ships inside the TypeScript SDK (bin: vouli). Key from the environment; never printed.
export VOULI_API_KEY=vk_test_…

vouli whoami
vouli list gaps --limit 10
vouli list gaps --limit 10 --cursor <next_cursor>
vouli get compliance
vouli list gaps --json

Meta

Discovery: the index and this document.

GET/

Discover the resources this API version exposes.

Unauthenticated index: the current dated version, the versions still served, every resource with its required scope, and where to find the spec and the SDKs.

Authentication
None — this endpoint is public.
Required scope
—
Rate limit
Not rate limited per key.
Idempotency
Safe to retry — reads never change data.

Responses

  • 200 The API index.
200 response of GET /
FieldTypeDescription
object"api_index"
api_version"2026-09-12"
supported_versionsarray of string
openapi_urlstring
documentation_url?string
resourcesarray of object
operations?array of objectEvery write and management operation, with the scope it needs.

Examples

curl

curl -X GET "https://www.vouliiq.com/api/v1" \
  -H "Vouli-Version: 2026-09-12"

TypeScript

// Not wrapped by the SDK: a plain HTTP call.
const res = await fetch('https://www.vouliiq.com/api/v1', {
  method: 'GET',
  headers: {
    'Vouli-Version': '2026-09-12',
  },
});
console.log(res.status, await res.json());

Python

# Not wrapped by the SDK: a plain HTTP call.
import json
import urllib.request

req = urllib.request.Request(
    "https://www.vouliiq.com/api/v1",
    method="GET",
    headers={"Vouli-Version": "2026-09-12"},
)
with urllib.request.urlopen(req) as res:
    print(res.status, json.loads(res.read()))

Try it

Send GET / from this browser

Use a vk_test_ sandbox key: it returns synthetic data only. The key stays in this tab's memory — it is never saved, and it is gone when you reload.

GET /api/v1

GET/openapi.json

Fetch this OpenAPI 3.1 document.

Unauthenticated. Generated from the same schemas the handlers enforce, so it cannot drift from the API.

Authentication
None — this endpoint is public.
Required scope
—
Rate limit
Not rate limited per key.
Idempotency
Safe to retry — reads never change data.

Responses

  • 200 The OpenAPI 3.1 document.

object

Examples

curl

curl -X GET "https://www.vouliiq.com/api/v1/openapi.json" \
  -H "Vouli-Version: 2026-09-12"

TypeScript

// Not wrapped by the SDK: a plain HTTP call.
const res = await fetch('https://www.vouliiq.com/api/v1/openapi.json', {
  method: 'GET',
  headers: {
    'Vouli-Version': '2026-09-12',
  },
});
console.log(res.status, await res.json());

Python

# Not wrapped by the SDK: a plain HTTP call.
import json
import urllib.request

req = urllib.request.Request(
    "https://www.vouliiq.com/api/v1/openapi.json",
    method="GET",
    headers={"Vouli-Version": "2026-09-12"},
)
with urllib.request.urlopen(req) as res:
    print(res.status, json.loads(res.read()))

Try it

Send GET /openapi.json from this browser

Use a vk_test_ sandbox key: it returns synthetic data only. The key stays in this tab's memory — it is never saved, and it is gone when you reload.

GET /api/v1/openapi.json

MCP

Model Context Protocol server: the same resources as read-only tools for AI assistants.

POST/mcp

Model Context Protocol (MCP) server: read-only tools for AI assistants.

Streamable HTTP transport, JSON-RPC 2.0, one message per POST, single application/json responses (no SSE stream: GET and DELETE answer 405 with Allow: POST; no session id). Methods: initialize, notifications/initialized (202, no body), ping, tools/list, tools/call. Authentication, the plan gate, the per-key rate limit and the sandbox are the same as every other operation here; each tool requires its resource's scope and returns the same envelope as the matching GET, as structuredContent and as JSON text. Tool errors (a missing scope, a read failure) are results with isError: true; unknown methods are -32601, bad arguments -32602.

Authentication
Authorization: Bearer vk_…
Required scope
any valid key
Rate limit
Per key. X-RateLimit-* on every response; a 429 carries Retry-After.
Idempotency
Not declared idempotent.

Parameters

Parameters of POST /mcp
NameInTypeDescription
MCP-Protocol-Versionheaderstringone of: 2025-06-18, 2025-03-26, 2024-11-05Sent by a client after initialize. An unsupported value is a 400.

Request body (application/json, required)

Request body of POST /mcp
FieldTypeDescription
jsonrpc"2.0"
id?string | integerOmit for a notification.
methodstringone of: initialize, notifications/initialized, ping, tools/list, tools/call
params?object
{
  "jsonrpc": "2.0",
  "id": "example_id",
  "method": "initialize",
  "params": {}
}

Responses

  • 200 A JSON-RPC 2.0 response: a result, or an error for an unknown method or invalid params.
  • 202 A notification (or a client response) was accepted. No body.
  • 400 Parse error (-32700), an invalid or batched message (-32600), or an unsupported MCP-Protocol-Version.
  • 401 No key, or the key does not verify. Carries WWW-Authenticate: Bearer.
  • 403 The plan does not include API access, the subscription is not live, or the request came from a foreign browser origin.
  • 413 Body over 64 KB.
  • 429 Per-key rate limit exhausted. Wait Retry-After seconds.
200 response of POST /mcp
FieldTypeDescription
jsonrpc"2.0"

Examples

curl

curl -X POST "https://www.vouliiq.com/api/v1/mcp" \
  -H "Authorization: Bearer $VOULI_API_KEY" \
  -H "Vouli-Version: 2026-09-12" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":"example_id","method":"initialize","params":{}}'

TypeScript

// Not wrapped by the SDK: a plain HTTP call.
const res = await fetch('https://www.vouliiq.com/api/v1/mcp', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.VOULI_API_KEY}`,
    'Vouli-Version': '2026-09-12',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    jsonrpc: '2.0',
    id: 'example_id',
    method: 'initialize',
    params: {},
  }),
});
console.log(res.status, await res.json());

Python

# Not wrapped by the SDK: a plain HTTP call.
import json
import os
import urllib.request

req = urllib.request.Request(
    "https://www.vouliiq.com/api/v1/mcp",
    method="POST",
    headers={"Authorization": f"Bearer {os.environ['VOULI_API_KEY']}", "Vouli-Version": "2026-09-12", "Content-Type": "application/json"},
    data=json.dumps({
        "jsonrpc": "2.0",
        "id": "example_id",
        "method": "initialize",
        "params": {},
    }).encode(),
)
with urllib.request.urlopen(req) as res:
    print(res.status, json.loads(res.read()))

Compliance

GET/compliance

Retrieve the active compliance posture.

Overall posture per regime and jurisdiction. Individual obligations are a separate, paginated list at /compliance/obligations.

Authentication
Authorization: Bearer vk_…
Required scope
read:compliance
Rate limit
Per key. X-RateLimit-* on every response; a 429 carries Retry-After.
Idempotency
Safe to retry — reads never change data.

Responses

  • 200 The current object, or null.
  • 400 Malformed request: a bad limit, a cursor we did not issue, an unknown filter value, or an unsupported version.
  • 401 No key presented, or the key does not verify (revoked, expired, mistyped).
  • 403 The key verified but the plan does not include API access, or the key lacks the resource’s scope.
  • 429 Per-key rate limit exhausted. Wait Retry-After seconds.
  • 500 Something failed on our side. The request_id identifies this exact request.
200 response of GET /compliance
FieldTypeDescription
object"compliance_posture"
api_version"2026-09-12"
dataCompliancePosture | null · CompliancePostureThe current object, or null when the org has none yet.
has_morefalse
next_cursornull
Sandbox example of data
{
  "object": "compliance_posture",
  "id": "00000000-0000-4000-8000-000000000601",
  "version_number": 2,
  "overall_posture": "PARTIAL",
  "primary_jurisdictions": [
    "EU",
    "UK",
    "IN"
  ],
  "regimes": [
    {
      "regime_id": "eu-ai-act",
      "posture": "PARTIAL",
      "open_obligations": 4
    }
  ],
  "executive_summary": "[SANDBOX] Four EU AI Act obligations are open, two of them high priority.",
  "generated_at": "2026-09-01T09:00:00.000Z"
}

Examples

curl

curl -X GET "https://www.vouliiq.com/api/v1/compliance" \
  -H "Authorization: Bearer $VOULI_API_KEY" \
  -H "Vouli-Version: 2026-09-12"

TypeScript SDK

import { VouliIQ } from '@vouli-iq/sdk';

const vouli = new VouliIQ({ apiKey: process.env.VOULI_API_KEY! });

const result = await vouli.compliance();
console.log(result.data);

Python SDK

import os
from vouli_iq import VouliIQ

vouli = VouliIQ(api_key=os.environ["VOULI_API_KEY"])

result = vouli.compliance()
print(result["data"])

Try it

Send GET /compliance from this browser

Use a vk_test_ sandbox key: it returns synthetic data only. The key stays in this tab's memory — it is never saved, and it is gone when you reload.

GET /api/v1/compliance

GET/compliance/obligations

List compliance obligations, newest first.

Every tracked obligation with its regime, jurisdiction, priority band, owner and target date. Cursor-paginated.

Authentication
Authorization: Bearer vk_…
Required scope
read:compliance
Rate limit
Per key. X-RateLimit-* on every response; a 429 carries Retry-After.
Idempotency
Safe to retry — reads never change data.

Parameters

Parameters of GET /compliance/obligations
NameInTypeDescription
cursorquerystringOpaque cursor from a previous response’s next_cursor. A cursor this API did not issue is rejected with 400 invalid_cursor.
limitqueryinteger (1–100)Rows to return, 1–100. Defaults to 25.
regime_idquerystringOnly obligations arising from this regime.
statusquerystringOnly obligations in this status.
updated_sincequerystringOnly rows created or changed at or after this instant — an ISO-8601 date (midnight UTC) or timestamp. Use it for incremental sync: store the time you started a sync and pass it next time.

Responses

  • 200 A page of results.
  • 400 Malformed request: a bad limit, a cursor we did not issue, an unknown filter value, or an unsupported version.
  • 401 No key presented, or the key does not verify (revoked, expired, mistyped).
  • 403 The key verified but the plan does not include API access, or the key lacks the resource’s scope.
  • 429 Per-key rate limit exhausted. Wait Retry-After seconds.
  • 500 Something failed on our side. The request_id identifies this exact request.
200 response of GET /compliance/obligations
FieldTypeDescription
object"list"
api_version"2026-09-12"
dataarray of Obligation · Obligation
has_morebooleanTrue when another page exists.
next_cursorstring | nullOpaque cursor for the next page. Pass it back as ?cursor=. Null when has_more is false.
Sandbox example of data
[
  {
    "object": "obligation",
    "id": "00000000-0000-4000-8000-000000000701",
    "obligation_key": "SANDBOX-O-001",
    "regime_id": "eu-ai-act",
    "regime_name": "EU AI Act",
    "jurisdiction": "EU",
    "title": "[SANDBOX] Maintain technical documentation for each high-risk system.",
    "priority_band": "HIGH",
    "status": "open",
    "owner_role": "Head of Compliance",
    "target_date": "2026-12-01",
    "created_at": "2026-09-01T09:00:00.000Z"
  },
  {
    "object": "obligation",
    "id": "00000000-0000-4000-8000-000000000702",
    "obligation_key": "SANDBOX-O-002",
    "regime_id": "dpdp",
    "regime_name": "India DPDP Act",
    "jurisdiction": "IN",
    "title": "[SANDBOX] Register a grievance officer and publish the contact route.",
    "priority_band": "MEDIUM",
    "status": "in_progress",
    "owner_role": null,
    "target_date": null,
    "created_at": "2026-08-01T09:00:00.000Z"
  }
]

Examples

curl

curl -X GET "https://www.vouliiq.com/api/v1/compliance/obligations?limit=10" \
  -H "Authorization: Bearer $VOULI_API_KEY" \
  -H "Vouli-Version: 2026-09-12"

TypeScript SDK

import { VouliIQ } from '@vouli-iq/sdk';

const vouli = new VouliIQ({ apiKey: process.env.VOULI_API_KEY! });

const page = await vouli.obligations({ limit: 10 });
console.log(page.data, page.has_more, page.next_cursor);

Python SDK

import os
from vouli_iq import VouliIQ

vouli = VouliIQ(api_key=os.environ["VOULI_API_KEY"])

page = vouli.obligations(limit=10)
print(page["data"], page["has_more"], page["next_cursor"])

Try it

Send GET /compliance/obligations from this browser

Use a vk_test_ sandbox key: it returns synthetic data only. The key stays in this tab's memory — it is never saved, and it is gone when you reload.

Parameters
GET /api/v1/compliance/obligations

Incidents

GET/incidents

List AI incidents, newest first.

The AI Incident Command register: every declared incident with its type, severity, status, owner and notification clock. Requires the plan that includes Incident Command. Cursor-paginated.

Authentication
Authorization: Bearer vk_…
Required scope
read:incidents
Rate limit
Per key. X-RateLimit-* on every response; a 429 carries Retry-After.
Idempotency
Safe to retry — reads never change data.

Parameters

Parameters of GET /incidents
NameInTypeDescription
cursorquerystringOpaque cursor from a previous response’s next_cursor. A cursor this API did not issue is rejected with 400 invalid_cursor.
limitqueryinteger (1–100)Rows to return, 1–100. Defaults to 25.
severityquerystringone of: low, medium, high, criticalOnly incidents at this severity.
statusquerystringone of: open, investigating, contained, resolved, post_review, closedOnly incidents in this lifecycle state.
updated_sincequerystringOnly rows created or changed at or after this instant — an ISO-8601 date (midnight UTC) or timestamp. Use it for incremental sync: store the time you started a sync and pass it next time.

Responses

  • 200 A page of results.
  • 400 Malformed request: a bad limit, a cursor we did not issue, an unknown filter value, or an unsupported version.
  • 401 No key presented, or the key does not verify (revoked, expired, mistyped).
  • 403 The key verified but the plan does not include API access, or the key lacks the resource’s scope.
  • 429 Per-key rate limit exhausted. Wait Retry-After seconds.
  • 500 Something failed on our side. The request_id identifies this exact request.
200 response of GET /incidents
FieldTypeDescription
object"list"
api_version"2026-09-12"
dataarray of Incident · Incident
has_morebooleanTrue when another page exists.
next_cursorstring | nullOpaque cursor for the next page. Pass it back as ?cursor=. Null when has_more is false.
Sandbox example of data
[
  {
    "object": "incident",
    "id": "00000000-0000-4000-8000-000000001201",
    "title": "[SANDBOX] Credit model drifted outside its approved fairness band",
    "description": "[SANDBOX] Approval-rate gap between cohorts exceeded the approved threshold for two days.",
    "incident_type": "bias_harm",
    "severity": "high",
    "status": "investigating",
    "affected_systems": "[SANDBOX] Credit decisioning",
    "jurisdictions": "EU",
    "owner": "[SANDBOX] Head of Model Risk",
    "notification_required": true,
    "notification_deadline": "2026-09-04T09:00:00.000Z",
    "notified_at": null,
    "created_at": "2026-09-01T09:00:00.000Z",
    "updated_at": "2026-09-01T09:00:00.000Z"
  },
  {
    "object": "incident",
    "id": "00000000-0000-4000-8000-000000001202",
    "title": "[SANDBOX] Support agent quoted an unpublished refund policy",
    "description": null,
    "incident_type": "agent_failure",
    "severity": "medium",
    "status": "resolved",
    "affected_systems": null,
    "jurisdictions": null,
    "owner": null,
    "notification_required": false,
    "notification_deadline": null,
    "notified_at": null,
    "created_at": "2026-08-01T09:00:00.000Z",
    "updated_at": "2026-08-01T09:00:00.000Z"
  }
]

Examples

curl

curl -X GET "https://www.vouliiq.com/api/v1/incidents?limit=10" \
  -H "Authorization: Bearer $VOULI_API_KEY" \
  -H "Vouli-Version: 2026-09-12"

TypeScript SDK

import { VouliIQ } from '@vouli-iq/sdk';

const vouli = new VouliIQ({ apiKey: process.env.VOULI_API_KEY! });

const page = await vouli.incidents({ limit: 10 });
console.log(page.data, page.has_more, page.next_cursor);

Python SDK

import os
from vouli_iq import VouliIQ

vouli = VouliIQ(api_key=os.environ["VOULI_API_KEY"])

page = vouli.incidents(limit=10)
print(page["data"], page["has_more"], page["next_cursor"])

Try it

Send GET /incidents from this browser

Use a vk_test_ sandbox key: it returns synthetic data only. The key stays in this tab's memory — it is never saved, and it is gone when you reload.

Parameters
GET /api/v1/incidents

POST/incidents

Declare an AI incident.

Declares an incident in Incident Command, starting its timeline and notification clock. Requires an Idempotency-Key.

Authentication
Authorization: Bearer vk_…
Required scope
write:incidents
Rate limit
Per key. X-RateLimit-* on every response; a 429 carries Retry-After.
Idempotency
Send an Idempotency-Key; a retry with the same key replays the first response for 24h.

Parameters

Parameters of POST /incidents
NameInTypeDescription
Idempotency-KeyrequiredheaderstringREQUIRED. A unique string you generate per logical request (a UUID), resent unchanged on every retry. The first response is replayed for 24h (with Idempotency-Replayed: true); the same key with a different body is a 409 idempotency_key_reused.

Request body (application/json, required)

Request body of POST /incidents
FieldTypeDescription
affected_systems?stringSystems the incident touched.
description?stringLonger account.
incident_type?stringone of: model_failure, bias_harm, data_exposure, agent_failure, regulatory_inquiry, shadow_ai_breach, otherKind of incident. Defaults to other.
jurisdictions?stringJurisdictions whose notification rules apply.
notification_deadline?string | null (date-time)When the notification is due (ISO-8601).
notification_required?booleanWhether a regulator or stakeholder must be notified.
owner?stringAccountable incident owner (a name or role).
severity?stringone of: low, medium, high, criticalDefaults to medium.
status?stringone of: open, investigating, contained, resolved, post_review, closedDefaults to open.
titlestringWhat happened, in one line.
{
  "affected_systems": "example_affected_systems",
  "description": "example_description",
  "incident_type": "model_failure",
  "jurisdictions": "example_jurisdictions",
  "notification_deadline": "2026-01-01T00:00:00.000Z",
  "notification_required": true,
  "owner": "example_owner",
  "severity": "low",
  "status": "open",
  "title": "example_title"
}

Responses

  • 201 Created. The record as stored.
  • 400 Malformed request: a bad limit, a cursor we did not issue, an unknown filter value, or an unsupported version.
  • 401 No key presented, or the key does not verify (revoked, expired, mistyped).
  • 403 The key verified but the plan does not include API access, or the key lacks the resource’s scope.
  • 409 Idempotency conflict (key reused with another body, or still in flight), or a duplicate record (resource_conflict).
  • 413 Body over 64 KB.
  • 415 Body sent with a Content-Type other than application/json.
  • 429 Per-key rate limit exhausted. Wait Retry-After seconds.
  • 500 Something failed on our side. The request_id identifies this exact request.
  • 503 The idempotency store or the module is unavailable; nothing was done. Retry with the same Idempotency-Key.
201 response of POST /incidents
FieldTypeDescription
object"incident"
api_version"2026-09-12"
dataIncident · Incident
has_morefalse
next_cursornull

Examples

curl

curl -X POST "https://www.vouliiq.com/api/v1/incidents" \
  -H "Authorization: Bearer $VOULI_API_KEY" \
  -H "Vouli-Version: 2026-09-12" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"affected_systems":"example_affected_systems","description":"example_description","incident_type":"model_failure","jurisdictions":"example_jurisdictions","notification_deadline":"2026-01-01T00:00:00.000Z","notification_required":true,"owner":"example_owner","severity":"low","status":"open","title":"example_title"}'

TypeScript

// Not wrapped by the SDK: a plain HTTP call.
const res = await fetch('https://www.vouliiq.com/api/v1/incidents', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.VOULI_API_KEY}`,
    'Vouli-Version': '2026-09-12',
    'Idempotency-Key': crypto.randomUUID(),
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    affected_systems: 'example_affected_systems',
    description: 'example_description',
    incident_type: 'model_failure',
    jurisdictions: 'example_jurisdictions',
    notification_deadline: '2026-01-01T00:00:00.000Z',
    notification_required: true,
    owner: 'example_owner',
    severity: 'low',
    status: 'open',
    title: 'example_title',
  }),
});
console.log(res.status, await res.json());

Python

# Not wrapped by the SDK: a plain HTTP call.
import json
import os
import urllib.request
import uuid

req = urllib.request.Request(
    "https://www.vouliiq.com/api/v1/incidents",
    method="POST",
    headers={"Authorization": f"Bearer {os.environ['VOULI_API_KEY']}", "Vouli-Version": "2026-09-12", "Idempotency-Key": str(uuid.uuid4()), "Content-Type": "application/json"},
    data=json.dumps({
        "affected_systems": "example_affected_systems",
        "description": "example_description",
        "incident_type": "model_failure",
        "jurisdictions": "example_jurisdictions",
        "notification_deadline": "2026-01-01T00:00:00.000Z",
        "notification_required": True,
        "owner": "example_owner",
        "severity": "low",
        "status": "open",
        "title": "example_title",
    }).encode(),
)
with urllib.request.urlopen(req) as res:
    print(res.status, json.loads(res.read()))

PATCH/incidents/{id}

Change an incident.

Changes the fields given; a status change is logged on the incident timeline. An incident id from another organisation is a 404.

Authentication
Authorization: Bearer vk_…
Required scope
write:incidents
Rate limit
Per key. X-RateLimit-* on every response; a 429 carries Retry-After.
Idempotency
Send an Idempotency-Key; a retry with the same key replays the first response for 24h.

Parameters

Parameters of PATCH /incidents/{id}
NameInTypeDescription
idrequiredpathstring (uuid)The record id. An id that is not in this organisation (including one from another organisation) is a 404.
Idempotency-KeyheaderstringOptional. Same replay semantics as on POST, for 24h.

Request body (application/json, required)

Request body of PATCH /incidents/{id}
FieldTypeDescription
affected_systems?string | nullSystems the incident touched.
description?string | nullLonger account.
incident_type?stringone of: model_failure, bias_harm, data_exposure, agent_failure, regulatory_inquiry, shadow_ai_breach, otherKind of incident.
jurisdictions?string | nullJurisdictions whose notification rules apply.
notification_deadline?string | null (date-time)When the notification is due (ISO-8601).
notification_required?booleanWhether a regulator or stakeholder must be notified.
owner?string | nullAccountable incident owner (a name or role).
severity?stringone of: low, medium, high, criticalSeverity.
status?stringone of: open, investigating, contained, resolved, post_review, closedLifecycle state.
title?stringWhat happened, in one line.
{
  "affected_systems": "example_affected_systems",
  "description": "example_description",
  "incident_type": "model_failure",
  "jurisdictions": "example_jurisdictions",
  "notification_deadline": "2026-01-01T00:00:00.000Z",
  "notification_required": true,
  "owner": "example_owner",
  "severity": "low",
  "status": "open",
  "title": "example_title"
}

Responses

  • 200 Done. The record as it now stands.
  • 400 Malformed request: a bad limit, a cursor we did not issue, an unknown filter value, or an unsupported version.
  • 401 No key presented, or the key does not verify (revoked, expired, mistyped).
  • 403 The key verified but the plan does not include API access, or the key lacks the resource’s scope.
  • 404 No such record in this organisation.
  • 409 Idempotency conflict (key reused with another body, or still in flight), or a duplicate record (resource_conflict).
  • 413 Body over 64 KB.
  • 415 Body sent with a Content-Type other than application/json.
  • 429 Per-key rate limit exhausted. Wait Retry-After seconds.
  • 500 Something failed on our side. The request_id identifies this exact request.
  • 503 The idempotency store or the module is unavailable; nothing was done. Retry with the same Idempotency-Key.
200 response of PATCH /incidents/{id}
FieldTypeDescription
object"incident"
api_version"2026-09-12"
dataIncident · Incident
has_morefalse
next_cursornull

Examples

curl

curl -X PATCH "https://www.vouliiq.com/api/v1/incidents/00000000-0000-4000-8000-000000000000" \
  -H "Authorization: Bearer $VOULI_API_KEY" \
  -H "Vouli-Version: 2026-09-12" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"affected_systems":"example_affected_systems","description":"example_description","incident_type":"model_failure","jurisdictions":"example_jurisdictions","notification_deadline":"2026-01-01T00:00:00.000Z","notification_required":true,"owner":"example_owner","severity":"low","status":"open","title":"example_title"}'

TypeScript

// Not wrapped by the SDK: a plain HTTP call.
const res = await fetch('https://www.vouliiq.com/api/v1/incidents/00000000-0000-4000-8000-000000000000', {
  method: 'PATCH',
  headers: {
    Authorization: `Bearer ${process.env.VOULI_API_KEY}`,
    'Vouli-Version': '2026-09-12',
    'Idempotency-Key': crypto.randomUUID(),
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    affected_systems: 'example_affected_systems',
    description: 'example_description',
    incident_type: 'model_failure',
    jurisdictions: 'example_jurisdictions',
    notification_deadline: '2026-01-01T00:00:00.000Z',
    notification_required: true,
    owner: 'example_owner',
    severity: 'low',
    status: 'open',
    title: 'example_title',
  }),
});
console.log(res.status, await res.json());

Python

# Not wrapped by the SDK: a plain HTTP call.
import json
import os
import urllib.request
import uuid

req = urllib.request.Request(
    "https://www.vouliiq.com/api/v1/incidents/00000000-0000-4000-8000-000000000000",
    method="PATCH",
    headers={"Authorization": f"Bearer {os.environ['VOULI_API_KEY']}", "Vouli-Version": "2026-09-12", "Idempotency-Key": str(uuid.uuid4()), "Content-Type": "application/json"},
    data=json.dumps({
        "affected_systems": "example_affected_systems",
        "description": "example_description",
        "incident_type": "model_failure",
        "jurisdictions": "example_jurisdictions",
        "notification_deadline": "2026-01-01T00:00:00.000Z",
        "notification_required": True,
        "owner": "example_owner",
        "severity": "low",
        "status": "open",
        "title": "example_title",
    }).encode(),
)
with urllib.request.urlopen(req) as res:
    print(res.status, json.loads(res.read()))

KPIs

GET/kpis

List KPIs (key results) with their targets and latest actuals, newest first.

Every key result in the OKR & KPI Engine with its unit, direction, target and current value — the targets an external system pushes actuals to (REST: POST /kpis/checkins). Cursor-paginated.

Authentication
Authorization: Bearer vk_…
Required scope
read:kpis
Rate limit
Per key. X-RateLimit-* on every response; a 429 carries Retry-After.
Idempotency
Safe to retry — reads never change data.

Parameters

Parameters of GET /kpis
NameInTypeDescription
cursorquerystringOpaque cursor from a previous response’s next_cursor. A cursor this API did not issue is rejected with 400 invalid_cursor.
limitqueryinteger (1–100)Rows to return, 1–100. Defaults to 25.
codequerystringOnly the key result with this code.
statusquerystringone of: draft, active, achieved, missed, retiredOnly key results in this lifecycle state.
updated_sincequerystringOnly rows created or changed at or after this instant — an ISO-8601 date (midnight UTC) or timestamp. Use it for incremental sync: store the time you started a sync and pass it next time.

Responses

  • 200 A page of results.
  • 400 Malformed request: a bad limit, a cursor we did not issue, an unknown filter value, or an unsupported version.
  • 401 No key presented, or the key does not verify (revoked, expired, mistyped).
  • 403 The key verified but the plan does not include API access, or the key lacks the resource’s scope.
  • 429 Per-key rate limit exhausted. Wait Retry-After seconds.
  • 500 Something failed on our side. The request_id identifies this exact request.
200 response of GET /kpis
FieldTypeDescription
object"list"
api_version"2026-09-12"
dataarray of Kpi · Kpi
has_morebooleanTrue when another page exists.
next_cursorstring | nullOpaque cursor for the next page. Pass it back as ?cursor=. Null when has_more is false.
Sandbox example of data
[
  {
    "object": "kpi",
    "id": "00000000-0000-4000-8000-000000001301",
    "code": "SANDBOX-O1-KR1",
    "objective_id": "00000000-0000-4000-8000-000000001300",
    "statement": "[SANDBOX] Lift the share of AI use cases with a named accountable owner to 90%.",
    "metric_name": "[SANDBOX] Use cases with an accountable owner",
    "unit": "%",
    "direction": "increase",
    "baseline_value": 42,
    "target_value": 90,
    "current_value": 61,
    "current_as_of": "2026-09-01T09:00:00.000Z",
    "measurement_frequency": "monthly",
    "status": "active",
    "created_at": "2026-09-01T09:00:00.000Z",
    "updated_at": "2026-09-01T09:00:00.000Z"
  },
  {
    "object": "kpi",
    "id": "00000000-0000-4000-8000-000000001302",
    "code": "SANDBOX-O1-KR2",
    "objective_id": "00000000-0000-4000-8000-000000001300",
    "statement": "[SANDBOX] Cut median model-approval cycle time to 10 days.",
    "metric_name": "[SANDBOX] Median approval cycle time",
    "unit": "days",
    "direction": "decrease",
    "baseline_value": 34,
    "target_value": 10,
    "current_value": null,
    "current_as_of": null,
    "measurement_frequency": "quarterly",
    "status": "active",
    "created_at": "2026-08-01T09:00:00.000Z",
    "updated_at": "2026-08-01T09:00:00.000Z"
  }
]

Examples

curl

curl -X GET "https://www.vouliiq.com/api/v1/kpis?limit=10" \
  -H "Authorization: Bearer $VOULI_API_KEY" \
  -H "Vouli-Version: 2026-09-12"

TypeScript SDK

import { VouliIQ } from '@vouli-iq/sdk';

const vouli = new VouliIQ({ apiKey: process.env.VOULI_API_KEY! });

const page = await vouli.kpis({ limit: 10 });
console.log(page.data, page.has_more, page.next_cursor);

Python SDK

import os
from vouli_iq import VouliIQ

vouli = VouliIQ(api_key=os.environ["VOULI_API_KEY"])

page = vouli.kpis(limit=10)
print(page["data"], page["has_more"], page["next_cursor"])

Try it

Send GET /kpis from this browser

Use a vk_test_ sandbox key: it returns synthetic data only. The key stays in this tab's memory — it is never saved, and it is gone when you reload.

Parameters
GET /api/v1/kpis

POST/kpis/checkins

Push KPI actuals — up to 500 data points, all or nothing.

Records actuals against key results (by id or code) from an external system — a warehouse, a BI tool, a monitoring pipeline. Every point is validated first; one bad point refuses the whole batch and nothing is written. Each key result's current value follows its LATEST period, so back-filling history never rolls it backwards. Requires an Idempotency-Key.

Authentication
Authorization: Bearer vk_…
Required scope
write:kpis
Rate limit
Per key. X-RateLimit-* on every response; a 429 carries Retry-After.
Idempotency
Send an Idempotency-Key; a retry with the same key replays the first response for 24h.

Parameters

Parameters of POST /kpis/checkins
NameInTypeDescription
Idempotency-KeyrequiredheaderstringREQUIRED. A unique string you generate per logical request (a UUID), resent unchanged on every retry. The first response is replayed for 24h (with Idempotency-Replayed: true); the same key with a different body is a 409 idempotency_key_reused.

Request body (application/json, required)

Request body of POST /kpis/checkins
FieldTypeDescription
checkinsarray of object1–500 data points. All are validated before any is written; one bad point refuses the batch.
{
  "checkins": [
    {
      "evidence_ref": "example_evidence_ref",
      "key_result_code": "example_key_result_code",
      "key_result_id": "00000000-0000-4000-8000-000000000000",
      "note": "example_note",
      "period_start": "example_period_start",
      "source": "telemetry",
      "value": 1
    }
  ]
}

Responses

  • 201 Created. The record as stored.
  • 400 Malformed request: a bad limit, a cursor we did not issue, an unknown filter value, or an unsupported version.
  • 401 No key presented, or the key does not verify (revoked, expired, mistyped).
  • 403 The key verified but the plan does not include API access, or the key lacks the resource’s scope.
  • 404 No such record in this organisation.
  • 409 Idempotency conflict (key reused with another body, or still in flight), or a duplicate record (resource_conflict).
  • 413 Body over 512 KB.
  • 415 Body sent with a Content-Type other than application/json.
  • 429 Per-key rate limit exhausted. Wait Retry-After seconds.
  • 500 Something failed on our side. The request_id identifies this exact request.
  • 503 The idempotency store or the module is unavailable; nothing was done. Retry with the same Idempotency-Key.
201 response of POST /kpis/checkins
FieldTypeDescription
object"kpi_checkin_batch"
api_version"2026-09-12"
dataKpiCheckinBatch · KpiCheckinBatch
has_morefalse
next_cursornull

Examples

curl

curl -X POST "https://www.vouliiq.com/api/v1/kpis/checkins" \
  -H "Authorization: Bearer $VOULI_API_KEY" \
  -H "Vouli-Version: 2026-09-12" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"checkins":[{"evidence_ref":"example_evidence_ref","key_result_code":"example_key_result_code","key_result_id":"00000000-0000-4000-8000-000000000000","note":"example_note","period_start":"example_period_start","source":"telemetry","value":1}]}'

TypeScript

// Not wrapped by the SDK: a plain HTTP call.
const res = await fetch('https://www.vouliiq.com/api/v1/kpis/checkins', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.VOULI_API_KEY}`,
    'Vouli-Version': '2026-09-12',
    'Idempotency-Key': crypto.randomUUID(),
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    checkins: [
      {
        evidence_ref: 'example_evidence_ref',
        key_result_code: 'example_key_result_code',
        key_result_id: '00000000-0000-4000-8000-000000000000',
        note: 'example_note',
        period_start: 'example_period_start',
        source: 'telemetry',
        value: 1,
      },
    ],
  }),
});
console.log(res.status, await res.json());

Python

# Not wrapped by the SDK: a plain HTTP call.
import json
import os
import urllib.request
import uuid

req = urllib.request.Request(
    "https://www.vouliiq.com/api/v1/kpis/checkins",
    method="POST",
    headers={"Authorization": f"Bearer {os.environ['VOULI_API_KEY']}", "Vouli-Version": "2026-09-12", "Idempotency-Key": str(uuid.uuid4()), "Content-Type": "application/json"},
    data=json.dumps({
        "checkins": [
            {
                "evidence_ref": "example_evidence_ref",
                "key_result_code": "example_key_result_code",
                "key_result_id": "00000000-0000-4000-8000-000000000000",
                "note": "example_note",
                "period_start": "example_period_start",
                "source": "telemetry",
                "value": 1,
            },
        ],
    }).encode(),
)
with urllib.request.urlopen(req) as res:
    print(res.status, json.loads(res.read()))

Risks

GET/risks

List the AI risk register, newest first.

The org’s risk register with inherent and residual severity, owner, modelled exposure and target date. Cursor-paginated.

Authentication
Authorization: Bearer vk_…
Required scope
read:risks
Rate limit
Per key. X-RateLimit-* on every response; a 429 carries Retry-After.
Idempotency
Safe to retry — reads never change data.

Parameters

Parameters of GET /risks
NameInTypeDescription
cursorquerystringOpaque cursor from a previous response’s next_cursor. A cursor this API did not issue is rejected with 400 invalid_cursor.
limitqueryinteger (1–100)Rows to return, 1–100. Defaults to 25.
severityquerystringone of: CRITICAL, HIGH, MEDIUM, LOWOnly risks at this inherent severity.
statusquerystringone of: Open, InProgress, Mitigated, Accepted, ClosedOnly risks in this lifecycle state.
updated_sincequerystringOnly rows created or changed at or after this instant — an ISO-8601 date (midnight UTC) or timestamp. Use it for incremental sync: store the time you started a sync and pass it next time.

Responses

  • 200 A page of results.
  • 400 Malformed request: a bad limit, a cursor we did not issue, an unknown filter value, or an unsupported version.
  • 401 No key presented, or the key does not verify (revoked, expired, mistyped).
  • 403 The key verified but the plan does not include API access, or the key lacks the resource’s scope.
  • 429 Per-key rate limit exhausted. Wait Retry-After seconds.
  • 500 Something failed on our side. The request_id identifies this exact request.
200 response of GET /risks
FieldTypeDescription
object"list"
api_version"2026-09-12"
dataarray of Risk · Risk
has_morebooleanTrue when another page exists.
next_cursorstring | nullOpaque cursor for the next page. Pass it back as ?cursor=. Null when has_more is false.
Sandbox example of data
[
  {
    "object": "risk",
    "id": "00000000-0000-4000-8000-000000000201",
    "risk_id": "SANDBOX-R-001",
    "dimension": "Oversight",
    "description": "[SANDBOX] No named accountable owner for model approvals.",
    "severity": "CRITICAL",
    "residual_severity": "HIGH",
    "status": "Open",
    "source": "sovereign_red",
    "owner_name": "[SANDBOX] Chief Risk Officer",
    "financial_exposure_min": 250000,
    "financial_exposure_max": 1200000,
    "target_date": "2026-12-31",
    "created_at": "2026-09-01T09:00:00.000Z"
  },
  {
    "object": "risk",
    "id": "00000000-0000-4000-8000-000000000202",
    "risk_id": "SANDBOX-R-002",
    "dimension": "Value",
    "description": "[SANDBOX] Benefit claims for two initiatives are unevidenced.",
    "severity": "HIGH",
    "residual_severity": "MEDIUM",
    "status": "InProgress",
    "source": "ai_generated",
    "owner_name": "[SANDBOX] CFO",
    "financial_exposure_min": 80000,
    "financial_exposure_max": 300000,
    "target_date": null,
    "created_at": "2026-08-01T09:00:00.000Z"
  }
]

Examples

curl

curl -X GET "https://www.vouliiq.com/api/v1/risks?limit=10" \
  -H "Authorization: Bearer $VOULI_API_KEY" \
  -H "Vouli-Version: 2026-09-12"

TypeScript SDK

import { VouliIQ } from '@vouli-iq/sdk';

const vouli = new VouliIQ({ apiKey: process.env.VOULI_API_KEY! });

const page = await vouli.risks({ limit: 10 });
console.log(page.data, page.has_more, page.next_cursor);

Python SDK

import os
from vouli_iq import VouliIQ

vouli = VouliIQ(api_key=os.environ["VOULI_API_KEY"])

page = vouli.risks(limit=10)
print(page["data"], page["has_more"], page["next_cursor"])

Try it

Send GET /risks from this browser

Use a vk_test_ sandbox key: it returns synthetic data only. The key stays in this tab's memory — it is never saved, and it is gone when you reload.

Parameters
GET /api/v1/risks

POST/risks

Add a risk to the register.

Records a risk exactly as the Risk Radar's "add risk" form does (source user_added, status Open, the untreated baseline set from the severity and likelihood given). Requires an Idempotency-Key.

Authentication
Authorization: Bearer vk_…
Required scope
write:risks
Rate limit
Per key. X-RateLimit-* on every response; a 429 carries Retry-After.
Idempotency
Send an Idempotency-Key; a retry with the same key replays the first response for 24h.

Parameters

Parameters of POST /risks
NameInTypeDescription
Idempotency-KeyrequiredheaderstringREQUIRED. A unique string you generate per logical request (a UUID), resent unchanged on every retry. The first response is replayed for 24h (with Idempotency-Replayed: true); the same key with a different body is a 409 idempotency_key_reused.

Request body (application/json, required)

Request body of POST /risks
FieldTypeDescription
descriptionstringWhat the risk is, in at least 20 characters.
dimension?stringSOVEREIGN dimension, if known. Defaults to OTHER.
likelihoodstringone of: RARE, UNLIKELY, POSSIBLE, LIKELY, ALMOST_CERTAINLikelihood.
mitigation_action?stringWhat is being done about it.
owner_name?stringAccountable owner (a name or role, not a user id).
severitystringone of: CRITICAL, HIGH, MEDIUM, LOWInherent severity.
target_date?stringTarget mitigation date.
titlestringShort name of the risk.
{
  "description": "example_description",
  "dimension": "example_dimension",
  "likelihood": "RARE",
  "mitigation_action": "example_mitigation_action",
  "owner_name": "example_owner_name",
  "severity": "CRITICAL",
  "target_date": "example_target_date",
  "title": "example_title"
}

Responses

  • 201 Created. The record as stored.
  • 400 Malformed request: a bad limit, a cursor we did not issue, an unknown filter value, or an unsupported version.
  • 401 No key presented, or the key does not verify (revoked, expired, mistyped).
  • 403 The key verified but the plan does not include API access, or the key lacks the resource’s scope.
  • 409 Idempotency conflict (key reused with another body, or still in flight), or a duplicate record (resource_conflict).
  • 413 Body over 64 KB.
  • 415 Body sent with a Content-Type other than application/json.
  • 429 Per-key rate limit exhausted. Wait Retry-After seconds.
  • 500 Something failed on our side. The request_id identifies this exact request.
  • 503 The idempotency store or the module is unavailable; nothing was done. Retry with the same Idempotency-Key.
201 response of POST /risks
FieldTypeDescription
object"risk"
api_version"2026-09-12"
dataRisk · Risk
has_morefalse
next_cursornull

Examples

curl

curl -X POST "https://www.vouliiq.com/api/v1/risks" \
  -H "Authorization: Bearer $VOULI_API_KEY" \
  -H "Vouli-Version: 2026-09-12" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"description":"example_description","dimension":"example_dimension","likelihood":"RARE","mitigation_action":"example_mitigation_action","owner_name":"example_owner_name","severity":"CRITICAL","target_date":"example_target_date","title":"example_title"}'

TypeScript

// Not wrapped by the SDK: a plain HTTP call.
const res = await fetch('https://www.vouliiq.com/api/v1/risks', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.VOULI_API_KEY}`,
    'Vouli-Version': '2026-09-12',
    'Idempotency-Key': crypto.randomUUID(),
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    description: 'example_description',
    dimension: 'example_dimension',
    likelihood: 'RARE',
    mitigation_action: 'example_mitigation_action',
    owner_name: 'example_owner_name',
    severity: 'CRITICAL',
    target_date: 'example_target_date',
    title: 'example_title',
  }),
});
console.log(res.status, await res.json());

Python

# Not wrapped by the SDK: a plain HTTP call.
import json
import os
import urllib.request
import uuid

req = urllib.request.Request(
    "https://www.vouliiq.com/api/v1/risks",
    method="POST",
    headers={"Authorization": f"Bearer {os.environ['VOULI_API_KEY']}", "Vouli-Version": "2026-09-12", "Idempotency-Key": str(uuid.uuid4()), "Content-Type": "application/json"},
    data=json.dumps({
        "description": "example_description",
        "dimension": "example_dimension",
        "likelihood": "RARE",
        "mitigation_action": "example_mitigation_action",
        "owner_name": "example_owner_name",
        "severity": "CRITICAL",
        "target_date": "example_target_date",
        "title": "example_title",
    }).encode(),
)
with urllib.request.urlopen(req) as res:
    print(res.status, json.loads(res.read()))

PATCH/risks/{id}

Change a risk.

Changes the fields given, through the same rules as the app: lowering or clearing a severity or likelihood, or closing, accepting or mitigating a CRITICAL risk, needs a severity_change_reason and is recorded as evidence. A risk id from another organisation is a 404.

Authentication
Authorization: Bearer vk_…
Required scope
write:risks
Rate limit
Per key. X-RateLimit-* on every response; a 429 carries Retry-After.
Idempotency
Send an Idempotency-Key; a retry with the same key replays the first response for 24h.

Parameters

Parameters of PATCH /risks/{id}
NameInTypeDescription
idrequiredpathstring (uuid)The record id. An id that is not in this organisation (including one from another organisation) is a 404.
Idempotency-KeyheaderstringOptional. Same replay semantics as on POST, for 24h.

Request body (application/json, required)

Request body of PATCH /risks/{id}
FieldTypeDescription
likelihood?stringone of: RARE, UNLIKELY, POSSIBLE, LIKELY, ALMOST_CERTAINLikelihood.
mitigation_action?string | nullWhat is being done about it.
owner_name?string | nullAccountable owner (a name or role).
residual_gap_reason?string | nullStanding explanation for a residual below the untreated baseline.
residual_severity?string | nullone of: CRITICAL, HIGH, MEDIUM, LOW, nullSeverity after mitigation.
severity?stringone of: CRITICAL, HIGH, MEDIUM, LOWInherent severity.
severity_change_reason?string | nullRequired when the change LOWERS or clears a severity or likelihood, or closes, accepts or mitigates a CRITICAL risk (at least 20 characters). Recorded as evidence.
status?stringone of: Open, InProgress, Mitigated, Accepted, ClosedLifecycle state.
target_date?string | nullTarget mitigation date.
title?string | nullShort name of the risk.
{
  "likelihood": "RARE",
  "mitigation_action": "example_mitigation_action",
  "owner_name": "example_owner_name",
  "residual_gap_reason": "example_residual_gap_reason",
  "residual_severity": "CRITICAL",
  "severity": "CRITICAL",
  "severity_change_reason": "example_severity_change_reason",
  "status": "Open",
  "target_date": "example_target_date",
  "title": "example_title"
}

Responses

  • 200 Done. The record as it now stands.
  • 400 Malformed request: a bad limit, a cursor we did not issue, an unknown filter value, or an unsupported version.
  • 401 No key presented, or the key does not verify (revoked, expired, mistyped).
  • 403 The key verified but the plan does not include API access, or the key lacks the resource’s scope.
  • 404 No such record in this organisation.
  • 409 Idempotency conflict (key reused with another body, or still in flight), or a duplicate record (resource_conflict).
  • 413 Body over 64 KB.
  • 415 Body sent with a Content-Type other than application/json.
  • 429 Per-key rate limit exhausted. Wait Retry-After seconds.
  • 500 Something failed on our side. The request_id identifies this exact request.
  • 503 The idempotency store or the module is unavailable; nothing was done. Retry with the same Idempotency-Key.
200 response of PATCH /risks/{id}
FieldTypeDescription
object"risk"
api_version"2026-09-12"
dataRisk · Risk
has_morefalse
next_cursornull

Examples

curl

curl -X PATCH "https://www.vouliiq.com/api/v1/risks/00000000-0000-4000-8000-000000000000" \
  -H "Authorization: Bearer $VOULI_API_KEY" \
  -H "Vouli-Version: 2026-09-12" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"likelihood":"RARE","mitigation_action":"example_mitigation_action","owner_name":"example_owner_name","residual_gap_reason":"example_residual_gap_reason","residual_severity":"CRITICAL","severity":"CRITICAL","severity_change_reason":"example_severity_change_reason","status":"Open","target_date":"example_target_date","title":"example_title"}'

TypeScript

// Not wrapped by the SDK: a plain HTTP call.
const res = await fetch('https://www.vouliiq.com/api/v1/risks/00000000-0000-4000-8000-000000000000', {
  method: 'PATCH',
  headers: {
    Authorization: `Bearer ${process.env.VOULI_API_KEY}`,
    'Vouli-Version': '2026-09-12',
    'Idempotency-Key': crypto.randomUUID(),
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    likelihood: 'RARE',
    mitigation_action: 'example_mitigation_action',
    owner_name: 'example_owner_name',
    residual_gap_reason: 'example_residual_gap_reason',
    residual_severity: 'CRITICAL',
    severity: 'CRITICAL',
    severity_change_reason: 'example_severity_change_reason',
    status: 'Open',
    target_date: 'example_target_date',
    title: 'example_title',
  }),
});
console.log(res.status, await res.json());

Python

# Not wrapped by the SDK: a plain HTTP call.
import json
import os
import urllib.request
import uuid

req = urllib.request.Request(
    "https://www.vouliiq.com/api/v1/risks/00000000-0000-4000-8000-000000000000",
    method="PATCH",
    headers={"Authorization": f"Bearer {os.environ['VOULI_API_KEY']}", "Vouli-Version": "2026-09-12", "Idempotency-Key": str(uuid.uuid4()), "Content-Type": "application/json"},
    data=json.dumps({
        "likelihood": "RARE",
        "mitigation_action": "example_mitigation_action",
        "owner_name": "example_owner_name",
        "residual_gap_reason": "example_residual_gap_reason",
        "residual_severity": "CRITICAL",
        "severity": "CRITICAL",
        "severity_change_reason": "example_severity_change_reason",
        "status": "Open",
        "target_date": "example_target_date",
        "title": "example_title",
    }).encode(),
)
with urllib.request.urlopen(req) as res:
    print(res.status, json.loads(res.read()))

Roadmap

GET/roadmap

Retrieve the active transformation roadmap.

The active roadmap and its phases. Its milestones are a separate, paginated list at /roadmap/milestones.

Authentication
Authorization: Bearer vk_…
Required scope
read:roadmap
Rate limit
Per key. X-RateLimit-* on every response; a 429 carries Retry-After.
Idempotency
Safe to retry — reads never change data.

Responses

  • 200 The current object, or null.
  • 400 Malformed request: a bad limit, a cursor we did not issue, an unknown filter value, or an unsupported version.
  • 401 No key presented, or the key does not verify (revoked, expired, mistyped).
  • 403 The key verified but the plan does not include API access, or the key lacks the resource’s scope.
  • 429 Per-key rate limit exhausted. Wait Retry-After seconds.
  • 500 Something failed on our side. The request_id identifies this exact request.
200 response of GET /roadmap
FieldTypeDescription
object"roadmap"
api_version"2026-09-12"
dataRoadmap | null · RoadmapThe current object, or null when the org has none yet.
has_morefalse
next_cursornull
Sandbox example of data
{
  "object": "roadmap",
  "id": "00000000-0000-4000-8000-000000000401",
  "version_number": 3,
  "executive_summary": "[SANDBOX] Three-phase plan: stabilise oversight, then prove value, then scale.",
  "framework_blend": {
    "NIST AI RMF": 0.6,
    "ISO/IEC 42001": 0.4
  },
  "phases": [
    {
      "phase": "Phase 1",
      "objective": "[SANDBOX] Stand up the governance spine."
    },
    {
      "phase": "Phase 2",
      "objective": "[SANDBOX] Evidence the value of three initiatives."
    }
  ],
  "milestones_count": 2,
  "generated_at": "2026-09-01T09:00:00.000Z"
}

Examples

curl

curl -X GET "https://www.vouliiq.com/api/v1/roadmap" \
  -H "Authorization: Bearer $VOULI_API_KEY" \
  -H "Vouli-Version: 2026-09-12"

TypeScript SDK

import { VouliIQ } from '@vouli-iq/sdk';

const vouli = new VouliIQ({ apiKey: process.env.VOULI_API_KEY! });

const result = await vouli.roadmap();
console.log(result.data);

Python SDK

import os
from vouli_iq import VouliIQ

vouli = VouliIQ(api_key=os.environ["VOULI_API_KEY"])

result = vouli.roadmap()
print(result["data"])

Try it

Send GET /roadmap from this browser

Use a vk_test_ sandbox key: it returns synthetic data only. The key stays in this tab's memory — it is never saved, and it is gone when you reload.

GET /api/v1/roadmap

GET/roadmap/milestones

List milestones of the active roadmap, newest first.

Delivery milestones under the active roadmap, with owner, RAG band and progress. Cursor-paginated.

Authentication
Authorization: Bearer vk_…
Required scope
read:roadmap
Rate limit
Per key. X-RateLimit-* on every response; a 429 carries Retry-After.
Idempotency
Safe to retry — reads never change data.

Parameters

Parameters of GET /roadmap/milestones
NameInTypeDescription
cursorquerystringOpaque cursor from a previous response’s next_cursor. A cursor this API did not issue is rejected with 400 invalid_cursor.
limitqueryinteger (1–100)Rows to return, 1–100. Defaults to 25.
statusquerystringOnly milestones in this status.
updated_sincequerystringOnly rows created or changed at or after this instant — an ISO-8601 date (midnight UTC) or timestamp. Use it for incremental sync: store the time you started a sync and pass it next time.

Responses

  • 200 A page of results.
  • 400 Malformed request: a bad limit, a cursor we did not issue, an unknown filter value, or an unsupported version.
  • 401 No key presented, or the key does not verify (revoked, expired, mistyped).
  • 403 The key verified but the plan does not include API access, or the key lacks the resource’s scope.
  • 429 Per-key rate limit exhausted. Wait Retry-After seconds.
  • 500 Something failed on our side. The request_id identifies this exact request.
200 response of GET /roadmap/milestones
FieldTypeDescription
object"list"
api_version"2026-09-12"
dataarray of Milestone · Milestone
has_morebooleanTrue when another page exists.
next_cursorstring | nullOpaque cursor for the next page. Pass it back as ?cursor=. Null when has_more is false.
Sandbox example of data
[
  {
    "object": "milestone",
    "id": "00000000-0000-4000-8000-000000000501",
    "roadmap_id": "00000000-0000-4000-8000-000000000401",
    "milestone_key": "SANDBOX-M-001",
    "title": "[SANDBOX] Appoint an accountable owner for model approvals.",
    "phase": "Phase 1",
    "sequence": 1,
    "status": "in_progress",
    "rag_status": "AMBER",
    "progress_pct": 40,
    "owner_role": "Chief Risk Officer",
    "target_date": "2026-11-30",
    "created_at": "2026-09-01T09:00:00.000Z"
  },
  {
    "object": "milestone",
    "id": "00000000-0000-4000-8000-000000000502",
    "roadmap_id": "00000000-0000-4000-8000-000000000401",
    "milestone_key": "SANDBOX-M-002",
    "title": "[SANDBOX] Publish the model inventory to the board.",
    "phase": "Phase 2",
    "sequence": 1,
    "status": "not_started",
    "rag_status": "GREEN",
    "progress_pct": 0,
    "owner_role": "CIO",
    "target_date": null,
    "created_at": "2026-08-01T09:00:00.000Z"
  }
]

Examples

curl

curl -X GET "https://www.vouliiq.com/api/v1/roadmap/milestones?limit=10" \
  -H "Authorization: Bearer $VOULI_API_KEY" \
  -H "Vouli-Version: 2026-09-12"

TypeScript SDK

import { VouliIQ } from '@vouli-iq/sdk';

const vouli = new VouliIQ({ apiKey: process.env.VOULI_API_KEY! });

const page = await vouli.milestones({ limit: 10 });
console.log(page.data, page.has_more, page.next_cursor);

Python SDK

import os
from vouli_iq import VouliIQ

vouli = VouliIQ(api_key=os.environ["VOULI_API_KEY"])

page = vouli.milestones(limit=10)
print(page["data"], page["has_more"], page["next_cursor"])

Try it

Send GET /roadmap/milestones from this browser

Use a vk_test_ sandbox key: it returns synthetic data only. The key stays in this tab's memory — it is never saved, and it is gone when you reload.

Parameters
GET /api/v1/roadmap/milestones

Scores

GET/scores

List completed assessment scores, newest first.

The org’s assessment history — score, verdict and confidence per completed cycle — so a BI tool, GRC platform or warehouse can cite Vouli IQ as the instrument of record. Cursor-paginated.

Authentication
Authorization: Bearer vk_…
Required scope
read:scores
Rate limit
Per key. X-RateLimit-* on every response; a 429 carries Retry-After.
Idempotency
Safe to retry — reads never change data.

Parameters

Parameters of GET /scores
NameInTypeDescription
cursorquerystringOpaque cursor from a previous response’s next_cursor. A cursor this API did not issue is rejected with 400 invalid_cursor.
limitqueryinteger (1–100)Rows to return, 1–100. Defaults to 25.
updated_sincequerystringOnly rows created or changed at or after this instant — an ISO-8601 date (midnight UTC) or timestamp. Use it for incremental sync: store the time you started a sync and pass it next time.

Responses

  • 200 A page of results.
  • 400 Malformed request: a bad limit, a cursor we did not issue, an unknown filter value, or an unsupported version.
  • 401 No key presented, or the key does not verify (revoked, expired, mistyped).
  • 403 The key verified but the plan does not include API access, or the key lacks the resource’s scope.
  • 429 Per-key rate limit exhausted. Wait Retry-After seconds.
  • 500 Something failed on our side. The request_id identifies this exact request.
200 response of GET /scores
FieldTypeDescription
object"list"
api_version"2026-09-12"
dataarray of Score · Score
has_morebooleanTrue when another page exists.
next_cursorstring | nullOpaque cursor for the next page. Pass it back as ?cursor=. Null when has_more is false.
Sandbox example of data
[
  {
    "object": "score",
    "id": "00000000-0000-4000-8000-000000000101",
    "overall_score": 61.5,
    "adjusted_score": 58.2,
    "verdict": "REMEDIATION",
    "confidence": 0.82,
    "methodology_version": "2.0-weighted (SANDBOX)",
    "completed_at": "2026-09-01T09:00:00.000Z"
  },
  {
    "object": "score",
    "id": "00000000-0000-4000-8000-000000000102",
    "overall_score": 54,
    "adjusted_score": 50.1,
    "verdict": "REMEDIATION",
    "confidence": 0.77,
    "methodology_version": "2.0-weighted (SANDBOX)",
    "completed_at": "2026-08-01T09:00:00.000Z"
  }
]

Examples

curl

curl -X GET "https://www.vouliiq.com/api/v1/scores?limit=10" \
  -H "Authorization: Bearer $VOULI_API_KEY" \
  -H "Vouli-Version: 2026-09-12"

TypeScript SDK

import { VouliIQ } from '@vouli-iq/sdk';

const vouli = new VouliIQ({ apiKey: process.env.VOULI_API_KEY! });

const page = await vouli.scores({ limit: 10 });
console.log(page.data, page.has_more, page.next_cursor);

Python SDK

import os
from vouli_iq import VouliIQ

vouli = VouliIQ(api_key=os.environ["VOULI_API_KEY"])

page = vouli.scores(limit=10)
print(page["data"], page["has_more"], page["next_cursor"])

Try it

Send GET /scores from this browser

Use a vk_test_ sandbox key: it returns synthetic data only. The key stays in this tab's memory — it is never saved, and it is gone when you reload.

Parameters
GET /api/v1/scores

GET/scorecard

Retrieve the most recent completed SOVEREIGN scorecard.

The current scorecard with its completeness and evidence ratios. data is null before the first completed assessment.

Authentication
Authorization: Bearer vk_…
Required scope
read:scorecard
Rate limit
Per key. X-RateLimit-* on every response; a 429 carries Retry-After.
Idempotency
Safe to retry — reads never change data.

Responses

  • 200 The current object, or null.
  • 400 Malformed request: a bad limit, a cursor we did not issue, an unknown filter value, or an unsupported version.
  • 401 No key presented, or the key does not verify (revoked, expired, mistyped).
  • 403 The key verified but the plan does not include API access, or the key lacks the resource’s scope.
  • 429 Per-key rate limit exhausted. Wait Retry-After seconds.
  • 500 Something failed on our side. The request_id identifies this exact request.
200 response of GET /scorecard
FieldTypeDescription
object"scorecard"
api_version"2026-09-12"
dataScorecard | null · ScorecardThe current object, or null when the org has none yet.
has_morefalse
next_cursornull
Sandbox example of data
{
  "object": "scorecard",
  "id": "00000000-0000-4000-8000-000000000101",
  "overall_score": 61.5,
  "adjusted_score": 58.2,
  "verdict": "REMEDIATION",
  "confidence": 0.82,
  "completeness": 0.94,
  "evidence_ratio": 0.61,
  "methodology_version": "2.0-weighted (SANDBOX)",
  "frameworks_applied": [
    "NIST AI RMF",
    "ISO/IEC 42001"
  ],
  "completed_at": "2026-09-01T09:00:00.000Z"
}

Examples

curl

curl -X GET "https://www.vouliiq.com/api/v1/scorecard" \
  -H "Authorization: Bearer $VOULI_API_KEY" \
  -H "Vouli-Version: 2026-09-12"

TypeScript SDK

import { VouliIQ } from '@vouli-iq/sdk';

const vouli = new VouliIQ({ apiKey: process.env.VOULI_API_KEY! });

const result = await vouli.scorecard();
console.log(result.data);

Python SDK

import os
from vouli_iq import VouliIQ

vouli = VouliIQ(api_key=os.environ["VOULI_API_KEY"])

result = vouli.scorecard()
print(result["data"])

Try it

Send GET /scorecard from this browser

Use a vk_test_ sandbox key: it returns synthetic data only. The key stays in this tab's memory — it is never saved, and it is gone when you reload.

GET /api/v1/scorecard

Signals

GET/signals

List the current signal set.

The deterministic numbers the Board Cockpit tiles render — one current row per signal type. Superseded values are not returned. Cursor-paginated.

Authentication
Authorization: Bearer vk_…
Required scope
read:signals
Rate limit
Per key. X-RateLimit-* on every response; a 429 carries Retry-After.
Idempotency
Safe to retry — reads never change data.

Parameters

Parameters of GET /signals
NameInTypeDescription
cursorquerystringOpaque cursor from a previous response’s next_cursor. A cursor this API did not issue is rejected with 400 invalid_cursor.
limitqueryinteger (1–100)Rows to return, 1–100. Defaults to 25.
typequerystringOnly the signal of this canonical type.
updated_sincequerystringOnly rows created or changed at or after this instant — an ISO-8601 date (midnight UTC) or timestamp. Use it for incremental sync: store the time you started a sync and pass it next time.

Responses

  • 200 A page of results.
  • 400 Malformed request: a bad limit, a cursor we did not issue, an unknown filter value, or an unsupported version.
  • 401 No key presented, or the key does not verify (revoked, expired, mistyped).
  • 403 The key verified but the plan does not include API access, or the key lacks the resource’s scope.
  • 429 Per-key rate limit exhausted. Wait Retry-After seconds.
  • 500 Something failed on our side. The request_id identifies this exact request.
200 response of GET /signals
FieldTypeDescription
object"list"
api_version"2026-09-12"
dataarray of Signal · Signal
has_morebooleanTrue when another page exists.
next_cursorstring | nullOpaque cursor for the next page. Pass it back as ?cursor=. Null when has_more is false.
Sandbox example of data
[
  {
    "object": "signal",
    "id": "00000000-0000-4000-8000-000000000301",
    "signal_type": "maturity.overall",
    "module_id": "M1",
    "numeric_value": 61.5,
    "rag": "AMBER",
    "source_version": "2.0-weighted (SANDBOX)",
    "as_of": "2026-09-01T09:00:00.000Z"
  },
  {
    "object": "signal",
    "id": "00000000-0000-4000-8000-000000000302",
    "signal_type": "risk.exposure_usd",
    "module_id": "M6",
    "numeric_value": 1500000,
    "rag": "RED",
    "source_version": null,
    "as_of": "2026-08-01T09:00:00.000Z"
  }
]

Examples

curl

curl -X GET "https://www.vouliiq.com/api/v1/signals?limit=10" \
  -H "Authorization: Bearer $VOULI_API_KEY" \
  -H "Vouli-Version: 2026-09-12"

TypeScript SDK

import { VouliIQ } from '@vouli-iq/sdk';

const vouli = new VouliIQ({ apiKey: process.env.VOULI_API_KEY! });

const page = await vouli.signals({ limit: 10 });
console.log(page.data, page.has_more, page.next_cursor);

Python SDK

import os
from vouli_iq import VouliIQ

vouli = VouliIQ(api_key=os.environ["VOULI_API_KEY"])

page = vouli.signals(limit=10)
print(page["data"], page["has_more"], page["next_cursor"])

Try it

Send GET /signals from this browser

Use a vk_test_ sandbox key: it returns synthetic data only. The key stays in this tab's memory — it is never saved, and it is gone when you reload.

Parameters
GET /api/v1/signals

Talent

GET/talent

Retrieve the active talent readiness assessment.

Overall readiness and the recommended build / buy / borrow mix. Individual gaps are a separate, paginated list at /talent/gaps.

Authentication
Authorization: Bearer vk_…
Required scope
read:talent
Rate limit
Per key. X-RateLimit-* on every response; a 429 carries Retry-After.
Idempotency
Safe to retry — reads never change data.

Responses

  • 200 The current object, or null.
  • 400 Malformed request: a bad limit, a cursor we did not issue, an unknown filter value, or an unsupported version.
  • 401 No key presented, or the key does not verify (revoked, expired, mistyped).
  • 403 The key verified but the plan does not include API access, or the key lacks the resource’s scope.
  • 429 Per-key rate limit exhausted. Wait Retry-After seconds.
  • 500 Something failed on our side. The request_id identifies this exact request.
200 response of GET /talent
FieldTypeDescription
object"talent_readiness"
api_version"2026-09-12"
dataTalentReadiness | null · TalentReadinessThe current object, or null when the org has none yet.
has_morefalse
next_cursornull
Sandbox example of data
{
  "object": "talent_readiness",
  "id": "00000000-0000-4000-8000-000000001001",
  "version_number": 1,
  "overall_readiness": "DEVELOPING",
  "executive_summary": "[SANDBOX] Governance capability is the binding constraint, not engineering.",
  "build_buy_borrow": {
    "build": 0.4,
    "buy": 0.35,
    "borrow": 0.25
  },
  "generated_at": "2026-09-01T09:00:00.000Z"
}

Examples

curl

curl -X GET "https://www.vouliiq.com/api/v1/talent" \
  -H "Authorization: Bearer $VOULI_API_KEY" \
  -H "Vouli-Version: 2026-09-12"

TypeScript SDK

import { VouliIQ } from '@vouli-iq/sdk';

const vouli = new VouliIQ({ apiKey: process.env.VOULI_API_KEY! });

const result = await vouli.talent();
console.log(result.data);

Python SDK

import os
from vouli_iq import VouliIQ

vouli = VouliIQ(api_key=os.environ["VOULI_API_KEY"])

result = vouli.talent()
print(result["data"])

Try it

Send GET /talent from this browser

Use a vk_test_ sandbox key: it returns synthetic data only. The key stays in this tab's memory — it is never saved, and it is gone when you reload.

GET /api/v1/talent

GET/talent/gaps

List talent gaps, newest first.

Every open capability gap with the headcount needed, priority band and recommended path. Cursor-paginated.

Authentication
Authorization: Bearer vk_…
Required scope
read:talent
Rate limit
Per key. X-RateLimit-* on every response; a 429 carries Retry-After.
Idempotency
Safe to retry — reads never change data.

Parameters

Parameters of GET /talent/gaps
NameInTypeDescription
cursorquerystringOpaque cursor from a previous response’s next_cursor. A cursor this API did not issue is rejected with 400 invalid_cursor.
limitqueryinteger (1–100)Rows to return, 1–100. Defaults to 25.
statusquerystringOnly gaps in this status.
updated_sincequerystringOnly rows created or changed at or after this instant — an ISO-8601 date (midnight UTC) or timestamp. Use it for incremental sync: store the time you started a sync and pass it next time.

Responses

  • 200 A page of results.
  • 400 Malformed request: a bad limit, a cursor we did not issue, an unknown filter value, or an unsupported version.
  • 401 No key presented, or the key does not verify (revoked, expired, mistyped).
  • 403 The key verified but the plan does not include API access, or the key lacks the resource’s scope.
  • 429 Per-key rate limit exhausted. Wait Retry-After seconds.
  • 500 Something failed on our side. The request_id identifies this exact request.
200 response of GET /talent/gaps
FieldTypeDescription
object"list"
api_version"2026-09-12"
dataarray of TalentGap · TalentGap
has_morebooleanTrue when another page exists.
next_cursorstring | nullOpaque cursor for the next page. Pass it back as ?cursor=. Null when has_more is false.
Sandbox example of data
[
  {
    "object": "talent_gap",
    "id": "00000000-0000-4000-8000-000000001101",
    "gap_key": "SANDBOX-G-001",
    "role_title": "[SANDBOX] AI Assurance Lead",
    "function_area": "Risk & Compliance",
    "headcount_needed": 1,
    "priority_band": "HIGH",
    "recommended_path": "BUY",
    "status": "open",
    "owner_role": "CHRO",
    "target_date": "2026-11-15",
    "created_at": "2026-09-01T09:00:00.000Z"
  },
  {
    "object": "talent_gap",
    "id": "00000000-0000-4000-8000-000000001102",
    "gap_key": "SANDBOX-G-002",
    "role_title": "[SANDBOX] MLOps Engineer",
    "function_area": "Engineering",
    "headcount_needed": 2,
    "priority_band": "MEDIUM",
    "recommended_path": "BUILD",
    "status": "in_progress",
    "owner_role": null,
    "target_date": null,
    "created_at": "2026-08-01T09:00:00.000Z"
  }
]

Examples

curl

curl -X GET "https://www.vouliiq.com/api/v1/talent/gaps?limit=10" \
  -H "Authorization: Bearer $VOULI_API_KEY" \
  -H "Vouli-Version: 2026-09-12"

TypeScript SDK

import { VouliIQ } from '@vouli-iq/sdk';

const vouli = new VouliIQ({ apiKey: process.env.VOULI_API_KEY! });

const page = await vouli.gaps({ limit: 10 });
console.log(page.data, page.has_more, page.next_cursor);

Python SDK

import os
from vouli_iq import VouliIQ

vouli = VouliIQ(api_key=os.environ["VOULI_API_KEY"])

page = vouli.gaps(limit=10)
print(page["data"], page["has_more"], page["next_cursor"])

Try it

Send GET /talent/gaps from this browser

Use a vk_test_ sandbox key: it returns synthetic data only. The key stays in this tab's memory — it is never saved, and it is gone when you reload.

Parameters
GET /api/v1/talent/gaps

Vendor

GET/vendor

Retrieve the active AI vendor portfolio posture.

Portfolio-level posture and cross-vendor risks. The vendor inventory is a separate, paginated list at /vendor/inventory.

Authentication
Authorization: Bearer vk_…
Required scope
read:vendor
Rate limit
Per key. X-RateLimit-* on every response; a 429 carries Retry-After.
Idempotency
Safe to retry — reads never change data.

Responses

  • 200 The current object, or null.
  • 400 Malformed request: a bad limit, a cursor we did not issue, an unknown filter value, or an unsupported version.
  • 401 No key presented, or the key does not verify (revoked, expired, mistyped).
  • 403 The key verified but the plan does not include API access, or the key lacks the resource’s scope.
  • 429 Per-key rate limit exhausted. Wait Retry-After seconds.
  • 500 Something failed on our side. The request_id identifies this exact request.
200 response of GET /vendor
FieldTypeDescription
object"vendor_portfolio"
api_version"2026-09-12"
dataVendorPortfolio | null · VendorPortfolioThe current object, or null when the org has none yet.
has_morefalse
next_cursornull
Sandbox example of data
{
  "object": "vendor_portfolio",
  "id": "00000000-0000-4000-8000-000000000801",
  "version_number": 1,
  "portfolio_posture": "CONCENTRATED",
  "executive_summary": "[SANDBOX] Two of three model vendors share one upstream provider.",
  "cross_vendor_risks": [
    "[SANDBOX] Shared upstream provider concentration."
  ],
  "recommended_actions": [
    {
      "action": "[SANDBOX] Qualify a second inference provider.",
      "priority": "HIGH"
    }
  ],
  "generated_at": "2026-09-01T09:00:00.000Z"
}

Examples

curl

curl -X GET "https://www.vouliiq.com/api/v1/vendor" \
  -H "Authorization: Bearer $VOULI_API_KEY" \
  -H "Vouli-Version: 2026-09-12"

TypeScript SDK

import { VouliIQ } from '@vouli-iq/sdk';

const vouli = new VouliIQ({ apiKey: process.env.VOULI_API_KEY! });

const result = await vouli.vendor();
console.log(result.data);

Python SDK

import os
from vouli_iq import VouliIQ

vouli = VouliIQ(api_key=os.environ["VOULI_API_KEY"])

result = vouli.vendor()
print(result["data"])

Try it

Send GET /vendor from this browser

Use a vk_test_ sandbox key: it returns synthetic data only. The key stays in this tab's memory — it is never saved, and it is gone when you reload.

GET /api/v1/vendor

GET/vendor/inventory

List the AI vendor inventory, newest first.

Every vendor in the AI supply chain with its risk band, intake recommendation, contract end and annual cost. Cursor-paginated.

Authentication
Authorization: Bearer vk_…
Required scope
read:vendor
Rate limit
Per key. X-RateLimit-* on every response; a 429 carries Retry-After.
Idempotency
Safe to retry — reads never change data.

Parameters

Parameters of GET /vendor/inventory
NameInTypeDescription
cursorquerystringOpaque cursor from a previous response’s next_cursor. A cursor this API did not issue is rejected with 400 invalid_cursor.
limitqueryinteger (1–100)Rows to return, 1–100. Defaults to 25.
statusquerystringOnly vendors in this review status.
updated_sincequerystringOnly rows created or changed at or after this instant — an ISO-8601 date (midnight UTC) or timestamp. Use it for incremental sync: store the time you started a sync and pass it next time.

Responses

  • 200 A page of results.
  • 400 Malformed request: a bad limit, a cursor we did not issue, an unknown filter value, or an unsupported version.
  • 401 No key presented, or the key does not verify (revoked, expired, mistyped).
  • 403 The key verified but the plan does not include API access, or the key lacks the resource’s scope.
  • 429 Per-key rate limit exhausted. Wait Retry-After seconds.
  • 500 Something failed on our side. The request_id identifies this exact request.
200 response of GET /vendor/inventory
FieldTypeDescription
object"list"
api_version"2026-09-12"
dataarray of Vendor · Vendor
has_morebooleanTrue when another page exists.
next_cursorstring | nullOpaque cursor for the next page. Pass it back as ?cursor=. Null when has_more is false.
Sandbox example of data
[
  {
    "object": "vendor",
    "id": "00000000-0000-4000-8000-000000000901",
    "vendor_key": "SANDBOX-V-001",
    "vendor_name": "[SANDBOX] Northwind Models",
    "category": "Foundation model API",
    "ai_role": "inference",
    "initial_risk_band": "HIGH",
    "initial_recommendation": "RENEGOTIATE",
    "status": "under_review",
    "contract_end": "2027-03-31",
    "annual_cost_usd": 420000,
    "created_at": "2026-09-01T09:00:00.000Z"
  },
  {
    "object": "vendor",
    "id": "00000000-0000-4000-8000-000000000902",
    "vendor_key": "SANDBOX-V-002",
    "vendor_name": "[SANDBOX] Contoso Vector",
    "category": "Vector database",
    "ai_role": "retrieval",
    "initial_risk_band": "MEDIUM",
    "initial_recommendation": "RETAIN",
    "status": "approved",
    "contract_end": null,
    "annual_cost_usd": 60000,
    "created_at": "2026-08-01T09:00:00.000Z"
  }
]

Examples

curl

curl -X GET "https://www.vouliiq.com/api/v1/vendor/inventory?limit=10" \
  -H "Authorization: Bearer $VOULI_API_KEY" \
  -H "Vouli-Version: 2026-09-12"

TypeScript SDK

import { VouliIQ } from '@vouli-iq/sdk';

const vouli = new VouliIQ({ apiKey: process.env.VOULI_API_KEY! });

const page = await vouli.inventory({ limit: 10 });
console.log(page.data, page.has_more, page.next_cursor);

Python SDK

import os
from vouli_iq import VouliIQ

vouli = VouliIQ(api_key=os.environ["VOULI_API_KEY"])

page = vouli.inventory(limit=10)
print(page["data"], page["has_more"], page["next_cursor"])

Try it

Send GET /vendor/inventory from this browser

Use a vk_test_ sandbox key: it returns synthetic data only. The key stays in this tab's memory — it is never saved, and it is gone when you reload.

Parameters
GET /api/v1/vendor/inventory

POST/vendor/inventory

Add a vendor to the AI vendor inventory.

Adds a vendor the organisation already contracts with. vendor_key is unique in the organisation: a second create for the same key is a 409. Requires an Idempotency-Key.

Authentication
Authorization: Bearer vk_…
Required scope
write:vendor
Rate limit
Per key. X-RateLimit-* on every response; a 429 carries Retry-After.
Idempotency
Send an Idempotency-Key; a retry with the same key replays the first response for 24h.

Parameters

Parameters of POST /vendor/inventory
NameInTypeDescription
Idempotency-KeyrequiredheaderstringREQUIRED. A unique string you generate per logical request (a UUID), resent unchanged on every retry. The first response is replayed for 24h (with Idempotency-Replayed: true); the same key with a different body is a 409 idempotency_key_reused.

Request body (application/json, required)

Request body of POST /vendor/inventory
FieldTypeDescription
ai_role?string | nullThe vendor’s role in the AI stack.
annual_cost_usd?number | null (0–…)Annual contract value, USD.
categorystringWhat the vendor supplies.
contract_end?string | nullContract end date.
contract_start?string | nullContract start date.
initial_recommendation?string | nullone of: consolidate, negotiate, maintain, exit, onboard, nullRecommendation at intake.
initial_risk_bandstringone of: CRITICAL, HIGH, MEDIUM, LOWRisk band at intake.
notes?string | nullWorking notes.
renewal_owner_role?string | nullRole accountable for the renewal.
status?stringone of: active, under_review, negotiating, sunsetting, exited, blockedReview status. Defaults to active.
vendor_keystringStable identifier, unique in the organisation (e.g. "openai").
vendor_namestringVendor name.
{
  "ai_role": "example_ai_role",
  "annual_cost_usd": 0,
  "category": "example_category",
  "contract_end": "example_contract_end",
  "contract_start": "example_contract_start",
  "initial_recommendation": "consolidate",
  "initial_risk_band": "CRITICAL",
  "notes": "example_notes",
  "renewal_owner_role": "example_renewal_owner_role",
  "status": "active",
  "vendor_key": "example_vendor_key",
  "vendor_name": "example_vendor_name"
}

Responses

  • 201 Created. The record as stored.
  • 400 Malformed request: a bad limit, a cursor we did not issue, an unknown filter value, or an unsupported version.
  • 401 No key presented, or the key does not verify (revoked, expired, mistyped).
  • 403 The key verified but the plan does not include API access, or the key lacks the resource’s scope.
  • 409 Idempotency conflict (key reused with another body, or still in flight), or a duplicate record (resource_conflict).
  • 413 Body over 64 KB.
  • 415 Body sent with a Content-Type other than application/json.
  • 429 Per-key rate limit exhausted. Wait Retry-After seconds.
  • 500 Something failed on our side. The request_id identifies this exact request.
  • 503 The idempotency store or the module is unavailable; nothing was done. Retry with the same Idempotency-Key.
201 response of POST /vendor/inventory
FieldTypeDescription
object"vendor"
api_version"2026-09-12"
dataVendor · Vendor
has_morefalse
next_cursornull

Examples

curl

curl -X POST "https://www.vouliiq.com/api/v1/vendor/inventory" \
  -H "Authorization: Bearer $VOULI_API_KEY" \
  -H "Vouli-Version: 2026-09-12" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"ai_role":"example_ai_role","annual_cost_usd":0,"category":"example_category","contract_end":"example_contract_end","contract_start":"example_contract_start","initial_recommendation":"consolidate","initial_risk_band":"CRITICAL","notes":"example_notes","renewal_owner_role":"example_renewal_owner_role","status":"active","vendor_key":"example_vendor_key","vendor_name":"example_vendor_name"}'

TypeScript

// Not wrapped by the SDK: a plain HTTP call.
const res = await fetch('https://www.vouliiq.com/api/v1/vendor/inventory', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.VOULI_API_KEY}`,
    'Vouli-Version': '2026-09-12',
    'Idempotency-Key': crypto.randomUUID(),
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    ai_role: 'example_ai_role',
    annual_cost_usd: 0,
    category: 'example_category',
    contract_end: 'example_contract_end',
    contract_start: 'example_contract_start',
    initial_recommendation: 'consolidate',
    initial_risk_band: 'CRITICAL',
    notes: 'example_notes',
    renewal_owner_role: 'example_renewal_owner_role',
    status: 'active',
    vendor_key: 'example_vendor_key',
    vendor_name: 'example_vendor_name',
  }),
});
console.log(res.status, await res.json());

Python

# Not wrapped by the SDK: a plain HTTP call.
import json
import os
import urllib.request
import uuid

req = urllib.request.Request(
    "https://www.vouliiq.com/api/v1/vendor/inventory",
    method="POST",
    headers={"Authorization": f"Bearer {os.environ['VOULI_API_KEY']}", "Vouli-Version": "2026-09-12", "Idempotency-Key": str(uuid.uuid4()), "Content-Type": "application/json"},
    data=json.dumps({
        "ai_role": "example_ai_role",
        "annual_cost_usd": 0,
        "category": "example_category",
        "contract_end": "example_contract_end",
        "contract_start": "example_contract_start",
        "initial_recommendation": "consolidate",
        "initial_risk_band": "CRITICAL",
        "notes": "example_notes",
        "renewal_owner_role": "example_renewal_owner_role",
        "status": "active",
        "vendor_key": "example_vendor_key",
        "vendor_name": "example_vendor_name",
    }).encode(),
)
with urllib.request.urlopen(req) as res:
    print(res.status, json.loads(res.read()))

PATCH/vendor/inventory/{id}

Change a vendor’s working state.

Changes the team-owned fields (status, contract dates, renewal owner role, annual cost, notes). A vendor id from another organisation is a 404.

Authentication
Authorization: Bearer vk_…
Required scope
write:vendor
Rate limit
Per key. X-RateLimit-* on every response; a 429 carries Retry-After.
Idempotency
Send an Idempotency-Key; a retry with the same key replays the first response for 24h.

Parameters

Parameters of PATCH /vendor/inventory/{id}
NameInTypeDescription
idrequiredpathstring (uuid)The record id. An id that is not in this organisation (including one from another organisation) is a 404.
Idempotency-KeyheaderstringOptional. Same replay semantics as on POST, for 24h.

Request body (application/json, required)

Request body of PATCH /vendor/inventory/{id}
FieldTypeDescription
annual_cost_usd?number | null (0–…)Annual contract value, USD.
contract_end?string | nullContract end date.
contract_start?string | nullContract start date.
notes?string | nullWorking notes.
renewal_owner_role?string | nullRole accountable for the renewal.
status?stringone of: active, under_review, negotiating, sunsetting, exited, blockedReview status.
{
  "annual_cost_usd": 0,
  "contract_end": "example_contract_end",
  "contract_start": "example_contract_start",
  "notes": "example_notes",
  "renewal_owner_role": "example_renewal_owner_role",
  "status": "active"
}

Responses

  • 200 Done. The record as it now stands.
  • 400 Malformed request: a bad limit, a cursor we did not issue, an unknown filter value, or an unsupported version.
  • 401 No key presented, or the key does not verify (revoked, expired, mistyped).
  • 403 The key verified but the plan does not include API access, or the key lacks the resource’s scope.
  • 404 No such record in this organisation.
  • 409 Idempotency conflict (key reused with another body, or still in flight), or a duplicate record (resource_conflict).
  • 413 Body over 64 KB.
  • 415 Body sent with a Content-Type other than application/json.
  • 429 Per-key rate limit exhausted. Wait Retry-After seconds.
  • 500 Something failed on our side. The request_id identifies this exact request.
  • 503 The idempotency store or the module is unavailable; nothing was done. Retry with the same Idempotency-Key.
200 response of PATCH /vendor/inventory/{id}
FieldTypeDescription
object"vendor"
api_version"2026-09-12"
dataVendor · Vendor
has_morefalse
next_cursornull

Examples

curl

curl -X PATCH "https://www.vouliiq.com/api/v1/vendor/inventory/00000000-0000-4000-8000-000000000000" \
  -H "Authorization: Bearer $VOULI_API_KEY" \
  -H "Vouli-Version: 2026-09-12" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"annual_cost_usd":0,"contract_end":"example_contract_end","contract_start":"example_contract_start","notes":"example_notes","renewal_owner_role":"example_renewal_owner_role","status":"active"}'

TypeScript

// Not wrapped by the SDK: a plain HTTP call.
const res = await fetch('https://www.vouliiq.com/api/v1/vendor/inventory/00000000-0000-4000-8000-000000000000', {
  method: 'PATCH',
  headers: {
    Authorization: `Bearer ${process.env.VOULI_API_KEY}`,
    'Vouli-Version': '2026-09-12',
    'Idempotency-Key': crypto.randomUUID(),
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    annual_cost_usd: 0,
    contract_end: 'example_contract_end',
    contract_start: 'example_contract_start',
    notes: 'example_notes',
    renewal_owner_role: 'example_renewal_owner_role',
    status: 'active',
  }),
});
console.log(res.status, await res.json());

Python

# Not wrapped by the SDK: a plain HTTP call.
import json
import os
import urllib.request
import uuid

req = urllib.request.Request(
    "https://www.vouliiq.com/api/v1/vendor/inventory/00000000-0000-4000-8000-000000000000",
    method="PATCH",
    headers={"Authorization": f"Bearer {os.environ['VOULI_API_KEY']}", "Vouli-Version": "2026-09-12", "Idempotency-Key": str(uuid.uuid4()), "Content-Type": "application/json"},
    data=json.dumps({
        "annual_cost_usd": 0,
        "contract_end": "example_contract_end",
        "contract_start": "example_contract_start",
        "notes": "example_notes",
        "renewal_owner_role": "example_renewal_owner_role",
        "status": "active",
    }).encode(),
)
with urllib.request.urlopen(req) as res:
    print(res.status, json.loads(res.read()))

Audit

GET/audit

Export the audit log, oldest first (Enterprise).

The organisation's audit trail as SIEM-ready events — the same feed as Settings → SIEM export — oldest first, so a collector can poll it incrementally: keep the last next_cursor and pass it back. since and until bound the window. Enterprise plan only; read:all does not grant it. Every pull is itself audited. limit 1–1000 (default 100).

Authentication
Authorization: Bearer vk_…
Required scope
read:audit
Rate limit
Per key. X-RateLimit-* on every response; a 429 carries Retry-After.
Idempotency
Safe to retry — reads never change data.

Parameters

Parameters of GET /audit
NameInTypeDescription
cursorquerystringThe next_cursor of the previous page (strictly after that event).
limitqueryinteger (1–1000)Events to return, 1–1000. Defaults to 100.
formatquerystringone of: json, ndjsonjson (the list envelope, default) or ndjson (one event per line; the next cursor is in X-Next-Cursor).
sincequerystringEvents at or after this instant (ignored when cursor is given — the cursor is the finer bound).
untilquerystringEvents strictly before this instant.

Responses

  • 200 A page of results.
  • 400 Malformed request: a bad limit, a cursor we did not issue, an unknown filter value, or an unsupported version.
  • 401 No key presented, or the key does not verify (revoked, expired, mistyped).
  • 403 The key verified but the plan does not include API access, or the key lacks the resource’s scope.
  • 429 Per-key rate limit exhausted. Wait Retry-After seconds.
  • 500 Something failed on our side. The request_id identifies this exact request.
200 response of GET /audit
FieldTypeDescription
object"list"
api_version"2026-09-12"
dataarray of AuditEvent · AuditEvent
has_moreboolean
next_cursorstring | null

Examples

curl

curl -X GET "https://www.vouliiq.com/api/v1/audit?limit=10" \
  -H "Authorization: Bearer $VOULI_API_KEY" \
  -H "Vouli-Version: 2026-09-12"

TypeScript

// Not wrapped by the SDK: a plain HTTP call.
const res = await fetch('https://www.vouliiq.com/api/v1/audit?limit=10', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.VOULI_API_KEY}`,
    'Vouli-Version': '2026-09-12',
  },
});
console.log(res.status, await res.json());

Python

# Not wrapped by the SDK: a plain HTTP call.
import json
import os
import urllib.request

req = urllib.request.Request(
    "https://www.vouliiq.com/api/v1/audit?limit=10",
    method="GET",
    headers={"Authorization": f"Bearer {os.environ['VOULI_API_KEY']}", "Vouli-Version": "2026-09-12"},
)
with urllib.request.urlopen(req) as res:
    print(res.status, json.loads(res.read()))

Try it

Send GET /audit from this browser

Use a vk_test_ sandbox key: it returns synthetic data only. The key stays in this tab's memory — it is never saved, and it is gone when you reload.

Parameters
GET /api/v1/audit

Webhooks

GET/webhooks/subscriptions

List webhook subscriptions, newest first.

Every endpoint registered for this organisation, by the dashboard or by a key. The signing secret is never listed. Cursor-paginated.

Authentication
Authorization: Bearer vk_…
Required scope
manage:webhooks
Rate limit
Per key. X-RateLimit-* on every response; a 429 carries Retry-After.
Idempotency
Safe to retry — reads never change data.

Parameters

Parameters of GET /webhooks/subscriptions
NameInTypeDescription
cursorquerystringOpaque cursor from a previous response’s next_cursor. A cursor this API did not issue is rejected with 400 invalid_cursor.
limitqueryinteger (1–100)Rows to return, 1–100. Defaults to 25.

Responses

  • 200 A page of results.
  • 400 Malformed request: a bad limit, a cursor we did not issue, an unknown filter value, or an unsupported version.
  • 401 No key presented, or the key does not verify (revoked, expired, mistyped).
  • 403 The key verified but the plan does not include API access, or the key lacks the resource’s scope.
  • 429 Per-key rate limit exhausted. Wait Retry-After seconds.
  • 500 Something failed on our side. The request_id identifies this exact request.
200 response of GET /webhooks/subscriptions
FieldTypeDescription
object"list"
api_version"2026-09-12"
dataarray of WebhookSubscription · WebhookSubscription
has_moreboolean
next_cursorstring | null

Examples

curl

curl -X GET "https://www.vouliiq.com/api/v1/webhooks/subscriptions?limit=10" \
  -H "Authorization: Bearer $VOULI_API_KEY" \
  -H "Vouli-Version: 2026-09-12"

TypeScript

// Not wrapped by the SDK: a plain HTTP call.
const res = await fetch('https://www.vouliiq.com/api/v1/webhooks/subscriptions?limit=10', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.VOULI_API_KEY}`,
    'Vouli-Version': '2026-09-12',
  },
});
console.log(res.status, await res.json());

Python

# Not wrapped by the SDK: a plain HTTP call.
import json
import os
import urllib.request

req = urllib.request.Request(
    "https://www.vouliiq.com/api/v1/webhooks/subscriptions?limit=10",
    method="GET",
    headers={"Authorization": f"Bearer {os.environ['VOULI_API_KEY']}", "Vouli-Version": "2026-09-12"},
)
with urllib.request.urlopen(req) as res:
    print(res.status, json.loads(res.read()))

Try it

Send GET /webhooks/subscriptions from this browser

Use a vk_test_ sandbox key: it returns synthetic data only. The key stays in this tab's memory — it is never saved, and it is gone when you reload.

Parameters
GET /api/v1/webhooks/subscriptions

POST/webhooks/subscriptions

Register a webhook endpoint.

Registers an https endpoint and returns its signing secret ONCE. The URL must resolve to a public address (no localhost, private, link-local or metadata addresses, no http://). Requires an Idempotency-Key; a replay of the same request returns the subscription with secret: null.

Authentication
Authorization: Bearer vk_…
Required scope
manage:webhooks
Rate limit
Per key. X-RateLimit-* on every response; a 429 carries Retry-After.
Idempotency
Send an Idempotency-Key; a retry with the same key replays the first response for 24h.

Parameters

Parameters of POST /webhooks/subscriptions
NameInTypeDescription
Idempotency-KeyrequiredheaderstringREQUIRED. A unique string you generate per logical request (a UUID), resent unchanged on every retry. The first response is replayed for 24h (with Idempotency-Replayed: true); the same key with a different body is a 409 idempotency_key_reused.

Request body (application/json, required)

Request body of POST /webhooks/subscriptions
FieldTypeDescription
active?booleanStart paused with false. Defaults to true.
description?string | nullFree-text label.
event_types?array of stringEvents to deliver. Omit or [] for every subscribable event.
urlstring (uri)https endpoint. Private, loopback, link-local and non-https addresses are refused.
{
  "active": true,
  "description": "example_description",
  "event_types": [
    "example_event_types"
  ],
  "url": "example_url"
}

Responses

  • 201 Created. The record as stored.
  • 400 Malformed request: a bad limit, a cursor we did not issue, an unknown filter value, or an unsupported version.
  • 401 No key presented, or the key does not verify (revoked, expired, mistyped).
  • 403 The key verified but the plan does not include API access, or the key lacks the resource’s scope.
  • 409 Idempotency conflict (key reused with another body, or still in flight), or a duplicate record (resource_conflict).
  • 413 Body over 64 KB.
  • 415 Body sent with a Content-Type other than application/json.
  • 429 Per-key rate limit exhausted. Wait Retry-After seconds.
  • 500 Something failed on our side. The request_id identifies this exact request.
  • 503 The idempotency store or the module is unavailable; nothing was done. Retry with the same Idempotency-Key.
201 response of POST /webhooks/subscriptions
FieldTypeDescription
object"webhook_subscription"
api_version"2026-09-12"
dataWebhookSubscription · WebhookSubscription
has_morefalse
next_cursornull

Examples

curl

curl -X POST "https://www.vouliiq.com/api/v1/webhooks/subscriptions" \
  -H "Authorization: Bearer $VOULI_API_KEY" \
  -H "Vouli-Version: 2026-09-12" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"active":true,"description":"example_description","event_types":["example_event_types"],"url":"example_url"}'

TypeScript

// Not wrapped by the SDK: a plain HTTP call.
const res = await fetch('https://www.vouliiq.com/api/v1/webhooks/subscriptions', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.VOULI_API_KEY}`,
    'Vouli-Version': '2026-09-12',
    'Idempotency-Key': crypto.randomUUID(),
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    active: true,
    description: 'example_description',
    event_types: [
      'example_event_types',
    ],
    url: 'example_url',
  }),
});
console.log(res.status, await res.json());

Python

# Not wrapped by the SDK: a plain HTTP call.
import json
import os
import urllib.request
import uuid

req = urllib.request.Request(
    "https://www.vouliiq.com/api/v1/webhooks/subscriptions",
    method="POST",
    headers={"Authorization": f"Bearer {os.environ['VOULI_API_KEY']}", "Vouli-Version": "2026-09-12", "Idempotency-Key": str(uuid.uuid4()), "Content-Type": "application/json"},
    data=json.dumps({
        "active": True,
        "description": "example_description",
        "event_types": [
            "example_event_types",
        ],
        "url": "example_url",
    }).encode(),
)
with urllib.request.urlopen(req) as res:
    print(res.status, json.loads(res.read()))

PATCH/webhooks/subscriptions/{id}

Pause, resume or change a webhook endpoint.

Changes the URL (re-checked), the event list, the description or active. A subscription id from another organisation is a 404.

Authentication
Authorization: Bearer vk_…
Required scope
manage:webhooks
Rate limit
Per key. X-RateLimit-* on every response; a 429 carries Retry-After.
Idempotency
Send an Idempotency-Key; a retry with the same key replays the first response for 24h.

Parameters

Parameters of PATCH /webhooks/subscriptions/{id}
NameInTypeDescription
idrequiredpathstring (uuid)The record id. An id that is not in this organisation (including one from another organisation) is a 404.
Idempotency-KeyheaderstringOptional. Same replay semantics as on POST, for 24h.

Request body (application/json, required)

Request body of PATCH /webhooks/subscriptions/{id}
FieldTypeDescription
active?booleanfalse pauses deliveries; true resumes them.
description?string | nullFree-text label.
event_types?array of stringReplace the event list. [] for every event.
url?string (uri)New https endpoint (the same address checks apply).
{
  "active": true,
  "description": "example_description",
  "event_types": [
    "example_event_types"
  ],
  "url": "example_url"
}

Responses

  • 200 Done. The record as it now stands.
  • 400 Malformed request: a bad limit, a cursor we did not issue, an unknown filter value, or an unsupported version.
  • 401 No key presented, or the key does not verify (revoked, expired, mistyped).
  • 403 The key verified but the plan does not include API access, or the key lacks the resource’s scope.
  • 404 No such record in this organisation.
  • 409 Idempotency conflict (key reused with another body, or still in flight), or a duplicate record (resource_conflict).
  • 413 Body over 64 KB.
  • 415 Body sent with a Content-Type other than application/json.
  • 429 Per-key rate limit exhausted. Wait Retry-After seconds.
  • 500 Something failed on our side. The request_id identifies this exact request.
  • 503 The idempotency store or the module is unavailable; nothing was done. Retry with the same Idempotency-Key.
200 response of PATCH /webhooks/subscriptions/{id}
FieldTypeDescription
object"webhook_subscription"
api_version"2026-09-12"
dataWebhookSubscription · WebhookSubscription
has_morefalse
next_cursornull

Examples

curl

curl -X PATCH "https://www.vouliiq.com/api/v1/webhooks/subscriptions/00000000-0000-4000-8000-000000000000" \
  -H "Authorization: Bearer $VOULI_API_KEY" \
  -H "Vouli-Version: 2026-09-12" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"active":true,"description":"example_description","event_types":["example_event_types"],"url":"example_url"}'

TypeScript

// Not wrapped by the SDK: a plain HTTP call.
const res = await fetch('https://www.vouliiq.com/api/v1/webhooks/subscriptions/00000000-0000-4000-8000-000000000000', {
  method: 'PATCH',
  headers: {
    Authorization: `Bearer ${process.env.VOULI_API_KEY}`,
    'Vouli-Version': '2026-09-12',
    'Idempotency-Key': crypto.randomUUID(),
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    active: true,
    description: 'example_description',
    event_types: [
      'example_event_types',
    ],
    url: 'example_url',
  }),
});
console.log(res.status, await res.json());

Python

# Not wrapped by the SDK: a plain HTTP call.
import json
import os
import urllib.request
import uuid

req = urllib.request.Request(
    "https://www.vouliiq.com/api/v1/webhooks/subscriptions/00000000-0000-4000-8000-000000000000",
    method="PATCH",
    headers={"Authorization": f"Bearer {os.environ['VOULI_API_KEY']}", "Vouli-Version": "2026-09-12", "Idempotency-Key": str(uuid.uuid4()), "Content-Type": "application/json"},
    data=json.dumps({
        "active": True,
        "description": "example_description",
        "event_types": [
            "example_event_types",
        ],
        "url": "example_url",
    }).encode(),
)
with urllib.request.urlopen(req) as res:
    print(res.status, json.loads(res.read()))

DELETE/webhooks/subscriptions/{id}

Delete a webhook endpoint.

Removes the endpoint and its queued deliveries. A subscription id from another organisation is a 404.

Authentication
Authorization: Bearer vk_…
Required scope
manage:webhooks
Rate limit
Per key. X-RateLimit-* on every response; a 429 carries Retry-After.
Idempotency
Send an Idempotency-Key; a retry with the same key replays the first response for 24h.

Parameters

Parameters of DELETE /webhooks/subscriptions/{id}
NameInTypeDescription
idrequiredpathstring (uuid)The record id. An id that is not in this organisation (including one from another organisation) is a 404.
Idempotency-KeyheaderstringOptional. Same replay semantics as on POST, for 24h.

Responses

  • 200 Done. The record as it now stands.
  • 400 Malformed request: a bad limit, a cursor we did not issue, an unknown filter value, or an unsupported version.
  • 401 No key presented, or the key does not verify (revoked, expired, mistyped).
  • 403 The key verified but the plan does not include API access, or the key lacks the resource’s scope.
  • 404 No such record in this organisation.
  • 409 Idempotency conflict (key reused with another body, or still in flight), or a duplicate record (resource_conflict).
  • 429 Per-key rate limit exhausted. Wait Retry-After seconds.
  • 500 Something failed on our side. The request_id identifies this exact request.
  • 503 The idempotency store or the module is unavailable; nothing was done. Retry with the same Idempotency-Key.
200 response of DELETE /webhooks/subscriptions/{id}
FieldTypeDescription
object"webhook_subscription_deleted"
api_version"2026-09-12"
dataWebhookSubscriptionDeleted · WebhookSubscriptionDeleted
has_morefalse
next_cursornull

Examples

curl

curl -X DELETE "https://www.vouliiq.com/api/v1/webhooks/subscriptions/00000000-0000-4000-8000-000000000000" \
  -H "Authorization: Bearer $VOULI_API_KEY" \
  -H "Vouli-Version: 2026-09-12" \
  -H "Idempotency-Key: $(uuidgen)"

TypeScript

// Not wrapped by the SDK: a plain HTTP call.
const res = await fetch('https://www.vouliiq.com/api/v1/webhooks/subscriptions/00000000-0000-4000-8000-000000000000', {
  method: 'DELETE',
  headers: {
    Authorization: `Bearer ${process.env.VOULI_API_KEY}`,
    'Vouli-Version': '2026-09-12',
    'Idempotency-Key': crypto.randomUUID(),
  },
});
console.log(res.status, await res.json());

Python

# Not wrapped by the SDK: a plain HTTP call.
import json
import os
import urllib.request
import uuid

req = urllib.request.Request(
    "https://www.vouliiq.com/api/v1/webhooks/subscriptions/00000000-0000-4000-8000-000000000000",
    method="DELETE",
    headers={"Authorization": f"Bearer {os.environ['VOULI_API_KEY']}", "Vouli-Version": "2026-09-12", "Idempotency-Key": str(uuid.uuid4())},
)
with urllib.request.urlopen(req) as res:
    print(res.status, json.loads(res.read()))

POST/webhooks/subscriptions/{id}/test

Send a signed test `ping` to one endpoint, now.

Sends one ping event through the real delivery path — the same SSRF checks and the same X-Vouli-Signature (HMAC-SHA256 over the exact body) as every event — and reports what the receiver answered. Works on a paused endpoint. Requires an Idempotency-Key; send an empty body or {}.

Authentication
Authorization: Bearer vk_…
Required scope
manage:webhooks
Rate limit
Per key. X-RateLimit-* on every response; a 429 carries Retry-After.
Idempotency
Send an Idempotency-Key; a retry with the same key replays the first response for 24h.

Parameters

Parameters of POST /webhooks/subscriptions/{id}/test
NameInTypeDescription
idrequiredpathstring (uuid)The record id. An id that is not in this organisation (including one from another organisation) is a 404.
Idempotency-KeyrequiredheaderstringREQUIRED. A unique string you generate per logical request (a UUID), resent unchanged on every retry. The first response is replayed for 24h (with Idempotency-Replayed: true); the same key with a different body is a 409 idempotency_key_reused.

Request body (application/json)

object

{}

Responses

  • 200 Done. The record as it now stands.
  • 400 Malformed request: a bad limit, a cursor we did not issue, an unknown filter value, or an unsupported version.
  • 401 No key presented, or the key does not verify (revoked, expired, mistyped).
  • 403 The key verified but the plan does not include API access, or the key lacks the resource’s scope.
  • 404 No such record in this organisation.
  • 409 Idempotency conflict (key reused with another body, or still in flight), or a duplicate record (resource_conflict).
  • 413 Body over 64 KB.
  • 415 Body sent with a Content-Type other than application/json.
  • 429 Per-key rate limit exhausted. Wait Retry-After seconds.
  • 500 Something failed on our side. The request_id identifies this exact request.
  • 503 The idempotency store or the module is unavailable; nothing was done. Retry with the same Idempotency-Key.
200 response of POST /webhooks/subscriptions/{id}/test
FieldTypeDescription
object"webhook_test_delivery"
api_version"2026-09-12"
dataWebhookTestDelivery · WebhookTestDelivery
has_morefalse
next_cursornull

Examples

curl

curl -X POST "https://www.vouliiq.com/api/v1/webhooks/subscriptions/00000000-0000-4000-8000-000000000000/test" \
  -H "Authorization: Bearer $VOULI_API_KEY" \
  -H "Vouli-Version: 2026-09-12" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{}'

TypeScript

// Not wrapped by the SDK: a plain HTTP call.
const res = await fetch('https://www.vouliiq.com/api/v1/webhooks/subscriptions/00000000-0000-4000-8000-000000000000/test', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.VOULI_API_KEY}`,
    'Vouli-Version': '2026-09-12',
    'Idempotency-Key': crypto.randomUUID(),
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({}),
});
console.log(res.status, await res.json());

Python

# Not wrapped by the SDK: a plain HTTP call.
import json
import os
import urllib.request
import uuid

req = urllib.request.Request(
    "https://www.vouliiq.com/api/v1/webhooks/subscriptions/00000000-0000-4000-8000-000000000000/test",
    method="POST",
    headers={"Authorization": f"Bearer {os.environ['VOULI_API_KEY']}", "Vouli-Version": "2026-09-12", "Idempotency-Key": str(uuid.uuid4()), "Content-Type": "application/json"},
    data=json.dumps({}).encode(),
)
with urllib.request.urlopen(req) as res:
    print(res.status, json.loads(res.read()))

Webhook events

Outbound events, signed with HMAC-SHA256 in X-Vouli-Signature. Respond 2xx to acknowledge; anything else is retried with backoff.

  • assessment.completed A maturity assessment finished and a new SOVEREIGN scorecard is available.
  • verdict.changed The org’s headline verdict band changed versus the previous assessment.
  • roadmap.updated The active roadmap or one of its milestones changed.
  • risk.created A new risk was added to the register.
  • risk.severity_changed An existing risk’s severity was re-rated.
  • compliance.posture_changed A new compliance assessment went live and the overall posture, or a regime’s posture, moved versus the previous version (one event per movement; never on the first assessment or when nothing moved).
  • evidence.pack_filed An Evidence Vault pack was assembled and filed (stored ready with every item). manifest_sha256 is the sha256 of the manifest’s canonical JSON (keys sorted).

Schemas

Error

Every 4xx and 5xx response has exactly this shape.

Error schema
FieldTypeDescription
errorobject

Score

Score schema
FieldTypeDescription
adjusted_scorenumber | null (0–100)Score after finding-pressure adjustment.
completed_atstring (date-time)When the assessment was completed.
confidencenumber | null (0–1)Confidence in the score, 0–1.
idstring (uuid)Assessment id.
methodology_versionstringScoring methodology the score was computed under.
object"score"
overall_scorenumber | null (0–100)Raw SOVEREIGN score, 0–100.
verdictstring | nullone of: CRISIS, EMERGENCY, REMEDIATION, OPTIMIZATION, EXCELLENCE, nullBanded verdict for the score.

Scorecard

Scorecard schema
FieldTypeDescription
adjusted_scorenumber | null (0–100)Score after finding-pressure adjustment.
completed_atstring (date-time)When the assessment was completed.
completenessnumber | null (0–1)Share of questions answered.
confidencenumber | null (0–1)Confidence in the score, 0–1.
evidence_rationumber | null (0–1)Share of answers backed by evidence.
frameworks_appliedarray of stringFrameworks the assessment was mapped against.
idstring (uuid)Assessment id.
methodology_versionstringScoring methodology the score was computed under.
object"scorecard"
overall_scorenumber | null (0–100)Raw SOVEREIGN score, 0–100.
verdictstring | nullone of: CRISIS, EMERGENCY, REMEDIATION, OPTIMIZATION, EXCELLENCE, nullBanded verdict for the score.

Risk

Risk schema
FieldTypeDescription
created_atstring (date-time)When the risk was registered.
descriptionstringWhat the risk is.
dimensionstring | nullSOVEREIGN dimension the risk sits in.
financial_exposure_maxnumber | nullHigh end of modelled exposure, USD.
financial_exposure_minnumber | nullLow end of modelled exposure, USD.
idstring (uuid)Risk row id.
object"risk"
owner_namestring | nullAccountable owner.
residual_severitystring | nullone of: CRITICAL, HIGH, MEDIUM, LOW, nullSeverity after mitigation.
risk_idstringHuman-readable risk reference, unique within the org.
severitystring | nullone of: CRITICAL, HIGH, MEDIUM, LOW, nullInherent severity.
sourcestring | nullHow the risk entered the register.
statusstringone of: Open, InProgress, Mitigated, Accepted, ClosedWhere the risk is in its lifecycle.
target_datestring | null (date)Target mitigation date.

Signal

Signal schema
FieldTypeDescription
as_ofstring (date-time)When this value became the current one.
idstring (uuid)Signal row id.
module_idstringModule that published the signal, e.g. "M1".
numeric_valuenumber | nullThe headline number, when the signal is numeric.
object"signal"
ragstring | nullone of: RED, AMBER, GREEN, nullRed/amber/green band for the signal.
signal_typestringCanonical signal type, e.g. "maturity.overall".
source_versionstring | nullMethodology version the value was produced under.

Roadmap

Roadmap schema
FieldTypeDescription
executive_summarystring | nullBoard-level summary of the plan.
framework_blend?anyFramework weighting used to shape the phases.
generated_atstring (date-time)When the roadmap was generated.
idstring (uuid)Roadmap id.
milestones_countinteger | nullNumber of milestones under this roadmap.
object"roadmap"
phases?anyOrdered phases with their objectives.
version_numberintegerMonotonic version of the roadmap.

Milestone

Milestone schema
FieldTypeDescription
created_atstring (date-time)When the milestone was created.
idstring (uuid)Milestone id.
milestone_keystringStable key for the milestone.
object"milestone"
owner_rolestringRole accountable for delivery.
phasestringRoadmap phase the milestone sits in.
progress_pctnumberPercent complete, 0–100.
rag_statusstringRed/amber/green delivery band.
roadmap_idstring (uuid)Roadmap the milestone belongs to.
sequenceintegerOrder within the phase.
statusstringMilestone status.
target_datestring | null (date)Target completion date.
titlestringMilestone title.

CompliancePosture

CompliancePosture schema
FieldTypeDescription
executive_summarystringBoard-level summary of the posture.
generated_atstring (date-time)When the assessment was generated.
idstring (uuid)Compliance assessment id.
object"compliance_posture"
overall_posturestringOverall compliance posture band.
primary_jurisdictionsarray of stringJurisdictions in scope.
regimes?anyPer-regime posture detail.
version_numberintegerMonotonic version of the assessment.

Obligation

Obligation schema
FieldTypeDescription
created_atstring (date-time)When the obligation was recorded.
idstring (uuid)Obligation id.
jurisdictionstringJurisdiction the obligation arises in.
object"obligation"
obligation_keystringStable key for the obligation.
owner_rolestring | nullRole accountable for the obligation.
priority_bandstringPriority band for sequencing.
regime_idstringRegime identifier, e.g. "eu-ai-act".
regime_namestringHuman-readable regime name.
statusstringWhere the obligation stands.
target_datestring | null (date)Target compliance date.
titlestringWhat must be done.

VendorPortfolio

VendorPortfolio schema
FieldTypeDescription
cross_vendor_risksarray of stringRisks that span more than one vendor.
executive_summarystringBoard-level summary of the portfolio.
generated_atstring (date-time)When the assessment was generated.
idstring (uuid)Vendor assessment id.
object"vendor_portfolio"
portfolio_posturestringOverall posture of the AI vendor portfolio.
recommended_actions?anyRecommended portfolio actions.
version_numberintegerMonotonic version of the assessment.

Vendor

Vendor schema
FieldTypeDescription
ai_rolestring | nullThe vendor’s role in the AI stack.
annual_cost_usdnumber | nullAnnual contract value, USD.
categorystringWhat the vendor supplies.
contract_endstring | null (date)Contract end date.
created_atstring (date-time)When the vendor entered the inventory.
idstring (uuid)Vendor inventory row id.
initial_recommendationstring | nullRecommendation at intake.
initial_risk_bandstringRisk band at intake.
object"vendor"
statusstringWhere the vendor stands in the review cycle.
vendor_keystringStable key for the vendor.
vendor_namestringVendor name.

TalentReadiness

TalentReadiness schema
FieldTypeDescription
build_buy_borrow?anyBuild / buy / borrow mix recommended.
executive_summarystringBoard-level summary of readiness.
generated_atstring (date-time)When the assessment was generated.
idstring (uuid)Talent assessment id.
object"talent_readiness"
overall_readinessstringOverall talent readiness band.
version_numberintegerMonotonic version of the assessment.

TalentGap

TalentGap schema
FieldTypeDescription
created_atstring (date-time)When the gap was recorded.
function_areastringFunction the role sits in.
gap_keystringStable key for the gap.
headcount_neededintegerHeadcount required to close the gap.
idstring (uuid)Talent gap id.
object"talent_gap"
owner_rolestring | nullRole accountable for closing the gap.
priority_bandstringPriority band for sequencing.
recommended_pathstringRecommended way to close the gap.
role_titlestringRole that is missing or under-staffed.
statusstringWhere closing the gap stands.
target_datestring | null (date)Target close date.

Incident

Incident schema
FieldTypeDescription
affected_systemsstring | nullSystems the incident touched.
created_atstring (date-time)When the incident was declared.
descriptionstring | nullLonger account of the incident.
idstring (uuid)Incident id.
incident_typestringone of: model_failure, bias_harm, data_exposure, agent_failure, regulatory_inquiry, shadow_ai_breach, otherKind of AI incident.
jurisdictionsstring | nullJurisdictions whose notification rules apply.
notification_deadlinestring | null (date-time)When the notification is due.
notification_requiredbooleanWhether a regulator or stakeholder must be notified.
notified_atstring | null (date-time)When the notification was recorded as made.
object"incident"
ownerstring | nullAccountable incident owner.
severitystringone of: low, medium, high, criticalSeverity of the incident.
statusstringone of: open, investigating, contained, resolved, post_review, closedWhere the incident is in its lifecycle.
titlestringWhat happened, in one line.
updated_atstring (date-time)When the incident last changed.

Kpi

Kpi schema
FieldTypeDescription
baseline_valuenumber | nullWhere the metric started.
codestringStable code, unique in the organisation (e.g. "O1-KR2") — accepted as key_result_code.
created_atstring (date-time)When the key result was created.
current_as_ofstring | null (date-time)When the current value was recorded.
current_valuenumber | nullLatest actual (the latest period’s check-in).
directionstringone of: increase, decrease, maintainWhich way is good.
idstring (uuid)Key result id — what POST /kpis/checkins takes as key_result_id.
measurement_frequencystringone of: weekly, monthly, quarterly, annualHow often an actual is expected.
metric_namestringWhat is measured.
object"kpi"
objective_idstring (uuid)Objective the key result rolls up to.
statementstringThe key result as written.
statusstringone of: draft, active, achieved, missed, retiredLifecycle state of the key result.
target_valuenumberValue that counts as 100% attainment.
unitstringUnit of the value: %, USD, days, ratio, index, count…
updated_atstring (date-time)When the key result last changed (a new actual included).

CreateRiskBody

CreateRiskBody schema
FieldTypeDescription
descriptionstringWhat the risk is, in at least 20 characters.
dimension?stringSOVEREIGN dimension, if known. Defaults to OTHER.
likelihoodstringone of: RARE, UNLIKELY, POSSIBLE, LIKELY, ALMOST_CERTAINLikelihood.
mitigation_action?stringWhat is being done about it.
owner_name?stringAccountable owner (a name or role, not a user id).
severitystringone of: CRITICAL, HIGH, MEDIUM, LOWInherent severity.
target_date?stringTarget mitigation date.
titlestringShort name of the risk.

UpdateRiskBody

UpdateRiskBody schema
FieldTypeDescription
likelihood?stringone of: RARE, UNLIKELY, POSSIBLE, LIKELY, ALMOST_CERTAINLikelihood.
mitigation_action?string | nullWhat is being done about it.
owner_name?string | nullAccountable owner (a name or role).
residual_gap_reason?string | nullStanding explanation for a residual below the untreated baseline.
residual_severity?string | nullone of: CRITICAL, HIGH, MEDIUM, LOW, nullSeverity after mitigation.
severity?stringone of: CRITICAL, HIGH, MEDIUM, LOWInherent severity.
severity_change_reason?string | nullRequired when the change LOWERS or clears a severity or likelihood, or closes, accepts or mitigates a CRITICAL risk (at least 20 characters). Recorded as evidence.
status?stringone of: Open, InProgress, Mitigated, Accepted, ClosedLifecycle state.
target_date?string | nullTarget mitigation date.
title?string | nullShort name of the risk.

CreateIncidentBody

CreateIncidentBody schema
FieldTypeDescription
affected_systems?stringSystems the incident touched.
description?stringLonger account.
incident_type?stringone of: model_failure, bias_harm, data_exposure, agent_failure, regulatory_inquiry, shadow_ai_breach, otherKind of incident. Defaults to other.
jurisdictions?stringJurisdictions whose notification rules apply.
notification_deadline?string | null (date-time)When the notification is due (ISO-8601).
notification_required?booleanWhether a regulator or stakeholder must be notified.
owner?stringAccountable incident owner (a name or role).
severity?stringone of: low, medium, high, criticalDefaults to medium.
status?stringone of: open, investigating, contained, resolved, post_review, closedDefaults to open.
titlestringWhat happened, in one line.

UpdateIncidentBody

UpdateIncidentBody schema
FieldTypeDescription
affected_systems?string | nullSystems the incident touched.
description?string | nullLonger account.
incident_type?stringone of: model_failure, bias_harm, data_exposure, agent_failure, regulatory_inquiry, shadow_ai_breach, otherKind of incident.
jurisdictions?string | nullJurisdictions whose notification rules apply.
notification_deadline?string | null (date-time)When the notification is due (ISO-8601).
notification_required?booleanWhether a regulator or stakeholder must be notified.
owner?string | nullAccountable incident owner (a name or role).
severity?stringone of: low, medium, high, criticalSeverity.
status?stringone of: open, investigating, contained, resolved, post_review, closedLifecycle state.
title?stringWhat happened, in one line.

KpiCheckinBatch

KpiCheckinBatch schema
FieldTypeDescription
acceptedintegerHow many data points were recorded (always all of them).
checkinsarray of objectOne entry per data point, in request order.
object"kpi_checkin_batch"

RecordKpiCheckinsBody

RecordKpiCheckinsBody schema
FieldTypeDescription
checkinsarray of object1–500 data points. All are validated before any is written; one bad point refuses the batch.

CreateVendorBody

CreateVendorBody schema
FieldTypeDescription
ai_role?string | nullThe vendor’s role in the AI stack.
annual_cost_usd?number | null (0–…)Annual contract value, USD.
categorystringWhat the vendor supplies.
contract_end?string | nullContract end date.
contract_start?string | nullContract start date.
initial_recommendation?string | nullone of: consolidate, negotiate, maintain, exit, onboard, nullRecommendation at intake.
initial_risk_bandstringone of: CRITICAL, HIGH, MEDIUM, LOWRisk band at intake.
notes?string | nullWorking notes.
renewal_owner_role?string | nullRole accountable for the renewal.
status?stringone of: active, under_review, negotiating, sunsetting, exited, blockedReview status. Defaults to active.
vendor_keystringStable identifier, unique in the organisation (e.g. "openai").
vendor_namestringVendor name.

UpdateVendorBody

UpdateVendorBody schema
FieldTypeDescription
annual_cost_usd?number | null (0–…)Annual contract value, USD.
contract_end?string | nullContract end date.
contract_start?string | nullContract start date.
notes?string | nullWorking notes.
renewal_owner_role?string | nullRole accountable for the renewal.
status?stringone of: active, under_review, negotiating, sunsetting, exited, blockedReview status.

WebhookSubscription

WebhookSubscription schema
FieldTypeDescription
activebooleanFalse while the endpoint is paused: nothing is delivered to it.
created_atstring (date-time)When it was registered.
created_viastringone of: dashboard, apiWhether a signed-in admin or an API key registered it.
descriptionstring | nullFree-text label.
event_typesarray of stringEvents delivered to it. Empty means every subscribable event.
idstring (uuid)Subscription id.
object"webhook_subscription"
secret?string | nullThe signing secret — present ONCE, in the response to the create call (and null in an idempotent replay of it). Store it to verify X-Vouli-Signature.
updated_atstring (date-time)When it last changed.
urlstringThe https endpoint deliveries are POSTed to.

CreateWebhookSubscriptionBody

CreateWebhookSubscriptionBody schema
FieldTypeDescription
active?booleanStart paused with false. Defaults to true.
description?string | nullFree-text label.
event_types?array of stringEvents to deliver. Omit or [] for every subscribable event.
urlstring (uri)https endpoint. Private, loopback, link-local and non-https addresses are refused.

UpdateWebhookSubscriptionBody

UpdateWebhookSubscriptionBody schema
FieldTypeDescription
active?booleanfalse pauses deliveries; true resumes them.
description?string | nullFree-text label.
event_types?array of stringReplace the event list. [] for every event.
url?string (uri)New https endpoint (the same address checks apply).

WebhookSubscriptionDeleted

WebhookSubscriptionDeleted schema
FieldTypeDescription
deletedtrue
idstring (uuid)The deleted subscription.
object"webhook_subscription_deleted"

WebhookTestDelivery

WebhookTestDelivery schema
FieldTypeDescription
deliveredbooleanTrue when the receiver answered 2xx.
event_idstringThe id inside the signed body — what a receiver de-duplicates on.
event_type"ping"
idstring (uuid)Delivery id (it appears in the delivery log).
object"webhook_test_delivery"
outcomestringone of: delivered, rejected, unreachable, blockeddelivered (2xx), rejected (the receiver answered non-2xx), unreachable (no answer) or blocked (the address is private or not https).
response_status_codeinteger | nullThe receiver’s HTTP status, when it answered.

TestWebhookSubscriptionBody

object

AuditEvent

AuditEvent schema
FieldTypeDescription
actionstringWhat happened, e.g. "risk.updated", "api.write.risk".
actorstringWho acted: a user id, a Clerk id, or "system".
categorystringCoarse category for SIEM routing.
idstring (uuid)Audit event id (the SIEM event_id).
metadata?anyEvent detail, as recorded.
object"audit_event"
resource_idstring | nullId of the record acted on.
resource_typestring | nullKind of record acted on.
severityinteger (0–10)CEF-scale severity, 0–10.
timestring (date-time)When the event happened.