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.
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.allocated_unitsinfoSub-building allocation applied; the allocated value counts towards total insured value.
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 summary CSVs and full draw-level Parquet files by default, or as one JSON object of summaries 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. With allocation_share, supply the whole building value.
currencystringyesISO 4217 currency code of value.
latitudenumberyesLatitude in decimal degrees.
longitudenumberyesLongitude in decimal degrees.
iso3stringyesISO 3166-1 alpha-3 country code.
property_sqmnumberyesFloor area of the whole building in square metres.
building_floorsintegernoNumber of floors in the whole building.
building_height_mnumbernoHeight of the whole building in metres.
unit_sqmnumbernoOne-floor footprint of the insured unit in square metres. Used with unit_floors to estimate allocation_share when not supplied directly.
unit_floorsintegerno (default 1)Number of floors the insured unit spans.
allocation_sharenumberno (default 1.0)Insured share of the whole building, as a fraction in (0, 1]. Can be estimated from unit dimensions when omitted.
isic_codestringif no occupancyISIC Rev.5 industry code, mapped to an occupancy class.
occupancystringif no isic_codeOccupancy class: amenity, civic, commercial, critical_industrial, education, industrial, medical, military, religious, residential, sports_entertainment or transport. Used when isic_code is missing or not in the mapping.
Insuring part of a building

For apartments, floors, or strata lots, keep value, property_sqm, building_floors, and building_height_m at whole-building level. Set allocation_share to the insured unit’s share of reinstatement value, or its floor-area share if value share is unavailable. For example, a building worth $50 million with allocation_share of 0.018 represents $900,000 of exposure.

Ground-up loss is value × allocation_share × damage_ratio. Policy and reinsurance terms then apply to the unit’s loss. Without an explicit share, unit_sqm and unit_floors estimate it as unit_sqm × unit_floors × 3 ÷ (property_sqm × building_height_m), capped at 1.0. This assumes a 3 m unit storey height; prefer building floors where available. Do not shrink the building descriptors to the unit: that changes the vulnerability estimate.

Allocation uses the building’s damage ratio for every unit, understating unit-level volatility and tails. Units in one building are not automatically grouped, so group them yourself to avoid understating accumulation. Shares across records of the same building should total no more than 1; avoid double-counting a block policy alongside unit policies. Common parts, interiors, contents, improvements, and loss of rent are not modelled separately. For contents-only cover, set value × allocation_share to the relevant sum insured.

Validation reports allocated units under model.allocated_units; the allocated value counts towards total insured value.

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_typestringexcess (default): the deductible comes off each occurrence. franchise: once the loss exceeds the deductible it is paid in full.
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.

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 ZIP also includes full_hazard.parquet (conflict onset × draw, with severity shocks), full_net_losses.parquet (location × draw × policy layer, through the loss waterfall), and full_assets.parquet (asset × draw, with shocked values). These unaggregated files can be large; JSON responses contain summaries only.

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.

subthreshold_hit_percentage in the property and top-location summaries is the percentage (0–100) of hit simulations attributed to subthreshold events. Do not multiply it by 100. Each property counts once per simulation, regardless of policy layers or event count. Properties with no hits return 0; conflict hits take precedence when both sources hit the same property in a simulation.

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), conditional and unconditional means of deaths, 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, subthreshold_hit_percentage), 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. Each layer also includes an expected loss ratio.

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. Stages that do not apply are omitted. Columns: stage, expected_annual_loss, pct_of_ground_up, and description. Insurance applies coinsurance, deductible/attachment, occurrence limit, then participation; reinsurance follows the insurer’s gross share.

country_contributions.csv

Country tail-risk contributions

One row per country: expected_ground_up_loss, expected_insurer_net_loss, expected_asset_loss, expected_total_loss, tvar_95_contribution, tvar_99_contribution, and share_of_tvar_95. Co-TVaR is the country’s average loss in the portfolio’s worst 5% or 1% of simulated years. Contributions sum to portfolio TVaR; standalone country TVaRs do not.

top_locations.csv

Top property locations

The ten locations with the largest expected_ground_up_loss, including property value, hit frequency, subthreshold_hit_percentage, loss ratio, insured expected loss, and insurer expected loss.

policy_summary.csv

Per-policy loss trace

Only when policies are supplied. One row per layer: expected_ground_up_loss, expected_after_coinsurance, expected_layer_loss, expected_insurer_gross, expected_ri_recovery, expected_reinstatement_premium, expected_insurer_net_loss, insurer_net_tvar_95/99, insurer_net_max_loss, attachment_breaches, attachment_breach_frequency, n_properties, driving_country, and driving_country_share.

top_policies.csv

Top policy layers

When policies are supplied, the ten layers with the largest expected insurer net loss, including insurer, property count, driving country, attachment breach frequency, RI recovery, net loss, and TVaR 99.

Errors

StatusMeaning
503Authentication or underlying download data is temporarily unavailable; retry shortly.
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 summary CSVs and full draw-level Parquet 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).