API reference
Four endpoints and no personal data. Pricing needs no authentication; recording a quote in the audit ledger does. The server prices every quote and discards any price a client sends.
Base URL
https://api.riskrouter.eu
Browsers may call this API only from the console origin. Server-to-server callers are unaffected, because the browser enforces that rule rather than the API.
Prices a configuration, and appends an audit record when a distributor key is presented. The only endpoint that writes.
Authentication
Optional, and what it changes is whether the quote is recorded rather than whether it is priced. The ledger is tamper-evident, so an entry cannot be cleaned up later without breaking every digest after it. That is why writes are attributed and reads of the price are not.
| Header | Response | Ledger |
|---|---|---|
| Absent | 200 with a real price | Nothing written. attribution.mode is ANONYMOUS |
Authorization: Bearer <key> | 200 with a real price | Recorded against that distributor, inside the hash chain |
| A key that does not resolve | 401 INVALID_API_KEY | Nothing written |
An unresolvable key is refused rather than downgraded to an anonymous quote. Downgrading would let a distributor with a mistyped or revoked key collect 200s for months and discover at audit that nothing had been recorded. We store only the SHA-256 of a key, so a lost key can be revoked and replaced but never recovered.
Request
Send active_vertical as one of the four keys below. selected_components is
optional; leave it out to price the base layer alone. Only an explicit true enables a
component, so the string "false" selects nothing.
curl -X POST https://api.riskrouter.eu/api/v1/quote \
-H 'Content-Type: application/json' \
-d '{
"active_vertical": "urban_rental",
"selected_components": {
"deposit_liquidity_swap": true,
"smart_home_iot": true
}
}'
Response
selected_components comes back normalised to what was actually sold. The mandatory base
layer is always present and always true. Anything the engine did not recognise appears in
ignored_components, so a client that has drifted out of step shows up in testing.
{
"ok": true,
"quote": {
"quote_reference": "…",
"active_vertical": "urban_rental",
"currency": "EUR",
"billing_interval": "MONTHLY",
"selected_components": { "tenant_fire_water": true, … },
"ignored_components": [],
"line_items": [ … ],
"totals": {
"base_layer_premium": "3.50",
"add_on_premium": "6.00",
"total_monthly_premium": "9.50",
"indicative_annual_premium": "114.00",
"active_add_on_count": 2
}
},
"compliance": { "status": "MOCK_ROUTING_VERIFIED", "binding_state": "NOT_BOUND. Indicative simulation only, …" },
"persistence": { "attempted": true, "persisted": true, "record_id": "…", "latency_ms": 41 },
"attribution": { "recorded": true, "mode": "ATTRIBUTED", "distributor": "…", "environment": "sandbox" }
}
Reading persistence
A 200 does not prove an audit record exists. The write is reported separately, and a
failed write degrades the response instead of failing it, because a ledger fault should not take pricing
down with it. Treat a record as stored only when persistence.persisted is literally
true. persistence.attempted separates the two reasons it can be false:
no key was sent, or the write was tried and did not confirm. The first is a configuration choice,
the second is an incident.
Publishes the rating matrix, so a client renders exactly what the engine prices instead of carrying a copy that can drift. The console loads this on startup.
The entry count, the head hash, and the canonical form the digest is computed over. Record the head with a date and any later history that does not reproduce it has been altered. Nothing about any customer is exposed: the attestation commits to the ledger without disclosing it.
Your own entries in full, plus (chain_index, row_hash, prev_hash) for every entry
in the ledger, plus the signed attestation. Requires your API key: an export is scoped to one
distributor, and an anonymous caller has nothing to export.
Together those three pieces let you rebuild each of your digests from its own content and follow the chain to the head we signed — with no credential of ours and no call to us at verification time. The skeleton carries digests only, never another distributor’s content.
curl -s -H "Authorization: Bearer $RISKROUTER_API_KEY" \
https://api.riskrouter.eu/api/v1/ledger/export > export.json
node tools/verify-attestation.mjs export.json # we signed this head
node tools/verify-ledger.mjs export.json # your entries produce it
A ledger with more than 25 000 entries is refused with LEDGER_TOO_LARGE rather
than truncated. A partial skeleton cannot reach the head, so it would verify as broken and
send you hunting for tampering that never happened.
Rate limited twice, because two different people abuse it. By address before your key is resolved, so a stranger cannot spend a database round trip per request just by asking; then six a minute per distributor, keyed to you rather than to your address. An export is a periodic record, not a polling endpoint — take one a month and keep it.
One export proves consistency. Several, saved months apart, prove history — see verify for why that distinction is the whole point.
The public half of the attestation signing key, and its key id. Useful for a quick check, but
not the copy to trust: verifying a signature against a key we hand you at the moment you
verify proves nothing if we are the thing in question. The copy that counts is
anchors/signing-key.json in the public repository, which stays there across
rotations so an attestation saved years ago is still checkable.
Serves this site’s own contact form. It is listed because an undocumented public write endpoint is a thing an auditor is right to ask about, not because it is part of the integration surface — there is no reason to call it from your own code.
It writes to contact_enquiries, which unlike the ledger is deletable by design:
it holds personal data and must stay erasable under GDPR Article 17. The
data protection page sets out what is kept and for how long.
Liveness, plus whether the audit ledger binding resolved. binding_configured: false means
quotes still price but nothing is recorded. The bare domain answers this too.
Errors
Every error carries the same envelope: ok: false and an error object with a stable code.
| Code | HTTP | Cause |
|---|---|---|
UNKNOWN_VERTICAL | 400 | Missing or unrecognised active_vertical |
INVALID_COMPONENTS | 400 | selected_components was an array rather than an object |
INVALID_PAYLOAD | 400 | Body was not a JSON object |
MALFORMED_JSON | 400 | Body was not valid JSON |
INVALID_API_KEY | 401 | Key is unknown, revoked, or its distributor is suspended. Nothing was recorded |
KEY_CHECK_UNAVAILABLE | 503 | A key was sent but could not be checked. Refused rather than priced unrecorded |
NOT_AUTHENTICATED | 401 | An export was requested with no key. There is nothing to export for nobody in particular |
LEDGER_TOO_LARGE | 413 | The ledger is past the single-response export ceiling. Refused rather than truncated into a proof that fails |
UNSUPPORTED_MEDIA_TYPE | 415 | Content type was not application/json |
PAYLOAD_TOO_LARGE | 413 | Body exceeded 16 KB |
METHOD_NOT_ALLOWED | 405 | Wrong verb for the route |
ROUTE_NOT_FOUND | 404 | No route bound to that path |
ORCHESTRATION_FAILURE | 500 | Unhandled fault; no detail is leaked |
Rating matrix
All prices are monthly, in euro. Base layers are mandatory and cannot be switched off.
Urban Mobility mobility
| Component code | Cover | Premium |
|---|---|---|
public_liability | Public Liability Base | €2.00 |
battery_hardware_theft | Battery / Hardware Theft | €1.50 |
commuter_roadside | Commuter Roadside Assistance | €1.00 |
commercial_delivery | Commercial Delivery Extension | €5.00 |
carshare_deductible | Car-Share Deductible Buy-Down | €2.50 |
Digital Nomad & Freelance digital_nomad
| Component code | Cover | Premium |
|---|---|---|
cyber_extortion_data | Cyber-Extortion & Data Protection Base | €1.50 |
high_value_hardware | High-Value Hardware Protection | €3.00 |
international_roaming | International Roaming Expansion | €2.00 |
professional_indemnity | Freelance Professional Indemnity | €4.00 |
contractual_legal | Contractual Dispute Legal Protection | €1.50 |
Urban Rental & Housing urban_rental
| Component code | Cover | Premium |
|---|---|---|
tenant_fire_water | Tenant Fire & Water Liability Base | €3.50 |
deposit_liquidity_swap | Security Deposit Liquidity Swap | €4.00 |
landlord_legal_defense | Landlord Dispute Legal Defense | €1.00 |
roommate_sublet | Roommate / Sublet Liability | €1.50 |
smart_home_iot | Smart-Home / IoT Failure Cover | €2.00 |
Micro-Travel & Sports micro_travel
| Component code | Cover | Premium |
|---|---|---|
multimodal_medical_transit | Basic Multi-Modal Medical & Transit Base | €2.50 |
winter_sports_gear | Premium Winter Sports & Ski Gear | €3.50 |
parametric_delay | Parametric Flight / Train Delay | €2.00 |
adventure_sports | Extreme Adventure Sports Addendum | €3.00 |
baggage_tech_transit | Lost Baggage / High-Value Tech | €1.50 |