Skip to main content
Praxikon

For developers

Building on the Praxikon regulatory graph

This page describes the public v1 endpoints, their actual responses, what goes wrong when you call them incorrectly, and which promises about identifiers and versions will hold. Every example below was run against the live API.

What the regulatory graph is

The regulatory graph is the published form of the EU AI Act as Praxikon has worked it out: obligations, changes, actions, evidence, controls, definitions, guidance, standards, worked examples and actors, each a separate object with its own identifier, version and payload hash. Source traceability is not a footnote here, it is the shape itself. Every statement inside an object carries its kind (official fact, Praxikon interpretation or recommended action), its locator (source id, source locator such as "Article 50(1)-(5)", source URL and, where it exists, the ELI) and its review metadata (when checked, by whom, using which method). The source register behind those references travels in the same response under included.sources, with publication version, check date and a hash over our source record. Nothing is generated on request: every response is a projection of stored objects, which is precisely what makes a citation citable.

Two time axes keep law and knowledge apart. effective_at is the date on which a rule legally applied; known_at is the boundary of what we had published and checked at that moment. So you can ask not only what applies today, but what applied on 1 March 2025 and what we had on record about it then. New information does not overwrite that earlier snapshot. Substantive change arrives as a new version of the object with its own payload hash, never as a silent overwrite of the old one.

The identifier, and the older form beside it

Every object is named praxikon:{jurisdiction}:{regulation}:{type}:{slug}, for example praxikon:eu:ai-act:obligation:article-50-transparency. Five segments, always, so a parser never has to branch on arity. The first two segments exist because a flat form works exactly as long as there is one regulation: the AI Act has an Article 11, the GDPR has one, and a national implementing act will have one too. Jurisdiction and regulation answer two different questions, so they are two segments.

Up to and including dataset version 1.1.0 the published form was raip:{type}:{slug}. That form was not renamed and not withdrawn: every old identifier keeps resolving to the same object, permanently, every object carries its old name in legacy_id, and meta.identifiers on every graph response says which form is canonical and how your input resolved. An identifier that follows the brand is not an identifier, so renaming was never an option. The abbreviation therefore lives on in technical contracts (raip: in the old identifiers, raipv: in the JSON-LD context, X-RAIP-Signature as a header) and nowhere as the name of the platform.

The release this page describes

dataset_id
praxikon:sys:registry:dataset:ai-act-implementation-graph
meta.dataset_id on every graph response
dataset_version
2.1.0
meta.dataset_version
schema_version
1.4.0
meta.schema_version
Last release check
2026-08-08
meta.last_reviewed_at and the Last-Modified header

Five minutes

No key, no account, no registration. Five calls that take you from a question to a defensible dossier.

  1. Minute 1

    Ask a question

    The answer endpoint is the fastest way in: no key, no registration, and the result names the identifiers you continue with.

    Call
    curl -s "https://www.praxikon.com/api/v1/answer?q=our+chatbot+talks+to+customers&lang=en"
    Response (abbreviated)
    {
      "mode": "scenario",
      "question": "Our chatbot talks to customers. Does it have to say it is AI?",
      "likely_role": "Deployer (you use the system)",
      "obligations": [
        { "slug": "article-50-transparency", "legal_status": "applicable", "deadline_at": "2026-08-02T00:00:00.000Z" },
        { "slug": "article-4-ai-literacy",   "legal_status": "applicable", "deadline_at": "2025-02-02T00:00:00.000Z" }
      ],
      "dataset": { "version": "2.1.0", "effective_at": "2026-08-08T00:00:00.000Z", "known_at": "2026-08-14T00:00:00.000Z" }
    }
  2. Minute 2

    Read the object the answer rests on

    Every slug from step 1 belongs to an identifier of the form praxikon:eu:ai-act:obligation:{slug}. Fetch that object and you see the statements, the source locators and the review metadata the answer was assembled from.

    Call
    curl -s "https://www.praxikon.com/api/v1/obligations?lang=en&id=praxikon:eu:ai-act:obligation:article-50-transparency"
    Response (abbreviated)
    {
      "meta": { "count": 1, "dataset_version": "2.1.0", "effective_at": "2026-08-08T00:00:00.000Z", ... },
      "data": [
        {
          "id": "praxikon:eu:ai-act:obligation:article-50-transparency",
          "legacy_id": "raip:obligation:article-50-transparency",
          "version": "1.0.0",
          "effective_at": "2026-08-02T00:00:00.000Z",
          "known_at": "2026-08-14T00:00:00.000Z",
          "payload_hash_sha256": "239fbac0e4728dc239352b2f88b199c3cc098082ff72e2972b4b5e8a2b121406",
          "statements": [
            {
              "kind": "official_fact",
              "citations": [
                { "source_id": "praxikon:eu:ai-act:source:reg-eu-2024-1689", "source_locator": "Article 50(1)-(5) and Article 113" }
              ]
            }
          ]
        }
      ],
      "included": { "sources": [ { "id": "praxikon:eu:ai-act:source:reg-eu-2024-1689", "source_version": "original-oj-2024-07-12" } ] }
    }
  3. Minute 3

    Move the clock

    effective_at decides which law you are querying. On 1 March 2025 two obligations applied; today the graph carries 27. Same query, different moment, different answer, and that difference is the point.

    Call
    curl -s "https://www.praxikon.com/api/v1/obligations?lang=en&effective_at=2025-03-01"
    curl -s "https://www.praxikon.com/api/v1/obligations?lang=en"
    Response (abbreviated)
    # effective_at=2025-03-01
    "count": 2   ->  article-4-ai-literacy, article-5-prohibited-practices
    
    # default (2026-08-08T00:00:00.000Z)
    "count": 27  ->  annex-iii-high-risk, article-10-data-governance, article-11-technical-documentation, ...
  4. Minute 4

    Cache on the ETag

    The graph endpoints return an ETag over the payload and a Last-Modified equal to the last release check. Send the ETag back and you get a 304 with no body. An agent that follows the graph never has to repeat megabytes.

    Call
    ETAG=$(curl -sI "https://www.praxikon.com/api/v1/obligations?lang=en" \
      | awk 'tolower($1)=="etag:"{print $2}' | tr -d '\r')
    
    curl -s -o /dev/null -w '%{http_code}\n' \
      -H "If-None-Match: $ETAG" \
      "https://www.praxikon.com/api/v1/obligations?lang=en"
    Response (abbreviated)
    304
  5. Minute 5

    Run a full assessment

    If you need more than one question, hand a coded profile to the implementation map. Back comes a dossier with the outcome per obligation, the rule trace underneath, and a payload hash two parties can use to establish they are looking at the same dossier.

    Call
    curl -s -X POST "https://www.praxikon.com/api/v1/implementation-map" \
      -H "Content-Type: application/json" \
      -d '{"lang":"en","profile":{
            "actor_role":"deployer","object_type":"ai_system",
            "eu_nexus":"offered_or_used_in_eu","article_5_signal":"none_known",
            "annex_iii_domain":"employment","annex_iii_use_case":"emp-4a",
            "decision_influence":"materially_influences",
            "article_50_scenarios":["none"],"article_50_market_date":"not_relevant",
            "direct_interaction_obvious":"not_relevant",
            "public_interest_editorial_control":"not_relevant",
            "fria_context":"none","gpai_union_market":"not_relevant",
            "gpai_market_date":"not_relevant","gpai_open_source_status":"not_relevant"}}'
    Response (abbreviated)
    {
      "summary": { "applies": 2, "possibly_applies": 0, "not_indicated": 3, "insufficient_context": 0 },
      "results": [
        { "slug": "article-4-ai-literacy",     "applicability": "applies",       "timing": "current" },
        { "slug": "article-50-transparency",   "applicability": "not_indicated", "timing": "current" },
        { "slug": "annex-iii-high-risk",       "applicability": "applies",       "timing": "future"  },
        { "slug": "article-27-fria",           "applicability": "not_indicated", "timing": "future"  },
        { "slug": "article-53-gpai",           "applicability": "not_indicated", "timing": "current" }
      ],
      "snapshot_id": "praxikon:sys:assessment:implementation-snapshot:281d68208ff578821847c141",
      "payload_hash_sha256": "281d68208ff578821847c14127233cd3b52b4a700bb6662eefcc1df969db05b0",
      "decision_engine_version": "1.0.0"
    }

The endpoints

The OpenAPI contract is the normative shape; below, per endpoint, is what it is for, what it accepts and what it returns. A guard in the build checks this list against the routes in both directions, so no endpoint can be described here that does not exist and no endpoint can exist that is missing here.

GET/api/v1/answer

Match a plain-language question to the scenario lexicon and assemble a deterministic answer from it: which obligations are in play, which first actions belong to them, which evidence should be retained. No language model is involved. If the engine recognises nothing it returns mode "none" with alternatives, not a guess.

Parameters

q
Required. The question, truncated at 200 characters. Missing it returns 400 missing_q.
lang
"nl" or "en". Anything that is not "en" is read as "nl".
view
Optional. Omit it or send "full" for the complete response; that stays the default and does not change. With "brief" you get the compact agent form: the question, the likely role, the obligations with label, identifier, legal status and application date, the first actions, the required evidence and one locator per obligation. The same answer from the same engine, roughly a tenth of the bytes, with a "full" link back. An unrecognised value falls back to the full form and does not return 400.
Call
curl -s "https://www.praxikon.com/api/v1/answer?q=our+chatbot+talks+to+customers&lang=en"
Response (abbreviated)
{
  "query": "our chatbot talks to customers",
  "lang": "en",
  "mode": "scenario",
  "question": "Our chatbot talks to customers. Does it have to say it is AI?",
  "likely_role": "Deployer (you use the system)",
  "dataset": {
    "id": "praxikon:sys:registry:dataset:ai-act-implementation-graph",
    "version": "2.1.0",
    "schema_version": "1.4.0",
    "effective_at": "2026-08-08T00:00:00.000Z",
    "known_at": "2026-08-14T00:00:00.000Z",
    "last_reviewed_at": "2026-08-08T00:00:00.000Z",
    "licence": "https://www.praxikon.com/nl/legal/terms",
    "canonical_url": "https://www.praxikon.com/api/v1/entities"
  },
  "obligations": [
    {
      "slug": "article-50-transparency",
      "label": "Article 50: transparency",
      "legal_status": "applicable",
      "deadline_at": "2026-08-02T00:00:00.000Z",
      "human_page": "https://www.praxikon.com/en/verplichtingen/article-50-transparency",
      "citations": [
        {
          "kind": "official_fact",
          "source_id": "praxikon:eu:ai-act:source:reg-eu-2024-1689",
          "source_locator": "Article 50(1)-(5) and Article 113",
          "source_url": "https://eur-lex.europa.eu/eli/reg/2024/1689/oj",
          "eli": "http://data.europa.eu/eli/reg/2024/1689/oj"
        }
      ]
    }
  ],
  "sources": [ ... ],
  "first_actions": [ ... ],
  "evidence": [ ... ],
  "guidance": [ ... ],
  "examples": [ ... ],
  "follow_up_questions": [ ... ],
  "disclaimer": "General interpretation, not legal advice. The official source remains authoritative.",
  "methodology": "https://www.praxikon.com/en/methodologie"
}

GET/api/v1/obligations

The obligation objects: the core of the graph. Each object carries its conditions, exceptions, linked actions, evidence and controls, and per statement the source it rests on.

Parameters

lang
"nl" or "en". Defaults to "nl". Anything else returns 400 invalid_lang.
id
Fetch a single object by its full identifier, for example praxikon:eu:ai-act:obligation:article-50-transparency. The older form raip:obligation:article-50-transparency keeps working and resolves to the same object. Maximum 200 characters.
role
Filter by actor, for example praxikon:eu:ai-act:actor:deployer. This filters on actor_ids and answers which objects are about this role, including the cases where the role is the recipient or an affected party. Maximum 128 characters.
duty_holder
Filter by legal addressee, for example praxikon:eu:ai-act:actor:deployer. This filters on duty_holder_ids and answers the narrower question which duties rest on this role. Objects without a split-out duty holder stay out, so an empty field is never read as a duty. Resolves through the same role hierarchy as role and combines with it as an AND. Maximum 128 characters.
topic
Filter by topic, for example high-risk or timeline. Maximum 128 characters.
effective_at
The legal reference date. ISO date or full timestamp. Defaults to the release reference date.
known_at
The knowledge cut-off: how far our published and checked knowledge reaches. Defaults to the release knowledge date.
format
"json" (default) or "jsonld". An Accept header of application/ld+json does the same.
Call
curl -s "https://www.praxikon.com/api/v1/obligations?lang=en&id=praxikon:eu:ai-act:obligation:article-50-transparency"
Response (abbreviated)
{
  "meta": {
    "dataset_id": "praxikon:sys:registry:dataset:ai-act-implementation-graph",
    "dataset_version": "2.1.0",
    "schema_version": "1.4.0",
    "lang": "en",
    "effective_at": "2026-08-08T00:00:00.000Z",
    "known_at": "2026-08-14T00:00:00.000Z",
    "count": 1,
    "filters": { "id": "praxikon:eu:ai-act:obligation:article-50-transparency", "type": "obligation", "role": null, "duty_holder": null, "topic": null },
    "identifiers": {
      "canonical_namespace": "praxikon",
      "canonical_form": "praxikon:<jurisdiction>:<regulation>:<type>:<slug>",
      "legacy_namespace": "raip",
      "legacy_resolution": "permanent",
      "resolved": { "id": "praxikon:eu:ai-act:obligation:article-50-transparency", "role": null, "duty_holder": null }
    }
  },
  "data": [
    {
      "id": "praxikon:eu:ai-act:obligation:article-50-transparency",
      "legacy_id": "raip:obligation:article-50-transparency",
      "type": "obligation",
      "slug": "article-50-transparency",
      "version": "1.0.0",
      "effective_at": "2026-08-02T00:00:00.000Z",
      "known_at": "2026-08-14T00:00:00.000Z",
      "valid_until": null,
      "payload_hash_sha256": "239fbac0e4728dc239352b2f88b199c3cc098082ff72e2972b4b5e8a2b121406",
      "label": "Article 50: transparency",
      "topics": ["transparency"],
      "actor_ids": ["praxikon:eu:ai-act:actor:deployer", "praxikon:eu:ai-act:actor:provider"],
      "duty_holder_ids": ["praxikon:eu:ai-act:actor:deployer", "praxikon:eu:ai-act:actor:provider"],
      "affected_actor_ids": [],
      "oversight_actor_ids": [],
      "evidence_owner_ids": [],
      "duty_holder_uncertainty_status": null,
      "action_ids": ["praxikon:eu:ai-act:action:article-50-disclosure"],
      "evidence_ids": ["praxikon:eu:ai-act:evidence:article-50-implementation-record"],
      "conditions": [ ... ],
      "exceptions": [ ... ],
      "statements": [
        {
          "kind": "official_fact",
          "text": "Article 50 applies since 2 August 2026. The precise duty differs by scenario: ...",
          "citations": [
            {
              "source_id": "praxikon:eu:ai-act:source:reg-eu-2024-1689",
              "source_locator": "Article 50(1)-(5) and Article 113",
              "source_url": "https://eur-lex.europa.eu/eli/reg/2024/1689/oj",
              "eli": "http://data.europa.eu/eli/reg/2024/1689/oj"
            }
          ],
          "review": {
            "reviewed_at": "2026-08-14T00:00:00.000Z",
            "reviewer": "Praxikon release validation",
            "review_method": "source_link_and_rule_validation",
            "legal_status": "source_checked"
          }
        }
      ],
      "legal_status": "applicable",
      "deadline_at": "2026-08-02T00:00:00.000Z"
    }
  ],
  "included": {
    "sources": [
      {
        "id": "praxikon:eu:ai-act:source:reg-eu-2024-1689",
        "title": { "nl": "...", "en": "..." },
        "publisher": { "nl": "...", "en": "..." },
        "canonical_url": "https://eur-lex.europa.eu/eli/reg/2024/1689/oj",
        "eli": "http://data.europa.eu/eli/reg/2024/1689/oj",
        "source_version": "original-oj-2024-07-12",
        "verified_at": "2026-08-14T00:00:00.000Z",
        "fingerprint_basis": "canonical_url|source_version|verified_at",
        "source_record_hash_sha256": "bf0fca3e..."
      }
    ]
  },
  "links": {
    "self": "https://www.praxikon.com/api/v1/obligations?lang=en&id=praxikon%3Aeu%3Aai-act%3Aobligation%3Aarticle-50-transparency",
    "alternate": "https://www.praxikon.com/api/v1/obligations?lang=en&id=...&format=jsonld",
    "licence": "https://www.praxikon.com/nl/legal/terms"
  }
}

GET/api/v1/changes

The timeline as objects: what started applying when, what moved, and which obligations it touched. Same shape as the obligations, with type "change".

Parameters

lang
"nl" or "en". Defaults to "nl". Anything else returns 400 invalid_lang.
id
Fetch a single object by its full identifier, for example praxikon:eu:ai-act:obligation:article-50-transparency. The older form raip:obligation:article-50-transparency keeps working and resolves to the same object. Maximum 200 characters.
role
Filter by actor, for example praxikon:eu:ai-act:actor:deployer. This filters on actor_ids and answers which objects are about this role, including the cases where the role is the recipient or an affected party. Maximum 128 characters.
duty_holder
Filter by legal addressee, for example praxikon:eu:ai-act:actor:deployer. This filters on duty_holder_ids and answers the narrower question which duties rest on this role. Objects without a split-out duty holder stay out, so an empty field is never read as a duty. Resolves through the same role hierarchy as role and combines with it as an AND. Maximum 128 characters.
topic
Filter by topic, for example high-risk or timeline. Maximum 128 characters.
effective_at
The legal reference date. ISO date or full timestamp. Defaults to the release reference date.
known_at
The knowledge cut-off: how far our published and checked knowledge reaches. Defaults to the release knowledge date.
format
"json" (default) or "jsonld". An Accept header of application/ld+json does the same.
Call
curl -s "https://www.praxikon.com/api/v1/changes?lang=en"
Response (abbreviated)
{
  "meta": { "count": 20, "filters": { "type": "change", ... }, "identifiers": { ... }, ... },
  "data": [
    {
      "id": "praxikon:eu:ai-act:change:2024-08-01-entry-into-force",
      "legacy_id": "raip:change:2024-08-01-entry-into-force",
      "type": "change",
      "slug": "2024-08-01-entry-into-force",
      "version": "1.0.0",
      "effective_at": "2024-08-01T00:00:00.000Z",
      "known_at": "2026-08-14T00:00:00.000Z",
      "payload_hash_sha256": "3005c4a62c53f5c6606f6537159ec01ea6aaf834bfdc68728e4d8d8dece75259",
      "label": "The AI Act enters into force",
      "topics": ["timeline"],
      "obligation_ids": [
        "praxikon:eu:ai-act:obligation:article-4-ai-literacy",
        "praxikon:eu:ai-act:obligation:article-5-prohibited-practices"
      ],
      "statements": [ ... ],
      "legal_status": "applicable",
      "deadline_at": "2024-08-01T00:00:00.000Z"
    }
  ],
  "included": { "sources": [ ... ] },
  "links": { ... }
}

GET/api/v1/entities

Every object type in one endpoint. Without a filter you get the whole graph for the requested moment; with type= you fetch a single kind.

Parameters

lang
"nl" or "en". Defaults to "nl". Anything else returns 400 invalid_lang.
id
Fetch a single object by its full identifier, for example praxikon:eu:ai-act:obligation:article-50-transparency. The older form raip:obligation:article-50-transparency keeps working and resolves to the same object. Maximum 200 characters.
role
Filter by actor, for example praxikon:eu:ai-act:actor:deployer. This filters on actor_ids and answers which objects are about this role, including the cases where the role is the recipient or an affected party. Maximum 128 characters.
duty_holder
Filter by legal addressee, for example praxikon:eu:ai-act:actor:deployer. This filters on duty_holder_ids and answers the narrower question which duties rest on this role. Objects without a split-out duty holder stay out, so an empty field is never read as a duty. Resolves through the same role hierarchy as role and combines with it as an AND. Maximum 128 characters.
topic
Filter by topic, for example high-risk or timeline. Maximum 128 characters.
effective_at
The legal reference date. ISO date or full timestamp. Defaults to the release reference date.
known_at
The knowledge cut-off: how far our published and checked knowledge reaches. Defaults to the release knowledge date.
format
"json" (default) or "jsonld". An Accept header of application/ld+json does the same.
type
One of: actor, obligation, change, action, evidence, control, template, definition, guidance, standard, example. Anything else returns 400 invalid_type.
Call
curl -s "https://www.praxikon.com/api/v1/entities?type=evidence&lang=en"
Response (abbreviated)
{
  "meta": {
    "dataset_version": "2.1.0",
    "schema_version": "1.4.0",
    "lang": "en",
    "count": 36,
    "filters": { "id": null, "type": "evidence", "role": null, "duty_holder": null, "topic": null },
    ...
  },
  "data": [
    {
      "id": "praxikon:eu:ai-act:evidence:annex-iii-article-49-2-registration-record",
      "type": "evidence",
      "version": "1.0.0",
      "payload_hash_sha256": "6b15d546edc41ffd88f61b5c03d4f1c57d60aac40411f5339e09eb7399081b1e",
      "label": "Article 49(2) registration record for the system assessed as not high-risk",
      "summary": "...",
      "obligation_ids": ["praxikon:eu:ai-act:obligation:annex-iii-high-risk"],
      "statements": [ ... ]
    }
  ],
  "included": { "sources": [ ... ] },
  "links": { ... }
}

Search across every object type when you do not yet know which obligation is relevant. Each result carries a score. Search returns stored objects and does not generate an answer.

Parameters

q
Required, maximum 200 characters. Missing it returns 400 missing_q.
limit
Integer from 1 to 50 inclusive. Defaults to 20. Outside that range returns 400 invalid_limit.
lang
"nl" or "en". Defaults to "nl". Anything else returns 400 invalid_lang.
id
Fetch a single object by its full identifier, for example praxikon:eu:ai-act:obligation:article-50-transparency. The older form raip:obligation:article-50-transparency keeps working and resolves to the same object. Maximum 200 characters.
role
Filter by actor, for example praxikon:eu:ai-act:actor:deployer. This filters on actor_ids and answers which objects are about this role, including the cases where the role is the recipient or an affected party. Maximum 128 characters.
duty_holder
Filter by legal addressee, for example praxikon:eu:ai-act:actor:deployer. This filters on duty_holder_ids and answers the narrower question which duties rest on this role. Objects without a split-out duty holder stay out, so an empty field is never read as a duty. Resolves through the same role hierarchy as role and combines with it as an AND. Maximum 128 characters.
topic
Filter by topic, for example high-risk or timeline. Maximum 128 characters.
effective_at
The legal reference date. ISO date or full timestamp. Defaults to the release reference date.
known_at
The knowledge cut-off: how far our published and checked knowledge reaches. Defaults to the release knowledge date.
format
"json" (default) or "jsonld". An Accept header of application/ld+json does the same.
type
Restrict the search to a single object type.
Call
curl -s "https://www.praxikon.com/api/v1/search?q=chatbot&lang=en&limit=1"
Response (abbreviated)
{
  "meta": {
    "dataset_version": "2.1.0",
    "lang": "en",
    "count": 1,
    "query": "chatbot",
    "limit": 1,
    "filters": { "id": null, "type": null, "role": null, "duty_holder": null, "topic": null },
    ...
  },
  "data": [
    {
      "id": "praxikon:eu:ai-act:example:example-high-risk-benefits-chatbot-factual",
      "type": "example",
      "slug": "example-high-risk-benefits-chatbot-factual",
      "version": "1.0.0",
      "payload_hash_sha256": "ccd4592906b30dd15e8f2c82b781487573d638c6715c0b0b4593ee2316f7fac9",
      "label": "Chatbot answering factual questions from a benefits case handler",
      "obligation_ids": ["praxikon:eu:ai-act:obligation:annex-iii-high-risk"],
      "statements": [ ... ],
      "legal_status": "guidance",
      "score": 80
    }
  ],
  "included": { "sources": [ ... ] },
  "links": { ... }
}

GET/api/v1/enforcement

The EU-wide state of enforcement: which member state designated which market surveillance authority, how far the implementing act has come, and the investigations, decisions and fines on record. Since dataset version 2.0.0 the ruling register travels with it: per ruling the relation it bears to a provision (interprets, narrows, broadens, confirms or contradicts). Every entry carries its primary source.

Parameters

country
Two-letter ISO country code. Uppercased server-side and applied to the events as well as the rulings. An unknown code returns 200 with empty lists, not an error; meta.empty_result_note then says why the list is empty.
type
Filter by event kind: designation, implementing_law, investigation, decision, fine, enforcement_signal or related_gdpr.
obligation
Slug of an obligation, for example article-5-prohibited-practices. Returns only the rulings that touch that provision. A slug the graph does not know is distinguishable from a provision nothing has been decided about.
known_at
The knowledge axis of the ruling layer, as an ISO date. Pin it and you get the rulings as we had recorded them at that moment. A ruling does not move the text of a provision; what shifts is recorded_at, the moment we recorded what we know it means.
Call
curl -s "https://www.praxikon.com/api/v1/enforcement?country=NL"
Response (abbreviated)
{
  "meta": {
    "verified_at": "2026-08-10",
    "version": "1.1.0",
    "source": "https://www.praxikon.com/nl/enforcement",
    "stats": {
      "totalEvents": 88,
      "aiActEvents": 79,
      "totalFinesEur": 138200000,
      "countriesWithDesignatedMsa": 11,
      "countriesTotal": 27
    },
    "rulings": {
      "register_id": "praxikon:sys:registry:register:rulings",
      "register_started_at": "2026-08-13",
      "source": "https://www.praxikon.com/nl/rechtspraak",
      "stats": { "total": 7, "aiAct": 0, "relatedGdpr": 7, "obligationsTouched": 4, "byRelation": { "illustrates": 8, ... } },
      "ai_act_case_law_note": "No case law or supervisory decision under the AI Act itself has been recorded. ...",
      "axes": "A ruling does not move the text of a provision. ..."
    },
    "filters": { "country": "NL", "type": null, "known_at": null, "obligation": null },
    "unrecognised_filters": [],
    "empty_result_note": null
  },
  "countries": [
    {
      "country_code": "NL",
      "country_name_en": "Netherlands",
      "msa_status": "draft",
      "implementing_law_status": "consultation",
      ...
    }
  ],
  "events": [
    {
      "id": "implementing_law-nl-dutch-ai-act-implementation-act-enters-public-consultation",
      "type": "implementing_law",
      "date": "2026-04-20",
      "country": "NL",
      "authority": "Ministerie van Economische Zaken",
      "title_en": "Dutch AI Act Implementation Act enters public consultation",
      "summary_en": "...",
      ...
    }
  ],
  "rulings": [
    {
      "id": "praxikon:eu:gdpr:ruling:nl-ap-clearview-2024",
      "basis": "related_gdpr",
      "forum": "supervisory_authority",
      "authority": "Autoriteit Persoonsgegevens (AP)",
      "jurisdiction": "NL",
      "decided_at": "2024-09-03",
      "recorded_at": "2026-08-13",
      "status": "final",
      "title_en": "Dutch DPA fines Clearview AI 30.5 million euros",
      ...
    }
  ]
}

GET/api/v1/corrections

The correction log: every substantive error of ours that we repaired, with its date, what it said, what is correct now, why, and the object identifiers it touched. The register is always returned in full and never truncated. An empty list means nothing has been recorded since register_started_at, not that no error is possible.

Parameters

lang
nl or en. Sets the language of the explanations; the identifiers stay the same.
entity_id
Return only the corrections that touched this graph object, by stable identifier.
Call
curl -s "https://www.praxikon.com/api/v1/corrections?lang=en"
Response (abbreviated)
{
  "meta": {
    "register_id": "praxikon:sys:registry:register:corrections",
    "dataset_id": "praxikon:sys:registry:dataset:ai-act-implementation-graph",
    "dataset_version": "2.1.0",
    "schema_version": "1.4.0",
    "lang": "en",
    "register_started_at": "2026-08-14T00:00:00.000Z",
    "count": 0,
    "complete": true,
    "filters": { "id": null, "entity_id": null }
  },
  "data": [],
  "links": {
    "self": "https://www.praxikon.com/api/v1/corrections?lang=en",
    "licence": "https://www.praxikon.com/nl/legal/terms",
    "human_page": "https://www.praxikon.com/en/correcties",
    "methodology": "https://www.praxikon.com/en/methodologie"
  }
}

GET/api/v1/diff

What changed in the knowledge layer between two reference points, per object: the identifier, whether it was added, modified or lapsed, which fields moved, the payload hash on both sides, and the change event, correction or source it hangs on. Material changes (status, deadline, conditions, exceptions, duty holder, official fact) are kept apart from editorial ones. Two reference points are required: an unpinned diff is not reproducible.

Parameters

from_dataset_version, to_dataset_version
Compare two recorded releases, for example 1.1.0 and 2.1.0. A release without a recorded snapshot returns a 400 that names the releases that do have one.
from, to
Compare two known_at moments against the current release. Use this pair or the release pair, never both.
from_effective_at, to_effective_at
Move the legal axis as well. With it held still an object cannot lapse, because moving the knowledge axis forward only adds candidates.
materiality
material, editorial or all. Filters the result; the class per field stays visible either way.
id, type, lang, limit, offset
The same selection as on the graph endpoints, plus paging. limit defaults to 50 and meta.has_more says whether there is more.
Call
curl -s "https://www.praxikon.com/api/v1/diff?from=2026-08-08&to=2026-08-08&from_effective_at=2026-03-01&to_effective_at=2026-08-01&id=praxikon:eu:ai-act:obligation:article-4-ai-literacy&lang=en"
Response (abbreviated)
{
  "meta": {
    "mode": "known_at",
    "diff_engine_version": "1.1.0",
    "snapshot_format_version": "1.0.0",
    "axes_moved": ["effective_at"],
    "coverage": {
      "recorded_dataset_versions": ["1.1.0", "2.1.0"],
      "sufficient": false,
      "limits": [{ "code": "knowledge_axis_has_one_publication_instant", "detail": "..." }]
    },
    "count": 1, "total": 1, "has_more": false
  },
  "data": {
    "summary": { "status": "changes_found", "objects_compared": 155, "changed": 1, "material_change_present": true },
    "objects": [
      {
        "id": "praxikon:eu:ai-act:obligation:article-4-ai-literacy",
        "change_type": "modified",
        "materiality": "material",
        "version_from": "1.0.0",
        "version_to": "2.0.0",
        "payload_hash_sha256_from": "deadc6d9...",
        "payload_hash_sha256_to": "7a9516670dfca5dd7dcc96ae019e5714f843e20235abd3e9d92b71eb42445ff1",
        "changed_fields": [
          { "field": "conditions", "basis": "applicability", "materiality": "material" },
          { "field": "duty_holder_ids", "basis": "addressee", "materiality": "material" }
        ],
        "attribution": {
          "basis": "change_event",
          "change_events": [{ "id": "praxikon:eu:ai-act:change:2026-07-27-article-4-amended" }],
          "source_ids": ["praxikon:eu:ai-act:source:reg-eu-2026-1744"]
        }
      }
    ],
    "integrity_warnings": []
  }
}

POST/api/v1/impact

A manifest in with a window beside it, and the answer to one question: does what moved in the knowledge layer inside that window touch this dossier. The outcome is one of three statuses, with per touched object the ground on which it reaches you, what moved, the source and the required action. A second entrance runs beside the graph: a ruling moves no payload, yet it can shift what we know a provision means. Stateless: no subscription is created and no dossier is retained, so asking again is your own work.

Parameters

manifest
In the body, required. The complete manifest, in JSON or JSON-LD. If it does not satisfy the published schema the call returns 400 invalid_manifest with the findings and nothing is half assessed. A bare manifest as the body returns 400 missing_manifest.
reference_point
In the body, optional. The window: from_dataset_version and to_dataset_version, or from and to on the knowledge axis, plus optionally effective_at, from_effective_at and to_effective_at. Omit it and the window runs from the release the manifest was computed against to the current one. Both pairs at once returns 400 conflicting_reference_points, half a pair 400 incomplete_reference_points.
lang, emitted_at, max_objects
In the body, optional. lang is "nl" or "en". emitted_at is the moment carried in the event; omit it and the server stamps its clock, with meta.emitted_at_basis saying which of the two happened. max_objects is a whole number from 1 to 200, default 50; what does not fit does not disappear from view, because the counter stays complete and the truncation is flagged in the response.
Call
# impact-request.json:
# {"lang":"en",
#  "reference_point":{"from_dataset_version":"1.1.0","to_dataset_version":"2.1.0"},
#  "manifest": <het volledige manifest uit POST /api/v1/manifest>}

curl -s -X POST "https://www.praxikon.com/api/v1/impact" \
  -H "Content-Type: application/json" \
  -d @impact-request.json
Response (abbreviated)
{
  "meta": {
    "impact_engine_version": "1.1.0",
    "event_format_version": "1.1.0",
    "diff_engine_version": "1.1.0",
    "manifest_version": "1.1.0",
    "dataset_version": "2.1.0",
    "schema_version": "1.4.0",
    "mode": "dataset_version",
    "reference_basis": "caller_supplied",
    "coverage": { "recorded_dataset_versions": ["1.1.0", "2.1.0"], "sufficient": true, "limits": [] },
    "interpretation_window": { "rulings_recorded_in_window": 0, "rulings_in_register": 7, "note": "Rulings move on the knowledge axis and not on the legal axis: ..." },
    "manifest_integrity": { "checked": true, "payload_hash_matches": true, "manifest_id_matches": true },
    "privacy": { "processing": "stateless", "server_persistence": "none", "subscriptions": "none", "archiving": "caller" },
    "signature": { "header": "X-RAIP-Signature", "scheme": "v1", "algorithm": "hmac-sha256", "signed_string": "v1:<timestamp>:<body>", "tolerance_seconds": 300 }
  },
  "data": {
    "impact": "review_recommended",
    "required_action": "review_change",
    "reason": "264 of the 408 objects that moved touch this manifest, none of them on a field that decides whether, when or by whom a duty must be met. ...",
    "summary": {
      "changed_objects_in_window": 408,
      "impacted_objects": 264,
      "rulings_in_window": 0,
      "impacting_rulings": 0,
      "by_impact": { "no_impact": 0, "review_recommended": 264, "reassessment_required": 0 },
      "by_match_basis": { "obligation_carried": 98, "required_action": 2, "required_evidence": 2, "reassessment_trigger": 2, "obligation_evaluated": 83, "obligation_of_manifest_role": 83, "topic_of_carried_obligation": 0 },
      "material_change_present": false
    },
    "objects": [
      {
        "id": "praxikon:eu:ai-act:action:annex-iii-article-6-3-justification",
        "type": "action",
        "change_type": "modified",
        "materiality": "editorial",
        "resolution": "graph",
        "impact": "review_recommended",
        "required_action": "review_change",
        "match_bases": ["obligation_carried"],
        "matched_obligation_ids": ["praxikon:eu:ai-act:obligation:annex-iii-high-risk"],
        "changed_fields": ["actor_ids", "obligation_ids"],
        "material_fields": [],
        "attribution": { "basis": "unattributed", "change_events": [], "corrections": [], "source_ids": [] }
      }
    ],
    "objects_total": 264,
    "objects_truncated": true,
    "interpretations": [],
    "max_objects": 50,
    "manifest_features": { "carried_obligation_ids": [ ... ], "role_ids": ["praxikon:eu:ai-act:actor:deployer"], "topics": ["ai-literacy", "high-risk"] },
    "event": { "event": "impact.review_recommended", "event_format_version": "1.1.0", ... }
  },
  "links": { "self": "...", "diff": "...", "rulings": "https://www.praxikon.com/api/v1/enforcement", "manifest": "...", "openapi": "..." }
}

GET/api/v1/naleving-index

The State of AI Act compliance NL: the monthly measurement on the Dutch algorithm register. Per round five indicators (impact assessment, legal basis, proportionality, human oversight, currency), counted separately for all entries and for the self-declared high-risk systems, plus the composite index. We measure the register, not the organisation: an empty field means the register shows no evidence.

Parameters

round
Id of a measurement round, for example nl-2026-08. Returns only that round. An unknown round returns 200 with every round; meta.unrecognised_filters and meta.rounds_available then say what does exist.
Call
curl -s "https://www.praxikon.com/api/v1/naleving-index"
Response (abbreviated)
{
  "meta": {
    "verified_at": "2026-08-20",
    "version": "1.0.0",
    "cadence": "monthly",
    "source": {
      "url": "https://www.praxikon.com/nl/staat-van-naleving",
      "register_url": "https://algoritmes.overheid.nl",
      "register_name": "Algoritmeregister van de Nederlandse overheid"
    },
    "definitions": [ { "key": "impact_assessment", "label": { "nl": "Impacttoets vermeld", ... }, "counts": { ... } } ],
    "index_formula": "unweighted mean of the five indicator shares, expressed 0 to 100",
    "delta_with_previous": { "index_all": null, "index_high_risk": null, "total_entries": null },
    "rounds_available": ["nl-2026-08"],
    "filters": { "round": null },
    "unrecognised_filters": [],
    "empty_result_note": null
  },
  "measurements": [
    {
      "id": "nl-2026-08",
      "measured_at": "2026-08-20",
      "total_entries": 1536,
      "index_all": 71.9,
      "index_high_risk": 67.3,
      "all": { "entries": 1536, "impact_assessment": { "count": 655, "share": 42.6 }, ... },
      "high_risk": { "entries": 41, "impact_assessment": { "count": 17, "share": 41.5 }, ... },
      "top_organisations": [ { "organisation": "Gemeente Amsterdam", "entries": 71 } ]
    }
  ],
  "observations": [ { "id": "nl-2026-06", "total_entries": 1462, "high_risk_entries": 41, ... } ]
}

POST/api/v1/implementation-map

The full assessment: coded answers in, a signed dossier out. Per obligation you get whether it applies, when, why, and the rule trace underneath. Stateless: no profile is retained.

Parameters

lang
In the body. "nl" or "en".
profile
In the body. The coded answers: role, object type, EU nexus and the routing questions. An unknown, missing or conflicting code returns 400 invalid_profile.
previous_snapshot
In the body, optional. An earlier dossier the outcome is compared against. If its integrity check fails, the call returns 400 invalid_previous_snapshot.
Call
curl -s -X POST "https://www.praxikon.com/api/v1/implementation-map" \
  -H "Content-Type: application/json" \
  -d '{
    "lang": "en",
    "profile": {
      "actor_role": "deployer",
      "object_type": "ai_system",
      "eu_nexus": "offered_or_used_in_eu",
      "article_5_signal": "none_known",
      "annex_iii_domain": "employment",
      "annex_iii_use_case": "emp-4a",
      "decision_influence": "materially_influences",
      "article_50_scenarios": ["none"],
      "article_50_market_date": "not_relevant",
      "direct_interaction_obvious": "not_relevant",
      "public_interest_editorial_control": "not_relevant",
      "fria_context": "none",
      "gpai_union_market": "not_relevant",
      "gpai_market_date": "not_relevant",
      "gpai_open_source_status": "not_relevant"
    }
  }'
Response (abbreviated)
{
  "snapshot_schema_version": "1.0.0",
  "decision_engine_version": "1.0.0",
  "language": "en",
  "dataset": {
    "id": "praxikon:sys:registry:dataset:ai-act-implementation-graph",
    "version": "2.1.0",
    "schema_version": "1.4.0",
    "effective_at": "2026-08-08T00:00:00.000Z",
    "known_at": "2026-08-14T00:00:00.000Z",
    "last_reviewed_at": "2026-08-08T00:00:00.000Z"
  },
  "profile": { ... },
  "summary": { "applies": 2, "possibly_applies": 0, "not_indicated": 3, "insufficient_context": 0 },
  "results": [
    {
      "obligation_id": "praxikon:eu:ai-act:obligation:article-4-ai-literacy",
      "slug": "article-4-ai-literacy",
      "applicability": "applies",
      "timing": "current",
      "effective_on": "2025-02-02T00:00:00.000Z",
      "legal_status": "applicable",
      "deadline_at": "2025-02-02T00:00:00.000Z",
      "why": [ ... ],
      "rule_trace": [
        { "rule_id": "scope.eu-nexus", "outcome": "met", "explanation": "..." },
        { "rule_id": "article-4.actor-role", "outcome": "met", "explanation": "..." },
        { "rule_id": "article-4.ai-system", "outcome": "met", "explanation": "..." }
      ],
      "assumptions": [ ... ],
      "actions": [ ... ],
      "evidence": [ ... ],
      "controls": [ ... ],
      "sources": [ ... ],
      "entity_version": "2.0.0",
      "entity_payload_hash_sha256": "7a9516670dfca5dd7dcc96ae019e5714f843e20235abd3e9d92b71eb42445ff1",
      "result_payload_hash_sha256": "ab3d561e..."
    }
  ],
  "source_fingerprints": [ ... ],
  "snapshot_id": "praxikon:sys:assessment:implementation-snapshot:281d68208ff578821847c141",
  "payload_hash_sha256": "281d68208ff578821847c14127233cd3b52b4a700bb6662eefcc1df969db05b0",
  "generated_at": "2026-08-13T19:40:02.859Z",
  "source_fingerprint_basis": "canonical_url|source_version|verified_at",
  "source_fingerprint_scope": "...",
  "privacy": { "processing": "stateless", "server_persistence": "none", "profile_contains_free_text": false },
  "disclaimer": "...",
  "revalidation": { "status": "first_assessment", "compared_snapshot_id": null, "changed_results": [] }
}

POST/api/v1/manifest

System description in, a complete Regulatory Manifest out: indicated duties each with their own status, the rule trace underneath, what is still open, and a source_snapshot with the dataset version, the schema version and a fingerprint per source record. Stateless: nothing is stored, so archiving is your own work.

Parameters

lang
In the body. "nl" or "en". Sets the language of the labels and explanations; inside the manifest the field is called language.
system_version
In the body, required. The version of the assessed system as you name it: a release tag, a semantic version or a dated build label.
assessed_at
In the body, optional. The moment of assessment, in UTC with a Z. Supply it and the answer is byte-identical on every call against the same dataset version. Omit it and the server stamps its clock, which moves assessed_at, manifest_id and payload_hash_sha256 on every call; meta.assessed_at_basis says which of the two happened.
intended_purpose
In the body, optional. The intended purpose in free text. Carried along unchanged and never parsed; the coded answers do the routing.
profile
In the body, required. The same coded answers as /api/v1/implementation-map. An unknown, missing or conflicting code returns 400 invalid_manifest_request naming the fields, never a partial manifest.
format
As a query parameter. "json" (default) or "jsonld". With jsonld you get the manifest itself under its published context, without the envelope. An Accept header of application/ld+json does the same.
Call
curl -s -X POST "https://www.praxikon.com/api/v1/manifest" \
  -H "Content-Type: application/json" \
  -d '{
    "lang": "en",
    "system_version": "4.2.0",
    "assessed_at": "2026-08-12T09:00:00.000Z",
    "intended_purpose": "Ranking job applicants in the first selection round.",
    "profile": {
      "actor_role": "deployer",
      "object_type": "ai_system",
      "eu_nexus": "offered_or_used_in_eu",
      "article_5_signal": "none_known",
      "annex_iii_domain": "employment",
      "annex_iii_use_case": "emp-4a",
      "decision_influence": "materially_influences",
      "article_50_scenarios": ["none"],
      "article_50_market_date": "not_relevant",
      "direct_interaction_obvious": "not_relevant",
      "public_interest_editorial_control": "not_relevant",
      "fria_context": "none",
      "gpai_union_market": "not_relevant",
      "gpai_market_date": "not_relevant",
      "gpai_open_source_status": "not_relevant"
    }
  }'
Response (abbreviated)
{
  "meta": {
    "manifest_version": "1.1.0",
    "manifest_engine_version": "1.0.0",
    "decision_engine_version": "1.0.0",
    "dataset_id": "praxikon:sys:registry:dataset:ai-act-implementation-graph",
    "dataset_version": "2.1.0",
    "schema_version": "1.4.0",
    "lang": "en",
    "effective_at": "2026-08-08T00:00:00.000Z",
    "known_at": "2026-08-14T00:00:00.000Z",
    "assessed_at": "2026-08-12T09:00:00.000Z",
    "assessed_at_basis": "caller_supplied",
    "determinism": "The same input against the same dataset_version reproduces this manifest byte for byte, including manifest_id and payload_hash_sha256.",
    "privacy": {
      "processing": "stateless",
      "server_persistence": "none",
      "archiving": "caller",
      "note": "This manifest is not stored. There is no account, no server-side storage and no copy of your answers; archiving this document is your own responsibility."
    },
    "schema": "https://www.praxikon.com/schemas/regulatory-manifest-v1.schema.json"
  },
  "data": {
    "manifest_id": "praxikon:sys:assessment:manifest:c839fbce0cd03fd4c2c4296a",
    "manifest_version": "1.1.0",
    "system_version": "4.2.0",
    "language": "en",
    "assessed_at": "2026-08-12T09:00:00.000Z",
    "effective_at": "2026-08-08T00:00:00.000Z",
    "known_at": "2026-08-14T00:00:00.000Z",
    "roles": ["praxikon:eu:ai-act:actor:deployer"],
    "classification": { "result": "high_risk_annex_iii", "basis": [ ... ], "open_questions": [], "reassessment_triggers": [ ... ] },
    "obligations": [
      { "obligation_id": "praxikon:eu:ai-act:obligation:annex-iii-high-risk", "applicability": "applies", "uncertainty_status": "determined", "timing": "future", "deadline_at": "2027-12-02T00:00:00.000Z", ... },
      { "obligation_id": "praxikon:eu:ai-act:obligation:article-4-ai-literacy", "applicability": "applies", "uncertainty_status": "determined", "timing": "current", "deadline_at": "2025-02-02T00:00:00.000Z", ... }
    ],
    "uncertainty_status": "determined",
    "required_actions": [ ... ],
    "required_evidence": [ ... ],
    "assessment_input": { ... },
    "derived_from": { "snapshot_id": "praxikon:sys:assessment:implementation-snapshot:...", ... },
    "source_snapshot": { "dataset_version": "2.1.0", "schema_version": "1.4.0", "source_fingerprint_basis": "canonical_url|source_version|verified_at", "source_hashes": [ ... ] },
    "payload_hash_sha256": "c839fbce0cd03fd4c2c4296a25f766b11383680b6dfddbb0e4382d2ea8362645"
  },
  "links": { "self": "...", "alternate": "...", "schema": "...", "context": "...", ... }
}

POST/api/v1/manifest/validate

Checks a manifest you already hold against the published schema and returns readable findings: the path, the machine-readable code and one sentence in your language. On a schema-valid manifest it also recomputes the payload hash and reports whether the dataset release is still the published one. What it does not do: reassess, test the content against the law, or store the submitted manifest.

Parameters

lang
In the body. "nl" or "en". Sets the language of the findings; the code stays language-independent.
manifest
In the body, required. The manifest to check, in JSON or in JSON-LD. A bare manifest as the body is refused with 400 invalid_request, so a validation request can never be mistaken for a manifest. For JSON-LD, @context and the manifest type are set aside before the schema runs.
Call
# validate-request.json:
# {"lang":"en","manifest": <het manifest dat u wilt toetsen>}

curl -s -X POST "https://www.praxikon.com/api/v1/manifest/validate" \
  -H "Content-Type: application/json" \
  -d @validate-request.json
Response (abbreviated)
{
  "meta": {
    "schema_id": "https://www.praxikon.com/schemas/regulatory-manifest-v1.schema.json",
    "manifest_version": "1.1.0",
    "manifest_engine_version": "1.0.0",
    "lang": "en",
    "input_serialization": "json",
    "scope": "This endpoint checks shape and internal consistency: the manifest schema, the payload hash and the dataset release it was computed against. It does not reassess, it does not test the content against the law, and it is not a legal opinion.",
    "privacy": { "processing": "stateless", "server_persistence": "none", "archiving": "caller", ... }
  },
  "data": {
    "valid": false,
    "issue_count": 3,
    "issues": [
      { "path": "#/assessed_at", "code": "invalid_date_time", "message": "This must be a full ISO 8601 instant including a time zone, for example 2026-08-12T09:00:00.000Z. A bare date is not enough." },
      { "path": "#/assessed_at", "code": "shorter_than_20", "message": "This string is shorter than the required 20 characters." },
      { "path": "#/verdict", "code": "additional_property_not_allowed", "message": "This field is not part of the manifest schema. The schema allows no extra fields, so a reader never has to guess what an unknown field means." }
    ],
    "issues_truncated": false,
    "integrity": { "checked": false, "payload_hash_matches": null, "manifest_id_matches": null, "scope": "The hash was not recomputed, because the document does not satisfy the schema. Repair the shape first." },
    "dataset_alignment": null
  },
  "links": { "self": "...", "schema": "...", "context": "...", ... }
}

GET/api/v1/dataset

The dataset description as DCAT-3 JSON-LD: publisher, licence, version, language coverage, distributions and the endpoints that serve this dataset. Meant for catalogues and crawlers, not for day-to-day calls.

Parameters

No parameters.

Call
curl -s -H "Accept: application/ld+json" "https://www.praxikon.com/api/v1/dataset"
Response (abbreviated)
{
  "@context": [ "https://www.praxikon.com/contexts/praxikon-v1.jsonld", { ... } ],
  "@id": "urn:praxikon:sys:registry:dataset:ai-act-implementation-graph",
  "@type": ["dcat:Dataset", "schema:Dataset", "prov:Entity"],
  "dct:identifier": "praxikon:sys:registry:dataset:ai-act-implementation-graph",
  "dcat:version": "2.1.0",
  "raipv:schemaVersion": "1.4.0",
  "raipv:appendOnly": true,
  "raipv:defaultEffectiveAt": "2026-08-08T00:00:00.000Z",
  "raipv:defaultKnownAt": "2026-08-14T00:00:00.000Z",
  "raipv:sourceCount": 13,
  "raipv:entityCount": 424,
  "dcat:accessService": { "dcat:endpointDescription": { "@id": "https://www.praxikon.com/api/v1/openapi" }, ... },
  "dcat:distribution": [ ... ]
}

GET/api/v1/openapi

The OpenAPI 3.1 contract for this API, machine-readable. That contract is the shape that governs; this page is the readable commentary on it.

Parameters

No parameters.

Call
curl -s "https://www.praxikon.com/api/v1/openapi"
Response (abbreviated)
{
  "openapi": "3.1.1",
  "info": {
    "title": "Praxikon public API",
    "version": "1.11.0-beta.1",
    "summary": "Versioned, source-traceable EU AI Act implementation data for people and AI agents."
  },
  "servers": [{ "url": "https://www.praxikon.com", "description": "Public production endpoint" }],
  "paths": {
    "/api/v1/obligations": { ... },
    "/api/v1/changes": { ... },
    "/api/v1/entities": { ... },
    "/api/v1/answer": { ... },
    "/api/v1/enforcement": { ... },
    "/api/v1/dataset": { ... },
    "/api/v1/openapi": { ... },
    "/api/v1/status": { ... },
    "/changelog.json": { ... },
    "/api/v1/corrections": { ... },
    "/api/v1/diff": { ... },
    "/api/v1/search": { ... },
    "/api/v1/implementation-map": { ... },
    "/api/v1/manifest": { ... },
    "/api/v1/manifest/validate": { ... },
    "/api/v1/impact": { ... }
  }
}

GET/api/v1/status

The state of the release, in facts that come from somewhere: which dataset, schema and document version is being served, how far its knowledge date reaches, how many objects and object versions it holds, whether the release guards ran over exactly this content, and what does and does not apply in the way of request limits. This is the cheapest address to poll: send the ETag back as If-None-Match and you get 304 with no body. No availability figure, because we do not measure one, and it says so.

Parameters

format
Only "json". Anything else returns 400 unsupported_format rather than a document no context defines.
Call
curl -s "https://www.praxikon.com/api/v1/status"
Response (abbreviated)
{
  "meta": {
    "dataset_id": "praxikon:sys:registry:dataset:ai-act-implementation-graph",
    "dataset_version": "2.1.0",
    "schema_version": "1.4.0",
    "openapi_version": "1.11.0-beta.1"
  },
  "data": {
    "release": {
      "known_at": "2026-08-14T00:00:00.000Z",
      "append_only": true,
      "graph_fingerprint_sha256": "{{graph_fingerprint_sha256}}",
      "fingerprint_basis": "sha256 over the canonical JSON of the whole dataset, including its metadata. ..."
    },
    "objects": {
      "entities": 424,
      "entity_versions": 425,
      "sources": 13,
      "by_type": { "obligation": 27, "example": 121, "definition": 64, ... }
    },
    "release_guards": {
      "status": "passed",
      "command": "npm run lint:content",
      "guard_count": 79,
      "basis": "The recorded run covers this exact dataset: the fingerprint written when the guard chain completed equals the fingerprint of the dataset served here. ..."
    },
    "service_level": { "committed": false, "uptime_measured": false, "latency_measured": false },
    "rate_limit": {
      "committed_limit": false,
      "enforced": [
        { "path": "/api/v1/answer", "max_requests": 60, "window_seconds": 60, "shared_across_instances": false },
        { "path": "/api/v1/enforcement", "max_requests": 60, "window_seconds": 60, "shared_across_instances": false }
      ],
      "unlimited": [ { "path": "/api/v1/obligations", "enforced": false }, ... ]
    },
    "registers": { "corrections": { ... }, "graph_snapshots": { ... }, "changelog": { ... } }
  }
}

GET/changelog.json

The release history, newest first. Each entry states which of the three counters moved: the content, the shape of that content, or the API surface around it. A release that only widened the surface touched no citation, and then there is nothing for you to re-check. Where both sides have a recorded snapshot there is a link to the per-object comparison; where there is none, the reason is stated rather than left as an empty space.

Parameters

No parameters.

Call
curl -s "https://www.praxikon.com/changelog.json"
Response (abbreviated)
{
  "changelog_id": "praxikon:sys:registry:register:dataset-releases",
  "changelog_format_version": "1.0.0",
  "register_started_at": "2026-08-08",
  "current": { "dataset_version": "2.1.0", "schema_version": "1.4.0", "openapi_version": "1.11.0-beta.1" },
  "coverage": {
    "recorded_releases": 3,
    "recorded_dataset_versions": ["1.1.0", "2.0.0", "2.1.0"],
    "limits": [ { "code": "content_releases_without_a_snapshot", "detail": { "nl": "...", "en": "..." } } ]
  },
  "releases": [
    {
      "release_id": "praxikon:sys:registry:release:2026-08-13-ruling-register",
      "sequence": 9,
      "released_at": "2026-08-13",
      "dataset_version": "2.1.0",
      "schema_version": "1.4.0",
      "moved": ["api_surface"],
      "headline": { "en": "What we know a provision means" },
      "summary": { "en": "..." }
    }
  ]
}

Errors and limits

The shape of an error

Every public v1 endpoint returns an error as an object with a machine-readable code, a readable detail and a pointer to the contract. One shape, on the POST endpoints as well, so you only have to program against it once.

Code
{
  "error": {
    "code": "invalid_lang",
    "detail": "Invalid graph request: invalid_lang",
    "documentation": "https://www.praxikon.com/api/v1/openapi"
  }
}

Error codes

400
invalid_lang
lang was not "nl" or "en".
400
invalid_format
format was not "json" or "jsonld".
400
unsupported_format
A projection was requested that this endpoint does not have. The status document exists as JSON only.
400
invalid_type
type was not one of the object types.
400
invalid_limit
limit was not an integer from 1 to 50.
400
missing_q
The search or the question arrived without q.
400
invalid_effective_at, invalid_known_at
The date could not be read as an ISO date or timestamp.
400
id_too_long, role_too_long, topic_too_long
The filter value exceeded its length limit (200 characters for id, 128 for role and topic).
400
invalid_json
The body of a POST endpoint did not contain valid JSON.
400
invalid_request
The body carried a field the endpoint does not know. The message names the fields it does take.
400
invalid_profile, invalid_manifest_request
The profile contained an unknown, missing or conflicting answer code. The manifest variant names the fields and never returns a partial manifest.
400
invalid_previous_snapshot
The submitted earlier dossier failed its integrity check.
400
missing_manifest
The impact request arrived without a manifest field. A bare manifest as the body is refused, so a question can never be mistaken for a document.
400
invalid_manifest
The submitted manifest does not satisfy the published schema. The findings travel with the error; nothing is half assessed.
400
invalid_reference_point, conflicting_reference_points, incomplete_reference_points
The window did not hold up: an unknown field, two kinds of reference point at once, or half a pair. An unpinned comparison is not reproducible.
400
unknown_from_dataset_version, unknown_to_dataset_version
No snapshot is recorded for that release, so that side of the window cannot be reconstructed. The message names the releases that do have one.
400
invalid_max_objects
max_objects was not a whole number from 1 to 200.
413
payload_too_large
The body of a POST endpoint exceeded 128 KiB.
429
rate_limited
One of the two capped endpoints received too many requests. Retry-After is in the header, in seconds.

Request limits

Two endpoints carry a cap that is genuinely enforced. Above it you get 429 with a Retry-After header in seconds and the same error object as above, with code rate_limited. The list below comes from the same table the routes enforce and GET /api/v1/status serves, so it cannot claim anything other than what happens.

Enforced

/api/v1/answer
60 requests per 60 seconds per client address, per server process
/api/v1/enforcement
60 requests per 60 seconds per client address, per server process
/api/v1/naleving-index
60 requests per 60 seconds per client address, per server process

Uncapped today

/api/v1/obligations, /api/v1/changes, /api/v1/entities, /api/v1/search, /api/v1/corrections, /api/v1/diff, /api/v1/dataset, /api/v1/openapi, /api/v1/status, /api/v1/implementation-map, /api/v1/manifest, /api/v1/manifest/validate, /api/v1/impact

On the rest no limit is active today. That is a description, not a commitment: if traffic calls for a limit there will be one, and it will be announced in the OpenAPI document before it takes effect. The reverse holds too: uncapped is not a quota you can plan against. The counter that does exist lives in the memory of one server process and resets on restart, so it throttles a runaway script and guarantees nobody anything.

Code
HTTP/1.1 429 Too Many Requests
Retry-After: 60

{
  "error": {
    "code": "rate_limited",
    "detail": "This endpoint accepts 60 requests per 60 seconds per client address on one server instance. ...",
    "documentation": "https://www.praxikon.com/api/v1/openapi"
  }
}

Caching

The graph endpoints send an ETag over the payload, a Last-Modified equal to the last release check, and Cache-Control: public, max-age=300, s-maxage=3600, stale-while-revalidate=86400. Send the ETag back in If-None-Match and you get a 304. The implementation map, the two manifest endpoints and the impact request do the opposite and answer with no-store, because nothing there should be retained.

The documents underneath

Six documents in the repository pin down what this API does, what the agent surface on top of it does, and what may not quietly change. They are not yet published at a public URL; if you have the repository, you read them there.

docs-public/ONTOLOGY.md
The object types of the graph: what each type is for, which fields are mandatory, what the identifier looks like, how versions and hashes work, and which relations a type carries.
docs-public/LEGAL-MODEL.md
Relation precision (who carries the duty, who is affected, who supplies what to whom) and uncertainty as status: never a percentage, always a status with a prescribed next step.
docs-public/VERSIONING.md
The stability contract: identifier policy, semver rules, the two time axes, deprecation windows and the citation format. Where this document and the ontology touch, this one wins.
docs-public/IMPACT-FEED.md
What the impact request does and does not claim: the three statuses, the grounds on which a changed object reaches a dossier, and the shape and signature of an event.
docs-public/MCP.md
The agent surface: which tools the MCP server exposes, what each promises about its output, how to install it and what is tested. The HTTP API remains the contract; that layer adds no data and no rules.
docs-public/AGENT-CONFORMANCE.md
The chain of seven questions an agent can ask, and the conformance suite that establishes the answers line up.

The Regulatory Manifest

The manifest is the portable projection of a single assessment: which obligations were identified, what that rests on, what remains open, and against which dataset version and source fingerprints it was computed. It is meant to be handed over: to a supervisor, an auditor, a client or your own archive. Two identical inputs produce the same manifest_id, because that id is the first 24 characters of the payload hash.

The schema is normative and lives in schemas/regulatory-manifest-v1.schema.json (JSON Schema draft 2020-12), with a JSON-LD context beside it in schemas/regulatory-manifest-v1.context.jsonld. The TypeScript mirror is lib/manifest/types.ts and may not diverge from it. Generation runs through generateRegulatoryManifest in lib/manifest/generate.ts, validation through lib/manifest/validate.ts, and npm run test:manifest guards both.

Three endpoints, all stateless

POST /api/v1/manifest takes the system description and returns a complete manifest. POST /api/v1/manifest/validate checks a manifest you already hold against the schema and returns readable findings. POST /api/v1/impact says whether what moved in the knowledge layer since then touches that dossier. Nothing is stored: no account, no subscription, no server-side copy of your answers and no way to retrieve this manifest later, so keep it yourself. Supply assessed_at and the answer is byte-identical against the same dataset version. With format=jsonld you get the same document under the published context at /schemas/regulatory-manifest-v1.context.jsonld.

Example manifest (abbreviated)

The manifest below comes from the case that also sits in tests/manifest/regulatory-manifest.test.cjs: a deployer ranking job applicants, which is Annex III point 4(a).

Code
{
  "manifest_id": "praxikon:sys:assessment:manifest:825b9fec37d73d1f4def230d",
  "manifest_version": "1.1.0",
  "manifest_engine_version": "1.0.0",
  "decision_engine_version": "1.0.0",
  "system_version": "4.2.0",
  "language": "nl",
  "assessed_at": "2026-08-12T09:00:00.000Z",
  "effective_at": "2026-08-08T00:00:00.000Z",
  "known_at": "2026-08-14T00:00:00.000Z",
  "roles": ["praxikon:eu:ai-act:actor:deployer"],
  "intended_purpose": {
    "description": "Sollicitanten rangschikken in de eerste selectieronde.",
    "annex_iii_domain": "employment",
    "annex_iii_use_case": "emp-4a"
  },
  "eu_nexus": "offered_or_used_in_eu",
  "classification": {
    "result": "high_risk_annex_iii",
    "basis": [
      {
        "rule_id": "annex-iii.listed-purpose",
        "outcome": "met",
        "explanation": "Het bedoelde doel moet aan een concrete usecase in Bijlage III worden gekoppeld. Alleen een sector of domein kiezen is niet genoeg.",
        "obligation_id": "praxikon:eu:ai-act:obligation:annex-iii-high-risk"
      },
      {
        "rule_id": "annex-iii.article-6-3-filter",
        "outcome": "not_met",
        "explanation": "Artikel 6 lid 3 kan alleen via een gedocumenteerde, strikte toets uitkomst bieden. Profiling houdt het systeem hoog-risico.",
        "obligation_id": "praxikon:eu:ai-act:obligation:annex-iii-high-risk"
      }
    ],
    "open_questions": [],
    "reassessment_triggers": [
      {
        "kind": "control",
        "id": "praxikon:eu:ai-act:control:annex-iii-change-trigger",
        "obligation_id": "praxikon:eu:ai-act:obligation:annex-iii-high-risk",
        "label": "Herclassificatie bij doel- of contextwijziging",
        "at": null
      },
      {
        "kind": "deadline",
        "id": "praxikon:eu:ai-act:obligation:annex-iii-high-risk",
        "obligation_id": "praxikon:eu:ai-act:obligation:annex-iii-high-risk",
        "label": "Bijlage III: hoog-risico AI",
        "at": "2027-12-02T00:00:00.000Z"
      }
    ]
  },
  "evaluated_obligation_ids": [
    "praxikon:eu:ai-act:obligation:annex-iii-high-risk",
    "praxikon:eu:ai-act:obligation:article-27-fria",
    "praxikon:eu:ai-act:obligation:article-4-ai-literacy",
    "praxikon:eu:ai-act:obligation:article-50-transparency",
    "praxikon:eu:ai-act:obligation:article-53-gpai"
  ],
  "obligations": [
    {
      "obligation_id": "praxikon:eu:ai-act:obligation:annex-iii-high-risk",
      "slug": "annex-iii-high-risk",
      "applicability": "applies",
      "uncertainty_status": "determined",
      "timing": "future",
      "effective_on": "2027-12-02T00:00:00.000Z",
      "deadline_at": "2027-12-02T00:00:00.000Z",
      "legal_status": "upcoming",
      "blocking_flags": [],
      "human_page": "https://www.praxikon.com/nl/verplichtingen/annex-iii-high-risk",
      "entity_version": "1.0.0",
      "entity_payload_hash_sha256": "c0afd1789ed393ba3f9ce04205bd74b4831ff0fd58146108fdd8dd08d2f4f6c9"
    }
  ],
  "required_actions": [ ... ],
  "required_evidence": [ ... ],
  "uncertainty_status": "determined",
  "assessment_input": { ... },
  "derived_from": {
    "snapshot_id": "praxikon:sys:assessment:implementation-snapshot:...",
    "snapshot_payload_hash_sha256": "..."
  },
  "source_snapshot": {
    "dataset_id": "praxikon:sys:registry:dataset:ai-act-implementation-graph",
    "dataset_version": "2.1.0",
    "schema_version": "1.4.0",
    "last_reviewed_at": "2026-08-08T00:00:00.000Z",
    "source_fingerprint_basis": "canonical_url|source_version|verified_at",
    "source_fingerprint_scope": "De bronhash dekt onze registratie van de bron (URL, uitgaveversie, controledatum) en is geen archiefkopie of inhoudshash van het externe document.",
    "source_hashes": [
      {
        "source_id": "praxikon:eu:ai-act:source:reg-eu-2024-1689",
        "source_version": "original-oj-2024-07-12",
        "verified_at": "2026-08-14T00:00:00.000Z",
        "source_record_hash_sha256": "bf0fca3e..."
      }
    ]
  },
  "payload_hash_sha256": "825b9fec37d73d1f4def230d0d8d807d9eb17c7801cc0ea2d996d56ca0803824"
}

MCP server

The REST API is machine-readable, but it does not introduce itself: an agent still has to know that /api/v1/answer exists and which parameters it takes. The MCP server inverts that. The agent asks tools/list once and gets typed tools back, each with a description saying when to reach for it, a JSON Schema for the input and a JSON Schema for the output. The protocol is JSON-RPC 2.0 over stdin and stdout, the server has no dependencies and touches only public endpoints. Everything is read-only and no credentials are needed.

What it deliberately is not: a chat interface on the AI Act. There is no language model in it and nothing is formulated. Each tool is a thin projection of one or two published endpoints, and a derivation the API already performs is not rebuilt here, because a second implementation of a rule is a second truth and two truths drift. Every output carries three fixed blocks: provenance with each backend call and its status, pin with the release and the two time axes, and on every graph object a citation with identifier, version and payload hash. Malformed input is refused by the schema before a request goes out; an error from the API travels through with its own code.

The difference from a model that guesses

explain_applicability returns the legal path rather than only an outcome: the conditions under which the rule is about a situation, the exceptions under which it drops out again, and per official fact the source, the locator inside it and the source record behind it. Supply a profile and the rule trace of the decision engine is added: which rule was met, not met or unknown. Where the engine does not evaluate that obligation, no assessment appears and the reason is stated, because an empty field reads as "nothing applies".

Tools

Assessing one system

classify_system
Which routes of the Regulation come into view for one concrete system.
get_applicable_obligations
The indicated obligations themselves, with status, timing and the reasons the engine recorded.
explain_applicability
The legal path: condition, exception, source and locator, plus the rule trace when a profile is supplied.
get_required_actions
What has to be done, from the graph relations or from the assessment.
get_required_evidence
What has to be recorded and producible.
get_source_provenance
Statements with their kind, source, locator and the source record behind them.

The portable dossier

create_regulatory_manifest
The Regulatory Manifest for one assessed system.
validate_manifest
Check a manifest you already hold against the published schema.
get_regulatory_diff
What moved between two reference points, per object and per field.
analyse_change_impact
Whether what moved in the knowledge layer touches this dossier.

Reading the graph

get_obligations
The obligation catalogue, filtered by role, duty holder or topic.
get_graph_entities
Entities by type: guidance, standard, example, action, definition, actor.
search_implementation_graph
Search when it is not yet clear which object is relevant.
list_regulatory_changes
Timeline: what started applying when, and what moved.
get_national_enforcement
Supervisor, implementing act, signals and recorded rulings per member state.
list_corrections
What we got wrong and repaired, with the identifiers it touched.
answer_ai_act_question
Deterministic lookup of a recognised practical scenario.
get_dataset_status
Which release is being served, and what it is honest about.
Register it
claude mcp add praxikon -- node /path/to/EUAIACT/mcp/praxikon-mcp.mjs
Or through a JSON configuration
{
  "mcpServers": {
    "praxikon": {
      "command": "node",
      "args": ["/path/to/EUAIACT/mcp/praxikon-mcp.mjs"]
    }
  }
}
Against a local build
PRAXIKON_API=http://localhost:3031 node mcp/praxikon-mcp.mjs
Test it by hand, without a client
printf '%s\n%s\n' \
  '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{}}}' \
  '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' \
  | node mcp/praxikon-mcp.mjs

The server speaks protocol versions 2025-06-18, 2025-03-26 and 2024-11-05, and answers with structuredContent as well as the same object as a text block, so an older client misses nothing. The server file is mcp/praxikon-mcp.mjs, the full contract is in docs-public/MCP.md, and npm run test:mcp runs the real server against the route handlers in this repository.

What this adds up to

The promise is narrow and therefore keepable: the same question on the same release returns the same answer, every answer points at its official source, and what changes gets a new version instead of a silent overwrite. This remains general interpretation and not legal advice; the official source prevails over our summary.