REST API
Authenticate, browse the OpenAPI reference, and call the Noru API from curl, fetch, or CI.
What it is
The REST API exposes the same records you see in the app, controls, evidence, risks, vendors, policies, personnel, and the privacy data map, as JSON over HTTPS. It is the right choice for scripts, CI pipelines, and anything that needs to upload a file; AI clients should use the MCP server instead.
Base URL and reference
| What | Where |
|---|---|
| Base URL | https://api.noru.tech |
| OpenAPI 3 document | GET https://api.noru.tech/openapi |
| Interactive reference | https://api.noru.tech/ — press k to search |
| Markdown spec for LLM context | GET https://api.noru.tech/llms.txt |
| Health check | GET https://api.noru.tech/health (no auth) |
The public paths above are rate-limited to 60 requests per minute per IP.
Authentication
Every /v1 and /v2 request needs an Authorization: Bearer header. Two
credentials are accepted:
| Credential | Looks like | How to get one |
|---|---|---|
| API key | noru_ followed by 32 characters | An admin creates it under SettingsDeveloper. See API keys. |
| OAuth access token | Issued by the Noru authorization server | Obtained through the MCP OAuth flow; valid for 1 hour |
A missing, malformed, expired, or revoked credential returns 401 with a
WWW-Authenticate header and this body (the message says which check failed):
{ "error": { "code": "UNAUTHORIZED", "message": "..." } }A valid credential that lacks the scope for a route returns 403:
{ "error": { "code": "FORBIDDEN", "message": "Insufficient permissions" } }Resources
| Path | What it covers | Read scope | Write scope |
|---|---|---|---|
/v1/controls | Controls, status, ownership, framework mappings | read:controls | write:controls |
/v1/assets | Asset inventory | read:assets | write:assets |
/v1/evidence | Evidence items, mappings, file upload and download | read:evidence | write:evidence |
/v1/risks | Risk register and treatments | read:risks | write:risks |
/v1/security-findings | Security findings | read:risks | write:risks |
/v1/vendors | Vendor register, contacts, evidence | read:vendors | write:vendors |
/v1/policies | Policies, versions, logs | read:policies | write:policies |
/v1/privacy | Data map, RoPA, processing activities, security measures, fideslang ingestion | read:datamaps | write:datamaps |
/v1/personnel, /v2/personnel | People directory | read:personnel | write:personnel |
/v1/ownership-principals | Users and personnel that can own a record | read:users, read:personnel | — |
/v1/training | Training plans, campaigns, acknowledgements | read:personnel | write:personnel |
/v1/mcp | The MCP endpoint, not a REST resource | per tool | per tool |
The exact operations, parameters, and schemas for each resource are in the OpenAPI document; this page only shows the shape they share.
Privacy data map ingestion
POST /v1/privacy/datamaps accepts a fideslang manifest (.fides/datamap.yml
parsed to JSON) and materialises systems, datasets, and processing activities
into the data map. It is idempotent on slug: re-pushing identical content is a
no-op, a changed manifest creates a new immutable version, and anything the
manifest no longer names is archived rather than deleted. It requires
write:datamaps and the privacy segment. GET /v1/privacy/ropa/export
returns the RoPA register as CSV with read:datamaps. AI inventory manifests
have no REST route; push them through the MCP tool ingestAiInventory.
Example: list controls
GET /v1/controls returns the organization's controls, filterable by
status, frameworkId, domain, ownerType, ownerRefId, and search.
It needs read:controls.
curl -s "https://api.noru.tech/v1/controls?status=in_progress&limit=2" \
-H "Authorization: Bearer $NORU_API_KEY"const response = await fetch(
"https://api.noru.tech/v1/controls?status=in_progress&limit=2",
{ headers: { Authorization: `Bearer ${process.env.NORU_API_KEY}` } },
);
if (!response.ok) {
const { error } = await response.json();
throw new Error(`${error.code}: ${error.message}`);
}
const { data, pagination } = await response.json();
console.log(pagination.total, "controls;", data.length, "in this page");A trimmed response:
{
"data": [
{
"id": "sm-01",
"controlId": "SM-01",
"name": "Information security policy",
"status": "in_progress",
"owner": { "type": "user", "refId": "user_123", "resolvedUserId": "user_123" }
}
],
"pagination": { "total": 87, "limit": 2, "offset": 0 }
}GET /v1/controls/sm-01 returns one control. The lowercase id is the
canonical identifier; the uppercase controlId is for display, and routes
accept either.
Pagination and filters
| Parameter | Rule |
|---|---|
limit | 1 to 100, default 50 |
offset | 0 or greater, default 0 |
Named filters (status, frameworkId, and so on) | Exact match against the stored value |
search | Case-insensitive substring match on the resource's searchable text |
Every list response wraps rows in data and reports total, limit, and
offset under pagination. Page until offset + limit reaches total.
Errors
Every error uses the same envelope: an error object with a code and a
human-readable message.
| HTTP | code | When |
|---|---|---|
| 400 | BAD_REQUEST | Validation failed; the message names the field |
| 401 | UNAUTHORIZED | No credential, or it is invalid, expired, or revoked |
| 402 | PAYMENT_REQUIRED | The organization's billing is not active (see below) |
| 403 | FORBIDDEN | The credential lacks the scope for this route |
| 404 | NOT_FOUND | No record with that id in this organization |
| 429 | RATE_LIMITED | Rate limit exceeded; retry after Retry-After seconds |
| 500 | INTERNAL_ERROR | Unhandled failure; retry, then contact support with the timestamp |
Rate limits
Authenticated /v1 and /v2 calls are limited to 500 requests per 10
minutes per credential, counted per API key or per OAuth token. Every
response carries the current state:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Requests allowed in the window (500) |
X-RateLimit-Remaining | Requests left in the current window |
X-RateLimit-Reset | Seconds until the window resets |
Retry-After | Only on 429: seconds to wait |
Two keys mean two budgets. If a CI job and an MCP client share one key, a busy build can starve the client. Give each consumer its own key.
Billing gate
/v1/evidence additionally requires the organization's billing to be active.
When it is not, those routes answer 402 PAYMENT_REQUIRED until an admin
resolves it under SettingsBilling or with the Noru team.
Activity and audit trail
Every authenticated request is recorded in the organization's activity log
with source api, the action METHOD /path, and the API key (or OAuth user)
as actor. The key's Last Used timestamp is updated at the same time. Writes
made through the API are therefore visible in the same history as writes made
in the app.
What the REST API does not do
- It never returns data from an organization other than the credential's.
- It does not create or delete controls; controls come from the frameworks you enable.
- File uploads happen only through
POST /v1/evidence/uploadasmultipart/form-data, up to 50 MB. No other route accepts a file. - It does not push AI inventory manifests (use MCP
ingestAiInventory). - It does not stream or send webhooks; poll, or use the activity log.
Related
- API keys
- MCP server
- Roles and permissions
- Statuses for every value a
statusfilter accepts
Last updated on