{
  "$id": "urn:cede:api-contract:v0",
  "schema_version": "0.1.0",
  "title": "Cede API response contract, v0",
  "description": "Which published schema governs which API response. SPEC.md section 7 requires every API response to validate against the published schema; this document is the map from route and status to that schema, so a consumer can validate what Cede returns while holding no Cede code. It is generated from the server's own route registrations (uv run python -m cede.api.contract --write) and the contract suite fails if it and the running surface disagree.",
  "schemas": {
    "account": "account.v0.schema.json",
    "accumulation": "accumulation.v0.schema.json",
    "analysis": "analysis.v0.schema.json",
    "backtest-response": "backtest-response.v0.schema.json",
    "error": "error.v0.schema.json",
    "event-catalogue": "event-catalogue.v0.schema.json",
    "event-footprint": "event-footprint.v0.schema.json",
    "feed-data": "feed-data.v0.schema.json",
    "feed-index": "feed-index.v0.schema.json",
    "feed-object": "feed-object.v0.schema.json",
    "hazard": "hazard.v0.schema.json",
    "job": "job.v0.schema.json",
    "model-object": "model-object.v0.schema.json",
    "model-primitives": "model-primitives.v0.schema.json",
    "model-run": "model-run.v0.schema.json",
    "monitoring": "monitoring.v0.schema.json",
    "monitoring-index": "monitoring-index.v0.schema.json",
    "monitoring-notification": "monitoring-notification.v0.schema.json",
    "monitoring-webhook": "monitoring-webhook.v0.schema.json",
    "monitoring-webhook-index": "monitoring-webhook-index.v0.schema.json",
    "package": "package.v0.schema.json",
    "price-response": "price-response.v0.schema.json",
    "risk-object": "risk-object.v0.schema.json",
    "service-info": "service-info.v0.schema.json",
    "structure": "structure.v0.schema.json",
    "usage": "usage.v0.schema.json"
  },
  "versions": {
    "description": "The schema_version this build stamps on documents of each published schema. The file names above are version LINES — risk-object.v0 admits every 0.x.y document — so a consumer holding no Cede code reads the line from `schemas` and the version this deployment emits from here. Generated from the same constants the responses are built from, never typed.",
    "emitted": {
      "account": "0.1.0",
      "accumulation": "0.1.0",
      "analysis": "0.1.0",
      "backtest-response": "0.3.0",
      "error": "0.1.0",
      "event-catalogue": "0.1.0",
      "event-footprint": "0.1.0",
      "feed-data": "0.1.0",
      "feed-index": "0.1.0",
      "feed-object": "0.1.0",
      "hazard": "0.1.0",
      "job": "0.4.0",
      "model-object": "0.2.0",
      "model-primitives": "0.1.0",
      "model-run": "0.1.0",
      "monitoring": "0.1.0",
      "monitoring-index": "0.1.0",
      "monitoring-notification": "0.1.0",
      "monitoring-webhook": "0.1.0",
      "monitoring-webhook-index": "0.1.0",
      "package": "0.2.0",
      "price-response": "0.1.0",
      "risk-object": "0.1.0",
      "service-info": "0.1.0",
      "structure": "0.1.0",
      "usage": "0.1.0"
    }
  },
  "errors": {
    "schema": "error",
    "description": "Every response with status 400 or above, from every route, validates against this schema — including the ones the framework raises before any handler runs (unknown path, method a route does not serve, unreadable body). The statuses below are the ones this build names a code for; the schema governs any refusal, named or not.",
    "named_statuses": [
      400,
      401,
      402,
      403,
      404,
      405,
      413,
      415,
      422,
      429,
      500
    ],
    "named_codes_description": "Every error.code this build reports at a status it names, with the statuses it is reported with. error.code is a pattern in the error schema, not an enum, so this list is what this build does report and never a promise that no other code can appear: a client switches on the codes it knows and falls back to status for the rest. A status this build names no code for reports as client_error (4xx) or server_error (5xx).",
    "named_codes": [
      {
        "code": "account_cancelled",
        "statuses": [
          402
        ],
        "summary": "The account cancelled its plan, so its usage is no longer metered and the billable verbs no longer run for it. Usage recorded before the cancellation is still settled, on a final invoice raised at the moment of cancelling; nothing after it is metered or billed. Cancellation is not reversible: sign up again for a new account and a new key."
      },
      {
        "code": "currency_conflict",
        "statuses": [
          422
        ],
        "summary": "The ingest request declared a currency the file itself contradicts: its cells or column headers state a different code, or several. Ingest neither overwrites what a file says nor ignores what the submitter said, so it stores nothing and names both. Reported on the ingest job, not on the request that created it: what a file states is a fact about its bytes."
      },
      {
        "code": "feed_not_servable",
        "statuses": [
          403
        ],
        "summary": "The pinned snapshot exists and its manifest, coverage and digest are readable, but this build does not serve its content: SPEC §4 P10 holds that feeds are aggregate only — never individual-level records — and content is served only for declared aggregation levels that cannot carry one. 403 rather than 404 on purpose: the snapshot is there, and saying so is the honest answer."
      },
      {
        "code": "internal_error",
        "statuses": [
          500
        ],
        "summary": "Craton failed at something that is not the request's fault. Recorded for repair; the same request may work on a retry."
      },
      {
        "code": "invalid_composition",
        "statuses": [
          422
        ],
        "summary": "A model submitted to the builder is not in the v1 composition vocabulary: an unknown feed, measurement, transform or payout function, or one of the four missing. v1 logic is declarative only, so a composition the vocabulary does not cover is refused rather than executed. The message names the part that could not be read."
      },
      {
        "code": "invalid_model",
        "statuses": [
          422
        ],
        "summary": "A composed model does not validate against the published model object schema — step 1 of the automatic validation pass. `details` names each violation."
      },
      {
        "code": "invalid_request",
        "statuses": [
          400,
          422
        ],
        "summary": "The request itself could not be read or accepted: an unparseable body, a member the route does not take, a malformed value. 400 when the bytes are wrong, 422 when they parse but the content is not acceptable."
      },
      {
        "code": "invalid_risk_object",
        "statuses": [
          422
        ],
        "summary": "A risk object submitted to the API does not validate against the published risk object schema. `details` names each violation."
      },
      {
        "code": "invalid_structure",
        "statuses": [
          422
        ],
        "summary": "A structure could not be cut from the object: the request varies a block a structure does not vary, or the resulting structure is missing the peril, trigger or financial structure a verb needs to evaluate it."
      },
      {
        "code": "method_not_allowed",
        "statuses": [
          405
        ],
        "summary": "The path exists but does not serve that HTTP method."
      },
      {
        "code": "model_validation_failed",
        "statuses": [
          422
        ],
        "summary": "The composition is in the vocabulary but the automatic validation pass refused it, so the model was not created. SPEC §2A makes that pass mandatory on creation, so a model that fails it does not exist — there is no id to retry against. The message names the part that refused."
      },
      {
        "code": "no_event_catalogue",
        "statuses": [
          404
        ],
        "summary": "No pinned snapshot holds an event catalogue of what was asked for: a peril nothing is pinned for, a feed id or a vintage this build does not hold, or a peril whose only record is a gridded daily series rather than a set of events — this build will not manufacture events out of one. The message names the perils, feeds and vintages that do answer, and GET /feeds lists every snapshot behind them."
      },
      {
        "code": "not_found",
        "statuses": [
          404
        ],
        "summary": "No such route, or no such object, job or account *for the presented key*. Objects and jobs belong to the key that created them, so another key gets 404 rather than 403: the existence of your work is yours."
      },
      {
        "code": "outside_feed_coverage",
        "statuses": [
          422
        ],
        "summary": "The peril is one this build answers for, but no pinned snapshot of it covers the point asked about — a catalogue extract whose bounding box does not hold the whole cell around it, or a gridded feed measuring a different place. Coverage is checked rather than approximated: an extract that stops inside the area asked about would report less hazard than the record holds. The message names each snapshot and what it does cover."
      },
      {
        "code": "payload_too_large",
        "statuses": [
          413
        ],
        "summary": "The uploaded file is over the per-request size limit Ingest accepts."
      },
      {
        "code": "rate_limited",
        "statuses": [
          429
        ],
        "summary": "The presented key is over its request limit. The response carries `Retry-After`; the message names the limit, the window and the wait. Limits are per key."
      },
      {
        "code": "unauthenticated",
        "statuses": [
          401
        ],
        "summary": "No API key, a malformed `Authorization` header, or a key this environment does not hold. Every route except `GET /health` and `POST /signup` needs one."
      },
      {
        "code": "unbacktestable_structure",
        "statuses": [
          422
        ],
        "summary": "The structure is well-formed but Backtest v0 cannot replay it — a trigger the v0 parametric model does not read, or a window reaching outside the period of record the pinned archive holds. Reported on the backtest job, not on the request that created it."
      },
      {
        "code": "unmonitorable_structure",
        "statuses": [
          422
        ],
        "summary": "The structure is well-formed but its index cannot be measured against the pinned public record — no trigger, no term, more than one data source, or a feed nothing is pinned for. The message names what is missing, and for a feed it names what is pinned."
      },
      {
        "code": "unpackageable_object",
        "statuses": [
          422
        ],
        "summary": "The object exists and is well-formed, but it does not carry what a submission pack is assembled from: no price run in its provenance, or none of the blocks the draft slip is populated from (the parties, the period, the jurisdiction, the financial structure, the trigger, the technical premium a price run writes). Package restates records and computes nothing, so it will neither price the object nor draft around a gap; the message names every missing piece at once and the call that produces each. Reported on the request, before a job is created."
      },
      {
        "code": "unanalysable_object",
        "statuses": [
          422
        ],
        "summary": "The object exists and a named analysis cannot read it — no peril on the object, no locations on its exposure, no coordinates on any of them, more locations than one run reads, or no pinned snapshot covering any of them for that peril. The message names the block to write and the route that writes it. Reported on the analyse job, not on the request that created it."
      },
      {
        "code": "unknown_analysis",
        "statuses": [
          422
        ],
        "summary": "The request named an analysis this build does not run, or named none. The message lists every analysis that can be asked for, so the call is fixed in one round trip. Reported on the request itself: an analysis nothing can run never becomes a job."
      },
      {
        "code": "unpriceable_object",
        "statuses": [
          422
        ],
        "summary": "The object is well-formed but Price cannot compute a technical price for it — typically no financial structure or no trigger. Reported on the price job, not on the request that created it."
      },
      {
        "code": "unrunnable_object",
        "statuses": [
          422
        ],
        "summary": "The model and the object both exist, but the model cannot be run against that object — typically the object carries nothing the model's declared inputs read, or falls outside the feed's record. Reported on the model run job, not on the request that created it."
      },
      {
        "code": "unsupported_media_type",
        "statuses": [
          415
        ],
        "summary": "The request's content type is not one the route reads."
      },
      {
        "code": "unsupported_peril",
        "statuses": [
          422
        ],
        "summary": "A hazard lookup named a peril this build pins no data for. Perils are answerable when a snapshot of them is committed — the engine reads snapshots and never the network — so the message names the ones that are, and GET /feeds lists every feed and vintage behind them."
      },
      {
        "code": "unsupported_source",
        "statuses": [
          415
        ],
        "summary": "Ingest could not read the uploaded file at all — encoding, container format or structure. The message states what it tried. Reported on the ingest job, not on the request that created it: whether a file is readable is a fact about its bytes, and reading them is the job, so POST /ingest answers 202 even for a file that is obviously not a schedule."
      }
    ]
  },
  "embedded": [
    {
      "in": "job",
      "pointer": "/result/analysis",
      "schema": "analysis"
    },
    {
      "in": "job",
      "pointer": "/result/price",
      "schema": "price-response"
    },
    {
      "in": "job",
      "pointer": "/result/backtest",
      "schema": "backtest-response"
    },
    {
      "in": "job",
      "pointer": "/result/model_run",
      "schema": "model-run"
    },
    {
      "in": "job",
      "pointer": "/result/package",
      "schema": "package"
    },
    {
      "in": "package",
      "pointer": "/documents/pricing_exhibit/price",
      "schema": "price-response"
    },
    {
      "in": "package",
      "pointer": "/documents/backtest_exhibit/backtest",
      "schema": "backtest-response"
    }
  ],
  "routes": [
    {
      "method": "GET",
      "path": "/account",
      "answers": {
        "200": "account"
      }
    },
    {
      "method": "POST",
      "path": "/accumulation",
      "answers": {
        "200": "accumulation"
      }
    },
    {
      "method": "GET",
      "path": "/backtests/{backtest_id}",
      "answers": {
        "200": "backtest-response"
      }
    },
    {
      "method": "GET",
      "path": "/events/{peril}",
      "answers": {
        "200": "event-catalogue"
      }
    },
    {
      "method": "GET",
      "path": "/events/{peril}/{event_id}/footprint",
      "answers": {
        "200": "event-footprint"
      }
    },
    {
      "method": "GET",
      "path": "/feeds",
      "answers": {
        "200": "feed-index"
      }
    },
    {
      "method": "GET",
      "path": "/feeds/{feed_id}/versions/{version}",
      "answers": {
        "200": "feed-object"
      }
    },
    {
      "method": "GET",
      "path": "/feeds/{feed_id}/versions/{version}/data",
      "answers": {
        "200": "feed-data"
      }
    },
    {
      "method": "GET",
      "path": "/hazard",
      "answers": {
        "200": "hazard"
      }
    },
    {
      "method": "GET",
      "path": "/health",
      "answers": {
        "200": "service-info"
      }
    },
    {
      "method": "POST",
      "path": "/ingest",
      "answers": {
        "202": "job"
      }
    },
    {
      "method": "GET",
      "path": "/jobs/{job_id}",
      "answers": {
        "200": "job"
      }
    },
    {
      "method": "POST",
      "path": "/models",
      "answers": {
        "201": "model-object"
      }
    },
    {
      "method": "GET",
      "path": "/models/{model_id}",
      "answers": {
        "200": "model-object"
      }
    },
    {
      "method": "POST",
      "path": "/models/{model_id}/runs",
      "answers": {
        "202": "job"
      }
    },
    {
      "method": "GET",
      "path": "/monitoring",
      "answers": {
        "200": "monitoring-index"
      }
    },
    {
      "method": "GET",
      "path": "/monitoring/{monitoring_id}",
      "answers": {
        "200": "monitoring"
      }
    },
    {
      "method": "GET",
      "path": "/monitoring/{monitoring_id}/webhooks",
      "answers": {
        "200": "monitoring-webhook-index"
      }
    },
    {
      "method": "POST",
      "path": "/monitoring/{monitoring_id}/webhooks",
      "answers": {
        "201": "monitoring-webhook"
      }
    },
    {
      "method": "POST",
      "path": "/objects",
      "answers": {
        "201": "risk-object"
      }
    },
    {
      "method": "GET",
      "path": "/objects/{object_id}",
      "answers": {
        "200": "risk-object"
      }
    },
    {
      "method": "PATCH",
      "path": "/objects/{object_id}",
      "answers": {
        "200": "risk-object"
      }
    },
    {
      "method": "POST",
      "path": "/objects/{object_id}/analyse",
      "answers": {
        "202": "job"
      }
    },
    {
      "method": "POST",
      "path": "/objects/{object_id}/package",
      "answers": {
        "202": "job"
      }
    },
    {
      "method": "POST",
      "path": "/objects/{object_id}/price",
      "answers": {
        "202": "job"
      }
    },
    {
      "method": "POST",
      "path": "/objects/{object_id}/structures",
      "answers": {
        "201": "structure"
      }
    },
    {
      "method": "GET",
      "path": "/packages/{package_id}",
      "answers": {
        "200": "package"
      }
    },
    {
      "method": "GET",
      "path": "/primitives",
      "answers": {
        "200": "model-primitives"
      }
    },
    {
      "method": "POST",
      "path": "/signup",
      "answers": {
        "201": "account"
      }
    },
    {
      "method": "GET",
      "path": "/structures/{structure_id}",
      "answers": {
        "200": "structure"
      }
    },
    {
      "method": "POST",
      "path": "/structures/{structure_id}/backtest",
      "answers": {
        "202": "job"
      }
    },
    {
      "method": "POST",
      "path": "/structures/{structure_id}/monitoring",
      "answers": {
        "201": "monitoring"
      }
    },
    {
      "method": "DELETE",
      "path": "/subscription",
      "answers": {
        "200": "account"
      }
    },
    {
      "method": "GET",
      "path": "/usage",
      "answers": {
        "200": "usage"
      }
    }
  ]
}
