Skip to content

IRMA for developers: concepts

This page explains the concepts you need to work well with the IRMA API, yourself or through an agent. Each part says what works now and what is still coming.

Everything in IRMA belongs to one organization. A service token belongs to exactly one organization and never sees anything of another. With a user token you work in the organization(s) that user is a member of; if the account sees more than one, choose one with the header X-Irma-Active-Org: <uuid> (in the CLI: --active-org).

Within an organization the read scope decides what you may see. An administrator can limit read access per department (see IRMA: administration). Every /v1 answer states which read scope applied:

readScopeMeaning
allYou see everything of the organization.
beperktLimited: you only see what belongs to your departments.
geenNone: your read scope is empty.
mijnMine: you asked for your own part only (scope=mijn), within a full read scope.

With beperkt or geen, an empty or short list does not mean nothing exists. An agent should say so in its answer.

Some routes calculate over the whole organization and cannot honestly shrink to a part, such as the simulations and the audit checks. They refuse a limited read scope with 403 and { "error": "scope_te_smal" }. That is not a failure: those questions need full read access.

Every answer of the /v1 task layer has the same shape, the envelope:

{
"data": { "risks": [ { "id": "…", "title": "Phishing", "net": { "score": 8, "level": "medium" } } ] },
"context": {
"organization": { "id": "…", "name": "Gemeente Zuiderveld" },
"project": null,
"readScope": "all",
"asOf": "2026-10-24T10:00:00.000Z",
"filters": { "sort": "-netScore" }
},
"completeness": { "returned": 10, "total": 37, "truncated": true, "nextOffset": 10 },
"notIncluded": [ { "what": "Wijzigingshistorie met van/naar-waarden", "use": "GET /v1/risks/{id}/history" } ],
"provenance": { "kind": "stored", "note": "…" },
"next": [ { "action": "GET /v1/risks/{id}", "when": "voor één risico met maatregelen" } ]
}

(Example with the demo organization Gemeente Zuiderveld.) Keys are English; the texts in notIncluded, next and provenance.note are Dutch, the language of the app.

FieldWhat it saysWhy
dataThe actual answer.
contextFor which organization, project, read scope, moment and filters the answer holds.A number without context cannot be checked.
completenessHow many rows came back (returned) and how many exist (total), whether this is a part (truncated), where the next page starts (nextOffset), and what was left out on purpose, with the reason (excluded).A page must never be mistaken for the whole.
notIncludedWhat this answer does not contain, and which call has it.So an agent asks for the right thing instead of guessing.
provenanceWhere the numbers come from: stored (stored data), engine (a result of the calculation engine, with run, seed and iterations) or derived (a derived comparison).A percentile or loss must trace back to a real run.
nextThe logical next steps.So an agent does not invent endpoints.

Why this strict? An assistant that only got a raw list of tens of thousands of characters started calculating and estimating instead of looking things up. The envelope makes the honest answer the easy one: what is missing is stated, and so is where to find it.

Lists return 50 rows by default and 200 at most (limit). offset skips rows; completeness.nextOffset says where the next page starts and is null on the last one. total is correct with a limit too. In search the source itself may cut off; then totalIsLowerBound: true and total is a lower bound. Fetching a full list: Pagination (Prudai staff only, sign-in required).

An agent does not change your registers directly. What an agent proposes is prepared as a draft and a person accepts it or not. Write routes for drafts through the API come later.

There is one exception: reporting incidents. A signal from monitoring or a SIEM must not wait for a review, so it goes straight into the Incidenten register. The fingerprint keeps repeats from becoming duplicate incidents.

Starting a simulation (POST /v1/simulations) stores a calculation result and changes no register.

In the CLI every write is a dry run until you pass --no-dry-run --yes. On top of that, a human approval step per tool is coming (x-prudai-approval in the API description). How it is going to work: Approval of agent actions (Prudai staff only, sign-in required); the level per endpoint appears there once IRMA publishes it.

Today

  • /agent/v1 (reporting incidents): a service token with scopes an administrator chooses: incidents:write (report) and incidents:read (read). A missing scope gives 403.
  • /v1 (task layer): the access token of a signed-in user. You get exactly what that user may do in the app: the same role and the same read scope. Service tokens do not work on /v1 yet.

Coming

  • Scopes for service tokens on /v1, in the form object:verb (like incidents:read), with bundles for common tasks.
  • A scope never grants more than an IRMA role allows: what a token may do is the intersection of its scopes and the role.

A 403 is the design, not a bug. Do not ask for a broader key to make one action work.

  • An incident reported with a service token records which token reported it.
  • The change history of a risk (GET /v1/risks/{id}/history, irma risks history) shows per field who changed it: a user, the assistant, an agent, an import or the system.
  • Starting a simulation through /v1 goes into IRMA’s audit trail. That trail is immutable: entries cannot be changed or deleted, and each entry is linked to the previous one. See Audit trail.

A /v1 error is not an envelope but { "error": "<code>", "message": "…" }. 400 and 422 mean invalid input, 404 an unknown id (or one of another organization), 409 that something cannot be done yet (for example a run that is still running), 401 and 403 missing or insufficient permissions. The CLI maps those to exit code 2; network and server errors to 1. See CLI reference. Every error code per endpoint: Errors (Prudai staff only, sign-in required).

/v1 only changes by addition: new fields, new endpoints, new values where announced. A field does not disappear and does not change meaning. Every change goes into a changelog. Write your client so it ignores unknown fields.