{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "urn:cede:schema:monitoring-webhook:v0",
  "title": "CEDE monitoring webhook registration, v0",
  "description": "One address a monitoring record's measurement is delivered to when it moves — the body of POST /monitoring/{id}/webhooks (SPEC.md section 3.6). A registration holds three things: the address, the measurement it was last delivered or baselined on, and the log of every delivery attempt made for it. What moves it is a change in any of the four compared fields (the feed id, the vintage, the computed index value, the distance to attachment) and nothing else. Every measurement in this document — the baseline, and both readings of each notification — names the model and version that produced it, because a level served to an account is a computed result and SPEC.md section 4, P9 admits no unlabelled result on any route. What it delivers is a difference between two readings of an index computed by analytics software from pinned public snapshots. It is never a notice under any contract: Cede is not the calculation agent of record for anything, determines nothing about anybody's obligations, and moves no money. The disclosure member below names, in full, the kinds of notice this platform never sends. No amount of money appears anywhere in this document or in what it delivers.",
  "type": "object",
  "additionalProperties": false,
  "required": [
    "schema_version",
    "id",
    "monitoring_id",
    "structure_id",
    "url",
    "status",
    "registered_at",
    "triggers",
    "measurement",
    "delivery",
    "notifications",
    "notifications_created",
    "delivery_log",
    "delivery_log_limit",
    "disclosure",
    "links"
  ],
  "properties": {
    "schema_version": { "type": "string", "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$" },
    "id": { "type": "string", "minLength": 1, "description": "Server-assigned identity of this registration." },
    "monitoring_id": { "type": "string", "minLength": 1 },
    "structure_id": { "type": "string", "minLength": 1 },
    "url": { "type": "string", "minLength": 1, "description": "The address deliveries are POSTed to, echoed exactly as registered. http or https only, with a host and no credentials in it — anything else is refused at registration rather than dialled and logged as a failure." },
    "status": {
      "type": "string",
      "enum": ["registered"],
      "description": "What Cede holds. `registered` is the only value this build writes: a state of the record inside Cede, never a statement about anything outside it. There is no `active`, no `fired` and no `triggered`."
    },
    "registered_at": { "type": "string", "minLength": 20, "description": "When the registration was created, RFC 3339 in UTC." },
    "triggers": { "$ref": "#/$defs/triggers" },
    "measurement": {
      "$ref": "#/$defs/measurement",
      "description": "The measurement this registration currently stands on. Set from a fresh observation when the registration was created — so registering never delivers — and advanced the moment a read finds any compared field has moved. It advances before the notification it created is put on a socket, so a crash between the two loses the push and never the state."
    },
    "delivery": { "$ref": "#/$defs/delivery" },
    "notifications": {
      "type": "array",
      "description": "Every notification this registration has created, oldest first, each with the state of its delivery. A notification is created when a compared field moved; it is not an assertion that anything happened outside Cede.",
      "items": { "$ref": "#/$defs/notification" }
    },
    "notifications_created": { "type": "integer", "minimum": 0, "description": "How many notifications this registration has created since it was registered." },
    "delivery_log": {
      "type": "array",
      "description": "Every delivery attempt, newest first: which notification, when, which attempt of it, what happened, and the receiver's status code when it answered at all. This is how a missed delivery is visible rather than silent — an abandoned notification stays here with the reason of its last failure. Capped at delivery_log_limit entries.",
      "items": { "$ref": "#/$defs/delivery_log_entry" }
    },
    "delivery_log_limit": { "type": "integer", "minimum": 1, "description": "How many entries the log keeps, newest first. Stated rather than silently truncating: a permanently dead receiver must not grow this document without limit." },
    "disclosure": { "type": "string", "minLength": 1, "description": "The fixed sentence stating what a delivery is and is not — a measurement that moved, and never a notice under any contract: no claim notice, no settlement notice and no calculation-agent notice, to the account holder or to anybody else. Pinned as a constant in the product (cede.monitor.webhooks.DELIVERY_DISCLOSURE) and asserted by the contract suite, so it cannot be softened registration by registration." },
    "links": {
      "type": "object",
      "additionalProperties": false,
      "required": ["self", "monitoring"],
      "properties": {
        "self": { "type": "string", "minLength": 1 },
        "monitoring": { "type": "string", "minLength": 1 }
      }
    }
  },
  "$defs": {
    "measurement": {
      "type": "object",
      "additionalProperties": false,
      "required": ["feed_id", "vintage", "index_value", "index_unit", "distance_to_attachment", "model"],
      "description": "The four fields a registration compares, plus the unit they are read in and the label of what computed them. Exactly the observation members SPEC.md section 3.6 names as Monitor's output: the feed, its vintage, the computed index value, and the distance to attachment. index_unit and model ride along and are not compared — a unit that changed without the value changing would be a defect, not a delta, and a model version that moved while all four held is the same level reproduced by newer code. Both are still kept current: a read that finds either has moved replaces the stored measurement without creating a notification, so this document never serves a level beside a label that did not produce it.",
      "properties": {
        "feed_id": { "type": "string", "minLength": 1 },
        "model": {
          "type": "object",
          "additionalProperties": true,
          "required": ["name", "version"],
          "description": "SPEC.md section 4, P9: every result is labelled with the model and version that produced it. A level served on a registration is a computed result exactly as an observation, a price and a backtest are, and P9 is a property of a number having been computed rather than of which route answered it — so the label is a member of the measurement itself and travels with it onto this document, into the index at GET /monitoring/{id}/webhooks, and onto both readings a notification carries.",
          "properties": {
            "name": { "type": "string", "minLength": 1 },
            "version": { "type": "string", "minLength": 1 }
          }
        },
        "vintage": { "type": "string", "minLength": 1, "description": "The vintage the observation read, which is the newest pinned vintage of the feed (ADR-0046). While every feed in this build is an immutable snapshot, a new vintage landing is the only thing that can move any of these values." },
        "index_value": { "type": ["number", "null"], "description": "The index level over the measured window, in index_unit. Null when no level could be measured — never a zero, which is a measurement." },
        "index_unit": { "type": "string", "minLength": 1 },
        "distance_to_attachment": { "type": ["number", "null"], "description": "The structure's attachment level minus the index value, in index_unit. Positive is short of the attachment, negative is past it, null exactly when index_value is null. A receiver that needs to know whether the attachment was reached reads this: at or past it is zero or negative. Cede states the distance and draws nothing from it." }
      }
    },
    "triggers": {
      "type": "object",
      "additionalProperties": false,
      "required": ["compared_fields", "basis", "evaluated_on"],
      "description": "What moves a delivery, and when it is looked for.",
      "properties": {
        "compared_fields": {
          "type": "array",
          "minItems": 1,
          "description": "The members of `measurement` a change in which creates a notification. Nothing outside this list is compared.",
          "items": { "type": "string", "minLength": 1 }
        },
        "basis": { "type": "string", "minLength": 1 },
        "evaluated_on": { "type": "string", "minLength": 1, "description": "The limitation, said out loud: this build has no scheduler, so the comparison happens on a read of the monitoring record and a notification reaches you on the next read rather than the instant a newer snapshot lands." }
      }
    },
    "delivery": {
      "type": "object",
      "additionalProperties": false,
      "required": ["guarantee", "maximum_attempts", "timeout_seconds", "basis"],
      "description": "What Cede promises about getting the message to you.",
      "properties": {
        "guarantee": { "type": "string", "enum": ["at_least_once"], "description": "At least once, never exactly once. A receiver that answered after our socket timed out will see the same notification_id again." },
        "maximum_attempts": { "type": "integer", "minimum": 1, "description": "How many times one notification is attempted before it is abandoned." },
        "timeout_seconds": { "type": "number", "exclusiveMinimum": 0 },
        "basis": { "type": "string", "minLength": 1 }
      }
    },
    "notification": {
      "type": "object",
      "additionalProperties": false,
      "required": ["id", "created_at", "status", "attempts", "changed", "model", "measurement", "previous_measurement"],
      "properties": {
        "id": { "type": "string", "minLength": 1, "description": "Stable across every attempt of this notification. The identity a receiver deduplicates on." },
        "created_at": { "type": "string", "minLength": 20 },
        "status": {
          "type": "string",
          "enum": ["pending", "delivered", "abandoned"],
          "description": "The state of Cede's attempt to reach the address, and of nothing else. `pending` means not yet delivered and still to be retried on a later read; `abandoned` means every attempt failed and no more will be made."
        },
        "attempts": { "type": "integer", "minimum": 0 },
        "delivered_at": { "type": "string", "minLength": 20 },
        "abandoned_at": { "type": "string", "minLength": 20 },
        "changed": {
          "type": "array",
          "minItems": 1,
          "description": "Which compared fields moved. This is the whole of what the notification asserts.",
          "items": { "type": "string", "minLength": 1 }
        },
        "model": {
          "type": "object",
          "additionalProperties": true,
          "required": ["name", "version"],
          "description": "SPEC.md section 4 P9: every result is labelled with the model and version that produced it. A delivered measurement is a computed result exactly as an observation, a price and a backtest are.",
          "properties": {
            "name": { "type": "string", "minLength": 1 },
            "version": { "type": "string", "minLength": 1 }
          }
        },
        "measurement": { "$ref": "#/$defs/measurement" },
        "previous_measurement": { "$ref": "#/$defs/measurement" }
      }
    },
    "delivery_log_entry": {
      "type": "object",
      "additionalProperties": false,
      "required": ["notification_id", "attempt", "attempted_at", "outcome", "notification_status"],
      "properties": {
        "notification_id": { "type": "string", "minLength": 1 },
        "attempt": { "type": "integer", "minimum": 1 },
        "attempted_at": { "type": "string", "minLength": 20 },
        "outcome": { "type": "string", "enum": ["delivered", "failed"] },
        "notification_status": { "type": "string", "enum": ["pending", "delivered", "abandoned"], "description": "What the attempt left the notification as, so one line of the log answers 'will this be tried again?'." },
        "response_status": { "type": "integer", "description": "The HTTP status the receiver answered with, when it answered at all. The only thing kept of the receiver's response besides its size: a response body read back into a Cede document would be an exfiltration channel out of whatever the address reached." },
        "response_bytes": { "type": "integer", "minimum": 0 },
        "error": { "type": "string", "minLength": 1, "description": "Why the attempt failed, in the dialler's own words." }
      }
    }
  }
}
