Get a free API key

This page as Markdown, byte for byte: /price.md

A technical price, from nothing#

One terminal, no key in advance. By the end you will have an account of your own, a complete parametric object, and a technical price for it with every assumption behind the number attached to the same response.

Two blocks, because there are two ways to get to a priceable object: write the whole thing yourself, or send Craton a location schedule and finish the object it produces with PATCH /objects/{id}. Run either on its own; the second is the one to read if the exposure came out of a file.

Read the quickstart first if you have not: it explains CEDE_BASE_URL, the Authorization: Bearer header, and the job pattern every Craton verb answers with. This page repeats none of that and assumes all of it.


What you need#

export CEDE_BASE_URL="https://altier.ridgehead-hamlet.ts.net:8472"   # no trailing slash

**No CEDE_API_KEY here, on purpose.** The block below signs up for an account of its own every time it runs, so it works from a standing start and shows the two account routes doing their job:

Already holding a key from somewhere else? Then skip step 1 and send yours instead — everything after it is the same. Each run of the block as written is a separate tenant, and objects belong to the account that made them.


Before you copy the object: the feed is not yours to invent#

Everything in the object below is a decision you make — the limit, the attachment, the box, the ladder — with exactly one exception. A parametric trigger names the data it is measured against, an id and a version in trigger.data_sources[0], and both must already be pinned in the deployment you are pointed at. Craton prices from committed snapshots and makes no network call while it runs, which is what lets a result replay byte for byte a year later; the same rule read from the other end is that a vintage nobody committed cannot be fetched for you on demand.

Both blocks on this page name usgs-eq-kanto at version 2026-08-10 — the pinned USGS catalogue for the Kanto box the peril block describes — and that pair is why they price. Write a plausible-looking id of your own instead (a country-wide quake catalogue, say, or yesterday's vintage of a real feed) and nothing refuses you while you are writing: the object still validates, POST /objects still stores it and PATCH /objects/{id} still accepts it. The refusal arrives one call later, on the price job, as 422 unpriceable_object saying no pinned snapshot for feed … and listing every id@version that is pinned. It is determinism doing its job rather than a typo the engine will forgive, so the fix is always to name a feed that exists.

Two ways to read that list before you write the block rather than after:

A feed id is a place and a source, not a peril: there is no all-of-the-world catalogue to fall back on, and coverage is checked on every call.


Which model prices here: Craton's, not one you composed#

POST /objects/{id}/price prices with the platform pricing model. Craton picks one of its own two pricers from the shape of the object's trigger — the pair tabulated under what is in a technical price further down — and labels the response with that model and its version. The verb reads your object; it never reaches for a model in your account, so the name on this page's answer is a Craton model whatever you have composed in the builder. The label is the response telling you truthfully what computed the number.

A technical price labelled with a model you composed comes from a different route. **POST /models/{id}/runs** measures your model's index over a risk object's cover period and answers with a model_run whose model.id, model.name and model.version are yours, beside its own technical_price. Composing a model is that road end to end: compose from the four primitives, read the automatic validation pass and its forty-year backtest, then run the model against an object. This page walks the other one — everything below prices an object with Craton's own pricer.


The whole thing, in one block#

Run this in an empty directory.

set -euo pipefail

: "${CEDE_BASE_URL:?export CEDE_BASE_URL first — see 'What you need'}"

# Read one field out of a JSON document on stdin: `field a.b.0.c`.
field() {
  python3 -c 'import json, sys
document = json.load(sys.stdin)
for step in sys.argv[1].split("."):
    document = document[int(step)] if step.isdigit() else document[step]
print(document)' "$1"
}

# 1 — An account, from nothing. /signup takes no credential and answers with
#     the account and its first key. The secret is in this response and in no
#     other, so it is printed here: Craton stores only its digest.
account=$(curl -sS --fail-with-body \
  -H "Content-Type: application/json" \
  -d '{"label": "technical price walkthrough"}' \
  "$CEDE_BASE_URL/signup")
CEDE_API_KEY=$(printf '%s' "$account" | field api_key.secret)
export CEDE_API_KEY
auth="Authorization: Bearer $CEDE_API_KEY"
printf 'your key (shown once): %s\n' "$CEDE_API_KEY"

# 2 — Who that key belongs to. /account is the first call that needs it.
curl -sS --fail-with-body -H "$auth" "$CEDE_BASE_URL/account" > account.json
printf 'account %s on the %s plan, labelled %s\n' \
  "$(field id < account.json)" \
  "$(field plan < account.json)" \
  "$(field label < account.json)"

# 3 — What this deployment pins. The trigger below names one feed by id and
#     version, and a price reads committed snapshots and never the network, so
#     a pair that is not on this list cannot be priced against. Reading it here
#     turns a stale page into a failure now rather than a failed job later.
curl -sS --fail-with-body -H "$auth" "$CEDE_BASE_URL/primitives" > primitives.json
python3 -c '
import json, sys
pinned = [(feed["id"], feed["version"]) for feed in json.load(open("primitives.json"))["feeds"]]
for feed_id, version in pinned:
    print("pinned feed:", feed_id + "@" + version)
wanted = ("usgs-eq-kanto", "2026-08-10")
if wanted not in pinned:
    sys.exit("this page names %s@%s, which this deployment does not pin — "
             "write one of the pairs above into trigger.data_sources instead."
             % wanted)
print("the feed this page prices against is pinned here")
'

# 4 — Something to price. Ingest emits exposure and peril; a price needs the
#     rest — a limit, an attachment and the index that decides the payout — so
#     the whole object is written out here and sent as JSON.
cat > priceable-object.json <<'JSON'
{
  "schema_version": "0.1.0",
  "status": "analysed",
  "exposure": {
    "kind": "location_schedule",
    "currency": "JPY",
    "locations": [
      { "ref": "LOC-0001",
        "address_as_given": "2-16-1 Konan, Minato-ku, Tokyo 108-0075",
        "latitude": 35.6284, "longitude": 139.7387,
        "geocode": { "resolution": "rooftop", "confidence": 0.96 },
        "occupancy": "warehouse", "construction": "reinforced_concrete",
        "year_built": 2011,
        "values": { "building": { "amount": 8400000000, "currency": "JPY" } } }
    ],
    "source_fidelity": { "unmapped_columns": [], "guessed_units": [], "ambiguous_rows": [] }
  },
  "peril": {
    "code": "earthquake",
    "region": {
      "description": "Kanto cat-in-a-box: 34.9-36.2N, 139.0-140.6E.",
      "bounding_geometry": {
        "type": "Polygon",
        "coordinates": [[[139.0, 34.9], [140.6, 34.9], [140.6, 36.2],
                         [139.0, 36.2], [139.0, 34.9]]]
      }
    }
  },
  "financial_structure": {
    "limit": { "amount": 1000000000, "currency": "JPY" },
    "attachment": { "value": 6.0, "unit": "M", "index_ref": "kanto-eq-box-magnitude" }
  },
  "trigger": {
    "type": "parametric_cat_in_a_box",
    "index": {
      "name": "kanto-eq-box-magnitude",
      "version": "1.0.0",
      "description": "Largest catalogue magnitude inside the Kanto box, shallower than the depth threshold.",
      "measurement": { "variable": "catalogue_preferred_magnitude", "unit": "M", "statistic": "max" },
      "aggregation_window": { "duration": "PT72H", "alignment": "event" },
      "thresholds": [
        { "label": "attachment", "level": 6.0, "unit": "M" },
        { "label": "exhaustion", "level": 7.0, "unit": "M" },
        { "label": "max_focal_depth", "level": 100, "unit": "km" }
      ],
      "payout_function": {
        "type": "step",
        "points": [ { "level": 6.0, "payout_ratio": 0.25 },
                    { "level": 6.5, "payout_ratio": 0.5 },
                    { "level": 7.0, "payout_ratio": 1 } ],
        "maximum_payout_ratio": 1
      }
    },
    "data_sources": [
      { "id": "usgs-eq-kanto",
        "name": "USGS ANSS Comprehensive Earthquake Catalog - Kanto extract",
        "kind": "quake_catalogue",
        "version": "2026-08-10", "vintage": "2026-08-10" }
    ]
  },
  "period": {
    "inception": "2026-04-01T00:00:00+09:00",
    "expiry": "2027-04-01T00:00:00+09:00",
    "timezone": "Asia/Tokyo"
  }
}
JSON

object_id=$(curl -sS --fail-with-body -H "$auth" \
  -H "Content-Type: application/json" \
  --data-binary @priceable-object.json \
  "$CEDE_BASE_URL/objects" | field id)
echo "object: $object_id"

# 5 — Price it. A job, like every Craton verb: 202 and an id, never the answer
#     in the same breath as the request.
job_id=$(curl -sS --fail-with-body -X POST -H "$auth" \
  "$CEDE_BASE_URL/objects/$object_id/price" | field id)
echo "price job: $job_id"

# 6 — Poll it until it stops moving.
state=unknown
for _ in $(seq 1 300); do
  curl -sS --fail-with-body -H "$auth" "$CEDE_BASE_URL/jobs/$job_id" > job.json
  state=$(field status < job.json)
  case "$state" in succeeded|failed) break ;; esac
  sleep 0.2
done
if [ "$state" != succeeded ]; then
  printf 'the price job did not succeed: %s\n' "$(cat job.json)" >&2
  exit 1
fi

# 7 — The number, and the working behind it. Everything printed below came out
#     of the one response: the price is four lines of arithmetic, and the rest
#     of the document is what it was computed from.
python3 -c '
import json
price = json.load(open("job.json"))["result"]["price"]
assumptions = price["assumptions"]
premium, burn = price["technical_price"], assumptions["burn"]
record = assumptions["period_of_record"]

print("technical price:", premium["amount"], premium["currency"],
      "- rate on line", premium["rate_on_line"])
print("expected loss:  ", price["expected_loss"]["amount"],
      price["expected_loss"]["currency"])
print("method:", assumptions["method"]["name"], "over",
      record["years"], "years", record["start"][:4], "-", record["end"][:4])
print("burn rate:", burn["burn_rate"], "-", burn["triggering_years"],
      "of", burn["years"], "years would have paid")
for loading in assumptions["loadings"]:
    print("  loading:", loading["name"], "at", loading["rate"], "=",
          loading["amount"]["amount"], loading["amount"]["currency"])
    print("    base: ", loading["base"]["arithmetic"])
    print("    then: ", loading["arithmetic"])
build_up = assumptions["price_build_up"]
print("the whole ladder, from the expected loss up:")
print("  expected loss:", build_up["expected_loss"]["base"]["arithmetic"])
for step in build_up["steps"]:
    print("  +", step["component"], step["amount"]["amount"],
          "-> running total", step["running_total"]["amount"],
          build_up["currency"])
print("  = technical price", build_up["technical_price"]["amount"],
      build_up["technical_price"]["currency"])
assert build_up["steps"][-1]["running_total"] == build_up["technical_price"]
for source in assumptions["data_sources"]:
    print("  data:", source["id"], "vintage", source["vintage"],
          "sha256", source["sha256"][:16], "-", source["licence"])
print("model:", price["model"]["name"], price["model"]["version"])
print("run id:", price["run_id"], "- content-addressed, so a replay is this id")
print("limitations stated on the result:", len(assumptions["limitations"]))
for limitation in assumptions["limitations"]:
    print("  -", limitation)
print(premium["disclosure"])
'
echo "done — the priced object is at /objects/$object_id"

If the last line printed, you have a technical price and the whole assumption set that produced it.


The other way in: a file, then the parts a file cannot carry#

The block above writes the whole object by hand, which is the right shape when the exposure is an index or a single site you already have in front of you. The other way — the usual way — is to send Craton the schedule you were given and finish the object it produces.

A location schedule carries buildings and values. It does not carry the peril you are covering, the limit, the attachment or the trigger: those are decisions you make after reading it, and Ingest does not invent them (SPEC §3.1 is normalisation, no enrichment). The trigger object is the field-by-field reference for the hardest of the four — every member of the block, what each model requires of it, and the smallest trigger of each shape that prices. PATCH /objects/{id} is where you record them, on the object the file became — same id, same exposure, same source file in its provenance — rather than on a hand-copied replica of it.

Run this in an empty directory too. It signs up for its own account, ingests a two-row schedule that names no peril at all, writes the peril and the structure onto the object that comes back, and prices it.

set -euo pipefail

: "${CEDE_BASE_URL:?export CEDE_BASE_URL first — see 'What you need'}"

field() {
  python3 -c 'import json, sys
document = json.load(sys.stdin)
for step in sys.argv[1].split("."):
    document = document[int(step)] if step.isdigit() else document[step]
print(document)' "$1"
}

# 1 — An account of this block's own.
CEDE_API_KEY=$(curl -sS --fail-with-body \
  -H "Content-Type: application/json" \
  -d '{"label": "schedule to price walkthrough"}' \
  "$CEDE_BASE_URL/signup" | field api_key.secret)
export CEDE_API_KEY
auth="Authorization: Bearer $CEDE_API_KEY"
printf 'your key (shown once): %s\n' "$CEDE_API_KEY"

# 2 — A schedule with no peril column: buildings, values, and nothing that
#     says what the cover is against. This is the ordinary case.
cat > schedule.csv <<'CSV'
Location ID,Address,Latitude,Longitude,Occupancy,Building Value,Currency
LOC-0001,"2-16-1 Konan, Minato-ku, Tokyo",35.6284,139.7387,Warehouse,8400000000,JPY
LOC-0002,"1-1 Soga, Chuo-ku, Chiba",35.5687,140.1247,Light Industrial,5100000000,JPY
CSV

ingest_job=$(curl -sS --fail-with-body -H "$auth" \
  -F "[email protected];type=text/csv" "$CEDE_BASE_URL/ingest" | field id)

state=unknown
for _ in $(seq 1 150); do
  curl -sS --fail-with-body -H "$auth" "$CEDE_BASE_URL/jobs/$ingest_job" > job.json
  state=$(field status < job.json)
  case "$state" in succeeded|failed) break ;; esac
  sleep 0.2
done
if [ "$state" != succeeded ]; then
  printf 'ingest did not succeed: %s\n' "$(cat job.json)" >&2
  exit 1
fi
object_id=$(field result.objects.0.id < job.json)
echo "object out of the file: $object_id"

# 3 — What this deployment pins, before writing a trigger that names a feed.
#     The id and the version below have to be on this list: a snapshot that was
#     never committed cannot be fetched at price time, and the object the PATCH
#     writes would store cleanly and fail in the price job instead.
curl -sS --fail-with-body -H "$auth" "$CEDE_BASE_URL/primitives" > primitives.json
python3 -c '
import json, sys
pinned = [(feed["id"], feed["version"]) for feed in json.load(open("primitives.json"))["feeds"]]
for feed_id, version in pinned:
    print("pinned feed:", feed_id + "@" + version)
wanted = ("usgs-eq-kanto", "2026-08-10")
if wanted not in pinned:
    sys.exit("this page names %s@%s, which this deployment does not pin — "
             "write one of the pairs above into trigger.data_sources instead."
             % wanted)
print("the feed this page prices against is pinned here")
'

# 4 — The parts the file could not carry. Each block replaces the block of
#     that name whole; nothing else on the object is touched.
cat > completion.json <<'JSON'
{
  "peril": {
    "code": "earthquake",
    "region": {
      "description": "Kanto cat-in-a-box: 34.9-36.2N, 139.0-140.6E.",
      "bounding_geometry": {
        "type": "Polygon",
        "coordinates": [[[139.0, 34.9], [140.6, 34.9], [140.6, 36.2],
                         [139.0, 36.2], [139.0, 34.9]]]
      }
    }
  },
  "financial_structure": {
    "limit": { "amount": 1000000000, "currency": "JPY" },
    "attachment": { "value": 6.0, "unit": "M", "index_ref": "kanto-eq-box-magnitude" }
  },
  "trigger": {
    "type": "parametric_cat_in_a_box",
    "index": {
      "name": "kanto-eq-box-magnitude",
      "version": "1.0.0",
      "description": "Largest catalogue magnitude inside the Kanto box, shallower than the depth threshold.",
      "measurement": { "variable": "catalogue_preferred_magnitude", "unit": "M", "statistic": "max" },
      "aggregation_window": { "duration": "PT72H", "alignment": "event" },
      "thresholds": [
        { "label": "attachment", "level": 6.0, "unit": "M" },
        { "label": "exhaustion", "level": 7.0, "unit": "M" },
        { "label": "max_focal_depth", "level": 100, "unit": "km" }
      ],
      "payout_function": {
        "type": "step",
        "points": [ { "level": 6.0, "payout_ratio": 0.25 },
                    { "level": 6.5, "payout_ratio": 0.5 },
                    { "level": 7.0, "payout_ratio": 1 } ],
        "maximum_payout_ratio": 1
      }
    },
    "data_sources": [
      { "id": "usgs-eq-kanto",
        "name": "USGS ANSS Comprehensive Earthquake Catalog - Kanto extract",
        "kind": "quake_catalogue",
        "version": "2026-08-10", "vintage": "2026-08-10" }
    ]
  },
  "period": {
    "inception": "2026-04-01T00:00:00+09:00",
    "expiry": "2027-04-01T00:00:00+09:00",
    "timezone": "Asia/Tokyo"
  }
}
JSON

curl -sS --fail-with-body -X PATCH -H "$auth" \
  -H "Content-Type: application/json" \
  --data-binary @completion.json \
  "$CEDE_BASE_URL/objects/$object_id" > completed.json
printf 'peril now: %s - and the exposure is still the file'"'"'s\n' \
  "$(field peril.code < completed.json)"

# 5 — Price the object the file became.
price_job=$(curl -sS --fail-with-body -X POST -H "$auth" \
  "$CEDE_BASE_URL/objects/$object_id/price" | field id)

state=unknown
for _ in $(seq 1 300); do
  curl -sS --fail-with-body -H "$auth" "$CEDE_BASE_URL/jobs/$price_job" > priced.json
  state=$(field status < priced.json)
  case "$state" in succeeded|failed) break ;; esac
  sleep 0.2
done
if [ "$state" != succeeded ]; then
  printf 'the price job did not succeed: %s\n' "$(cat priced.json)" >&2
  exit 1
fi

python3 -c '
import json
price = json.load(open("priced.json"))["result"]["price"]
premium = price["technical_price"]
print("technical price:", premium["amount"], premium["currency"],
      "- rate on line", premium["rate_on_line"])
print("priced object:", price["subject"]["object_id"], "- the one the file became")
print(premium["disclosure"])
'
echo "done — the priced object is at /objects/$object_id"

Read the object back afterwards and everything is in one place: the locations as Ingest normalised them, exposure.source_fidelity recording how the file was read, the blocks you wrote, the premium Price computed and the run that produced it in provenance.

Change your mind about the limit and send another PATCH: the old premium comes off and the object returns to draft, because a premium computed for a different limit is a wrong number that reads exactly like a right one. Price it again — it is one call. The premium itself is never something you send; it is what Price writes, and the route refuses a hand-written one.


What PATCH /objects/{id} writes, and what it does not#

Six blocks, and those six only. Each one replaces the block of that name whole — there is no deep merge, so a financial_structure arrives with its limit and its attachment or it arrives half-written:

Block What you are recording
peril what the cover is against, and the region it is scoped to
financial_structure the limit and the attachment (premium is Price's output, not an input)
trigger the index or the wording reference — the trigger object is the field-by-field reference
period inception, expiry, and the zone day boundaries are read in
jurisdiction governing law and territories
counterparties who is on each side, as a record

The other four blocks of a canonical object are read-only on this route, and each has exactly one place it comes from. Sending one is a 422 invalid_request whose details say the same thing this table says, one line per block — but the table is here so it can be read before the round trip rather than after it:

Block Why not here Where it comes from
exposure It is Ingest's record of a file it read, and exposure.source_fidelity is its account of how that file was read. Edit the exposure by hand and that account describes a file nobody can re-read. POST /ingest, from the file. To state an exposure yourself, POST /objects — a different exposure is a different object, with its own provenance.
id, schema_version The identity of an object is the server's, and a document conforms to the schema version it was written against rather than to one a caller renames it to. The server, when the object is created.
status It is what the object has, not what a caller asserts about it. The verbs. A price run sets priced when it has priced; another PATCH to a pricing input sets it back to draft.
provenance Append-only: it is the record of what has happened to this object. Every verb, as it runs — the ingest run, each price, analysis and backtest.

The file did not say the currency — now what?#

The common case behind this, and worth walking because the answer is not a PATCH. A schedule whose money columns carry no currency anywhere produces an object with the amounts on it, no exposure.currency, and a line in exposure.source_fidelity.ambiguous_rows saying no currency is stated anywhere in the file. That line is the honest reading of what arrived, so overwriting the exposure to add "currency": "USD" would leave the object saying two things at once: that the file did not say, and that it did.

Two routes, and both keep the object saying one thing:

What is not ambiguous either way is the money on the structure. financial_structure.limit carries its own currency and you write it with PATCH, so a limit in USD over an exposure whose file named no currency is not a contradiction Craton is hiding — it is the file's silence, recorded where you can see it.


What just happened#

**POST /signup** — an account on the free sandbox plan and its first key, handed to a stranger holding no credential, live for the very next request. The api_key.secret member of that response is the value to export as CEDE_API_KEY; it is shown once because Craton holds only its digest, and a second signup is a second tenant that cannot see the first one's work.

**GET /account** — the account behind the key you presented, with its plan and label. Two things it deliberately does not carry: the secret, which Craton does not hold, and anyone else's account, which is not yours to read. A key that was configured into a deployment by whoever runs it — rather than handed out by signup — belongs to no account and gets a 404 here while continuing to work everywhere else.

**GET /primitives** — what this deployment pins, read before a trigger is written rather than after one is refused: every feed by id and version, with the measurements, transforms and payout functions the model builder composes from. Both blocks stop there if the feed this page names is not on the list they got back. The pinned data feeds is the same list as a page, with each feed's coverage and licence.

**POST /objects** — a complete canonical object, sent as JSON. The one the quickstart's ingest run produced carries exposure and peril, which is what Ingest promises and no more; a price needs a limit, an attachment and a trigger as well, so this page writes the whole document out and posts it.

**PATCH /objects/{id}** — the second block's way in, and the one to reach for when the exposure came out of a file. It writes six blocks onto an object that already exists — peril, financial_structure, trigger, period, jurisdiction, counterparties — and refuses everything else. [What PATCH /objects/{id} writes, and what it does not](#what-patch-objectsid-writes-and-what-it-does-not) is the whole table: the six, the four that are read-only here, and where each of those four comes from instead.

**POST /objects/{id}/price** — the Price verb. 202, a job id, and the job's own address in the Location header. Poll GET /jobs/{id} until it reads succeeded and the price is on result.price.


What is in a technical price#

The number is the small part of the response. SPEC's requirement is that the complete assumption set travels with it as first-class fields, so nothing downstream has to guess what produced it:

technical_price       amount, currency, rate on line, and the disclosure
                      that travels on every copy
expected_loss         the burn rate times the limit times the period factor
assumptions.burn      every year of the record with the payout ratio it would
                      have had, the burn rate, and how many years triggered
assumptions.loadings  each loading itemised: its rate, the money that rate
                      multiplies and the terms that money is made of, the
                      resulting amount, and why it is there — parameter
                      uncertainty, volatility, expense
assumptions.price_build_up
                      the ladder from the expected loss to the technical
                      price as a running total, one rung per loading, so the
                      itemisation can be checked without re-deriving anything
assumptions.event_selection
                      what the model measured, in the shape of the thing it
                      measured: the box, the catalogue events that fell in it
                      and each qualifying occurrence by the publisher's own
                      event id — or, for a station-measured index, the
                      measurement, the transform and every year's window
assumptions.data_sources
                      publisher, licence, vintage and the sha256 of the exact
                      snapshot the number was computed from
assumptions.method    the method by name, its clustering rule, and what it
                      does not do
assumptions.limitations
                      what this price is weak at, in the response rather than
                      in a footnote a reader never sees
run_id                content-addressed from the object, the model version and
                      the snapshot digest: the same inputs replay to the same
                      id and the same bytes

technical_price.basis is the constant technical, and the disclosure sentence beside it is part of the published schema rather than a convention of this page. A technical price is an actuarial estimate produced by analytics software from the assumptions stated next to it. Nobody stands ready to transact at it, it carries no capacity and no acceptance, and Craton never moves money or touches paper.

How the premium is built, and how to check it#

Four numbers, in one order, and every one of them is a field. The expected loss is the burn rate times the limit times the cover period factor. Each loading is a rate times a base. The premium is the four added up:

expected_loss              =  burn_rate x limit x cover_period.factor
+ parameter_uncertainty    =  0.15 x expected_loss
+ volatility               =  0.20 x (standard_deviation x limit x factor)
+ expense                  =  0.05 x (expected_loss + the two loadings above)
-------------------------------------------------------------------------
= technical_price

Read that ladder off the response rather than from this page. Every loading in assumptions.loadings carries:

So the three loadings do not compound one on top of another in a chain. Parameter uncertainty and volatility both sit on quantities derived from the expected loss and the limit; expense is the only one that loads the loadings, and it says so in its own base.terms.

assumptions.price_build_up is the same walk as a running total — steps[0] is the expected loss, one rung per loading in the order they are applied, and the last running_total is the technical price. Nothing in it is new information; it exists so that two readers checking the same response reconstruct the same ladder.

The published numbers are the arithmetic. Each rate multiplies its base as published, rounded to the currency's minor unit, so rate x base reproduces the loading exactly rather than to within a rounding step, and the four components add to the premium with nothing left over. A reader holding only the response can check the whole of it with a pocket calculator, and assumptions.currency_basis states the rounding rule the response was written under.

What Price reads. Two models, one per index shape, and the object is priced by the one that reads what it declares. Both read a pinned feed vintage — the id and version in trigger.data_sources[0] must already be committed to the build, because a result that replays byte for byte cannot fetch anything at price time. The pinned data feeds is the list to write that block from, and GET /primitives is the same list from the deployment you are pointed at:

Feed kind Measurement it publishes Perils it prices
rainfall_daily daily_precipitation_mm (mm) weather_station
wind_daily daily_maximum_wind_speed_mph (mph) weather_station, tropical_cyclone

Those variable names are copied into trigger.index.measurement.variable exactly as written, with the unit beside them, and they are neither the name the publisher uses nor the English in the feed's description — what each pinned feed publishes is the per-feed list, including catalogue_preferred_magnitude for the quake catalogue the block at the top of this page prices against. A variable this build does not read is refused by name rather than approximated.

So a named-storm (tropical_cyclone) structure prices when it attaches on a wind index and is refused when it attaches on a rainfall one — this build cannot attribute rain to a named storm — and the refusal names the feed rather than your peril.

A wind level is not a storm category. Every pinned wind feed is a cell average of hourly means from a public reanalysis, so it reads systematically lower than an anemometer in the eyewall and lower again than a gust; a Saffir-Simpson number written as an attachment is a level of that feed and nothing else. The price says so in assumptions.limitations, and you should read it before writing the ladder. The pinned data feeds tabulates the highest day each wind record holds over its forty years and how many of those years reach 40, 50, 60 and 74 mph, which is what decides whether your bands are measurable: a band no year reaches earns no expected loss, and a ladder written entirely above the record is refused rather than priced — the section below is that refusal. Only one of the four pinned wind series has any year reaching 74 mph at all, so a Saffir-Simpson attachment is the refused case on most of them; read the table before you pick a level, not after.

Nor is a wind feed a storm track archive. It knows no storm names, tracks or landfall points, so a trigger on it pays on wind speed alone whether or not a system was named, and cannot exclude a wind day no cyclone caused.

Both take a step payout function and both use the same method — empirical annual burning cost over forty years — with the same three loadings, and both name themselves in model on the result.

An object outside both is refused by name — a job that ends failed carrying 422 unpriceable_object and a message saying which part it could not read — because a plausible number from a model that does not fit the structure is worse than none: nothing downstream can tell the difference. That covers an object with no limit, no attachment or no trigger, and it covers the sharper case: an object whose declarations disagree with each other. The two index shapes look identical in JSON, so each model checks the measurement and the data-source kind against what it actually computes before reading a byte — a rainfall index can never be evaluated as a magnitude one, and the refusal says which declaration disagreed with which.

The burn rate underneath this price is the same burn rate Backtest a structure shows year by year, read through the same code — not a second number that happens to look similar.

A structure the record never paid on is refused, not priced at zero#

Both models price the years counted. If no year of the pinned record would have paid — the attachment sits above every level the feed ever recorded, or an index threshold excluded every event — then the burn rate is zero, its standard deviation is zero, all three loadings are zero, and the technical premium is exactly 0.00. That number is not a measurement that the structure is cheap. It is the absence of any observation in its trigger range, and the two would read identically in every field of the response.

So it is refused. The job ends failed with 422 unpriceable_object, and the message carries the number you actually need:

no year of the pinned record would have paid: this structure first pays at
90 mph and the highest 1-day rolling_maximum of daily_maximum_wind_speed_mph
in era5-wind-miami@2026-08-14 over 1986-2025 is 77.1 mph, in 2005 (window
2005-10-24 to 2005-10-24). No year in the period of record reaches 90 mph, so
the empirical burn assigns those bands no expected loss at all — every level
of this ladder, not only its attachment, sits above the record. […] Attach at
or below 77.1 mph to price against the observed record, or run POST
/structures/{id}/backtest to see the same 40 years with no payout in them,
which is a description of history rather than a price.

Two consequences worth knowing before you write a ladder:

The decision behind this refusal — including the alternative it rejected, publishing the zero alongside a data-gap field, and what refusing costs you — is recorded in the build's own decision log as ADR-0048.


When it does not work#

What you see What it means
422 invalid_request from /signup A signup body reads one optional member, label, and refuses anything else. Send {"label": "…"}, or no body at all.
404 from GET /account The key you sent was configured into that deployment by whoever runs it rather than handed out at sign-up, so it belongs to no account. Sign up for one of your own.
422 invalid_risk_object from POST /objects The document did not validate against the published schema. The response names the member and what it expected.
Job failed with 422 unpriceable_object The object is outside what Price v0 reads, or it carries no financial structure and no trigger. The message says which; the trigger object lists every message this refusal arrives with and what to change for each.
Job failed with 422 unpriceable_object saying prices earthquake only about a peril the table above prices Your object names no trigger the station-index model reads — usually none at all — so it fell through to the model that answers for whatever nothing else claimed, and that model refused on the peril. It is a fact about that model and not about this build: the message goes on to name the model that does price your peril and what that model reads. Write the parametric_index trigger over the feed for your peril, as above, and price again.
Job failed with 422 unpriceable_object saying no year of the pinned record would have paid Your attachment is above every level the pinned feed recorded in forty years, so the only price available would be 0.00 — which reads as a cheap structure rather than an untested one. The message names the highest level the record reached and when. Attach at or below it, or backtest the structure to see the same forty years with no payout in them.
Job failed with 422 unpriceable_object saying no pinned snapshot The id or the version in trigger.data_sources[0] is not one this build pins, and a feed version has to pre-exist to be priced against. The message lists every pinned id@version; the pinned data feeds is the same list with each feed's coverage, and GET /primitives is that list from the deployment itself — read it before you write the block and this refusal never reaches a job.
422 invalid_request from PATCH /objects/{id} The body named a block this route does not write — the message lists the six it does — or sent a financial_structure.premium, which is Price's output rather than an input.
422 invalid_risk_object from PATCH /objects/{id} The blocks are the caller's to write, but the object they would make does not validate. The response names each violation.
401 unauthenticated on any call after step 1 The header is exactly Authorization: Bearer <key>. Staging keeps accounts in memory and redeploys whenever the code moves, so a key from yesterday is a key from a previous life — sign up again, it takes one call.

Every refusal has the same shape and validates against the published error schema; the error reference is the full list.


Both blocks on this page are extracted verbatim and executed against a live environment on every change to Craton. If one stopped working, the build stops too.