Skip to main content
Main content

Developer Portal

Build on Vouli IQ

A read-only REST API over your organisation's governance data — scores, risks, signals, roadmap, compliance, vendors and talent — plus signed outbound webhooks. One envelope, cursor pagination, per-key rate limits, and a sandbox you can point CI at.

Current version
2026-09-12
Base URL
https://app.vouliiq.com/api/v1

Authentication

Every request carries an API key as a bearer token. Keys are minted in Dashboard → API keys by an admin, are scoped per resource family, and are stored only as a scrypt hash — the plaintext is shown once.

curl https://app.vouliiq.com/api/v1/risks?limit=10 \
  -H "Authorization: Bearer vk_live_XXXXXXXX_your_secret" \
  -H "Vouli-Version: 2026-09-12"

Resources and scopes

A key must hold the resource's scope (or read:all). A key without it gets 403 insufficient_scope.

Resources and scopes
EndpointReturnsScopeDescription
GET /api/v1/scoreslist of scoreread:scoresList completed assessment scores, newest first.
GET /api/v1/scorecardscorecardread:scorecardRetrieve the most recent completed SOVEREIGN scorecard.
GET /api/v1/riskslist of riskread:risksList the AI risk register, newest first.
GET /api/v1/signalslist of signalread:signalsList the current signal set.
GET /api/v1/roadmaproadmapread:roadmapRetrieve the active transformation roadmap.
GET /api/v1/roadmap/milestoneslist of milestoneread:roadmapList milestones of the active roadmap, newest first.
GET /api/v1/compliancecompliance_postureread:complianceRetrieve the active compliance posture.
GET /api/v1/compliance/obligationslist of obligationread:complianceList compliance obligations, newest first.
GET /api/v1/vendorvendor_portfolioread:vendorRetrieve the active AI vendor portfolio posture.
GET /api/v1/vendor/inventorylist of vendorread:vendorList the AI vendor inventory, newest first.
GET /api/v1/talenttalent_readinessread:talentRetrieve the active talent readiness assessment.
GET /api/v1/talent/gapslist of talent_gapread:talentList talent gaps, newest first.

Response envelopes

Two success shapes and one error shape, across the whole API. Read your rows from data.

// GET /api/v1/risks
{
  "object": "list",
  "api_version": "2026-09-12",
  "data": [ { "object": "risk", "id": "…", "severity": "HIGH", … } ],
  "has_more": true,
  "next_cursor": "djF8MjAyNi0wOS0wMVQwOTowMDowMC4wMDBafC4uLg"
}

// GET /api/v1/scorecard  (null until the first assessment completes)
{ "object": "scorecard", "api_version": "2026-09-12", "data": { … }, "has_more": false, "next_cursor": null }

Pagination

Collections are cursor-paginated (keyset, not offset — pages never skip or repeat a row when data changes underneath you). Pass limit (1100, default 25) and the previous response's next_cursor. A cursor is opaque: hand it back unchanged. A cursor we did not issue returns 400 invalid_cursor.

# page 1
curl "https://app.vouliiq.com/api/v1/risks?limit=50" -H "Authorization: Bearer $VOULI_API_KEY"

# page 2 — pass next_cursor back
curl "https://app.vouliiq.com/api/v1/risks?limit=50&cursor=$NEXT_CURSOR" -H "Authorization: Bearer $VOULI_API_KEY"

Both SDKs will follow the cursors for you: listAll() in TypeScript, list_all() in Python.

Rate limits

Per-key token bucket: the capacity equals your per-minute limit (so a burst up to it is allowed) and refills at limit/60 per second, with a second rolling 24-hour cap. Limits come from your plan and can be overridden per key. Every response — success or failure — tells you where you stand.

Rate-limit headers
HeaderMeaning
X-RateLimit-LimitRequests allowed per minute for this key.
X-RateLimit-RemainingRequests still available right now.
X-RateLimit-ResetUnix time (seconds) at which the bucket is full again.
X-RateLimit-Limit-DayRequests allowed per rolling 24 hours for this key.
Retry-AfterOn a 429 only: seconds to wait before retrying.

Errors

Every 4xx and 5xx has the same body. Branch on code — it is stable. The message is for humans and may be reworded. The request_id is also returned as X-Request-Id; quote it in a support request.

{
  "error": {
    "type": "permission_error",
    "code": "insufficient_scope",
    "message": "This key does not have the \"read:risks\" scope.",
    "request_id": "req_9f2c1d4e8a7b40f1a0c3e5d7b9f10246"
  }
}

Every code we emit

missing_credential · invalid_api_key · tier_required · subscription_inactive · insufficient_scope · invalid_parameter · invalid_cursor · unsupported_version · rate_limit_exceeded · idempotency_key_reused · idempotency_in_flight · internal_error

Versioning and deprecation

Additive changes (a new field, a new resource, a new enum member on a field documented as extensible) ship without a version bump — clients must ignore unknown fields. A BREAKING change ships as a new dated version: the operation gains an `x-deprecation` block naming the `sunset` date and its replacement, responses start carrying `Deprecation` and `Sunset` headers, and the old version keeps being served until that date. Pin a version with the `Vouli-Version` request header; omit it and you get the current one.

Idempotency

The public API is read-only today, so nothing here needs an idempotency key yet. When mutating endpoints arrive they will all accept Idempotency-Key: the first response for a key is cached for 24 hours and replayed for any retry carrying the same key, so a network blip never double-creates. The same key with a different request body is a 409 idempotency_key_reused.

Sandbox

Mint a Test key and it returns a fixed set of synthetic rows for every endpoint — every human-readable value prefixed [SANDBOX], every id in a reserved all-zero UUID range. A test key never reads tenant data, so you can build against it, demo with it and run it in CI. Responses carry Vouli-Environment: test.

Quickstart — TypeScript

npm install @vouli-iq/sdk

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

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

// One page, with the pagination state.
const page = await vouli.risks({ severity: 'HIGH', limit: 50 });
console.log(page.data.length, page.has_more);

// Or let the SDK follow the cursors.
for await (const risk of vouli.listAll('risks', { severity: 'HIGH' })) {
  console.log(risk.risk_id);
}

Quickstart — Python

pip install vouli-iq

import os
from vouli_iq import VouliIQ

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

page = vouli.risks(severity="HIGH", limit=50)
print(len(page["data"]), page["has_more"])

for risk in vouli.list_all("risks", severity="HIGH"):
    print(risk["risk_id"])

Stdlib-only (no third-party dependencies). Both SDKs' typed surfaces are generated from the OpenAPI document and a CI check fails the build if regeneration produces any diff — so an SDK method, its parameters and its return type cannot drift from the API. Registry publication is pending the first tagged release; until then install from the repository's sdk/ directory.

Webhooks

Subscribe an endpoint to receive signed events (X-Vouli-Signature, HMAC-SHA256). Deliveries retry with backoff and dead-letter; replay from the dashboard. Every envelope carries api_version.

Webhook events
EventWhenStability
assessment.completedA maturity assessment finished and a new SOVEREIGN scorecard is available.stable
verdict.changedThe org’s headline verdict band changed versus the previous assessment.stable
roadmap.updatedThe active roadmap or one of its milestones changed.stable
risk.createdA new risk was added to the register.stable
risk.severity_changedAn existing risk’s severity was re-rated.stable
compliance.posture_changedThe org’s overall compliance posture changed for a regime.beta
evidence.pack_filedA compliance evidence pack was assembled and filed with a fresh manifest hash.beta

API changelog

2026-09-12

Breaking

  • One response envelope everywhere. Lists return { object: "list", api_version, data, has_more, next_cursor }; single objects return { object: "<type>", api_version, data }. The old per-resource shapes ({ risks, count }, { scorecard }, { roadmap, milestones }, …) are gone — read `data`.
  • One error envelope everywhere: { error: { type, code, message, request_id } }. Branch on `code`, not on `message`.
  • Scopes are enforced on every route. /scores and /signals previously accepted any valid key; they now require read:scores and read:signals (read:all still satisfies everything). Re-mint narrow keys with the scopes they need.
  • Child collections moved out of their parent object and became paginated resources: /roadmap/milestones, /compliance/obligations, /vendor/inventory, /talent/gaps. The parent objects no longer embed those arrays (they were unbounded and could be truncated).
  • The OpenAPI document moved from GET /api/v1 to GET /api/v1/openapi.json. GET /api/v1 is now the resource index.
  • Risk `status` filter values are the real column values (Open, InProgress, Mitigated, Accepted, Closed) and `rag` is RED|AMBER|GREEN — the previous spec documented lowercase values the database never held.

Additive

  • Cursor pagination on every collection: `limit` (1–100, default 25) and an opaque `cursor`. /scores is no longer capped at 24 rows and /signals no longer at 200.
  • X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset and X-RateLimit-Limit-Day on every response; Retry-After on every 429.
  • X-Request-Id on every response, and the same id inside every error body.
  • Vouli-Version request header to pin the dated version; Vouli-Version and Vouli-Environment on every response.
  • Sandbox keys: a vk_test_ key returns a fixed set of clearly-labelled synthetic rows and never reads tenant data.
  • New key namespace vk_ for newly minted keys. Keys beginning bk_ remain valid indefinitely.
  • Idempotency-Key support for future mutating endpoints, with a 24h replay window.
  • Fixed: /scorecard selected a column that does not exist, so it returned null for every organisation.
  • Both SDKs are now generated from this document and checked for drift in CI.

Reference