This page as Markdown, byte for byte: /billing.md
Usage, and cancelling#
One block, one terminal, no key in advance. By the end you will have read what an account has used in the units Craton bills in, cancelled its plan self-service, and seen that the record of what was used survives the cancellation.
These are the two billing routes, and they are the whole of billing's public surface:
- **
GET /usage** — the platform's own record of what this account has used. It is the only thing an invoice is ever derived from, so it is what you reconcile an invoice against. - **
DELETE /subscription** — cancel the plan. Metering stops, and so do the metered verbs.
Read the quickstart first if you have not: it explains CEDE_BASE_URL, the Authorization: Bearer header, and the job pattern every Craton verb answers with. This page repeats none of that and assumes all of it.
What you need#
curl7.76 or newer (this page uses--fail-with-body) andpython3.CEDE_BASE_URL— the base URL of the Craton environment you are pointed at, with no trailing slash. That is the only thing this page needs from you.
export CEDE_BASE_URL="https://altier.ridgehead-hamlet.ts.net:8472" # no trailing slash
**No CEDE_API_KEY here, and that is not only a convenience.** The block below signs up for an account of its own every time it runs, because its last act is to cancel that account — and cancellation is not reversible. Never run this page against a key you want to keep using: point it at the account it makes for itself, which is what it does as written.
The whole thing, in one block#
Run this in an empty directory.
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 — An account of this page's own. It gets cancelled at step 5, so it is
# deliberately not yours: /signup takes no credential and hands back a new
# account on the free sandbox plan with its first key.
account=$(curl -sS --fail-with-body \
-H "Content-Type: application/json" \
-d '{"label": "usage and cancellation walkthrough"}' \
"$CEDE_BASE_URL/signup")
CEDE_API_KEY=$(printf '%s' "$account" | field api_key.secret)
export CEDE_API_KEY
auth="Authorization: Bearer $CEDE_API_KEY"
printf 'account %s, key (shown once): %s\n' \
"$(printf '%s' "$account" | field id)" "$CEDE_API_KEY"
# 2 — What it has used so far: nothing. A brand new account still answers with
# a complete document — both units at zero and the whole sandbox allowance
# still to spend — rather than with an absence you have to interpret.
curl -sS --fail-with-body -H "$auth" "$CEDE_BASE_URL/usage" > usage-before.json
printf 'before: plan %s, %s, %s unit(s) used, %s free unit(s) left\n' \
"$(field plan < usage-before.json)" \
"$(field status < usage-before.json)" \
"$(field total_units < usage-before.json)" \
"$(field free_units_remaining < usage-before.json)"
# 3 — Use something. Ingest is a metered verb and objects are one of the two
# units Craton bills in, so normalising one file is enough to put a row in
# the ledger.
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
job_id=$(curl -sS --fail-with-body -H "$auth" \
-F "[email protected];type=text/csv" \
"$CEDE_BASE_URL/ingest" | field id)
state=unknown
for _ in $(seq 1 150); do
curl -sS --fail-with-body -H "$auth" "$CEDE_BASE_URL/jobs/$job_id" > job.json
state=$(field status < job.json)
case "$state" in succeeded|failed) break ;; esac
sleep 0.2
done
if [ "$state" != succeeded ]; then
printf 'the ingest job did not succeed: %s\n' "$(cat job.json)" >&2
exit 1
fi
echo "ingest job $job_id: $state"
# 4 — The same read again, now with something to report. Usage is recorded
# when the work succeeds, inside the job — a verb that failed is not
# metered, and polling a job never adds a second row.
curl -sS --fail-with-body -H "$auth" "$CEDE_BASE_URL/usage" > usage-after.json
python3 -c '
import json
usage = json.load(open("usage-after.json"))
print("after: ", usage["total_units"], "unit(s) used,",
usage["free_units_remaining"], "free unit(s) left")
for unit, count in usage["units"].items():
print(" unit:", unit, "=", count)
for event in usage["events"]:
print(" event:", event["id"], "-", event["verb"], "->",
event["quantity"], event["unit"], "at", event["recorded_at"])
print(" already invoiced:", usage["billed_units"], "unit(s)")
'
# 5 — Cancel the plan. Self-service, immediate, and not reversible: after this
# the metered verbs refuse this key and nothing further is recorded for it.
curl -sS --fail-with-body -X DELETE -H "$auth" \
"$CEDE_BASE_URL/subscription" > cancelled.json
printf 'cancelled: account %s is now %s\n' \
"$(field id < cancelled.json)" \
"$(field billing.status < cancelled.json)"
# 6 — Usage after cancelling. /usage keeps answering, because what an account
# used is a fact about the past: the status reads "cancelled" and every
# event recorded before the cancellation is still there to reconcile
# against.
curl -sS --fail-with-body -H "$auth" "$CEDE_BASE_URL/usage" > usage-final.json
printf 'final: %s, %s unit(s) used, %s event(s) on the record\n' \
"$(field status < usage-final.json)" \
"$(field total_units < usage-final.json)" \
"$(python3 -c 'import json; print(len(json.load(open("usage-final.json"))["events"]))')"
echo "done — the account this page made is closed, and its usage is still readable"
If the last line printed, you have seen both billing routes answer.
What just happened#
**GET /usage** — what this account has used, in the two units Craton bills in (SPEC §6: per object and per model run). Five verbs are metered and each maps onto one of those two units:
| Verb | Unit |
|---|---|
| Ingest | object |
| Structure | object |
| Price | model_run |
| Backtest | model_run |
| A registry model run | model_run |
Package is deliberately absent from that table: assembling a submission pack reads runs that already happened — and were already metered by the verb that ran them — so it mints no object, runs no model and costs nothing.
Reading usage is not itself metered, and neither is anything else that only reads: GET /account, GET /objects/{id}, GET /jobs/{id} and their neighbours cost nothing, however often you call them.
**DELETE /subscription** — cancellation, done the same way signing up was done: one call, nobody in the loop, effective on the very next request. It answers with the account and its billing block reading cancelled. Usage recorded before the call is still settled; nothing after it is recorded at all.
Everything to do with payment itself lives at the payment processor, in test mode. Craton holds no money and no card, and the billing.mode member says which mode the deployment you are pointed at is in — test when a processor is configured, dark when none is and no network call is made at all.
What is in a usage document#
schema_version the version of the published usage schema this document
conforms to
account_id the account the presented key belongs to
plan the plan it is on — "sandbox" for the free tier
status "active", or "cancelled" once the plan is cancelled
units the count per billable unit: object, model_run. Both
members are always present, zero included
total_units the sum of them
billed_units how many of those units are already on an invoice
free_units_remaining what is left of the free sandbox allowance
events[] one row per metered call: its id, the verb, the unit, the
quantity, and when it was recorded
invoices[] present once this account has been invoiced at all
One call, one event. A metered verb call records exactly one event, keyed on the thing it produced, so a retry of an idempotent request or a job polled fifty times adds nothing. events[] is the itemisation an invoice line has to equal exactly — "an invoice appeared" is not the standard Craton holds itself to.
The free sandbox is an allowance of units rather than a trial that expires: free_units_remaining counts down as you use them, and it does not run out on a date.
After cancelling#
Cancellation stops the work, not only the recording. Every metered verb refuses at its own route, before it does anything:
curl -sS -X POST -H "Authorization: Bearer $CEDE_API_KEY" \
-F "[email protected];type=text/csv" \
"$CEDE_BASE_URL/ingest"
{
"schema_version": "0.1.0",
"error": {
"status": 402,
"code": "account_cancelled",
"message": "this account cancelled its plan, so its usage is no longer metered and the billable verbs no longer run for it. Cancellation is not reversible: sign up at POST /signup for a new account and a new key"
}
}
That refusal is the point of the design rather than a side effect of it. If a cancelled account still ran the verbs and simply went unrecorded, it would get the platform's most expensive work for free and leave no row behind by which anyone could find out — and comparing invoiced quantities could never catch it, because the quantities would stay right.
The read routes keep working, so you can still reconcile what you used — step 6 of the block above is GET /usage after the cancellation, and it answers with every event that was recorded before it.
There is no undo. Signing up again is a new account, a new key, and an empty ledger; the old account's objects and jobs belong to the old key and stay there.
When it does not work#
| What you see | What it means |
|---|---|
404 from GET /usage or DELETE /subscription |
The key you sent was configured into that deployment by whoever runs it rather than handed out at sign-up, so it belongs to no account, holds no plan, and has nothing metered against it. Sign up for an account of your own. |
402 account_cancelled from a verb |
That account cancelled its plan. It is not reversible — sign up again; it takes one call. |
total_units is 0 after a call you expected to be metered |
Usage is recorded when the work succeeds, inside the job. A job that ended failed is not metered, and a 202 on its own is not usage — wait for succeeded and read again. |
401 unauthenticated on any call after step 1 |
The header is exactly Authorization: Bearer <key>. Staging keeps accounts in memory and redeploys whenever the code moves, so a key from yesterday is a key from a previous life — sign up again. |
Every refusal has the same shape and validates against the published error schema; the error reference is the full list.
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.