← Back to Cybiq

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

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

EndpointWhat it doesCostRate limit
POST /api/classifyClassify one product. The engine runs in process.FreePer client address
POST /api/deadlinesCompute the Article 14 reporting deadlines from a timestamp you supply.FreePer client address, shared with classify
POST /mcpMCP server: classify_cra, start_pack_checkout, check_order_status, compute_reporting_deadlines.Free to callPer client address
GET /api/offers.jsonWhat the packs cost, and on what terms.FreeStatic file, cache it
GET /api/decisions.jsonEvery possible decision, pre-computed.FreeStatic file, cache it
GET /api/decisions.de.jsonThe same decisions with German prose.FreeStatic 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:

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.

CodeStatusTitleWhen
invalid_body400Malformed request bodyThe body was not JSON, or was not a JSON object.
body_too_large413Request body too largeThe body exceeded 64 KB. Reading stops at the cap rather than after the last byte.
unknown_field422Unknown fieldA top-level field we do not accept. Refused rather than ignored.
method_not_allowed405Method not allowedAnything but POST on a POST-only route.
rate_limited429Too many requestsThe per-address budget for this surface is spent. Wait and retry.
origin_not_allowed403Origin not allowedPOST /mcp carried an Origin header other than this site. MCP clients send none; send no Origin.
unsupported_media_type415Unsupported media typePOST /mcp with a Content-Type other than application/json. Send application/json.
missing_answers422Answers missinganswers was absent.
invalid_answers422Answers must be an objectanswers was not an object.
unknown_answer_key422Unknown answer keyAn 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_value422Unknown answer valueA value outside the enumeration for that key. Never defaulted.
invalid_locale422Unsupported localeA locale other than en or de.
invalid_deadline_input422Invalid deadline inputA 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_tier422Unknown pack tierA pack tier we do not sell.
tier_not_configured422Tier not configured on this deploymentA tier this deployment has no Stripe price for. An operator problem, not yours.
invalid_idempotency_key422Malformed idempotency keyThe key was not 8 to 200 characters of letters, digits, underscore, dot, colon or hyphen.
idempotency_key_conflict422Idempotency key already usedThe 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_progress409Idempotent request in progressThe identical request is still being created. Retry it shortly. Do not start a second checkout.
checkout_creation_not_permitted422Checkout creation not permittedThis 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_available422Automated checkout not availableThe same, with no Payment Link configured either. The detail names the mailbox to order from.
checkout_creation_failed502Checkout could not be createdStripe refused, or returned a session with no URL. Retry, or order by email.
invalid_session_id422Malformed Checkout Session idNot shaped like a Stripe Checkout Session id. Checked before Stripe is called at all.
unknown_session404Unknown Checkout SessionNo such session, or it could not be read.
invalid_params400Invalid tool parametersAn MCP tools/call without a usable name. A JSON-RPC error, with this document in its data member.
unknown_tool404No such toolNo tool by that name. Call tools/list.

The whole flow

  1. Determine as many of the six answers about the product as you can.
  2. Classify: POST /api/classify, or the classify_cra tool, or a lookup in /api/decisions.json. All three are the same engine.
  3. If verdict is needs_review, read missing and go back for those answers. The engine never guesses to fill a gap.
  4. If verdict is in_scope, packTier tells you which pack fits. Read /api/offers.json. Hand payment_link to the human, or call start_pack_checkout (which in v1 may return the same link inside 422 checkout_creation_not_permitted).
  5. check_order_status needs a Checkout Session id and will not track a Payment-Link purchase; those are human-tracked. If a later Session exists, poll until it says paid, then hand the unlock URL to the human; do not open it yourself.
  6. If not in scope, relay nextSteps: that is the honest answer, and there is nothing to sell.

Ground rules

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.

Background reading

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.