For AI agents: classifying CRA applicability over HTTP
Cybiq can be evaluated end to end by a software agent without a sales call and
without a human filling in a web form. There are three ways in, and they run
the same engine: a live endpoint at POST /api/classify, an MCP
server at POST /mcp, and the whole engine pre-computed as a
static JSON artifact for agents that would rather do a dictionary read than a
request. No account and no key for any of them.
Agent skill (install):
npx skills add maticijus/hexenkraft-agent-skills --skill cybiq-cra-scope
-
https://skills.sh/maticijus/hexenkraft-agent-skills/cybiq-cra-scope
This page is the human-readable version of the agent section of
llms.txt. The machine-readable description is
openapi.json, the MCP server card is
/.well-known/mcp.json, and what the packs
cost and on what terms is /api/offers.json.
Every page here carries all three in a Link header, so nothing
below has to be guessed.
What is on offer
- Free applicability classification (the artifact below): verdict, reasons, obligations with verbatim Official Journal sources, regulatory timeline, and next steps for grey zones. No account, no payment.
- Verification checklist pack, one-time EUR 49: role-specific verification checklist for importers and distributors, an Art. 19(2) or Art. 20(2) pre-market verification table, an Art. 21 self-test on rebranding and substantial modification, plus their vulnerability-notification duties under Articles 19 and 20.
- CRA compliance pack, one-time EUR 199: classification memo, vulnerability-handling procedure, awareness-timestamp log, four Art. 14 notification templates (24 hour early warning, 72 hour notification, 14 day final report for an actively exploited vulnerability, and the severe-incident set), each printing ENISA's platform fields and character limits next to the content the Regulation requires, Art. 14(7) and Art. 16(1) routing section, SBOM policy, secure-by-default checklist, support-period statement, Annex V declaration index, Annex VII technical documentation index (drawn up under Art. 31), a cybersecurity risk assessment worksheet, an Annex I requirements matrix, a reporting process worksheet (who does what, a decision record and a practice-run checklist), a coordinated vulnerability disclosure policy template with a security.txt example and a conformity-assessment readiness guide covering every Article 32 procedure. Templates and indexes you complete: Cybiq never files or transmits anything on your behalf.
- Agency co-branding licence, EUR 490 per 90 days: For agencies and consultancies delivering CRA work for clients: a signed token that puts your name on every document you generate here, for EUR 490 per 90 days, one agency name, no report limit. Co-branding only: the Cybiq attribution, the citations and the disclaimer stay exactly as they are. The token is a bearer credential: anyone holding it can generate co-branded documents. It is a co-branding unlock, not a paywall: the engine runs in your browser and every document is free to generate without a token. No per-report counting is possible without a backend, so this is a term licence and never a per-report price. Traceability, not prevention: the token id appears in every document generated with it, so a leaked token is attributable after the fact. Revocation means rotating the published public key, which invalidates every token signed with it, so keys are issued per cohort.
Offer for business customers only.
An agent can start a purchase with the start_pack_checkout tool
below. Calling it charges nothing. v1 commerce is Payment-Link
mode: the tool may hand a Stripe Payment Link for the human to open and pay,
and today's 422 checkout_creation_not_permitted with the link in
the detail is the intended v1 answer, not a temporary fallback. Each plan also
publishes that URL as payment_link in
/api/offers.json, so an agent can read it
without the error path. A later deployment may still create a Checkout
Session; that path is optional. The human buyer opens the URL, pays with
their own card, and ticks the terms box on Stripe's page. That box is where
they consent to immediate digital delivery and thereby waive the 14-day EU
right of withdrawal, so an agent must never tick it on their behalf, and
this service refuses to deliver a session that carries no acceptance.
check_order_status needs a Checkout Session id and will not
track a Payment-Link purchase; those are human-tracked. Price, VAT treatment,
what arrives and the refund terms are all in
/api/offers.json: read it before you send
anyone to a checkout. The agency licence is still ordered by email at
hello@cybiq.eu.
The endpoints
| Endpoint | What it does | Cost | Rate limit |
|---|---|---|---|
POST /api/classify | Classify one product. The engine runs in process. | Free | Per client address |
POST /api/deadlines | Compute the Article 14 reporting deadlines from a timestamp you supply. | Free | Per client address, shared with classify |
POST /mcp | MCP server: classify_cra, start_pack_checkout, check_order_status, compute_reporting_deadlines. | Free to call | Per client address |
GET /api/offers.json | What the packs cost, and on what terms. | Free | Static file, cache it |
GET /api/decisions.json | Every possible decision, pre-computed. | Free | Static file, cache it |
GET /api/decisions.de.json | The same decisions with German prose. | Free | Static file, cache it |
The two rate-limited surfaces have separate budgets, so classifying twenty
products never spends the budget a buyer needs to come back from Stripe.
Exceeding one is answered 429 in the envelope described under
Errors.
POST /api/classify
Send any subset of the six answers. A partial set is not an error: the engine
answers needs_review and names what it still needs in
missing. An unknown key or an unknown value is refused with
422 rather than dropped.
curl -s https://cybiq.eu/api/classify \
-H 'Content-Type: application/json' \
-d '{"answers":{"market":"commercial","ossMonetized":false,
"productKind":"installable_software","otherRegime":"none",
"role":"manufacturer","category":"vpn"},"locale":"en"}'
The response carries the verdict, the ordered reasons, the
obligations with their CRA articles, the timeline,
the conformity route, the packTier that fits, and
sources: corpus ids anchoring every claim to a verbatim Official
Journal quote. locale: "de" changes the prose only; the verdict,
the articles, the sources and the dates are identical in both languages.
POST /api/deadlines
Send kind (vulnerability or incident) and
awareness, the moment the manufacturer became aware, as ISO 8601
with Z or an offset. Add measureAvailable (vulnerability
only) or notificationSubmitted (incident only) to start the
final-report clock; without it the final report is listed with
dueBy: null. The response lists every deadline in due order with
its basis corpus id and pinpoint, the method used, and
an ics calendar with one event per dated deadline. These are the
earliest readings of the outer limits: each notification is due without undue
delay in any event. The same calculation is the MCP tool
compute_reporting_deadlines. Nothing is stored, and Cybiq never
files or transmits anything on your behalf; the response repeats that in
notice. A timestamp without an offset, an impossible date, or a
later instant that precedes awareness is refused with
422 invalid_deadline_input naming the field.
curl -s https://cybiq.eu/api/deadlines \
-H 'Content-Type: application/json' \
-d '{"kind":"vulnerability","awareness":"2027-03-10T08:30:00+01:00",
"measureAvailable":"2027-03-12T00:00:00Z","locale":"en"}'
POST /mcp
Model Context Protocol over Streamable HTTP: stateless, JSON responses rather
than an SSE stream, POST only. A GET is answered 405
with an Allow header rather than 404, because a
client that opened a stream has found the right endpoint. There is no session
id, so any request stands alone. JSON-RPC batches are refused. The requested
protocol revision is echoed when this server speaks it, and otherwise it offers
the newest it has.
curl -s https://cybiq.eu/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
curl -s https://cybiq.eu/mcp \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call",
"params":{"name":"classify_cra",
"arguments":{"answers":{"market":"commercial",
"ossMonetized":false,"productKind":"installable_software",
"otherRegime":"none","role":"manufacturer",
"category":"vpn"}}}}'
classify_cra runs the identical handler /api/classify
runs, so there is no second code path that could answer differently.
start_pack_checkout takes a tier of
verification_checklist_49 or compliance_pack_199, and
optionally a locale and an idempotency_key. In v1 it
may answer 422 checkout_creation_not_permitted with the public
Payment Link in the detail; that is intentional. Prefer
payment_link from /api/offers.json
when you only need the pay URL. A later deployment may still return
checkout_url, session_id, amount and
currency from a Checkout Session. The tier, the price and the
amount are decided server-side from the Stripe price id: nothing in the request
body can name a price. check_order_status needs a Checkout
Session id (shaped cs_...) and will not track a Payment-Link
purchase; those are human-tracked. When a Session exists it says
paid, unpaid or unknown
(the session is not a Cybiq purchase: its price is not Cybiq's or its mode is
not payment). A paid session carries the unlock URL only when the buyer accepted
the terms at checkout; otherwise it is paid without
unlock_url. Do not fetch the unlock URL yourself: it works once, and the documents belong to
the human who paid. compute_reporting_deadlines runs the handler
/api/deadlines runs and answers a refusal as a tool error carrying
the same problem document. Call tools/list for the authoritative schemas
rather than copying them from this page.
Idempotency
start_pack_checkout accepts an idempotency_key you
choose. It is not a credential and cannot be used as one. The key lives under
your own caller address, is bound to a digest of the request it was first used
for, carries a server-side nonce you never see, and expires after 24 hours.
Inside that window: the identical request replays the stored
session without calling Stripe again; the same key with a different tier or
locale is 422 idempotency_key_conflict and is a bug to fix rather
than a call to retry; the identical request while the first is still in flight
is 409 idempotency_key_in_progress, which means retry shortly and
do not start a second checkout. A key you never send simply gets a fresh
session, so buying the same pack twice on purpose works.
GET /api/offers.json
Read this before starting a checkout. It carries both plans with their plan id,
name, price in euro, currency, what is delivered, the checkout URL template and
the published payment_link (the Stripe Payment Link the human
opens); the VAT treatment, which is that the listed price is the whole amount
charged and nothing is added at checkout; the delivery and withdrawal terms
with the law they rest on; and every error code this API can return with its
status. It is generated from the same constants this site renders, so the
price you are quoted and the price a human is shown cannot differ.
GET /api/decisions.json
One request returns all 46,368 possible complete answer combinations mapped to
their full decisions (816 distinct outcomes). Schema:
cybiq.decisions/v1. Cache aggressively; re-fetch when
schema changes. Described in
openapi.json; validated against the live engine by
this site's own test suite on every deploy.
Lookup
The index object maps an answer tuple to an outcome id; the
outcomes object holds the decisions. Key format:
market|ossMonetized|productKind|otherRegime|role|category
with these values:
market:commercial|internal_only|free_ossossMonetized:true|falseproductKind:installable_software|hardware_with_software|standalone_hardware|pure_saasotherRegime:none|medical_devices|in_vitro_diagnostics|aviation|marine_equipment|road_vehicles|high_risk_airole:manufacturer|importer|distributorcategory:general_app|browser|password_manager|identity_access_management|anti_malware|vpn|network_management|siem|boot_manager|pki_certificate_issuance|operating_system|security_microprocessor_microcontroller|smart_home_assistant_or_security|connected_toy|health_wearable|network_device|network_critical_infrastructure|firewall_ids_ips|tamper_resistant_chip|hypervisor_container_runtime|hsm_secure_element|industrial_control|other
Example: classify a commercial installable network device made by a manufacturer.
curl -s https://cybiq.eu/api/decisions.json \
| node -e '
let raw = "";
process.stdin.on("data", (c) => (raw += c));
process.stdin.on("end", () => {
const d = JSON.parse(raw);
const key = ["commercial","false","installable_software","none",
"manufacturer","network_device"].join("|");
console.log(d.outcomes[d.index[key]].headline);
});'
Every decision carries sources: corpus quote IDs anchoring each
claim to verbatim text of Regulation (EU) 2024/2847 (CELEX 32024R2847),
re-verified against the live EUR-Lex document weekly by CI. Treat a verdict
without reading its reasons as misuse: grey zones (pure SaaS,
overlapping regimes) deliberately return
needs_review or likely_out_of_scope with concrete
nextSteps, not a confident guess.
Errors
Every failure, on every route, is an
RFC 9457 problem
document served as application/problem+json. The MCP tools return
the identical document in the tool result's structuredContent
beside isError: true, and protocol-level MCP refusals carry it in
the JSON-RPC error.data, so one vocabulary covers both transports.
{
"type": "https://cybiq.eu/for-agents.html#error-invalid_answer_value",
"title": "Unknown answer value",
"status": 422,
"detail": "Invalid value for \"market\". Allowed values: ...",
"code": "invalid_answer_value",
"field": "market",
"instance": "/api/classify"
}
Read code rather than parsing the type URI, though
the URI does resolve: it points at that code's row below. status
is a property of the code, registered once server-side, so no two routes give
one code two meanings, and the number is present even on an MCP tool result
that has no HTTP status of its own. instance is the route and
never the request URL, so a Checkout Session id cannot end up in an error body
you log. Every JSON response from this surface also carries
X-Robots-Tag: noindex; the two decisions artifacts deliberately do
not, because they are a dataset meant to be found.
| Code | Status | Title | When |
|---|---|---|---|
invalid_body | 400 | Malformed request body | The body was not JSON, or was not a JSON object. |
body_too_large | 413 | Request body too large | The body exceeded 64 KB. Reading stops at the cap rather than after the last byte. |
unknown_field | 422 | Unknown field | A top-level field we do not accept. Refused rather than ignored. |
method_not_allowed | 405 | Method not allowed | Anything but POST on a POST-only route. |
rate_limited | 429 | Too many requests | The per-address budget for this surface is spent. Wait and retry. |
origin_not_allowed | 403 | Origin not allowed | POST /mcp carried an Origin header other than this site. MCP clients send none; send no Origin. |
unsupported_media_type | 415 | Unsupported media type | POST /mcp with a Content-Type other than application/json. Send application/json. |
missing_answers | 422 | Answers missing | answers was absent. |
invalid_answers | 422 | Answers must be an object | answers was not an object. |
unknown_answer_key | 422 | Unknown answer key | An answer key the engine does not have. Never silently dropped: a dropped key would produce a confident verdict about a product nobody described. |
invalid_answer_value | 422 | Unknown answer value | A value outside the enumeration for that key. Never defaulted. |
invalid_locale | 422 | Unsupported locale | A locale other than en or de. |
invalid_deadline_input | 422 | Invalid deadline input | A deadline request that cannot be computed: an unknown kind, a timestamp that is not ISO 8601 with Z or an offset, measureAvailable on an incident, notificationSubmitted on a vulnerability, or either earlier than awareness. The field member names it. Also refused: awareness before 2026-09-11 on the caller's offset calendar (Article 14 applies from that day) and any timestamp after the year 2100. |
invalid_tier | 422 | Unknown pack tier | A pack tier we do not sell. |
tier_not_configured | 422 | Tier not configured on this deployment | A tier this deployment has no Stripe price for. An operator problem, not yours. |
invalid_idempotency_key | 422 | Malformed idempotency key | The key was not 8 to 200 characters of letters, digits, underscore, dot, colon or hyphen. |
idempotency_key_conflict | 422 | Idempotency key already used | The key is already held for a DIFFERENT tier or locale. A key may only be reused to retry the identical request. Choose a new key; do not retry this one. |
idempotency_key_in_progress | 409 | Idempotent request in progress | The identical request is still being created. Retry it shortly. Do not start a second checkout. |
checkout_creation_not_permitted | 422 | Checkout creation not permitted | This deployment's Stripe key may read Checkout Sessions but not create them. The detail carries the public Payment Link for the tier; give that to the human buyer. v1 commerce is Payment-Link mode: this 422 is the intended answer, not a temporary fallback. The same URL is on each plan as payment_link in /api/offers.json. |
checkout_not_available | 422 | Automated checkout not available | The same, with no Payment Link configured either. The detail names the mailbox to order from. |
checkout_creation_failed | 502 | Checkout could not be created | Stripe refused, or returned a session with no URL. Retry, or order by email. |
invalid_session_id | 422 | Malformed Checkout Session id | Not shaped like a Stripe Checkout Session id. Checked before Stripe is called at all. |
unknown_session | 404 | Unknown Checkout Session | No such session, or it could not be read. |
invalid_params | 400 | Invalid tool parameters | An MCP tools/call without a usable name. A JSON-RPC error, with this document in its data member. |
unknown_tool | 404 | No such tool | No tool by that name. Call tools/list. |
The whole flow
- Determine as many of the six answers about the product as you can.
- Classify:
POST /api/classify, or theclassify_cratool, or a lookup in/api/decisions.json. All three are the same engine. - If
verdictisneeds_review, readmissingand go back for those answers. The engine never guesses to fill a gap. - If
verdictisin_scope,packTiertells you which pack fits. Read /api/offers.json. Handpayment_linkto the human, or callstart_pack_checkout(which in v1 may return the same link inside422 checkout_creation_not_permitted). check_order_statusneeds a Checkout Session id and will not track a Payment-Link purchase; those are human-tracked. If a later Session exists, poll until it sayspaid, then hand the unlock URL to the human; do not open it yourself.- If not in scope, relay
nextSteps: that is the honest answer, and there is nothing to sell.
Ground rules
- Quote the
reasonsandsources, not just the verdict. - The output is automated compliance documentation, not legal advice; an agent relaying it does not change that. No language model writes any part of the legal output.
- Never accept the terms on a buyer's behalf. The checkbox on Stripe's page is where a human waives a statutory right, and it is theirs to tick.
- Never open an unlock URL. It works once, and the documents belong to the buyer.
- Four paths under
/api/carry a public contract:classify,offers.jsonand the twodecisionsartifacts. Everything else there is disallowed in robots.txt. - Cite the artifact's
celexandschemafields when quoting dates so staleness is detectable.
Free checks in the browser
Two further checks run as web pages in the visitor’s browser. They are not API endpoints and not MCP tools: your answers are not sent to a server and nothing is stored, and Cybiq never files or transmits anything on anyone’s behalf. A human opens them; an agent can send the human there with the URL.
- /report-check.html: “Is this vulnerability or incident reportable? Article 14 check”. The vulnerability track asks whether the flaw is actively exploited and contained in the product, the incident track whether the incident is severe. The result reads that the answers describe, or do not describe, an actively exploited vulnerability (Article 14) or a severe incident (Article 14), with the reasons, the clocks that follow and the quoted sources. It can also read that, on the answers, the vulnerability is not contained in the product (Article 14(1)), or that the case needs review. Orientation, not legal advice.
- /rdps-check.html: “Is my cloud backend part of the product? Remote data processing check”. Three questions on Article 3, point (2), a result with the reasons, and Commission guidance section 8.1 (non-binding).
Background reading
- llms.txt: this site in one machine-readable file.
- openapi.json: OpenAPI 3.1 description of the live surface.
- /.well-known/mcp.json: the MCP server card.
- /api/offers.json: the offer contract, prices and terms.
- Legal, privacy & terms.
Contact
Operator: HEXENKRAFT s.r.o., IČO 29665931, V zahradách 2462/31, Libeň, 180 00 Praha 8, Czech Republic. Questions, integration problems or a wrong result: hello@cybiq.eu.
Cybiq generates automated compliance documentation from Regulation (EU) 2024/2847 as published. It is not legal advice, and an agent evaluating it on someone's behalf does not change that.