This page as Markdown, byte for byte: /monitoring.md
Monitor: record a structure in force, then read the measurement#
POST /structures/{id}/monitoring records that a structure is in force. GET /monitoring and GET /monitoring/{id} read the measurement back, and recompute it every time you ask. POST /monitoring/{id}/webhooks has the measurement delivered to an address of yours when it moves, and GET /monitoring/{id}/webhooks shows you every delivery that was attempted.
Two halves, and they are not the same kind of thing. Recording is your assertion. You are telling Craton that a structure it holds is in force under a transaction executed entirely outside Craton — Craton did not arrange it, execute it or witness it, holds no document evidencing it, is not a party to it and is not an intermediary in it. Craton stores what you said, attributed to you, and that is the whole of it. A read is a measurement. Craton reads the structure's own index against the pinned public record, and reports the level, the levels it is read against, and the distance between them.
What a read is not, stated once and then carried in every response:
- not a determination. When a level is reached, the response says the level was reached. It says nothing about what follows from that, about anybody's obligations under any contract, or about any amount of money. There is no amount of money anywhere in a monitoring response.
- not a notice under anybody's contract. Craton is not the calculation agent of record for anything and has no mechanism to become one. A webhook delivers the difference between two readings of your index — see [deliver it, when it moves](#deliver-it-when-it-moves) — and Craton sends no claim notice, no settlement notice and no calculation-agent notice, to you or to anybody else.
- not a live sensor feed. Every feed in this build is a pinned, digest- addressed snapshot and no evaluation reaches the network, so "live" means the newest pinned vintage of the feed your trigger names — see [the vintage a read uses](#the-vintage-a-read-uses).
Read the quickstart first for the two values you export (CEDE_BASE_URL, CEDE_API_KEY) and how to get a key from POST /signup. Backtest a structure is the page that gets you a structure to record; this one repeats the object it uses so the walk below stands alone.
The vintage a read uses#
A price and a backtest read the feed version your trigger pins. That is what makes them replay byte for byte, years later, into the same digits.
Monitoring cannot work that way and be worth reading twice, so it reads the newest pinned vintage of the same feed and tells you, in every response, which vintage it used, which one your structure pins, and whether they are the same:
"vintage_selection": {
"pinned_version": "2026-08-10",
"read_version": "2026-08-10",
"is_the_pinned_version": true,
"rule": "An observation reads the newest pinned vintage of the feed this structure's trigger names…"
}
Nothing about your structure changes when a newer vintage lands. Your price and your backtest still read the version you pinned; your next monitoring read moves to the newer one and says that it moved.
The days a read measures#
The index is measured over the overlap of your cover period with the snapshot's period of record, and the response reports that window: measured_from, measured_to, record_ends, days_measured, days_not_yet_measured.
Neither end of that is negotiable. Measuring past the end of a snapshot would report a level computed from whichever days happened to be inside the file. Measuring the whole term as though it had elapsed would report a level for days that have not happened.
So a term that is entirely ahead of the record — which, with the feeds this build pins today, is any term in 2026 or later — reads back as "index_value": null with an unmeasured_reason naming both windows. Null is not zero. A level of zero is a measurement; an absence is not, and a client that read one as the other would report a quiet term when what it had was no data. The published schema types the field ["number", "null"] for exactly this reason. The walk below shows both answers, one after the other.
Deliver it, when it moves#
POST /monitoring/{id}/webhooks registers an address. Craton POSTs to it when the measurement of that record moves, and never otherwise.
Four fields decide that, and nothing else does: the feed id, the vintage read, the index value, and the distance to attachment. Those four are the whole of what a delivery says — the payload carries them as they are now, the same four as they were, and which of them changed. It carries no attachment flag, no payout ratio, no limit and no amount of money in any currency. If you need to know whether the attachment was reached, read distance_to_attachment: zero or negative is at or past it. Craton states the distance; what follows from it is yours to decide, outside Craton, exactly as it was when you executed the transaction.
Every measurement on these two routes — the baseline in the 201 body, each entry of the list, and both readings inside a delivery — names the model that computed it, name and version. Every result Craton produces carries what produced it, and a level is a result wherever you read it. The label is not one of the four compared fields: a model version that moves while all four hold produces the same level from newer code, so the label is brought up to date on your next read and nothing is delivered. Each reading carries its own, so previous_measurement is attributed to whatever computed it rather than to today's version.
Three things about this are limitations rather than features, and they are on the registration document itself so you never have to infer them:
- Registering never delivers. The registration is baselined on a measurement taken at that moment, which is in the 201 body. The first delivery it ever makes is a genuine change.
- A delivery arrives on your next read, not the instant a snapshot lands.
Every feed here is a pinned snapshot and there is no scheduler in this build,
so the comparison happens where the recomputation already happens: a read of
the record (
GET /monitoring/{id},GET /monitoring, or the webhook list itself). An account that never reads its own record is never delivered to.triggers.evaluated_onsays this in the response. - At least once, never exactly once. Every message carries a
notification_idthat is the same on every attempt of it — deduplicate on that. An attempt is one POST, no redirect followed, two-second timeout; a failure is retried on your next read, three attempts in all, after which the message isabandonedand stays in your delivery log with the reason it failed. Nothing of your response is stored or served beyond its status code and its size.
GET /monitoring/{id}/webhooks is where you read all of that back: every registration, the measurement it currently stands on, every notification it has created, and one delivery-log entry per attempt. That log is the point — a delivery that never arrived is visible there rather than silently absent.
The address must be http or https, must name a host, and must carry no credentials in it; anything else is refused at registration rather than dialled and logged as a failure. There is no request signing on this build: if you need to tell a Craton delivery from any other POST, answer on a path only you know and check the Cede-Notification-Id header.
Walk it#
The block records two structures: one written for a term the pinned catalogue covers in full, and one written for a term ahead of it. It prints the measurement for each, then reads the whole watch list back.
set -euo pipefail
: "${CEDE_BASE_URL:?export CEDE_BASE_URL first — see the quickstart}"
: "${CEDE_API_KEY:?export CEDE_API_KEY first — see the quickstart}"
auth="Authorization: Bearer $CEDE_API_KEY"
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 — A complete parametric object: exposure, the peril box, limit and
# attachment, the index that decides the payout, and the term. The term is
# what decides which days a measurement can see, so it is written twice
# below: once inside the pinned record, once ahead of it.
cat > in-force-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": 2000000000, "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": "2011-01-01T00:00:00+09:00",
"expiry": "2012-01-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 @in-force-object.json \
"$CEDE_BASE_URL/objects" | field id)
structure_id=$(curl -sS --fail-with-body -X POST -H "$auth" \
"$CEDE_BASE_URL/objects/$object_id/structures" | field id)
echo "structure inside the record: $structure_id"
# 2 — Record it as in force. The body is optional; `reference` is your own
# identifier for your own transaction, echoed back and read into nothing.
curl -sS --fail-with-body -X POST -H "$auth" \
-H "Content-Type: application/json" \
-d '{"reference": "your-own-reference-0001"}' \
"$CEDE_BASE_URL/structures/$structure_id/monitoring" > recorded.json
python3 -c 'import json
record = json.load(open("recorded.json"))
print("record :", record["id"], "-", record["status"])
print("asserted by:", record["in_force"]["asserted_by"])
print()
print(record["in_force"]["assertion"])
print()
observation = record["observation"]
model = observation["model"]
print("computed by:", model["name"], "version", model["version"])
feed = observation["feed"]
print("read from:", feed["id"], "vintage", feed["vintage"],
"(the version the structure pins:",
feed["vintage_selection"]["is_the_pinned_version"], ")")
window = observation["measurement_window"]
print("measured :", window.get("measured_from"), "to", window.get("measured_to"),
"-", window["days_measured"], "day(s) of",
window["cover_period"]["days"], "in the term")
print("index :", observation["index_value"], observation["index_unit"])
print("attaches :", observation["attachment_level"], observation["index_unit"])
print("distance :", observation["distance_to_attachment"])
print("reached :", observation["attachment_reached"])
for level in observation["trigger_levels"]:
print(" level %-5s reached=%-5s distance=%s"
% (level["level"], level["reached"], level["distance"]))
for occurrence in observation.get("occurrences_at_or_above_attachment", []):
print(" M%s on %s" % (occurrence["magnitude"], occurrence["time"]))
print()
print(observation["disclosure"])'
# 3 — The same object, written for a term ahead of the pinned record.
python3 -c 'import json
document = json.load(open("in-force-object.json"))
document["period"] = {"inception": "2026-04-01T00:00:00+09:00",
"expiry": "2027-04-01T00:00:00+09:00",
"timezone": "Asia/Tokyo"}
json.dump(document, open("ahead-object.json", "w"))'
ahead_object=$(curl -sS --fail-with-body \
-H "$auth" -H "Content-Type: application/json" \
--data-binary @ahead-object.json \
"$CEDE_BASE_URL/objects" | field id)
ahead_structure=$(curl -sS --fail-with-body -X POST -H "$auth" \
"$CEDE_BASE_URL/objects/$ahead_object/structures" | field id)
curl -sS --fail-with-body -X POST -H "$auth" \
"$CEDE_BASE_URL/structures/$ahead_structure/monitoring" > ahead.json
python3 -c 'import json
observation = json.load(open("ahead.json"))["observation"]
print()
print("a term ahead of the record:")
print("index :", observation["index_value"], "(null is not zero)")
print("distance :", observation["distance_to_attachment"])
print("why :", observation["unmeasured_reason"])'
# 4 — Everything this account watches, each measured again as it is listed.
curl -sS --fail-with-body -H "$auth" "$CEDE_BASE_URL/monitoring" > watching.json
python3 -c 'import json
page = json.load(open("watching.json"))
print()
print("watching:", page["count"], "structure(s)")
for record in page["monitoring"]:
observation = record["observation"]
print(" %s index=%-6s distance=%-6s read from %s@%s"
% (record["structure_id"], observation["index_value"],
observation["distance_to_attachment"],
observation["feed"]["id"], observation["feed"]["version"]))'
# 5 — One record at its own address. Measured again: the observed_at moves,
# and nothing about the measurement is served from storage.
address=$(field links.self < recorded.json)
curl -sS --fail-with-body -H "$auth" "$CEDE_BASE_URL$address" > again.json
python3 -c 'import json, sys
first = json.load(open("recorded.json"))["observation"]
again = json.load(open("again.json"))["observation"]
print()
print("first read :", first["observed_at"])
print("second read:", again["observed_at"])
print("same level :", first["index_value"] == again["index_value"])
sys.exit(0 if first["index_value"] == again["index_value"] else 1)'
# 6 — Have it delivered when it moves. The registration is baselined on a
# measurement taken right now, so this call never delivers: what you get
# back is where the record stands, and the first delivery this webhook
# ever makes is a genuine change.
monitoring_id=$(field id < recorded.json)
curl -sS --fail-with-body -X POST -H "$auth" \
-H "Content-Type: application/json" \
-d '{"url": "https://receiver.example.com/cede/monitoring"}' \
"$CEDE_BASE_URL/monitoring/$monitoring_id/webhooks" > webhook.json
python3 -c 'import json
registration = json.load(open("webhook.json"))
print()
print("webhook :", registration["id"], "-", registration["status"])
print("delivers to:", registration["url"])
print("compares :", ", ".join(registration["triggers"]["compared_fields"]))
measurement = registration["measurement"]
print("baseline :", measurement["feed_id"], "@", measurement["vintage"],
"index", measurement["index_value"], measurement["index_unit"],
"- distance", measurement["distance_to_attachment"])
print("computed by:", measurement["model"]["name"],
measurement["model"]["version"])
delivery = registration["delivery"]
print("guarantee:", delivery["guarantee"], "-", delivery["maximum_attempts"],
"attempt(s),", delivery["timeout_seconds"], "s each")
print()
print(registration["triggers"]["evaluated_on"])
print()
print(registration["disclosure"])'
# 7 — Every webhook on this record, each with its own delivery log. Nothing
# has moved since step 6, so nothing was created and nothing attempted —
# which is exactly what an empty log means, and why it is worth reading.
curl -sS --fail-with-body -H "$auth" \
"$CEDE_BASE_URL/monitoring/$monitoring_id/webhooks" > webhooks.json
python3 -c 'import json
page = json.load(open("webhooks.json"))
print()
print("registered:", page["count"], "webhook(s) on", page["monitoring_id"])
for registration in page["webhooks"]:
print(" %s -> %s" % (registration["id"], registration["url"]))
print(" notifications:", len(registration["notifications"]),
" delivery attempts:", len(registration["delivery_log"]))
for entry in registration["delivery_log"]:
print(" attempt %s of %s: %s (%s)"
% (entry["attempt"], entry["notification_id"],
entry["outcome"], entry.get("error",
entry.get("response_status"))))'
The last line exits non-zero if two reads of the same structure against the same vintage ever disagreed about the level. They cannot: the measurement is arithmetic over pinned bytes, and the only thing that can move it is a new vintage of the feed — which the response names.
Reading a record#
| Member | What it is |
|---|---|
status |
recorded, and that is the only value this build writes. It is the state of Craton's record, never a statement about anything outside Craton — there is no active and no triggered. |
in_force.asserted_by |
account_holder. The only truthful value: Craton has one source for the fact and it is you. |
in_force.assertion |
The fixed sentence saying what recording is and is not. The same words on every record. |
reference |
Your own string, echoed. Craton reads nothing into it and resolves it against nothing. |
observation |
The measurement, below. |
Reading an observation#
| Member | What it is |
|---|---|
model |
The name and version of the model that computed the level. Every result Craton produces is labelled with what produced it, and this is that label — the same pair a price or a backtest of the same structure carries, because it is the same reader. |
feed |
The one pinned snapshot the level was computed from: id, version, vintage, the SHA-256 of the exact bytes, the paths to fetch them, and vintage_selection. One snapshot per measurement, never a merge. |
index_value |
The index level over the measured window, in index_unit. null when no level could be measured, and then unmeasured_reason says which. |
attachment_level |
The level at which your payout function first pays. |
distance_to_attachment |
attachment_level minus index_value. Positive is short of the attachment, negative is past it; distance_basis says so in the response. |
attachment_reached |
Whether the measured level reached the attachment. A statement about a number, and about nothing else. |
trigger_levels |
Every level of your payout function, each with reached and distance. The payout_ratio on each is echoed from your own structure — it is not multiplied by anything here. |
measurement_window |
Which days were measured, which were not, and where the record ends. |
method |
How the level was computed, named and described, so you can reproduce it from the same snapshot. |
disclosure |
The fixed sentence: a measurement, not a determination. |
limitations |
What the measurement does not account for — snapshot vintages are revised, and the index is not the loss. |
A catalogue-based index also carries occurrences_considered, occurrences_at_or_above_attachment and largest_occurrence. The full selection behind them is the event catalogue's job, one call away.
Reading a webhook registration#
| Member | What it is |
|---|---|
url |
The address deliveries are POSTed to, echoed exactly as you registered it. |
status |
registered, and that is the only value this build writes. A state of Craton's record, never a statement about anything outside it. |
triggers.compared_fields |
The four fields a change in which creates a notification. Nothing outside this list is compared. |
triggers.evaluated_on |
Where the comparison happens — a read of the record — and therefore when a delivery can reach you. |
measurement |
The four fields this registration currently stands on: feed_id, vintage, index_value, distance_to_attachment — plus the index_unit they are read in and the model (name and version) that produced them, neither of which is compared. Every result Craton produces carries what produced it, and a level served here is a result like any other; if a model version moves while all four fields hold, the label is brought up to date on the next read and nothing is delivered, because nothing moved. |
delivery |
The promise: at_least_once, how many attempts one message gets, and how long each may take. |
notifications |
Every message this registration has created, oldest first, each pending, delivered or abandoned, with the four fields before and after and which of them changed. |
delivery_log |
One entry per attempt, newest first: the notification, the attempt number, the outcome, the receiver's status code if it answered, and the error if it did not. Capped at delivery_log_limit. |
disclosure |
The fixed sentence: a measurement that moved, delivered. Not a notice under any contract. |
Reading a notification#
This is the document Craton POSTs to your address, published as [monitoring-notification.v0.schema.json](/monitoring-notification.v0.schema.json) so you can validate it holding no Craton code.
| Member | What it is |
|---|---|
type |
measurement_changed, the only kind this build sends. The name is the whole of what it asserts. |
notification_id |
Stable across every attempt. Deduplicate on it — delivery is at least once. |
model |
The name and version of the model that computed the level in measurement. Every result Craton produces carries what produced it. |
changed |
Which of the four fields moved. |
measurement / previous_measurement |
The four fields now, and the four fields before, so a receiver holding nothing can still see the move. Each carries its own model: the earlier reading is attributed to whatever computed it, which need not be the version named at the top of the message. |
links.monitoring |
Where you read the full observation back — the window, the levels, the method and the limitations. The message carries the difference; the record carries the measurement in full. |
There is no attachment flag on a notification and no amount of money in one.
The response shapes are published as [monitoring.v0.schema.json](/monitoring.v0.schema.json), [monitoring-index.v0.schema.json](/monitoring-index.v0.schema.json), [monitoring-webhook.v0.schema.json](/monitoring-webhook.v0.schema.json) and [monitoring-webhook-index.v0.schema.json](/monitoring-webhook-index.v0.schema.json); the map from every route to the schema governing its responses is [api-contract.v0.json](/api-contract.v0.json).
When it refuses#
Every refusal is the standard error envelope (error reference).
| What happened | Answer |
|---|---|
| The structure id is not one of yours, or does not exist | 404 not_found. Another account's structure is not found, not forbidden: "it exists but is not yours" is a disclosure of its own. |
| The body carries a member this endpoint does not read | 422 invalid_request, naming the members it does read. A member answered as though it had been stored is a member you would believe Craton holds. |
reference is not a non-empty string, or is longer than 256 characters |
422 invalid_request. |
| The structure has no trigger, no term, names two data sources, or names a feed nothing is pinned for | 422 unmonitorable_structure, naming what is missing and — for a feed — what is pinned. |
| The monitoring id is not one of yours, or does not exist | 404 not_found. |
| A webhook body carries a member this endpoint does not read | 422 invalid_request. It reads url, and nothing else. |
url is missing, empty, longer than 2048 characters, names a scheme other than http or https, names no host, or carries credentials in it |
422 invalid_request, naming which. Refused at registration rather than dialled and logged as a failure. |
What is not here#
- Stopping. There is no way to stop watching a structure, or to remove a webhook registration, on this build. A registration whose receiver is gone abandons its messages and keeps saying so in the delivery log.
- A scheduler. Nothing recomputes in the background, so a delivery reaches you on your next read of the record rather than the instant a newer snapshot lands. See deliver it, when it moves.
- Request signing. Deliveries are not signed on this build. Answer on a
path only you know, and check the
Cede-Notification-Idheader. - A history of observations. Every read measures now; nothing keeps a series of past readings.
- Anything after the measurement. Craton reports what it measured. Everything that follows from a level being reached happens entirely outside Craton, as it did when the transaction was executed.