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"
vk_live_… reads your real data. vk_test_… reads the sandbox.- Keys minted before the namespace rename begin
bk_ and remain valid indefinitely. - The
x-api-key header still works but is deprecated (sunset 12 Sep 2027). It runs through the identical authentication, scope and rate-limit path — move to Authorization: Bearer. - Rotation: mint a new key, switch your callers, revoke the old one. Revocation takes effect immediately.
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| Endpoint | Returns | Scope | Description |
|---|
| GET /api/v1/scores | list of score | read:scores | List completed assessment scores, newest first. |
| GET /api/v1/scorecard | scorecard | read:scorecard | Retrieve the most recent completed SOVEREIGN scorecard. |
| GET /api/v1/risks | list of risk | read:risks | List the AI risk register, newest first. |
| GET /api/v1/signals | list of signal | read:signals | List the current signal set. |
| GET /api/v1/roadmap | roadmap | read:roadmap | Retrieve the active transformation roadmap. |
| GET /api/v1/roadmap/milestones | list of milestone | read:roadmap | List milestones of the active roadmap, newest first. |
| GET /api/v1/compliance | compliance_posture | read:compliance | Retrieve the active compliance posture. |
| GET /api/v1/compliance/obligations | list of obligation | read:compliance | List compliance obligations, newest first. |
| GET /api/v1/vendor | vendor_portfolio | read:vendor | Retrieve the active AI vendor portfolio posture. |
| GET /api/v1/vendor/inventory | list of vendor | read:vendor | List the AI vendor inventory, newest first. |
| GET /api/v1/talent | talent_readiness | read:talent | Retrieve the active talent readiness assessment. |
| GET /api/v1/talent/gaps | list of talent_gap | read:talent | List 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 (1–100, 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| Header | Meaning |
|---|
| X-RateLimit-Limit | Requests allowed per minute for this key. |
| X-RateLimit-Remaining | Requests still available right now. |
| X-RateLimit-Reset | Unix time (seconds) at which the bucket is full again. |
| X-RateLimit-Limit-Day | Requests allowed per rolling 24 hours for this key. |
| Retry-After | On 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.
- Currently served:
2026-09-12. - Omit
Vouli-Version and you get the current version; an unknown value is a 400 unsupported_version. - A deprecated operation carries an
x-deprecation block in the spec and Deprecation / Sunset headers on its responses. - Treat unknown response fields as additive and ignore them — that is what keeps an additive change non-breaking.
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| Event | When | Stability |
|---|
| assessment.completed | A maturity assessment finished and a new SOVEREIGN scorecard is available. | stable |
| verdict.changed | The org’s headline verdict band changed versus the previous assessment. | stable |
| roadmap.updated | The active roadmap or one of its milestones changed. | stable |
| risk.created | A new risk was added to the register. | stable |
| risk.severity_changed | An existing risk’s severity was re-rated. | stable |
| compliance.posture_changed | The org’s overall compliance posture changed for a regime. | beta |
| evidence.pack_filed | A 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.