API reference

Silfio API

The Silfio Geopolitical Risk Engine is exposed as a synchronous HTTP API. You send your portfolio (properties, investments, insurance policies, reinsurance treaties) as JSON; the model is run and returns aggregated loss summaries as CSV files inside a ZIP archive by default, or as a single JSON object with ?format=json.

Base URL: https://silfio-production.up.railway.app. All request and response bodies are UTF-8. Monetary inputs can be in any supported currency (set per record via currency); outputs are reported in USD.

Authentication

API key in the X-API-Key header.

X-API-Key: your-api-key

Programmatic users (Python, R, scripts) authenticate with an API key. Requests without a key run on the Free plan. Browser users on engine.silfio.com need no key — the site obtains a short-lived session token after login and refreshes it automatically.

Download example calls

Grab a complete, runnable example in your language of choice and adapt it to your own portfolio.

Access

Authentication and plans

Access is tiered by plan. Programmatic users send an API key in the X-API-Key header. Browser users on engine.silfio.com send Authorization: Bearer <token> — a short-lived session token obtained by the site after login. Tokens carry your plan and expire after ~15 minutes; the site refreshes them automatically. (This exchange uses POST /auth/token, a service-to-service endpoint not callable by end users.)

PlanAPI keyMax inventory items / requestMax investments / requestRate limit (per 60 s)
Freenot required10105
Advancedrequired10010030
Enterpriserequiredunlimited*unlimited*120

* Up to the global schema caps below.

Requests without a key run on the Free plan (keyless callers are rate-limited per IP). An unrecognised key receives 401 Unauthorized. A portfolio larger than your plan allows receives 403 with a message stating the cap. /health requires no key.

Keys are issued per user and shown once at creation — store yours securely and never share it or embed it in client-side code. If a key is compromised, contact Silfio to revoke it and issue a replacement; revocation takes effect within about a minute. If authentication is momentarily unavailable, keyed requests receive 503 — retry shortly.

Limits

Global limits

These limits are as of writing, and can be increased going forward.

LimitValue
Request body size20 MiB (413 when exceeded)
inventory records10,000
insurance_policies records10,000
reinsurance records2,000
investments records50,000
Scenario override countries250 per mapping

Numeric fields are validated: coordinates must be valid lat/long, monetary values non-negative, percentage fields fractions in [0, 1], and NaN/Infinity are rejected. expiry_date must be after inception_date. Scenario mapping keys must be uppercase ISO 3166-1 alpha-3 country codes.

Endpoints

Endpoint overview

MethodPathPurpose
GET/healthLiveness probe; lists loaded models. No auth.
GET/meResolve your plan and its limits (validates your credentials).
POST/auth/tokenService-to-service session-token exchange (frontend server only).
POST/validateDry-run validation of a /run payload — structured per-row findings, no simulation.
POST/runRun the engine on your portfolio.
POST/scenarioSame as /run, plus deterministic scenario overrides. Enterprise plan only.
GET/templatesList the sample-portfolio templates (insurer, reinsurer, corporate, investor).
GET/templates/{persona}Return one /run-ready template as JSON, or CSV ZIP with ?format=csv.
GET/schemasJSON Schemas generated from the API's own Pydantic models.
GET/get_conflictsDownload all pre-simulated conflicts (parquet).
GET/get_conflict_probDownload per-country conflict-probability draws (parquet).
GET

/health

Returns service status and the location-risk models currently loaded.

Response
{
  "status": "ok",
  "models_loaded": ["..."],
  "model_load_seconds": 1.234
}
GET

/me

Returns the plan your request resolves to and its portfolio limits — useful for validating an API key (invalid keys get 401) and displaying plan details. Without a key it reports the Free plan. max_inventory_items / max_investments are null for Enterprise (only the global caps apply).

Response
{ "plan": "advanced", "max_inventory_items": 100, "max_investments": 100 }
POST

/validate

Validates a /run-shaped payload without running a simulation. It always returns 200 OK with structured findings, so upload grids can show row-level errors instead of parsing a generic 422.

Response
{
  "valid": false,
  "errors": [
    {
      "section": "inventory",
      "index": 3,
      "field": "latitude",
      "code": "schema.less_than_equal",
      "message": "Input should be less than or equal to 90"
    }
  ],
  "warnings": [
    {
      "section": "insurance_policies",
      "index": 0,
      "field": "reinsurance_ids",
      "code": "reference.unknown_treaty_id",
      "message": "Referenced treaty was not supplied; no recovery applies."
    }
  ],
  "info": [
    {
      "section": "inventory",
      "index": null,
      "field": "policy_ids",
      "code": "model.uninsured_items",
      "message": "Some items carry no policy; loss is fully retained."
    }
  ],
  "summary": { "n_errors": 1, "n_warnings": 1, "n_info": 1 }
}

Finding codes

CodeSeverityMeaning
schema.*errorPydantic validation failure; suffix is the pydantic error type.
plan.cap_exceedederrorPortfolio exceeds the caller's plan cap.
reference.duplicate_policy_id / reference.duplicate_treaty_iderrorDuplicate IDs that would double-count layers.
reference.unknown_policy_id / reference.unknown_treaty_idwarningReferenced policy or treaty is missing; that cover has no effect.
reference.unattached_policy / reference.unattached_treatywarningSupplied but referenced by nothing; no effect.
coverage.unknown_iso3 / coverage.unknown_currencywarningCountry or currency is not covered by the model/FX table.
policy.expiredwarningExpiry date is in the past; terms are still applied.
model.*infoExplicit modelling behaviour such as uninsured items, coinsurance off, or layered programme order.

Errors block submission; warnings allow the run but describe inputs with no effect or surprising behaviour; info explains modelling behaviour such as uninsured items or layered programmes.

POST

/run

Runs the full engine on the supplied portfolio and returns the summary tables (see Response format) — as a ZIP of CSVs by default, or as one JSON object with ?format=json.

Request body
{
  "inventory": [ ... ],
  "insurance_policies": [ ... ],
  "reinsurance": [ ... ],
  "investments": [ ... ]
}

All four arrays are optional, subject to these rules:

  • At least one of inventory or investments must be provided.
  • insurance_policies requires inventory.
  • reinsurance requires insurance_policies.

Violations return 422 with a message explaining which rule failed.

inventory — insured properties

FieldTypeRequiredDescription
property_name_or_addressstringyesDisplay name or address of the property.
policy_idsstring[]yesIDs of the insurance policies covering this property.
property_typestringyesProperty category (e.g. warehouse, office).
valuenumberyesInsured/book value of the property.
currencystringyesISO 4217 currency code of value.
latitudenumberyesLatitude in decimal degrees.
longitudenumberyesLongitude in decimal degrees.
iso3stringyesISO 3166-1 alpha-3 country code.
property_sqmnumberyesFloor area in square metres.
building_floorsintegernoNumber of floors.
building_height_mnumbernoBuilding height in metres.
isic_codestringyesISIC industry classification code.

insurance_policies — insurance contracts

FieldTypeRequiredDescription
policy_idstringyesUnique policy identifier (referenced by inventory[].policy_ids).
policy_namestringnoDisplay name.
contract_typestringno (default insurance)insurance or reinsurance; distinguishes row types in combined uploads. Not used in pricing.
insurerstringnoThe insurer writing the layer.
brokerstringnoBroker.
inception_datedate (YYYY-MM-DD)yesPolicy start.
expiry_datedate (YYYY-MM-DD)yesPolicy end.
currencystringyesISO 4217 currency of monetary terms.
insured_valuenumbernoDeclared insured value. Only provide together with coinsurance_pct for single-policy placements with a coinsurance/average clause; leave blank for layered or shared placements.
notesstringnoFree text.
insuranceobjectyesLayer terms — see below.
reinsurance_idsstring[]noTreaty IDs (from reinsurance) covering this layer.
insurance object (layer terms)
FieldTypeDescription
occurrence_attachmentnumberAttachment point per occurrence.
occurrence_limitnumberLimit per occurrence.
deductible_typestringDeductible type.
participation_pctnumberInsurer participation share (fraction, e.g. 0.5 = 50%).
coinsurance_pctnumber (optional)Coinsurance/average-clause requirement (fraction, e.g. 0.8 = 80%). Leave blank if the policy has no such clause; underinsurance is then not assessed.

reinsurance — reinsurance treaties

FieldTypeRequiredDescription
treaty_idstringyesUnique treaty identifier (referenced by reinsurance_ids).
reinsurerstringnoReinsurer name (informational).
cedentstringnoCeding insurer (informational).
basisstringyesOne of per_risk, per_occurrence, aggregate, quota_share.
currencystringyesISO 4217 currency of monetary terms.
cession_pctnumberno (default 1.0)Ceded share (fraction).
attachmentnumberno (default 0)Attachment point (excess-of-loss bases).
limitnumberno (default unlimited)Layer limit. Omit for unlimited cover. An explicit 0 is rejected for all bases except quota_share.
premiumnumberno (default 0)Treaty premium.
reinstatementsintegerno (default 0)Number of reinstatements.
reinstatement_premium_pctnumberno (default 0)Reinstatement premium (fraction of original).

investments — financial assets

Each asset carries an asset_class discriminator — "Equity", "Bond", "Cash", or "Property" — that determines which extra fields apply.

Common fields (all classes)
FieldTypeRequiredDescription
asset_classstringyesEquity, Bond, Cash, or Property.
instrument_typestringyesInstrument type.
issuerstringnoIssuer name.
currencystringyesISO 4217 currency of market_value.
market_valuenumberyesCurrent market value.
iso3stringyesISO 3166-1 alpha-3 country of exposure.
Additional field for Equity
FieldTypeRequiredDescription
industrystringyesIssuer industry — drives the equity beta and conflict-relativity lookups.
Additional fields for Bond and Cash
FieldTypeRequiredDescription
credit_ratingstringnoCredit rating.
durationnumberyesModified duration in years.
POST

/scenario

Accepts the same body as /run, plus two optional override mappings keyed by ISO 3166-1 alpha-3 code (e.g. "UKR"). Countries not present in a mapping keep their simulated draws.

Scenario testing is an Enterprise-plan feature. Requests authenticated with a Free or Advanced key — or no key — receive a 403.

Request body
{
  "inventory": [ ... ],
  "investments": [ ... ],
  "event_scenario": {
    "UKR": { "annual_prob": 1.0, "conflict_type": "interstate" }
  },
  "hazard_scenario": {
    "UKR": { "damage": 0.25, "gdp_loss": -0.10 }
  }
}

event_scenario — per-country conflict onset overrides

FieldTypeRequiredDescription
annual_probnumber (0–1)yesPinned annual conflict-onset probability.
conflict_typestringyesinterstate, internationalised_internal, or internal.

hazard_scenario — per-country severity overrides

Any subset of fields may be supplied; omitted fields keep their simulated values. All values are fractions (0.25 = 25%). At least one field must be set per country entry.

FieldTypeDescription
damagenumber (0–1)Physical damage ratio.
gdp_lossnumberGDP shock (negative = contraction).
inflation_shocknumberChange in inflation.
equity_lossnumberEquity-market return shock (negative = drop).
bond_changenumberGovernment bond yield change.

Templates and schemas

These endpoints help clients create guided first-run experiences. They are rate-limited like other endpoints and work on every plan, including keyless Free usage.

GET

/templates

Lists available sample portfolios with a persona, description, and per-section row counts. Personas are insurer, reinsurer, corporate, and investor.

Response
{
  "templates": [
    {
      "persona": "insurer",
      "description": "Property insurance portfolio with treaty support.",
      "sections": { "inventory": 8, "insurance_policies": 3, "reinsurance": 2 }
    }
  ]
}
GET

/templates/{persona}

Returns a validated, /run-ready JSON payload for the selected persona. Add ?format=csv to download a ZIP of flat per-section CSV files plus a README. List columns are semicolon-separated and nested policy terms are dot-flattened.

GET

/schemas

Returns JSON Schemas for the run and scenario payloads, generated from the same Pydantic models enforced by the API. Use this endpoint for client-side pre-validation and upload-grid column definitions so validation rules do not drift from the server.

Response shape
{
  "run_request": { "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "properties": { "...": "..." } },
  "scenario_run_request": { "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "properties": { "...": "..." } }
}

Data downloads

GET

/get_conflicts

Downloads the full set of pre-simulated conflicts as a single zstd-compressed parquet file (Content-Type: application/vnd.apache.parquet). One row per simulated conflict onset (country × simulation draw), including the country (gw code and iso3), onset timing, and severity outcomes (physical damage, GDP / inflation / equity / bond shocks). The simulation set is regenerated nightly; within a UTC day, repeated downloads return identical data.

/get_conflicts
import io
import polars as pl
import requests

resp = requests.get(
    f"{BASE_URL}/get_conflicts",
    headers={"X-API-Key": API_KEY},
    timeout=300,
)
resp.raise_for_status()
conflicts = pl.read_parquet(io.BytesIO(resp.content))  # or pandas.read_parquet
GET

/get_conflict_prob

Downloads the simulated conflict-onset probability draws as a single zstd-compressed parquet file: one row per simulation draw (~10,000), one column per country (ISO3 code), plus a draw_id column. The period query parameter accepts annual (default) or daily: daily returns the stored daily onset-probability draws; annual converts each to an annual onset probability via 1 − (1 − pdaily)365.

/get_conflict_prob
resp = requests.get(
    f"{BASE_URL}/get_conflict_prob",
    params={"period": "annual"},
    headers={"X-API-Key": API_KEY},
    timeout=300,
)
resp.raise_for_status()
probs = pl.read_parquet(io.BytesIO(resp.content))
# probs["UKR"] -> 10,000 annual onset-probability draws for Ukraine

Both download endpoints are available on every plan (no key needed on Free), count toward your plan's rate limit, and return 503 if the underlying data is temporarily unavailable.

Response format

Successful /run and /scenario responses return 200 OK. By default the API responds with Content-Type: application/zip, Content-Disposition: attachment; filename="silfio_run.zip" (or silfio_scenario.zip). Add ?format=json to receive one JSON object keyed by table name instead. The X-Runtime-Seconds header is returned for both formats.

Keys always present in JSON are inventory_summary, assets_summary, conflict_summary, portfolio_summary, portfolio_waterfall, country_contributions, and top_locations. When insurance policies are supplied, policy_summary and top_policies are also returned. Non-finite numbers are serialized as null.

The raw engine output has one row per Monte Carlo draw; only these aggregations are returned. Unconditional statistics average over all simulated years (no-conflict years count as zero loss); conditional statistics average only over years in which a conflict occurred.

conflict_summary.csv

Per-country hazard summary

One row per country (iso3 code). Columns include conflicts_simulated, conflicts_simulated_ratio (share of simulations with a conflict), damage, GDP / inflation / equity / bond shocks, and percentile columns (p01, p05, p50, p95, p99) for damage and each market shock.

inventory_summary.csv

Per-property loss summary

One row per property (inventory_id). Columns: property identifiers (property_name_or_address, policy_id, iso3, country, property_value), hit statistics (number_of_hits, hit_frequency), ground-up fields (expected_ground_up_loss, ground_up_tvar_95, ground_up_tvar_99, ground_up_max_loss), and the insured, insurer, and reinsurer expected-loss and tail metrics. Ground-up loss is total property damage before any insurance.

assets_summary.csv

Per-asset investment impact

One row per asset (asset_id). Columns: asset_class, instrument_type, original_value, expected_loss, max_loss, tvar_95, tvar_99, expected_shocked_value, expected_loss_ratio. The expected_loss fields represent signed value impact: a negative loss means the investment is expected to grow. Per-asset VaR is deliberately not reported; portfolio-level VaR is in portfolio_summary.csv.

portfolio_summary.csv

Whole-book loss metrics

One row per view (layer column): each inventory layer alone (ground_up, insured, insurer, reinsurer), assets alone, and each layer combined with assets (ground_up_plus_assets, etc.). Layers that don't apply to your request are omitted. Columns: layer, expected_loss, var_95, var_99, tvar_95, tvar_99, max_loss, total_value (total USD book value of the view). Tail metrics are computed by summing losses within each simulated year first, so combined views are not simply the sum of their parts.

portfolio_waterfall.csv

Total-damage-to-net portfolio waterfall

One row per stage, in order: ground_up_loss, insurance_recovery, insured_net_loss, reinsurance_recovery, reinstatement_premiums, insurer_net_loss. Columns include expected_annual_loss, pct_of_ground_up, and a plain-language description for chart tooltips.

country_contributions.csv

Country tail-risk contributions

One row per country, including expected_ground_up_loss and sorted by tvar_95_contribution. Contributions are co-TVaR and sum to the portfolio TVaR, making them suitable for country-driver pies or stacked bars.

top_locations.csv

Top property locations

Pre-computed top 10 location drivers with property value, hit frequency, expected_ground_up_loss, loss ratio, insured expected loss, and insurer expected loss.

policy_summary.csv

Per-policy loss trace

One row per policy layer, sorted by expected_insurer_net_loss. Includes attachment breaches, the policy-level loss waterfall, TVaR tails, and the driving country attribution.

top_policies.csv

Top policy layers

Pre-computed top 10 policy drivers with insurer, property count, driving country, attachment breach frequency, RI recovery, net loss, and TVaR 99.

Errors

StatusMeaning
401Unrecognised X-API-Key (omit the header entirely to use the Free plan).
403Portfolio exceeds your plan's cap — the message states the limit.
413Request body exceeds the size limit.
422Invalid request body (schema/validation error) or an engine failure. Engine failures include an error_id you can quote when reporting the problem.
429Rate limit exceeded — retry after the number of seconds in the Retry-After header.
Engine failure
{ "detail": "Engine run failed. Check your input data. (error_id=1a2b3c4d5e6f)" }
Schema validation error
{
  "detail": [
    {
      "loc": ["body", "inventory", 0, "value"],
      "msg": "Input should be a valid number",
      "type": "float_parsing"
    }
  ]
}

Examples

Python examples are shown first, but the API is plain HTTP and JSON. The same requests work from R, JavaScript, cURL, or any client that can send an X-API-Key header and read a JSON response. Omit ?format=json when you want the default ZIP of CSV files.

Baseline run
import requests

BASE_URL = "https://silfio-production.up.railway.app"
API_KEY = "your-api-key"

payload = {
    "inventory": [
        {
            "property_name_or_address": "Warehouse A, Kyiv",
            "policy_ids": ["POL-001"],
            "property_type": "warehouse",
            "value": 25_000_000,
            "currency": "USD",
            "latitude": 50.4501,
            "longitude": 30.5234,
            "city": "Kyiv",
            "country": "Ukraine",
            "iso3": "UKR",
            "property_sqm": 12000,
            "isic_code": "5210",
        }
    ],
    "investments": [
        {
            "asset_class": "Bond",
            "instrument_type": "government_bond",
            "issuer": "Republic of Poland",
            "currency": "EUR",
            "market_value": 5_000_000,
            "iso3": "POL",
            "credit_rating": "A-",
            "duration": 6.5,
        }
    ],
}

resp = requests.post(
    f"{BASE_URL}/run",
    params={"format": "json"},
    json=payload,
    headers={"X-API-Key": API_KEY},
    timeout=300,
)
resp.raise_for_status()
print("Engine runtime:", resp.headers["X-Runtime-Seconds"], "s")

tables = resp.json()
portfolio = tables["portfolio_summary"]
waterfall = tables["portfolio_waterfall"]
top_locations = tables["top_locations"]
Scenario run
scenario_payload = {
    **payload,
    "event_scenario": {
        "UKR": {"annual_prob": 1.0, "conflict_type": "interstate"}
    },
    "hazard_scenario": {
        "UKR": {"damage": 0.30, "gdp_loss": -0.15, "equity_loss": -0.25}
    },
}

resp = requests.post(
    f"{BASE_URL}/scenario",
    params={"format": "json"},
    json=scenario_payload,
    headers={"X-API-Key": API_KEY},
    timeout=300,
)
resp.raise_for_status()
scenario_tables = resp.json()
Other clients
const resp = await fetch(`${BASE_URL}/run?format=json`, {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-API-Key": API_KEY,
  },
  body: JSON.stringify(payload),
});
if (!resp.ok) throw new Error(`Silfio API error ${resp.status}: ${await resp.text()}`);

console.log("Runtime:", resp.headers.get("X-Runtime-Seconds"), "s");
const tables = await resp.json();
console.log(tables.portfolio_waterfall, tables.top_locations);

Notes

  • Simulations are seeded per calendar day (UTC), so repeated identical requests on the same day return identical results; results change day to day as the seed and the nightly-refreshed model parameters update.
  • Model parameters, FX rates, and hazard calibrations are maintained server-side and refreshed daily at 00:00 UTC.
  • Runs are synchronous: keep your HTTP client timeout generous for large portfolios (the examples use 300 s).