The trigger object
A parametric structure pays on a measurement, and trigger is the block where that measurement is written down: which pinned feed is read, what is measured in it, over what window, and what fraction of the limit each level pays. It is the one block nothing else can supply (a schedule of buildings does not carry it, and Craton will not invent it), so POST /objects/{id}/price refuses an object without one.
This page is the field-by-field reference for that block and the smallest trigger of each shape that actually prices. Both are in a single example that is extracted from this page and executed against a live environment on every change to Craton, so the minimum documented here is the minimum the product enforces.
Read the quickstart first if you have not: it explains CEDE_BASE_URL, the Authorization: Bearer header and the job pattern. A technical price, from nothing is the fuller walkthrough: a complete object, a price, and every assumption behind the number. This page is narrower on purpose: the trigger, and nothing else.
On this page
The refusal that sends people here
Ingest a location schedule, write a peril and a limit onto the object it produced, ask for a price, and the job ends failed:
422 unpriceable_object
trigger is required to price this structure and is absentThat is not a defect in your object. A location schedule carries buildings, coordinates and values; it does not carry the cover written over them, and Ingest normalizes what a file says rather than deciding what it does not say. The peril, the limit, the attachment and the trigger are decisions you make after reading the file, and PATCH /objects/{id} is where they go: on the object the file became, keeping its exposure and its provenance.
What Price reads off an object
| Block | What Price does with it |
|---|---|
trigger |
The whole subject of this page: the index, its feed, its window and its payout ladder. |
financial_structure.limit |
The maximum payout. Every number in the price is this multiplied by a ratio, so it must be positive and currency-tagged. |
financial_structure.attachment |
Where the structure attaches, as an index level for a parametric structure. It must agree with the first level of the payout ladder (see below). |
peril.code |
Which model is asked, and (with the feed) whether that model can measure this peril at all. |
peril.region.bounding_geometry |
Cat-in-a-box only: the region the catalog is read inside. A single-ring, axis-aligned GeoJSON Polygon. |
period.inception, period.expiry |
The length of the cover period, which scales the expected loss. |
exposure is not read by either pricing model, and that is a property of parametric cover rather than an omission: the payout is the ladder times the limit, not a function of the insured values. The published schema still requires an exposure block on every risk object, so the example below carries a minimal one.
Four models
trigger.type chooses the shape, and for parametric_index, data_sources[0].kind chooses which archive it is measured against. Each shape is priced by its own model: they read differently shaped bytes, and a model that quietly grew a second scope is a model whose refusals stop meaning anything.
trigger.type |
What it measures | peril.code it prices |
data_sources[0].kind |
Model |
|---|---|---|---|---|
parametric_cat_in_a_box |
The largest catalog magnitude inside a region, per occurrence | earthquake |
any kind but storm_report_set (the feed's own kind must be quake_catalogue) |
cede/parametric-burn-eq-box |
parametric_cat_in_a_box |
The year's largest hail, thunderstorm wind or tornado report inside a region | severe_convective_storm |
storm_report_set, required: the member that chooses the report model over the earthquake one |
cede/parametric-report-box |
parametric_cat_in_a_box |
The year's largest mapped wildfire inside a region, by burned acres | wildfire |
storm_report_set over a mapped-wildfire feed: the same model, and the feed's kind decides the peril |
cede/parametric-report-box |
parametric_index |
A station-measured daily series aggregated over a rolling window | weather_station on any weather or river series, tropical_cyclone over a wind feed, flood over a river-gauge feed, grid_outage over a county outage feed |
station_network, required |
cede/parametric-burn-station-index |
parametric_index |
The season's highest best-track wind inside a gate | tropical_cyclone |
storm_track_set, required: the member that chooses the track model over the station one |
cede/parametric-track-gate |
wording |
Nothing: the indemnity-style branch, held and versioned but priced by no model | none | none | none |
Which peril a parametric_index structure can price depends on which feed the trigger names, because the feed is what decides whether a level says anything about that peril: a daily wind maximum is a serviceable named-storm index, a river gauge's daily discharge is a serviceable flood one, a daily rainfall total is neither, and the refusal names the feed rather than your peril. A storm_track_set source routes the same trigger type to the track model instead: a gate and whole storms rather than a point and days, walked end to end on the track-gate structure. A storm_report_set source does the same for parametric_cat_in_a_box: storm reports in a box, walked end to end on the storm-report box, and mapped wildfires in a box on wildfire, by the acre. The pinned data feeds is the list, and GET /primitives is the same list from the deployment you are pointed at.
The smallest trigger that prices
Three objects, one of each parametric shape. Every member below is either required by the published schema or required by the model that prices it; nothing here is decoration, and taking any of it out produces a named refusal rather than a number.
Run this in an empty directory.
set -euo pipefail
: "${CEDE_BASE_URL:?export CEDE_BASE_URL first, see the quickstart}"
# 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"
}
# Post an object, price it, wait for the job, print the number and the model.
price_it() {
local document="$1" object_id job_id state=unknown
object_id=$(curl -sS --fail-with-body -H "$auth" \
-H "Content-Type: application/json" \
--data-binary @"$document" "$CEDE_BASE_URL/objects" | field id)
job_id=$(curl -sS --fail-with-body -X POST -H "$auth" \
"$CEDE_BASE_URL/objects/$object_id/price" | field id)
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
return 1
fi
python3 -c '
import json, sys
price = json.load(open("job.json"))["result"]["price"]
premium, burn = price["technical_price"], price["assumptions"]["burn"]
print(sys.argv[1] + ":", premium["amount"], premium["currency"],
"- rate on line", premium["rate_on_line"])
print(" priced by", price["model"]["name"], price["model"]["version"],
"-", burn["triggering_years"], "of", burn["years"],
"years of the record would have paid")
' "$1"
}
# 1. An account of this block's own. No credential needed to ask for one.
CEDE_API_KEY=$(curl -sS --fail-with-body \
-H "Content-Type: application/json" \
-d '{"label": "trigger reference"}' \
"$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. Shape one: cat-in-a-box. The index reads the catalog's preferred
# magnitude inside the region, clusters events 72 hours apart into one
# occurrence, and pays the whole limit at M6.0 and above. The data source
# needs an id and a version and nothing else; the feed's own manifest is
# what says it is a quake catalog.
cat > cat-in-a-box.json <<'JSON'
{
"schema_version": "0.1.0",
"status": "draft",
"exposure": {
"kind": "index",
"source_fidelity": { "unmapped_columns": [], "guessed_units": [], "ambiguous_rows": [] }
},
"peril": {
"code": "earthquake",
"region": {
"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" }
},
"trigger": {
"type": "parametric_cat_in_a_box",
"index": {
"name": "kanto-eq-box-magnitude",
"version": "1.0.0",
"measurement": { "variable": "catalogue_preferred_magnitude", "unit": "M" },
"aggregation_window": { "duration": "PT72H" },
"thresholds": [ { "label": "attachment", "level": 6.0, "unit": "M" } ],
"payout_function": {
"type": "step",
"points": [ { "level": 6.0, "payout_ratio": 1 } ]
}
},
"data_sources": [ { "id": "usgs-eq-kanto", "version": "2026-08-10" } ]
},
"period": {
"inception": "2026-04-01T00:00:00+09:00",
"expiry": "2027-04-01T00:00:00+09:00",
"timezone": "Asia/Tokyo"
}
}
JSON
price_it cat-in-a-box.json
# 3. Shape two: a station-measured index. Three-day rainfall totals, paying
# the whole limit at 120 mm. Two members this shape needs that the other
# does not: measurement.statistic, which names the transform, and
# data_sources[0].kind, which decides the reader before a byte is parsed.
cat > station-index.json <<'JSON'
{
"schema_version": "0.1.0",
"status": "draft",
"exposure": {
"kind": "index",
"source_fidelity": { "unmapped_columns": [], "guessed_units": [], "ambiguous_rows": [] }
},
"peril": { "code": "weather_station" },
"financial_structure": {
"limit": { "amount": 2000000, "currency": "USD" },
"attachment": { "value": 120, "unit": "mm" }
},
"trigger": {
"type": "parametric_index",
"index": {
"name": "bangkok-3-day-rainfall",
"version": "1.0.0",
"measurement": {
"variable": "daily_precipitation_mm", "unit": "mm", "statistic": "sum"
},
"aggregation_window": { "duration": "P3D" },
"thresholds": [ { "label": "attachment", "level": 120, "unit": "mm" } ],
"payout_function": {
"type": "step",
"points": [ { "level": 120, "payout_ratio": 1 } ]
}
},
"data_sources": [
{ "id": "era5-rain-bangkok", "kind": "station_network", "version": "2026-08-11" }
]
},
"period": {
"inception": "2026-06-01T00:00:00+07:00",
"expiry": "2027-06-01T00:00:00+07:00",
"timezone": "Asia/Bangkok"
}
}
JSON
price_it station-index.json
# 4. Shape three: a track gate. The same trigger type as shape two, routed to
# a different model by the data source's kind: storm_track_set names a
# best-track archive, the gate is the peril region, the window is the
# season (P1Y), and the ladder is a wind in the archive's own kt. Two
# members this shape needs that the others do not: the kind, and a
# thresholds entry labeled attachment, required by this model at the
# ladder's lowest level, not documentation.
cat > track-gate.json <<'JSON'
{
"schema_version": "0.1.0",
"status": "draft",
"exposure": {
"kind": "index",
"source_fidelity": { "unmapped_columns": [], "guessed_units": [], "ambiguous_rows": [] }
},
"peril": {
"code": "tropical_cyclone",
"region": {
"bounding_geometry": {
"type": "Polygon",
"coordinates": [[[-98.0, 24.5], [-79.0, 24.5], [-79.0, 32.0],
[-98.0, 32.0], [-98.0, 24.5]]]
}
}
},
"financial_structure": {
"limit": { "amount": 5000000, "currency": "USD" },
"attachment": { "value": 64, "unit": "kt" }
},
"trigger": {
"type": "parametric_index",
"index": {
"name": "gulf-track-gate",
"version": "1.0.0",
"measurement": { "variable": "track_maximum_wind_kt", "unit": "kt", "statistic": "max" },
"aggregation_window": { "duration": "P1Y" },
"thresholds": [ { "label": "attachment", "level": 64, "unit": "kt" } ],
"payout_function": {
"type": "step",
"points": [ { "level": 64, "payout_ratio": 1 } ]
}
},
"data_sources": [
{ "id": "hurdat2-atlantic", "kind": "storm_track_set", "version": "2026-09-02" }
]
},
"period": {
"inception": "2026-06-01T00:00:00-05:00",
"expiry": "2027-06-01T00:00:00-05:00",
"timezone": "America/Chicago"
}
}
JSON
price_it track-gate.json
echo "done: three shapes, three models, three technical prices"Both prices came back with their complete assumption set attached; a technical price, from nothing reads that response field by field. What the two objects above show is only where the floor is.
Field by field
trigger
| Member | Required | What it is |
|---|---|---|
type |
always | parametric_index, parametric_cat_in_a_box or wording. |
index |
on every parametric shape | The index definition. Below. |
data_sources |
on every parametric shape | The pinned feeds the index reads. Exactly one, for every pricing model. |
wording_ref |
on the wording shape only |
The indemnity-style branch. Exactly one of index+data_sources and wording_ref may be present; the schema refuses an object carrying both. |
trigger.index
| Member | Required | What it is |
|---|---|---|
name |
schema | The index's name. Travels onto the price response so a number can be traced to the index that produced it. |
version |
schema | The index version, likewise. An index is versioned code: change what it measures and you have a new version, not an edit. |
description |
optional | Free text for humans. |
measurement.variable |
schema, and checked by the model | The measured quantity, named exactly as the feed publishes it: catalogue_preferred_magnitude, daily_precipitation_mm, daily_maximum_wind_speed_mph, track_maximum_wind_kt. Neither the publisher's own column name nor the English in the feed description. What each pinned feed publishes is the per-feed list. |
measurement.unit |
schema, and checked by the station-index and track-gate models | The unit the level is written in. It must be the unit the feed publishes that variable in; a disagreement is refused rather than converted. |
measurement.statistic |
required by the station-index and track-gate models, unused by the cat-in-a-box model | sum (rolling sum), peak (rolling maximum) or count (threshold day count) over a daily series; max (the season's highest in-gate wind, the one statistic the track-gate model computes) over a track archive. It names the transform applied to the measured record. |
aggregation_window.duration |
schema, all three models | ISO 8601. The cat-in-a-box model reads any duration and uses it as the occurrence-clustering window, measured from the first event of a cluster. PT72H is the usual one. The station-index model reads whole days only (P1D, P3D, P30D), because a daily series has no finer resolution and it will not pretend otherwise. The track-gate model reads exactly P1Y: a track structure pays at most once per season of the archive, and a finer window would need an occurrence rule it does not have. |
aggregation_window.alignment |
optional | event, rolling or calendar_day. The station-index model measures rolling windows, so it refuses any value other than rolling; the track-gate model's seasons are the archive's own buckets, so it refuses any value other than calendar_day. Leave it out and each model gives you its own behavior. |
thresholds[] |
schema (at least one), and the attachment label is required by the track-gate model |
label, level, unit. The labels the models act on are attachment (the track-gate model requires it at the ladder's lowest level, and refuses a ladder in any unit but the archive's), max_focal_depth (cat-in-a-box: events deeper than this are excluded, and no threshold means no depth filter) and day_threshold (station index, count statistic only: the level a day has to reach to be counted). Any other label is documentation. |
payout_function.type |
schema | binary, linear, piecewise_linear or step in the schema; all four pricing models evaluate step only and refuse the others by name. |
payout_function.points[] |
required by every model | The ladder: level and payout_ratio (0 to 1) per rung, in any order (they are sorted by level). The lowest level is where the structure attaches. A single rung is a valid ladder. |
payout_function.maximum_payout_ratio |
optional | A ceiling on the ratio, defaulting to 1. |
trigger.data_sources[]
| Member | Required | What it is |
|---|---|---|
id |
schema, every model | The feed id, exactly as the pinned data feeds and GET /primitives spell it. |
version |
schema, every model | The pinned version. A price replays byte for byte, so the version has to pre-exist in the build; nothing is fetched at price time. |
kind |
required by the station-index model (station_network) and the track-gate model (storm_track_set), not read by the cat-in-a-box model |
The data-source vocabulary term. It decides which reader parses the publisher's bytes (and, between the two parametric_index models, which model is asked at all), so it is checked against the feed's own manifest kind before a byte is read. |
name, vintage |
optional | Human labels. The price response carries the publisher's own name, license, vintage and the sha256 of the snapshot regardless of what you write here. |
Where two declarations have to agree
Most refusals are not a missing member. They are two members of your object saying different things. The models check each of these before reading a byte, because a plausible number from a model that does not fit the structure is worse than no number at all: nothing downstream can tell the difference.
- The attachment and the ladder.
financial_structure.attachment.valuemust equal the lowestpayout_function.points[].level. If they differ the price is refused rather than one of them being chosen. - The measurement and the feed.
measurement.variablemust be one the named feed publishes, andmeasurement.unitmust be the unit it publishes it in. - The trigger type and the model. A
parametric_indextrigger is never read by the cat-in-a-box model, and the other way round. Withinparametric_index, the declared source kind decides between the station model and the track model. The shapes look alike in JSON, which is exactly why each model re-checks what it was handed. - The peril and the feed.
weather_stationprices over rainfall, wind or a river gauge;floodprices over a gauge feed only;tropical_cycloneprices over a wind feed (or, as a track gate, over astorm_track_setarchive) and is refused over a rainfall one, because Craton cannot attribute rain to a named storm. - One feed, not several. All four models read exactly one pinned data source. Combining feeds is a later model, not an averaging any of them will do silently.
Three near misses, and what the refusal says about them
The tables above are the shape. These are the three ways a reader who wrote the block from memory rather than copying it gets a 422 invalid_risk_object from POST /objects or PATCH /objects/{id}:
| What was written | Why it is refused | What to write |
|---|---|---|
data_sources inside trigger.index |
trigger.index takes the members in the table above and no others. The feeds are a member of trigger itself. |
Move the array out one level, beside index. |
aggregation_window_days: 3 |
There is no such member. The window is an object, so that it can carry its alignment too. | "aggregation_window": { "duration": "P3D" } (ISO 8601), and alignment if you need it. |
payout_function as an array of rungs |
The ladder is one member of the payout function, which also names the function's type. | "payout_function": { "type": "step", "points": [ … ] }. |
None of the three is invented by this page to be knocked down: each is what the refusal now tells you by itself. It names the block that would not take the member, where the schema keeps that member instead, the real name when yours was close to one, and the kind a member takes when it arrived as another. Then it ends by pointing back here. If you are reading a refusal that does none of that, it came from an older build.
The wording branch
trigger.type may also be wording, for indemnity-style structures: a wording_ref pointing at versioned text with clause-level metadata, and no index and no data sources. Craton stores and versions wordings; it never executes them as contracts, and no pricing model reads them. A wording trigger sent to POST /objects/{id}/price is refused by name, saying which trigger types the model does read.
When it does not work
Every one of these arrives as a failed job carrying 422 unpriceable_object, with the message shown. The error reference is the full list of refusal shapes.
| The message says | What to change |
|---|---|
trigger is required to price this structure and is absent |
The object has no trigger block. PATCH /objects/{id} one on. |
index is required to price this structure and is absent |
The trigger has a type but no index. Both parametric shapes need one. |
reads … triggers only; this structure's trigger is … |
The trigger type is not one this model prices: a wording trigger, or an empty type. |
Price v0 evaluates step payout functions only |
payout_function.type is binary, linear or piecewise_linear. Write the same shape as a step ladder. |
a step payout function needs at least one point |
payout_function.points is absent or empty. The schema allows it; the models need a ladder to read. |
the v0 parametric model measures catalogue_preferred_magnitude; this index measures … |
The cat-in-a-box model was asked to read a variable it does not compute. Check the variable against the feed. |
index.measurement.statistic is … ; this model computes count, peak, sum |
The station-index model needs a statistic it has a transform for. |
trigger.data_sources[0] declares kind …; the station-index model reads station_network sources |
Add "kind": "station_network" to the data source. |
no pinned snapshot |
The id or the version is not one Craton pins. The message lists every pinned id@version. |
financial_structure.attachment is … but the payout function first pays at … |
The two disagree about where the structure attaches. |
a cat-in-a-box structure is priced from peril.region.bounding_geometry, which this object does not carry |
Add the region polygon: single ring, axis-aligned, closed. |
no year of the pinned record would have paid |
The ladder sits above everything the feed recorded in forty years. 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. |
The track-gate model's refusals (the season window, the required attachment threshold, the kt unit, the gate) are tabulated with the object they refuse on the track-gate structure.
Where to go next
- A technical price, from nothing: the same trigger inside a full object, and every field of the price response.
- The pinned data feeds: every feed id, version, variable, unit and period of record, and what each one is and is not.
- The track-gate structure: the third shape end to end: a gate over the Gulf, 175 seasons of best track, and what a best-track burn is not.
- Backtest a structure: the same trigger run year by year over the forty-year record.
- Compose a model: the registry's no-code route to the same primitives: a feed, a measurement, a transform, a payout function.
The example on this page is extracted verbatim and executed against a live environment on every change to Craton. If the minimum documented here stopped being the minimum, the build would stop too.