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.

POST/api/v1/quote

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.

HeaderResponseLedger
Absent200 with a real priceNothing written. attribution.mode is ANONYMOUS
Authorization: Bearer <key>200 with a real priceRecorded against that distributor, inside the hash chain
A key that does not resolve401 INVALID_API_KEYNothing 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.

GET/api/v1/verticals

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.

GET/api/v1/ledger/attestation

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.

GET/api/v1/ledger/export

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.

GET/api/v1/ledger/pubkey

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.

POST/api/v1/contact

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.

GET/api/v1/health

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.

CodeHTTPCause
UNKNOWN_VERTICAL400Missing or unrecognised active_vertical
INVALID_COMPONENTS400selected_components was an array rather than an object
INVALID_PAYLOAD400Body was not a JSON object
MALFORMED_JSON400Body was not valid JSON
INVALID_API_KEY401Key is unknown, revoked, or its distributor is suspended. Nothing was recorded
KEY_CHECK_UNAVAILABLE503A key was sent but could not be checked. Refused rather than priced unrecorded
NOT_AUTHENTICATED401An export was requested with no key. There is nothing to export for nobody in particular
LEDGER_TOO_LARGE413The ledger is past the single-response export ceiling. Refused rather than truncated into a proof that fails
UNSUPPORTED_MEDIA_TYPE415Content type was not application/json
PAYLOAD_TOO_LARGE413Body exceeded 16 KB
METHOD_NOT_ALLOWED405Wrong verb for the route
ROUTE_NOT_FOUND404No route bound to that path
ORCHESTRATION_FAILURE500Unhandled 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 codeCoverPremium
public_liabilityPublic Liability Base€2.00
battery_hardware_theftBattery / Hardware Theft€1.50
commuter_roadsideCommuter Roadside Assistance€1.00
commercial_deliveryCommercial Delivery Extension€5.00
carshare_deductibleCar-Share Deductible Buy-Down€2.50

Digital Nomad & Freelance digital_nomad

Component codeCoverPremium
cyber_extortion_dataCyber-Extortion & Data Protection Base€1.50
high_value_hardwareHigh-Value Hardware Protection€3.00
international_roamingInternational Roaming Expansion€2.00
professional_indemnityFreelance Professional Indemnity€4.00
contractual_legalContractual Dispute Legal Protection€1.50

Urban Rental & Housing urban_rental

Component codeCoverPremium
tenant_fire_waterTenant Fire & Water Liability Base€3.50
deposit_liquidity_swapSecurity Deposit Liquidity Swap€4.00
landlord_legal_defenseLandlord Dispute Legal Defense€1.00
roommate_subletRoommate / Sublet Liability€1.50
smart_home_iotSmart-Home / IoT Failure Cover€2.00

Micro-Travel & Sports micro_travel

Component codeCoverPremium
multimodal_medical_transitBasic Multi-Modal Medical & Transit Base€2.50
winter_sports_gearPremium Winter Sports & Ski Gear€3.50
parametric_delayParametric Flight / Train Delay€2.00
adventure_sportsExtreme Adventure Sports Addendum€3.00
baggage_tech_transitLost Baggage / High-Value Tech€1.50