# Craton quickstart

Ten minutes, one terminal, no SDK. By the end you will have turned a location
schedule into a **canonical risk object** — the machine-readable primitive
everything else in Craton is built on — and read it back over the API.

Everything you need is on this page. Copy the block, run it, read the rest
afterwards if you like.

---

## Which perils this build prices today

Read this before you plan an integration, because it is the limit most likely
to stop one: **Craton's canonical object names eight perils and this build prices
three of them.** Ingest normalises a schedule carrying any of the eight, and the
schema is versioned and open and carries all eight. Price runs two parametric
models, and a structure outside both is refused by name — a job that ends
`failed` carrying `422 unpriceable_object` — rather than handed a number no
model stands behind.

| `peril.code` | Priced in this build | What a priced structure declares |
| --- | --- | --- |
| `earthquake` | **yes** | a `parametric_cat_in_a_box` trigger over a pinned quake catalogue |
| `weather_station` | **yes** | a `parametric_index` trigger over a pinned daily rainfall or wind series |
| `tropical_cyclone` | **yes**, on a wind index | a `parametric_index` trigger over a pinned daily **wind** series. The same peril written on a rainfall series is refused: this build cannot attribute rain to a named storm |
| `flood` | **no** | this build has no pricing model for it |
| `wildfire` | **no** | this build has no pricing model for it |
| `severe_convective_storm` | **no** | this build has no pricing model for it |
| `cloud_outage` | **no** | this build has no pricing model for it |
| `grid_outage` | **no** | this build has no pricing model for it |

A **no** is a missing model, not a missing route. Ingest reads a wildfire
schedule, `POST /objects` stores a flood object — both are valid canonical
objects — and `POST /accumulation` rolls a set of any perils up, naming for each
one the pinned vintage a hazard question about it would be answered against, or
the absence of one. What a **no** costs you is the technical price and the
backtest under it, and the hazard readings behind them: `GET /hazard` answers
point lookups for `earthquake`, `tropical_cyclone` and `weather_station`, and
names the peril it holds no pinned feed for rather than guessing at it.

**A "yes" prices only where the data is pinned.** Both models read a feed
vintage this build already holds, because a result that replays byte for byte
cannot fetch anything at price time — and every pinned feed is one box or one
grid point: a quake catalogue over a single region, daily rainfall and wind
series at single points. [The pinned data feeds](feeds.md) is the list, and
`GET /feeds` is that same list from the deployment you are pointed at. A risk
somewhere the pinned feeds do not reach cannot be priced here yet, whatever its
peril.

Both models, what each one reads, and how each refusal reads, are on
[A technical price, from nothing](price.md). None of this is a limit of the
canonical object: an object for any of the eight perils is valid and storable
today, and the peril vocabulary grows only by a schema version bump.

---

## What you need

* `curl` 7.76 or newer (this page uses `--fail-with-body`) and `python3`.
  Nothing else — no SDK, no package to install.
* `CEDE_BASE_URL` — the base URL of the Craton environment you are pointed at.
* An API key for that environment — **and you do not need one in advance.**
  If `CEDE_API_KEY` is unset, the block below signs you up and issues one, in
  the same run, before it does anything that needs it.

Craton is pre-release. The environment you can reach today is **staging**:

```sh
export CEDE_BASE_URL="https://altier.ridgehead-hamlet.ts.net:8472"   # no trailing slash
export CEDE_API_KEY="…"              # optional: leave it unset and sign up below
```

Staging is reachable to members of the operator's tailnet, not to the open
internet — a public sandbox on its own name is waiting on a domain. If you
cannot reach the URL above, ask whoever pointed you at this page for access,
or point `CEDE_BASE_URL` at any environment someone runs for you. Nothing else
on this page changes either way: the quickstart only ever speaks HTTP to
`$CEDE_BASE_URL`, so a sandbox on someone's desk, staging, and the hosted
environment to come all behave identically from here.

### Getting a key: `POST /signup`

Signing up is self-service and takes one unauthenticated call. No invitation,
no approval queue, no email round trip, no payment details — nothing on this
path waits for a human, here or at our end. Creating an account means you
agree to the [terms of service](terms.md) and the
[acceptable-use policy](acceptable-use.md); both are short and both are
served on this site.

```sh
curl -sS --fail-with-body \
  -H "Content-Type: application/json" \
  -d '{"label": "quickstart"}' \
  "$CEDE_BASE_URL/signup"
```

`201` and an account on the free sandbox plan, holding its first key:

```json
{
  "schema_version": "0.1.0",
  "id": "acct_9f4c1e0b7a2d5c38",
  "created_at": "2026-08-11T02:33:54.512907Z",
  "plan": "sandbox",
  "label": "quickstart",
  "api_key": {
    "id": "key_3b71c0d94e26af58",
    "secret": "cede_sk_…",
    "created_at": "2026-08-11T02:33:54.512907Z",
    "scheme": "Bearer",
    "expires_at": null
  }
}
```

`api_key.secret` is the value you export as `CEDE_API_KEY`. **Keep it when it
prints.** Craton stores only its digest, so that response is the one and only
place the secret ever exists on our side: no later call returns it, nobody can
look it up for you, and a lost secret means signing up again. The body is
optional — `curl -X POST "$CEDE_BASE_URL/signup"` with nothing in it works
identically; `label` is just a name you will recognise the account by.

**How long the key lasts: indefinitely.** `expires_at` is `null` and that is
the only value this build issues — there is no time-to-live, no session, no
refresh call and no age at which a key stops working. Write it into your
environment file and leave it there. Two consequences worth having in advance,
because they are what a `401` on a key that worked five minutes ago actually
means:

* A key survives a redeploy of the environment that issued it. Staging redeploys
  whenever the code moves, and keys, accounts, objects, jobs and usage are held
  in a database across it.
* So a `401` is about the *credential that arrived*, not about time. The usual
  cause is a shell variable that lost its value between one command and the
  next — `-H "Authorization: Bearer $CEDE_API_KEY"` with `CEDE_API_KEY` unset
  sends the scheme and no key, and the response says so in those words.

Your key goes in an `Authorization: Bearer …` header on every call except
`GET /health` and `POST /signup` — the two routes that answer without one,
because one tells you the environment is up and the other is how you get a key
at all. There is no other authentication scheme and no unauthenticated data
route.

---

## The whole thing, in one block

Run this in an empty directory. It signs you up if you have no key yet, writes
one CSV, sends it, waits for the job, and prints the risk object that came
back. `CEDE_BASE_URL` is the only thing it needs from you.

<!-- cede:runnable -->

```bash
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 — Is this environment serving? /health answers without a credential.
curl -sS --fail-with-body "$CEDE_BASE_URL/health" | field status

# 2 — A key, if you do not have one. /signup is the other route that needs no
#     credential: it creates an account on the free sandbox plan and issues its
#     first key, with nobody in the loop. The secret is shown here and nowhere
#     else, so save what this prints if you want to reuse the account.
if [ -z "${CEDE_API_KEY:-}" ]; then
  account=$(curl -sS --fail-with-body \
    -H "Content-Type: application/json" \
    -d '{"label": "quickstart"}' \
    "$CEDE_BASE_URL/signup")
  CEDE_API_KEY=$(printf '%s' "$account" | field api_key.secret)
  export CEDE_API_KEY
  printf 'signed up: %s on the %s plan\n' \
    "$(printf '%s' "$account" | field id)" \
    "$(printf '%s' "$account" | field plan)"
  printf 'your key (shown once): %s\n' "$CEDE_API_KEY"
fi

# 3 — A location schedule. Two rows, written here so you need no file from
#     anywhere else. Craton reads far uglier spreadsheets than this one.
cat > schedule.csv <<'CSV'
Location ID,Address,Latitude,Longitude,Occupancy,Construction,Year Built,Building,Contents,Peril
LOC-0001,"2-16-1 Konan, Minato-ku, Tokyo",35.6284,139.7387,Warehouse,Reinforced Concrete,2011,"¥8,400,000,000","¥2,600,000,000",Earthquake
LOC-0002,"1-1 Soga, Chuo-ku, Chiba",35.5687,140.1247,Light Industrial,Steel Frame,1998,"¥5,100,000,000","¥900,000,000",Earthquake
CSV

# 4 — Ingest it. First call that needs your key. It answers 202 and a job:
#     every Craton verb answers with a job, never with the answer itself.
job_id=$(curl -sS --fail-with-body \
  -H "Authorization: Bearer $CEDE_API_KEY" \
  -F "file=@schedule.csv;type=text/csv" \
  "$CEDE_BASE_URL/ingest" | field id)
echo "ingest job: $job_id"

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

# 6 — Read back the canonical risk object the file became.
object_path=$(printf '%s' "$job" | field result.objects.0.links.self)
curl -sS --fail-with-body \
  -H "Authorization: Bearer $CEDE_API_KEY" \
  "$CEDE_BASE_URL$object_path" > object.json

python3 -m json.tool object.json
echo "done — your first risk object is at $object_path"
```

If the last line printed, you are through. If something went wrong, jump to
[When it does not work](#when-it-does-not-work).

---

## What just happened

**`GET /health`** — reports the build that is running, which verbs it
serves, and — in its `discovery` member — the addresses that list what it
holds: `GET /feeds` for the pinned data snapshots and `GET /primitives` for the
model builder's vocabulary. So a load balancer, a deploy script or a nervous
human can ask "is this thing up?" without holding a credential, and a client
can find where to start without reading this page.

**`POST /signup`** — the way in, and the other route served without a key,
because a route that needed a key to get a key would need a human to break the
circle. It creates an account on the free sandbox plan and issues that
account's first key, live for the very next request — no restart, no
propagation delay, no waiting. Objects, jobs and usage belong to the account,
so a second signup is a second tenant that cannot see the first one's work.
`GET /account` reads back the account your key belongs to; it never returns
the secret, because Craton does not have it.

**`POST /ingest`** — file in, job out (`202 Accepted`, with the job's address
in the `Location` header). Ingest normalises: it decodes the file, works out
which column meant what, parses money and coordinates, and emits canonical
risk objects. It does not enrich, guess quietly, or discard what it could not
read — see `source_fidelity` below.

The request is `multipart/form-data` with the file in a part named `file` —
that is what `-F "file=@schedule.csv;type=text/csv"` builds above, and posting
the file as the whole request body answers `415` saying so. What the *part*
may declare is the next table; it is checked rather than believed, and
anything else answers `415 unsupported_media_type` before a job exists.

| The part declares | What happens |
|---|---|
| any `text/*` — `text/csv`, `text/plain`, `text/tab-separated-values` | read; recorded on the job as declared, without its `charset` parameter |
| `application/json`, `application/x-ndjson` (and the `application/csv`, `application/jsonl`, `application/ndjson`, `application/x-csv`, `application/x-jsonlines` spellings) | the same |
| `application/octet-stream`, or no part header at all | read; **nothing recorded** — this is your client saying it could not place the file's extension, not a claim about the file, and the job does not restate it as one |
| anything else — `image/png`, `application/pdf`, `application/zip`, a spreadsheet's binary container | `415 unsupported_media_type`, naming what it refused and what to send |

The form takes one other part, and it is optional: **`currency`**, an ISO 4217
code such as `USD`. Send it only for a schedule that names no currency of its
own — no `USD` in a column header, no `$` in any cell. Ingest never infers a
currency (reading USD off a `State` column saying `FL` is enrichment, which is
Analyse's job and not this one), so without this part such a file's amounts
come back untagged and `ambiguous_rows` says so. With it, `exposure.currency`
and every value carry the code you gave, and `guessed_units` records that it
came from your request rather than from the file — a difference a reader of
the object should be able to see. If the file *does* state a currency and it
is not the one you sent, the job fails with `422 currency_conflict` naming
both: Ingest will not overwrite the file, and will not ignore you. The job
echoes what you declared back at `subject.declared_currency`, so a client
holding only the job id can still say what it sent — the same reason
`subject.source` is there.

```sh
curl -sS --fail-with-body -H "$auth" "$CEDE_BASE_URL/ingest" \
  -F "file=@schedule.csv;type=text/csv" -F "currency=USD"
```

Ingest v0 reads delimited text (comma, semicolon, tab, pipe) and JSON Lines,
so those four rows are the whole of it. A declared type is a *claim*, never a
finding: the media type the reader actually read the bytes as is written onto
the object it produced, at `provenance.source_files[].media_type`, and a file
whose declaration was plausible but whose bytes are not readable is refused on
the job with `unsupported_source`.

#### A `202` is not a statement that your file is readable

`POST /ingest` answers `202` for every request it can accept **at the door** —
and that includes a file it will turn out to be unable to read. Two kinds of
fault reach this route, and one rule decides where each is answered: *can it be
known without reading the file?*

| Fault | Where you see it | What arrives |
| --- | --- | --- |
| No key, or one this environment does not hold | the response to `POST /ingest` | `401 unauthenticated` |
| The request body is not `multipart/form-data` | the response | `415 unsupported_media_type` |
| It is a form, but carries no part named `file` | the response | `422 invalid_request` |
| The file part declares a type Ingest does not read | the response | `415 unsupported_media_type` |
| The `currency` part is not three letters | the response | `422 invalid_request` |
| The file is over 32 MB | the response | `413 payload_too_large` |
| The account cancelled its plan | the response | `402 account_cancelled` |
| The bytes will not decode, or are not delimited text or JSON Lines at all | the **job** | `failed`, with `415 unsupported_source` in `error` |
| The file states a currency the `currency` part contradicts | the **job** | `failed`, with `422 currency_conflict` in `error` |
| A row, a column or a unit Craton could not read confidently | the **job** | `succeeded`, with the detail in `exposure.source_fidelity` |

Everything in the top half is a fact about the *request*, knowable before a
single byte of the file is looked at, so it is answered before a job exists.
Everything in the bottom half is a fact about the *bytes*, and finding it out
**is** the ingest work — decoding the file, looking for a delimiter, finding the
header under the preamble. `POST /ingest` never does that work, so it has no
answer to give.

The consequence, stated plainly because it surprises people: post a file of
prose (`This is not CSV`) declaring `Content-Type: text/csv`, and you get `202`
and a job id, not a `4xx`. Poll the job and it is `failed`, carrying
`415 unsupported_source` and a message saying no delimiter occurs anywhere in
the file. The status is the same one a synchronous refusal would have carried,
in the same shape — it just arrives on the job, because that is where it was
discovered. **Always poll the job. A `202` means "accepted for reading", never
"read".**

**`GET /jobs/{id}`** — every verb runs as a job and moves through
`queued → running → succeeded | failed`. Ingest is fast, so you will usually
see `succeeded` on the first poll; write your client against the states
anyway, because a verb that reads forty years of history will not answer on
the first one. A job that fails carries an `error` object in the same shape a
synchronous refusal would have had — a failure is a job state, not a dropped
connection.

**`GET /objects/{id}`** — the canonical risk object. Objects and jobs belong to
the key that created them; another key gets a `404`, not a `403`, because the
existence of your objects is itself yours.

### The parts of the object worth looking at first

```
exposure.locations[]      each row, normalised: ref, address as given,
                          decimal coordinates, occupancy, construction,
                          year built, and values per coverage part
exposure.values           the schedule's totals, summed per coverage part
exposure.currency         read from the money it actually parsed, or the
                          code you declared on the request for a file that
                          states none. Absent when neither happened — the
                          amounts are still there, just untagged, and
                          ambiguous_rows says so
peril.code                from the closed peril vocabulary — "Earthquake"
                          in the file became "earthquake" here
provenance                the source file's name, its sha256, and the id of
                          the ingest run that produced this object
status                    "draft": exposure and peril, no structure yet
```

**`exposure.source_fidelity` is the part to read second, and the reason to
trust the first.** It is Craton's honesty channel, and it is always present:

* `unmapped_columns` — columns Craton did not recognise, each with a sample of
  the values it left behind. Empty in the run above; add a column called
  `Notes` to `schedule.csv` and re-run to watch it fill.
* `guessed_units` — every place a unit was inferred rather than stated.
* `ambiguous_rows` — rows that could be read more than one way.

Nothing is dropped silently. If a column did not make it into the object, it is
named here.

---

## Where to go next

* **Finish the object.** `PATCH /objects/{id}` writes onto the object you just
  ingested the things the file could not tell Craton: which **peril** the cover
  is against, the **limit** and **attachment**, the **trigger** that decides
  the payout, and the **period**. Ingest emits exposure — and a peril only when
  the file named one in words, as the schedule above did — because
  normalisation is all it promises (SPEC §3.1); everything else is a decision
  you make after reading the file, and this is where you record it. Send only
  the blocks you are writing; each one replaces the block of that name whole,
  so a `financial_structure` arrives complete rather than merged into a
  half-remembered one. It writes those six blocks and refuses the rest: the id
  and schema version are the server's, `status` is what the object has rather
  than what you assert, `provenance` only ever grows, and `exposure` is Ingest's
  record of a file it read — a different exposure is a different object.
  *What `PATCH /objects/{id}` writes, and what it does not* on
  [the price page](price.md) is the full table, including where each read-only
  block does come from and what to do when the file did not state the
  currency. If the
  object had already been priced and you move something the price was computed
  from, the old premium comes off and the object goes back to `draft`, because
  a premium under a limit it was never computed for is a wrong number that
  reads exactly like a right one. Price it again; it is one call.
* **Price.** `POST /objects/{id}/price` computes a **technical price** for an
  object, with its assumptions attached and the model and version that produced
  it named on the result. It needs an object that carries a financial structure
  (limit, attachment) and a trigger — so either patch them onto the object you
  ingested, as above, or send a complete object with `POST /objects`. That is
  the third runnable page: [A technical price, from nothing](price.md) signs up
  for an account of its own, reads it back, posts a complete parametric object
  and prints the price with the whole assumption set that produced it — and its
  second block walks the other way in, from a schedule through `PATCH
  /objects/{id}` to a price on the object the file became. The trigger is the
  one part of that object no file can carry and Craton will not invent, so it has
  a reference page of its own: [The trigger object](trigger.md) is every member
  of the block field by field — what the schema requires against what the model
  pricing it requires — and the smallest trigger of each shape that prices.
* **Structure and Backtest.** There is a second runnable page for these two:
  [Backtest a structure](backtest.md), which builds a complete parametric
  object, cuts a structure from it and replays it against forty years of the
  pinned public archive — and whose second block walks the same road from a
  schedule you were given: ingest it, `PATCH /objects/{id}` the peril, limit,
  attachment, index and term onto the object the file became, cut the
  structure from that, and replay. In short: `POST /objects/{id}/structures` cuts a candidate
  structure from an object — the trigger, limit, attachment and term a verb
  evaluates — and answers with a structure of its own that has an id. Send no
  body to take the object's own terms, or send the blocks you are varying
  (`trigger`, `financial_structure`, `period`) to make a variant.
  `POST /structures/{id}/backtest` then replays it against the pinned public
  archive and answers with a job like every other verb; the finished job
  carries the artifact, and `GET /backtests/{id}` serves it again at the
  address in `links.self`.

  The artifact is year by year: every calendar year in the window with what the
  structure would have paid, the occurrences behind it named by the publisher's
  own event ids, loss statistics and a burning-cost view, and a
  `data_coverage` block naming every year the pinned archive holds no record
  for — disclosed, never filled in. It is history, not a forecast, and it says
  so on every copy. Send `{"window": {"from_year": 1996, "to_year": 2015}}` to
  narrow it; the default is the archive's full period of record, and a window
  reaching outside what the archive holds is refused rather than quietly
  clipped.

  Backtest and Price read a structure through the same code, so the burn rate
  in a backtest is the burn rate underneath the technical price of the same
  structure — not a second number that happens to look similar.

  Two halves of Structure (SPEC §3.4) are here: creating one and reading it
  back. Revisions, standalone indices and wordings are outside this build, so
  `GET /health` lists `structure` because those two routes answer — route
  existence, not a finished verb — and `backtest`, whose routes are all here,
  on the same rule.
* **Compose a model.** [Composing a model](model-builder.md) is the same shape
  of walkthrough for the model builder: pick a feed, a measurement, a transform
  and a payout function, and Craton validates, backtests and cards the model
  before it exists. No code — a v1 model is those four primitives and nothing
  else. It ends with a technical price produced by the model you composed.
* **The schema.** The canonical risk object is published as JSON Schema and is
  open source. Validate against it locally, generate types from it, or diff two
  versions of an object — it is a file, not an API call.

---

## The whole surface, and where it stops

Craton's specification names seven verbs. **This build answers on all seven** —
Ingest, which you just used, Analyse, Price, Structure, Backtest, Monitor and
Package — each on its first slice and no wider, and the sections after the
table say where each one stops. Rather than let you find the edge by hitting
it, here is the entire route table this build answers on:

| Route | What it does | Key needed |
| --- | --- | --- |
| `GET /health` | Is this environment up, which verbs does it serve, and where are its catalogues — `discovery` names `GET /feeds` and `GET /primitives` | no |
| `POST /signup` | Create an account, get its first key | no |
| `GET /account` | The account your key belongs to | yes |
| `POST /ingest` | A file in, a job out (Ingest) | yes |
| `GET /jobs/{id}` | The state and result of any job | yes |
| `POST /objects` | Store a complete canonical object sent as JSON | yes |
| `GET /objects/{id}` | Read a canonical object back — and `PATCH /objects/{id}` writes onto it what the file could not say: peril, limit and attachment, trigger, term. It writes those six blocks and no others; `exposure`, `id`, `schema_version`, `status` and `provenance` are read-only there, and [the price page](price.md) says where each comes from | yes |
| `POST /objects/{id}/analyse` | Run named analyses over an object and record them on its provenance, as a job (Analyse) | yes |
| `POST /objects/{id}/price` | A technical price, as a job (Price) | yes |
| `POST /objects/{id}/structures` | Cut a candidate structure from an object (Structure) | yes |
| `GET /structures/{id}` | Read a structure back | yes |
| `POST /structures/{id}/backtest` | Replay a structure over the pinned archive, as a job (Backtest) | yes |
| `GET /backtests/{id}` | Read a finished backtest artifact again | yes |
| `POST /structures/{id}/monitoring` | Record a structure as in force — your assertion about a transaction executed entirely outside Craton — and measure its index (Monitor) | yes |
| `GET /monitoring` | Every structure this account has recorded, each measured again as it is listed | yes |
| `GET /monitoring/{id}` | One recorded structure, measured now against the newest pinned snapshot | yes |
| `POST /monitoring/{id}/webhooks` | Have the measurement delivered to an address of yours when it moves — the feed, the vintage, the index value and the distance to attachment, and nothing that follows from them — and `GET /monitoring/{id}/webhooks` reads every webhook on the record back with its delivery log, so a delivery that never arrived is visible rather than silently absent | yes |
| `GET /hazard` | What the pinned public record holds at one point — takes `peril`, `lat` and `lon` in the query (Analyse) | yes |
| `GET /events/{peril}` | The pinned event catalogue for a peril, filtered the way a structure filters it (Analyse) | yes |
| `GET /events/{peril}/{event_id}/footprint` | One event's footprint as the record has it: the occurrence it belongs to | yes |
| `POST /accumulation` | Roll a set of your own objects up: exposure per peril, per declared region, and where it is concentrated (Analyse) | yes |
| `GET /feeds` | Every pinned data snapshot this environment holds, with the digest of the exact bytes | yes |
| `GET /feeds/{feed_id}/versions/{version}` | One version's manifest as committed: publisher, coverage, licence, aggregation level | yes |
| `GET /feeds/{feed_id}/versions/{version}/data` | The publisher's response, verbatim, with the pinned digest beside it | yes |
| `GET /primitives` | The model builder's vocabulary: feeds, measurements, transforms, payout functions | yes |
| `POST /models` | Compose a model, validated on the way in | yes |
| `GET /models/{id}` | A model, its validation report and its card | yes |
| `POST /models/{id}/runs` | Run a model against a risk object, as a job | yes |
| `POST /objects/{id}/package` | Assemble a submission pack from the runs an object already carries, as a job (Package) | yes |
| `GET /packages/{id}` | Read an assembled pack again, exactly as it was assembled | yes |
| `GET /usage` | What this account has used, in the units Craton bills in | yes |
| `DELETE /subscription` | Cancel the plan: metering stops, and so do the billable verbs | yes |

That is all of it. There is no other route, and nothing on this page needs one
past the first eight. `GET /usage` and `DELETE /subscription` are the billing
pair: usage is metered per object and per model run, the free sandbox is an
allowance of units rather than a trial that expires, and cancelling is
self-service and immediate — after it, metered verbs answer `402
account_cancelled` and nothing further is recorded. Everything to do with
payment itself lives at the payment processor, in test mode; Craton holds no
money and no card. Those two have their own runnable page as well, [Usage, and
cancelling](billing.md), which reads an account's usage, cancels its plan and
reads the usage back afterwards. The four structure and backtest routes have
their own runnable page, [Backtest a structure](backtest.md); the two account routes and
the price route have theirs, [A technical price, from nothing](price.md); the
four registry routes have theirs, [Composing a model](model-builder.md) — a
registry surface rather
than one of the seven verbs, whose models produce an index and a technical
price under exactly the same rules as a platform model. The two event routes have theirs,
[Events: the hazard record behind a result](events.md), which reads the
catalogue a backtest paid on and takes one of its events apart into the
occurrence behind it. The three feed routes
have theirs too, [Feeds: check the data a result was computed
from](feeds.md): a price and a backtest name the id, the version and the
SHA-256 of the data they read, and that page walks from those three strings to
the bytes themselves and recomputes the digest.

Two lines of that table carry two methods each: `/objects/{id}` answers `GET`
to read an object and `PATCH` to write onto it, and
`/monitoring/{id}/webhooks` answers `POST` to register an address and `GET` to
read the registrations and their delivery logs back. So the thirty-two rows are
thirty-two addresses and thirty-four route entries. Every other address answers
one method.

**32 routes, and `GET /health` names seven verbs — Ingest, Analyse, Price,
Structure, Backtest, Monitor and Package — one for every verb this table has an address
behind, and no others.** That is a checked invariant, not a coincidence of editing: on every
change to Craton, a gate reads the `verbs` a running server returns against that
server's own route registry and fails in both directions, so the list can
neither invent a verb this build has nothing behind nor drop one it answers on.
It used to trail the surface — Backtest answered for a day while `verbs` still
said Ingest and Price — and that is the gap the gate closes.

A route answering is not the same as a job you can finish, so a second gate
checks the harder half: it signs up holding no key, ingests a schedule, writes
the terms onto the object, looks up the hazard at one of its locations, reads
the event catalogue behind its peril, prices it, cuts a structure and backtests
it, and assembles the submission pack for it — every step a call you could
make from this page — and fails if any verb in `verbs` was not carried all the
way to a finished result. A name in that list is
therefore something a stranger has reached, not something a route table
allows.

Read a name in `verbs` as *reachable*, not complete: `structure` is
there because two of its routes answer, while revisions, `/indices` and
`/wordings` are outside this build, and `analyse` is there because the point
hazard lookup, the event catalogue, the footprint route, the analyse job over
an object and the accumulation rollup all answer. `monitor` is there
because a structure can be recorded in force and read back as a measurement,
and a webhook can be registered on the record and read back with its delivery
log. The four registry
routes are counted in the thirty-two but appear in the response's `registry`
member, not `verbs` — a
registry surface rather than one of the seven. The two billing routes are
counted there too and appear in neither member: what an account owes is not a
capability of the engine. A name in `registry` is a surface, not an address,
which is why `discovery` sits beside it and gives the two catalogue routes —
`/feeds` and `/primitives` — as paths a client can call: the builder's
vocabulary used to be findable only from a page like this one (CEDE-187), and
now the health document itself points at it. Write your client against the
routes; read `verbs` to learn which of the seven a deployment reaches at all.

**Where Structure stops.** Two halves of it are here: cutting a structure from
an object, and reading one back. Revisions, standalone indices and wordings are
outside this build, and `POST /objects` above remains the way to bring a
complete structured object in as JSON.

**Where Analyse stops.** It does not stop: every route the specification names
for it answers here. `GET /hazard` answers what the pinned public record holds
at a single point, synchronously and with the snapshot it read named in the
answer — [the point hazard lookup](hazard.md) is the page for it. The public
event catalogue and event footprints are two more routes above, with their own
page, [Events: the hazard record behind a result](events.md). `POST
/objects/{id}/analyse` runs those same two readings over an object — at its own
coordinates, for the peril it carries — and records the artifacts on its
provenance, which is [Analyse an object](analyse.md). `POST /accumulation`
rolls a set of your own objects up into exposure per peril, per declared region
and by concentration — [Accumulation across a set of
objects](accumulation.md) is its page.

**Where Monitor stops.** Its first slice is here: record a structure as in
force — your own assertion about a transaction executed entirely outside Craton —
and read its index against the newest pinned snapshot, with the distance to
attachment on every read. [Monitor a structure](monitoring.md) is the page for
it, and it is worth reading for one thing in particular: a term ahead of the
pinned record measures nothing, and says so, rather than reporting a zero.
Webhooks have no address in this build. When a level is reached, Craton reports
the measurement and nothing that follows from it.

**Where Package stops.** `POST /objects/{object_id}/package` assembles a
submission pack from the runs an object already carries, and `GET
/packages/{package_id}` serves it back — the exposure summary, the pricing
exhibit with its complete assumption set, the draft slip restated from the
object's own terms, and the backtest exhibit when a backtest of a structure
cut from that object is recorded. Every document in it is watermarked as a
draft produced by analytics software, the slip included, and nothing in the
pack signs, accepts or executes anything. [Package: the submission
pack](package.md) is the page for it. An object that cannot fill the slip is
refused with the missing blocks named, rather than handed a slip with blank
sections.

**Every verb this build serves is above, and each stops where its section
says.** No verb SPEC names is missing an address now, so this page has no list
of the unbuilt — what it has instead is the boundary of each slice, stated
where you meet it.

Do not take any of that on trust, and do not take it as permanent either — ask
the environment you are pointed at, because it answers for itself:

```sh
curl -sS "$CEDE_BASE_URL/health"
```

Read three members of it. `verbs` is the seven of the specification this build
reaches. `registry` is the model-builder surfaces beside them. `discovery` is
the two addresses that list what this deployment actually holds — `GET /feeds`
for the pinned data snapshots, `GET /primitives` for the feeds, measurements,
transforms and payout functions a model is composed from — each with a sentence
saying what it lists, so a client can walk from "the service answered" to the
vocabulary it can build with without reading a page first. Nothing in it needs
this page: point your own code at `/health`, follow `discovery`, and you have
the same two catalogues.

The `verbs` member of that response is checked against the server's own route
registry on every change, in both directions, and against a walk that starts at
`POST /signup` and finishes a job for every verb it names, so it is the list of
what that deployment really serves — no more and no less — at the moment you
ask. A verb that appears there has been called end to end from a fresh signup;
one that does not is not built there yet, whatever any page says. When one lands it will run behind the same job
pattern you already used — `POST` to start it, `GET /jobs/{id}` until it
stops moving — so the client you wrote above is the client you will keep.

---

## When it does not work

| What you see | What it means |
| --- | --- |
| `curl: (7) Failed to connect` | `CEDE_BASE_URL` is wrong, or that environment is not serving. Try `curl "$CEDE_BASE_URL/health"` on its own. |
| `401` with `"code": "unauthenticated"` | No key, a malformed header, or a key that environment does not hold. The header is exactly `Authorization: Bearer <key>`. Never an expired key: keys do not expire. A lost secret cannot be recovered — sign up again. |
| `401` saying the header carries the scheme and no key | `Authorization: Bearer` arrived with nothing after it, which is what an empty `CEDE_API_KEY` looks like on the wire. Check the variable is still set in *this* shell — each new shell starts without it unless your environment file exports it. |
| `422 invalid_request` from `/signup` | A signup body reads one optional member, `label`, and refuses anything else rather than ignoring it. Send `{"label": "…"}`, or no body at all. |
| `404` from `GET /account` | Your key was configured into that deployment by whoever runs it rather than handed out by sign-up, so it belongs to no account. It still works; sign up for an account of your own. |
| `404` on an object or a job you just made | You are using a different key than the one that created it. Objects and jobs belong to one key. |
| `401` on a key that worked earlier | Not expiry — keys have none, and staging holds accounts, keys, objects, jobs and usage in a database that outlives a redeploy. Look at the credential that arrived: a truncated copy, a key from a different environment, or an unset variable. A whole secret begins `cede_sk_`. If you no longer have it, sign up again; it takes one call. |
| `413 payload_too_large` | Ingest takes files up to 32 MB in one request. |
| Job `failed` with `415 unsupported_source` | Ingest could not read the file at all — the message says what it tried. It arrives on the job, not on the `POST /ingest` response, which answered `202`: whether a file is readable is a fact about its bytes, and reading them is the job. See "A `202` is not a statement that your file is readable". |
| Job `failed` with `422 unpriceable_object` | The object has no financial structure or no trigger, so there is nothing to price yet. See "Where to go next". |
| Job `failed` with `422 unbacktestable_structure` | The structure is outside what the v0 parametric model reads, or its window reaches outside the pinned archive. The message says which. |

Every refusal has the same shape — `status`, `code`, `message`, and sometimes
`details` — and validates against the published error schema. Including the
ones nobody predicted: a `500` is a structured document too.

The table above is the short list. **[The error reference](errors.md)** is the
full one: the envelope field by field, every code this build reports and the
status it arrives with, the same shape as it appears on a failed job, and the
rules saying what can change under a client and what cannot. Read it once and
write your failure path once.

---

*The block on this page is extracted verbatim and executed against a live
environment on every change to Craton. If it stopped working, the build stops
too — these instructions cannot quietly drift away from the product.*
