Get a free API key

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:

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:

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#