Get a free API key

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

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 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. 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#

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

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 and the acceptable-use policy; both are short and both are served on this site.

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:

{
  "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:

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.

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 "[email protected];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.


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 "[email protected];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.

curl -sS --fail-with-body -H "$auth" "$CEDE_BASE_URL/ingest" \
  -F "[email protected];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:

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


Where to go next#

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.


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 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, 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; the two account routes and the price route have theirs, A technical price, from nothing; the four registry routes have theirs, Composing a model — 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, 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: 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 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. 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. 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 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 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 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:

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 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.