Integrate
There is no SDK to install and nothing to license. The engine is an HTTP endpoint, the rating matrix is published at another one, and the audit ledger is a single table. Everything below runs against the live validation build right now.
Quickstart
Price a configuration in one request
No key, no account, no session. Post a vertical and the components the customer switched on. You get a real price. Recording that quote in the audit ledger is a separate decision, and it is the one that needs a key — see Keys and the audit trail.
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 }
}'
You get back priced line items, a total, the normalised component state the engine used, and a separate report of whether an audit write was attempted and whether it landed.
From a Node backend
const res = await fetch('https://api.riskrouter.eu/api/v1/quote', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
active_vertical: 'mobility',
selected_components: { commuter_roadside: true }
})
});
const quote = await res.json();
if (!res.ok || quote.ok !== true) throw new Error(quote.error?.code ?? `HTTP ${res.status}`);
// Show this. Never show a number your own code calculated.
const total = quote.quote.totals.total_monthly_premium; // "3.00"
// A 200 does not mean a record exists. Check the write separately.
// Without an API key this is false by design, not by failure.
const recorded = quote.persistence.persisted === true;
From Python
import requests
r = requests.post(
"https://api.riskrouter.eu/api/v1/quote",
json={"active_vertical": "micro_travel",
"selected_components": {"winter_sports_gear": True}},
timeout=10,
)
r.raise_for_status()
quote = r.json()
total = quote["quote"]["totals"]["total_monthly_premium"] # "6.00"
recorded = quote["persistence"]["persisted"] is True
Keys and the audit trail
Pricing is open. Recording is attributed.
The ledger is tamper-evident: every entry seals the one before it. That is what makes it worth anything to an auditor, and it is also why the write path cannot be open. An entry written by nobody in particular is sealed in permanently, and removing it later would break every digest after it. So the engine treats three cases differently, and the third one is the point.
| You send | You get | Ledger |
|---|---|---|
| No key | A real price, immediately | Nothing written. attribution.mode is ANONYMOUS |
| A valid key | A real price | Recorded against your distributor, inside the hash chain |
| An invalid, revoked or suspended key | HTTP 401, no price | Nothing written |
A bad key is refused outright and never quietly downgraded to an anonymous quote. The alternative looks friendlier and is far worse: a distributor whose key was mistyped or revoked would keep getting 200s for months, believing they were building an audit trail, and would find out during the audit that nothing had been written. Loud now beats silent then.
curl -X POST https://api.riskrouter.eu/api/v1/quote \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $RISKROUTER_API_KEY" \
-d '{ "active_vertical": "mobility", "selected_components": { "commuter_roadside": true } }'
// Two different questions. Ask both.
const priced = quote.ok === true; // is this number real
const recorded = quote.attribution.recorded === true; // is it in the ledger
We store the SHA-256 of your key, never the key. That means we cannot show it to you again, cannot email it to you, and cannot recover it if you lose it — we can only revoke it and issue another. It also means a breach of our database does not hand anyone the ability to write to your audit trail.
Keys are per environment. A sandbox key and a production key are separate rows and separate digests, so a test quote can never land in a production ledger by accident.
Getting your evidence back out
The same key exports it. This is the endpoint to wire into a monthly job on day one, not the day you need it.
curl -s -H "Authorization: Bearer $RISKROUTER_API_KEY" \
https://api.riskrouter.eu/api/v1/ledger/export > "ledger-$(date +%F).json"
You get your entries in full, the digest skeleton of the whole chain they sit inside, and the signed attestation for the head. Two standard-library Node scripts check all three, offline, with no key of ours and no call to us:
node tools/verify-attestation.mjs ledger-2026-09-20.json # we signed this head
node tools/verify-ledger.mjs ledger-2026-09-20.json # your entries produce it
Keep them. An export you downloaded last year still verifies after we have stopped answering the phone, which is the only honest answer to what happens to my audit trail if you go under. It is also why there is no SDK and nothing to license: a product that is hard to leave is a product that has to be, and we would rather not need that.
Three patterns
Pick the one that matches your risk appetite
1. Call the API from your backend
Your server calls ours, your interface renders the result. You keep full control of the customer relationship and we never see your traffic pattern or your customer. This is what we would choose. Cross-origin calls from a browser are restricted to our own console, so this pattern is server-to-server by design.
2. Link out to the hosted console
Point a customer at the demo console to configure cover and hand the quote reference back to your flow. The fastest thing to stand up, and the least control: the page is ours, so its appearance and copy change when ours do.
3. Run the whole thing yourself
Three files and one table. Deploy the engine to your own Cloudflare account, point it at your own database, and we are no longer in the path at all. Covered in Run it yourself below.
Rendering
Ask the engine what it charges
Do not hardcode the price list. GET /api/v1/verticals publishes every vertical,
its mandatory base layer and its optional modules, with the prices the engine will actually
apply. Build your interface from that response and a price change never desynchronises your
checkout from the quote.
const { verticals } = await (await fetch(BASE + '/api/v1/verticals')).json();
for (const v of verticals) {
renderTab(v.key, v.label, v.description);
renderLocked(v.base); // mandatory, always on
v.addons.forEach(renderToggle); // optional modules
}
If you must cache it, cache it with an expiry and re-check on load. Our own console keeps an embedded copy for the first paint and hydrates from this endpoint immediately, and a test fails the build if the two ever disagree.
Failure cases
Handle these five before you ship
Each one has burned a real integration somewhere. They are cheap to handle now and expensive to discover in an audit.
| What happens | What to do |
|---|---|
| A 200 comes back but no audit record was written You sent no key, or the ledger was briefly unreachable. |
Read persistence.persisted. Show the price, but never tell a customer their quote is on file unless it is literally true. persistence.attempted tells the two causes apart. |
| The customer toggles quickly Replies arrive out of order and a stale price lands last. |
Tag each request with an incrementing number and ignore any reply older than the newest one you have rendered. Abort the previous request. |
| You send a component we do not price Your build is ahead of ours, or behind it. |
Check ignored_components on every response. It is never empty by accident, so treat anything in it as a deployment mismatch and alert on it. |
| A 401 comes back where a 200 used to Your key was revoked, or your distributor was suspended. |
Treat it as an outage, not as a validation error, and page someone. Do not strip the key and retry: that turns a loud failure into a silent one, and every quote after it disappears from your audit trail. |
| The engine does not answer Network fault or timeout. |
Set a client timeout of a few seconds. Do not fall back to a locally computed price: showing a number we did not produce is how a customer gets quoted something you cannot honour. |
Limits
What the engine refuses
| Limit | Value | On breach |
|---|---|---|
| Request body | 16 KB | 413 PAYLOAD_TOO_LARGE |
| Ledger write timeout | 5 s | Quote still returns, marked not persisted |
| Ledger writes, all callers | 120 / min | Quote still returns, marked not persisted |
| Browser origins | console only | Blocked by the browser, not by us |
The write ceiling is deliberately shared rather than per-caller. The ledger refuses deletion, so a flood would be permanent damage, and a cap that one caller cannot talk its way around is worth more than a generous one that can be.
No lock-in
Run it yourself
The platform is three files: a SQL schema, a Cloudflare Worker and a single-page console. There is no proprietary runtime and nothing phones home. Standing up your own copy takes about ten minutes.
# 1. Create the audit ledger in your own Postgres or Supabase project
psql -f schema.sql
# 2. Point the engine at it and deploy to your own Cloudflare account
wrangler secret put SUPABASE_SERVICE_ROLE_KEY
wrangler deploy
# 3. Publish the console
npm run deploy:console
The rating matrix lives in one frozen object at the top of the Worker. Change a price there, run the tests, deploy. The test suite recomputes every premium from the matrix and fails if the console or the documentation disagrees with it.
We would rather you could leave than have you locked in. An integration you can walk away from is one you can commit to.
Before a pilot
What you bring
RiskRouter is infrastructure. It does not make anyone an insurance distributor, and it cannot stand in for the things a regulator expects you to hold.
- Your own FSMA registration, or the equivalent in your member state
- A carrier or underwriting pool willing to stand behind the cover
- Your own product approval and demand-and-needs process under the IDD
- Your own customer-facing terms, and the policy documents that go with them
We supply the routing, the pricing determinism and the audit trail. The regulatory position explains where the boundary sits and why it is drawn there.