{
  "components": {
    "schemas": {
      "Attempt": {
        "additionalProperties": false,
        "description": "One line of the service log: one try at one delivery.",
        "properties": {
          "at": {
            "description": "When it was tried.",
            "type": "string"
          },
          "attempt": {
            "description": "1 to 5.",
            "type": "integer"
          },
          "delivery": {
            "type": "string"
          },
          "endpoint": {
            "type": "string"
          },
          "error": {
            "type": [
              "string",
              "null"
            ]
          },
          "outcome": {
            "enum": [
              "delivered",
              "failed"
            ],
            "type": "string"
          },
          "retry_at": {
            "description": "When this delivery becomes due again, or null when it landed or has used all 5 attempts. The schedule after the first attempt is 60, 300, 1800, 7200 seconds.",
            "type": [
              "string",
              "null"
            ]
          },
          "status": {
            "description": "The subscriber's status code, or null where no response was had at all. A 4xx or a 5xx is a refusal rather than an absence and is retried the same.",
            "type": [
              "integer",
              "null"
            ]
          },
          "subscription": {
            "type": "string"
          }
        },
        "required": [
          "at",
          "attempt",
          "delivery",
          "endpoint",
          "error",
          "outcome",
          "retry_at",
          "status",
          "subscription"
        ],
        "type": "object"
      },
      "AttemptList": {
        "additionalProperties": false,
        "properties": {
          "attempts": {
            "items": {
              "$ref": "#/components/schemas/Attempt"
            },
            "type": "array"
          }
        },
        "required": [
          "attempts"
        ],
        "type": "object"
      },
      "Change": {
        "additionalProperties": false,
        "description": "One atom change of one published release.",
        "properties": {
          "atom": {
            "description": "The obligation atom that moved.",
            "type": "string"
          },
          "change": {
            "description": "The manifest's own word: added, changed, removed or deprecated.",
            "type": "string"
          },
          "citation": {
            "description": "The change's citation, copied out of the release's diff manifest unaltered. Its shape is the release plane's — an eId, the quoted text and the character span it occupies in the source — and this service restates none of it.",
            "type": "object"
          },
          "classification": {
            "description": "Substantive or editorial, copied from the manifest. The release plane decided it from the amending act's own recitals, and this service does not re-decide it: a second reading would eventually disagree with the one the release shipped, and a consumer could not tell which was the release's answer.",
            "type": "string"
          },
          "classification_basis": {
            "description": "Why the classification is what it is, again the manifest's own.",
            "type": "string"
          },
          "detail": {
            "description": "What the change was, as the manifest states it: the atom before and after, and the amending instruction responsible.",
            "type": "object"
          },
          "facets": {
            "additionalProperties": false,
            "description": "What a filter is written against. The manifest states none of it: `entity_types` is the atom's `applies_to` and `jurisdiction` is that of the unit the atom cites, both read out of the release's own corpus bundle once at publication and stored — so a feed request is a query, and the feed keeps saying what it said after the checkout has moved on.",
            "properties": {
              "entity_types": {
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              "jurisdiction": {
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              "regime": {
                "type": "string"
              },
              "topics": {
                "items": {
                  "type": "string"
                },
                "type": "array"
              }
            },
            "required": [
              "entity_types",
              "jurisdiction",
              "regime",
              "topics"
            ],
            "type": "object"
          },
          "id": {
            "description": "`openregs:<regime>@<tag>:<atom id>` — permanent, and a function of the release and the atom, so re-announcing a release cannot make an old change look new. A reader that has seen this id has seen this change.",
            "type": "string"
          }
        },
        "required": [
          "atom",
          "change",
          "citation",
          "classification",
          "classification_basis",
          "detail",
          "facets",
          "id"
        ],
        "type": "object"
      },
      "Delivery": {
        "additionalProperties": false,
        "description": "One payload owed to one subscriber. The payload bytes are not in it: they are in the store, and what a reader of this endpoint needs is the state and the schedule.",
        "properties": {
          "attempts": {
            "description": "How many attempts have been made.",
            "type": "integer"
          },
          "created_at": {
            "type": "string"
          },
          "event": {
            "enum": [
              "release.published"
            ],
            "type": "string"
          },
          "id": {
            "description": "A function of the subscription and the payload bytes, so it is stable across a re-announcement and worth deduplicating on. It travels on the `X-OpenRegs-Delivery` header of the POST.",
            "type": "string"
          },
          "last_error": {
            "type": [
              "string",
              "null"
            ]
          },
          "next_attempt_at": {
            "description": "When it becomes due again; null when it is not waiting.",
            "type": [
              "string",
              "null"
            ]
          },
          "regime": {
            "type": "string"
          },
          "signature": {
            "description": "`sha256=<hex>` over the payload bytes.",
            "type": "string"
          },
          "state": {
            "enum": [
              "delivered",
              "exhausted",
              "pending"
            ],
            "type": "string"
          },
          "subscription": {
            "type": "string"
          },
          "tag": {
            "type": "string"
          }
        },
        "required": [
          "attempts",
          "created_at",
          "event",
          "id",
          "last_error",
          "next_attempt_at",
          "regime",
          "signature",
          "state",
          "subscription",
          "tag"
        ],
        "type": "object"
      },
      "DeliveryList": {
        "additionalProperties": false,
        "properties": {
          "deliveries": {
            "items": {
              "$ref": "#/components/schemas/Delivery"
            },
            "type": "array"
          }
        },
        "required": [
          "deliveries"
        ],
        "type": "object"
      },
      "DiffManifest": {
        "description": "The release's own diff manifest, served verbatim. Its shape belongs to the release plane, which is why this schema is open: it names the members a client needs to navigate and leaves the rest to the artifact.",
        "properties": {
          "changes": {
            "description": "Every change the release reported, atom and unit alike. The feed fans out only the atom ones; both are here, because this is the manifest and not a projection of it.",
            "type": "array"
          },
          "regime": {
            "type": "string"
          },
          "tag": {
            "type": "string"
          }
        },
        "required": [
          "changes",
          "regime",
          "tag"
        ],
        "type": "object"
      },
      "Error": {
        "additionalProperties": false,
        "description": "The one refusal shape this service uses, on every failure path it owns. It carries a sentence and nothing else: no code to branch on and no correlation id, which is a real difference from the serving API and is stated here rather than left for a client to discover. The one refusal not in this shape is the standard library's 501 for a method the handler implements nothing for, which is HTML and belongs to no route.",
        "properties": {
          "error": {
            "description": "What went wrong, in words a caller can act on.",
            "type": "string"
          }
        },
        "required": [
          "error"
        ],
        "type": "object"
      },
      "Fanout": {
        "additionalProperties": false,
        "description": "What one publication announced, owed, and managed to hand over.",
        "properties": {
          "attempts": {
            "description": "The first attempt at each delivery, made inline.",
            "items": {
              "$ref": "#/components/schemas/Attempt"
            },
            "type": "array"
          },
          "deliveries": {
            "description": "Created by this publication, in subscription order.",
            "items": {
              "$ref": "#/components/schemas/Delivery"
            },
            "type": "array"
          },
          "publication": {
            "$ref": "#/components/schemas/Publication"
          },
          "silent": {
            "description": "Subscriptions this release matched nothing for, and which were therefore not written to. Named rather than omitted, because 'nobody heard' and 'this one did not' are different answers.",
            "items": {
              "type": "string"
            },
            "type": "array"
          }
        },
        "required": [
          "attempts",
          "deliveries",
          "publication",
          "silent"
        ],
        "type": "object"
      },
      "FeedIndex": {
        "additionalProperties": false,
        "properties": {
          "base_url": {
            "description": "What the links in this instance's feeds are written against. It is the bound address unless `--base-url` said otherwise, which is what a deployment behind a proxy sets.",
            "type": "string"
          },
          "feeds": {
            "additionalProperties": false,
            "properties": {
              "json": {
                "type": "string"
              },
              "rss": {
                "type": "string"
              }
            },
            "required": [
              "json",
              "rss"
            ],
            "type": "object"
          },
          "filters": {
            "description": "The dimensions a filter may name.",
            "items": {
              "enum": [
                "jurisdiction",
                "regime",
                "topic",
                "entity_type"
              ],
              "type": "string"
            },
            "type": "array"
          },
          "service": {
            "const": "openregs-feed",
            "type": "string"
          },
          "signature": {
            "additionalProperties": false,
            "description": "How to verify a delivery, said where a subscriber will look.",
            "properties": {
              "algorithm": {
                "type": "string"
              },
              "header": {
                "const": "X-OpenRegs-Signature",
                "type": "string"
              },
              "over": {
                "type": "string"
              }
            },
            "required": [
              "algorithm",
              "header",
              "over"
            ],
            "type": "object"
          }
        },
        "required": [
          "base_url",
          "feeds",
          "filters",
          "service",
          "signature"
        ],
        "type": "object"
      },
      "Filter": {
        "additionalProperties": false,
        "description": "The four dimensions a subscription or a feed request may narrow on, each a sorted list of terms. A dimension left empty matches everything; a dimension with terms matches a change carrying any of them, and every named dimension must match — so a filter is an and of ors.\n\n`entity_type` is the one dimension where membership is not the test: the taxonomy's `parent` relation decides, so an obligation declared on a broader type reaches a subscription for a narrower one and not the other way round. `topic` matches nothing today, because no atom in the corpus carries a subject vocabulary yet; it is wired rather than removed so that subscriptions written against it start matching the day one lands.",
        "properties": {
          "entity_type": {
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "jurisdiction": {
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "regime": {
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "topic": {
            "items": {
              "type": "string"
            },
            "type": "array"
          }
        },
        "required": [
          "jurisdiction",
          "regime",
          "topic",
          "entity_type"
        ],
        "type": "object"
      },
      "JsonFeed": {
        "description": "A JSON Feed 1.1 document. Its members are the JSON Feed specification's and are not restated here; the schema names `version` and `items` because a client needs them to navigate, and describes the `_openregs` member because that one is this project's, in the `_`-prefixed namespace the specification reserves for extensions.",
        "properties": {
          "_openregs": {
            "additionalProperties": false,
            "description": "The filter this document was rendered for, echoed back.",
            "properties": {
              "filter": {
                "$ref": "#/components/schemas/Filter"
              },
              "schema_version": {
                "const": 1,
                "type": "integer"
              }
            },
            "required": [
              "filter",
              "schema_version"
            ],
            "type": "object"
          },
          "items": {
            "items": {
              "$ref": "#/components/schemas/JsonFeedItem"
            },
            "type": "array"
          },
          "version": {
            "const": "https://jsonfeed.org/version/1.1",
            "type": "string"
          }
        },
        "required": [
          "items",
          "version"
        ],
        "type": "object"
      },
      "JsonFeedItem": {
        "description": "One JSON Feed item. `title`, `content_text`, `date_published` and `tags` are the specification's members, carried as it defines them.",
        "properties": {
          "_openregs": {
            "additionalProperties": false,
            "description": "The change in the same shape a webhook payload states it, so a consumer that polls the feed and a consumer that is pushed to read one document and not two.",
            "properties": {
              "change": {
                "$ref": "#/components/schemas/Change"
              },
              "release": {
                "$ref": "#/components/schemas/ReleaseRef"
              }
            },
            "required": [
              "change",
              "release"
            ],
            "type": "object"
          },
          "id": {
            "description": "The feed item id; see `Change.id`.",
            "type": "string"
          },
          "url": {
            "description": "The release's diff manifest on this service, with the atom id as the fragment. A link and not a decoration: it resolves.",
            "type": "string"
          }
        },
        "required": [
          "id",
          "url"
        ],
        "type": "object"
      },
      "Publication": {
        "additionalProperties": false,
        "description": "One release this service has announced.",
        "properties": {
          "as_of": {
            "type": "string"
          },
          "built_at": {
            "type": "string"
          },
          "changes": {
            "description": "How many atom changes were announced.",
            "type": "integer"
          },
          "commit": {
            "type": "string"
          },
          "diff_manifest": {
            "type": "string"
          },
          "published_at": {
            "description": "When this service was told about the release. A genuine event, and the reason it is stored and not what an item is dated by.",
            "type": "string"
          },
          "regime": {
            "type": "string"
          },
          "tag": {
            "type": "string"
          }
        },
        "required": [
          "as_of",
          "built_at",
          "changes",
          "commit",
          "diff_manifest",
          "published_at",
          "regime",
          "tag"
        ],
        "type": "object"
      },
      "PublishRequest": {
        "description": "Which built release to announce. A member this operation does not read is ignored rather than refused — see the operation's request body.",
        "properties": {
          "regime": {
            "description": "The regime whose release this is.",
            "type": "string"
          },
          "tag": {
            "description": "The release tag, as it was built.",
            "type": "string"
          }
        },
        "required": [
          "regime",
          "tag"
        ],
        "type": "object"
      },
      "ReleaseRef": {
        "additionalProperties": false,
        "description": "The release a change was announced from.",
        "properties": {
          "as_of": {
            "description": "The date the release's texts speak from.",
            "type": "string"
          },
          "built_at": {
            "description": "That commit's committer date, and every date this feed publishes. A feed entry is content, and content derived from a commit is dated by it.",
            "type": "string"
          },
          "commit": {
            "description": "The canon commit it was compiled from.",
            "type": "string"
          },
          "diff_manifest": {
            "description": "The manifest filename inside the release directory.",
            "type": "string"
          },
          "regime": {
            "type": "string"
          },
          "tag": {
            "type": "string"
          }
        },
        "required": [
          "as_of",
          "built_at",
          "commit",
          "diff_manifest",
          "regime",
          "tag"
        ],
        "type": "object"
      },
      "RssFeed": {
        "description": "An RSS 2.0 document with an Atom self-link. RSS is a specified format and is not restated here.",
        "type": "string"
      },
      "Subscription": {
        "additionalProperties": false,
        "description": "A registered subscription. The signing secret is deliberately not a property of this schema, because it is in no response.",
        "properties": {
          "active": {
            "description": "Whether a publication fans out to it.",
            "type": "boolean"
          },
          "created_at": {
            "description": "When the registration happened — a real moment, and one of the two things in this service that a commit cannot date.",
            "type": "string"
          },
          "endpoint": {
            "description": "Where a matching change is POSTed.",
            "type": "string"
          },
          "filter": {
            "$ref": "#/components/schemas/Filter"
          },
          "id": {
            "description": "Minted as `sub-0001` when none was given.",
            "type": "string"
          },
          "name": {
            "description": "Defaults to the endpoint.",
            "type": "string"
          }
        },
        "required": [
          "active",
          "created_at",
          "endpoint",
          "filter",
          "id",
          "name"
        ],
        "type": "object"
      },
      "SubscriptionCreated": {
        "additionalProperties": false,
        "properties": {
          "subscription": {
            "$ref": "#/components/schemas/Subscription"
          }
        },
        "required": [
          "subscription"
        ],
        "type": "object"
      },
      "SubscriptionList": {
        "additionalProperties": false,
        "properties": {
          "subscriptions": {
            "items": {
              "$ref": "#/components/schemas/Subscription"
            },
            "type": "array"
          }
        },
        "required": [
          "subscriptions"
        ],
        "type": "object"
      },
      "SubscriptionRegistration": {
        "description": "What a registration states. A member this operation does not read is ignored rather than refused; the one thing inside it that *is* refused is a filter naming something that is not a dimension.",
        "properties": {
          "active": {
            "description": "Defaults to true.",
            "type": "boolean"
          },
          "endpoint": {
            "description": "An http(s) URL. Cleartext `http` only to loopback — see the operation's description for why.",
            "type": "string"
          },
          "filter": {
            "description": "A filter as a caller writes it. Any dimension may be omitted, and a bare string is read as a one-element list.\n\nA key that is not a dimension is **refused**, not ignored — this is the one place in a request body where that is true, and it is true because a subscriber who misspells a dimension and is quietly registered for everything has been widened without being told. The schema is left open rather than closed because the service reads a plural by stripping trailing letters, so the exact set of spellings it tolerates is wider than the eight named here, and a schema claiming a refusal the service does not make would be the first false sentence in this document.",
            "properties": {
              "entity_type": {
                "description": "One `entity_type` term or a list of them. Both the singular and the plural spelling are read.",
                "oneOf": [
                  {
                    "type": "string"
                  },
                  {
                    "items": {
                      "type": "string"
                    },
                    "type": "array"
                  }
                ]
              },
              "entity_types": {
                "description": "One `entity_type` term or a list of them. Both the singular and the plural spelling are read.",
                "oneOf": [
                  {
                    "type": "string"
                  },
                  {
                    "items": {
                      "type": "string"
                    },
                    "type": "array"
                  }
                ]
              },
              "jurisdiction": {
                "description": "One `jurisdiction` term or a list of them. Both the singular and the plural spelling are read.",
                "oneOf": [
                  {
                    "type": "string"
                  },
                  {
                    "items": {
                      "type": "string"
                    },
                    "type": "array"
                  }
                ]
              },
              "jurisdictions": {
                "description": "One `jurisdiction` term or a list of them. Both the singular and the plural spelling are read.",
                "oneOf": [
                  {
                    "type": "string"
                  },
                  {
                    "items": {
                      "type": "string"
                    },
                    "type": "array"
                  }
                ]
              },
              "regime": {
                "description": "One `regime` term or a list of them. Both the singular and the plural spelling are read.",
                "oneOf": [
                  {
                    "type": "string"
                  },
                  {
                    "items": {
                      "type": "string"
                    },
                    "type": "array"
                  }
                ]
              },
              "regimes": {
                "description": "One `regime` term or a list of them. Both the singular and the plural spelling are read.",
                "oneOf": [
                  {
                    "type": "string"
                  },
                  {
                    "items": {
                      "type": "string"
                    },
                    "type": "array"
                  }
                ]
              },
              "topic": {
                "description": "One `topic` term or a list of them. Both the singular and the plural spelling are read.",
                "oneOf": [
                  {
                    "type": "string"
                  },
                  {
                    "items": {
                      "type": "string"
                    },
                    "type": "array"
                  }
                ]
              },
              "topics": {
                "description": "One `topic` term or a list of them. Both the singular and the plural spelling are read.",
                "oneOf": [
                  {
                    "type": "string"
                  },
                  {
                    "items": {
                      "type": "string"
                    },
                    "type": "array"
                  }
                ]
              }
            },
            "type": "object"
          },
          "id": {
            "description": "Minted when absent; must not already exist.",
            "type": "string"
          },
          "name": {
            "description": "Defaults to the endpoint.",
            "type": "string"
          },
          "secret": {
            "description": "The key deliveries to this endpoint are signed with. Held by this service and by the subscriber, and returned by no response.",
            "type": "string"
          }
        },
        "required": [
          "endpoint",
          "secret"
        ],
        "type": "object"
      },
      "SubscriptionRemoved": {
        "additionalProperties": false,
        "properties": {
          "removed": {
            "description": "The id that is now gone.",
            "type": "string"
          }
        },
        "required": [
          "removed"
        ],
        "type": "object"
      },
      "WebhookPayload": {
        "additionalProperties": false,
        "description": "The document POSTed to a subscriber. Its bytes are a function of the release and the filter — sorted keys, two-space indent, one trailing newline — which is what makes the signature reproducible and the delivery id stable.",
        "properties": {
          "changes": {
            "description": "Only the changes this subscription's filter matched.",
            "items": {
              "$ref": "#/components/schemas/Change"
            },
            "type": "array"
          },
          "counts": {
            "additionalProperties": false,
            "properties": {
              "changes": {
                "type": "integer"
              }
            },
            "required": [
              "changes"
            ],
            "type": "object"
          },
          "event": {
            "const": "release.published",
            "type": "string"
          },
          "filter": {
            "$ref": "#/components/schemas/Filter"
          },
          "release": {
            "$ref": "#/components/schemas/ReleaseRef"
          },
          "schema_version": {
            "const": 1,
            "type": "integer"
          },
          "subscription": {
            "description": "Which subscription this delivery answers.",
            "type": "string"
          }
        },
        "required": [
          "changes",
          "counts",
          "event",
          "filter",
          "release",
          "schema_version",
          "subscription"
        ],
        "type": "object"
      }
    }
  },
  "info": {
    "description": "The outbound side of a release. When a release is built, its diff manifest says which obligation atoms were added, changed, removed or deprecated and whether each change was substantive or editorial. This service is how a downstream system learns that without watching a repository: it serves those changes as a JSON Feed and an RSS feed, and it POSTs the ones a subscription asked for to that subscription's endpoint, signed and retried.\n\n**It is a different server from the serving API.** That one answers questions over a pinned release on its own port and has its own description in `openapi.json`. This one has its own port, its own store and its own lifecycle. It touches the checkout it was started over in exactly two places — when a release is announced, and when a feed item's link is followed back to that release's diff manifest — and everything else it answers comes out of its own store. It reads no canon, no git history and nothing off the network.\n\n**It re-decides nothing.** The changes, and their substantive-or-editorial classification, are copied out of the release's own diff manifest verbatim. The two things this service adds — which entity types a change binds and which jurisdiction it belongs to — come from the release's own corpus bundle and are resolved once, at publication, because they are what a subscription filters on.\n\n**No authentication, and that is a deployment decision rather than an oversight.** Everything it serves is already public; the one thing it holds that is not — the subscription signing secrets — is in no response. It binds loopback unless told otherwise and is meant to sit behind whatever terminates TLS and authenticates. There is no bearer token, no rate limit and no 401 anywhere in this document.\n\n**Every refusal is `{\"error\": \"<sentence>\"}`** — a message and no code to branch on, and no correlation header. That is a real difference from the serving API and is stated here rather than left to be discovered.\n\n**A method a path does not answer is 404, not 405.** Dispatch resolves a path first and a verb second, so a path nothing declares and a verb no route on that path answers are one refusal. A verb the server implements no handler for at all — anything but GET, POST and DELETE — is the standard library's 501 in HTML, which belongs to no operation and appears under none.\n\n**The webhook is the other half of this API.** It is declared under `webhooks`: a request your endpoint receives rather than one you make, with the headers to verify it by. A signature proves who sent the payload, not what the law says — a handler's second step is `openregs pull`, which verifies the release's own signature chain before anything is used.\n\nThe engine is Apache-2.0; the corpus a release carries is CC-BY-4.0.",
    "license": {
      "identifier": "Apache-2.0",
      "name": "Apache-2.0"
    },
    "summary": "Every regulatory change as a diff, served as a feed and pushed as a webhook.",
    "title": "OpenRegs feed service",
    "version": "0.1.0"
  },
  "jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
  "openapi": "3.1.0",
  "paths": {
    "/": {
      "get": {
        "description": "The root document. It names the two feed URLs written against this instance's own base URL, the dimensions a filter may use, and the header and algorithm a webhook signature travels in — so a subscriber can be configured from one request rather than from a runbook.\n\nIt is a function of the base URL and of this build's constants, and reads no state: an instance answers it before anything has been published.",
        "operationId": "getFeedIndex",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FeedIndex"
                }
              }
            },
            "description": "What this instance is and where the rest of it is."
          }
        },
        "summary": "What this service is, where its feeds are, and how a delivery is signed",
        "tags": [
          "feeds"
        ]
      }
    },
    "/deliveries": {
      "get": {
        "description": "Every delivery this service has created, oldest first: `pending` while it is still owed an attempt, `delivered` once a subscriber answered 2xx, and `exhausted` once it has used all 5 attempts. The payload bytes are not in the response — they are in the store, and what a reader needs here is the signature, the state and when the next attempt is due.",
        "operationId": "listDeliveries",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeliveryList"
                }
              }
            },
            "description": "Every delivery and its state."
          }
        },
        "summary": "What is owed, and what state each delivery is in",
        "tags": [
          "deliveries"
        ]
      }
    },
    "/deliveries/retry": {
      "post": {
        "description": "Attempts every pending delivery whose backoff has expired, oldest first, and returns one log line per attempt. Nothing is due before its backoff expires, so calling this in a loop does not shorten the schedule; an empty `attempts` list means nothing was owed yet, not that nothing is owed.\n\nThe same code path a publication's first attempt takes and the same one `openregs feed drain` takes, so a retry is never a second implementation of a delivery.\n\n**It takes no body.** A POST with nothing in it is what this operation is; a body sent anyway is read and ignored, which is why none is described.",
        "operationId": "retryDeliveries",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AttemptList"
                }
              }
            },
            "description": "One line per attempt made by this pass."
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "description": "a delivery that came due is owed to a subscription that has since been unregistered. The pass stops there rather than guessing where the payload should have gone"
          }
        },
        "summary": "The retry pass: attempt everything now due",
        "tags": [
          "deliveries"
        ]
      }
    },
    "/feed.json": {
      "get": {
        "description": "A JSON Feed 1.1 document (`https://jsonfeed.org/version/1.1`), one item per atom change of every release this service has published that the query string admits. The format is the JSON Feed specification's and is not restated by this project; what is this project's is the `_openregs` member the spec reserves for extensions, which carries the change and the release it came from so a reader does not have to parse the prose back out of `content_text`.\n\nEvery date in the document is the release's own build stamp, which is the committer date of the commit it was compiled from. No clock is read, so two services that have published the same releases render identical bytes.\n\nA release that was built but never announced to this service is not in the feed: a feed is a log of publications and not a walk of a checkout.",
        "operationId": "getJsonFeed",
        "parameters": [
          {
            "description": "Only changes whose `jurisdiction` is one of the values given. Repeat the key to mean 'or'; leave it out to match every `jurisdiction`.",
            "explode": true,
            "in": "query",
            "name": "jurisdiction",
            "required": false,
            "schema": {
              "items": {
                "type": "string"
              },
              "type": "array"
            },
            "style": "form"
          },
          {
            "description": "Only changes whose `regime` is one of the values given. Repeat the key to mean 'or'; leave it out to match every `regime`.",
            "explode": true,
            "in": "query",
            "name": "regime",
            "required": false,
            "schema": {
              "items": {
                "type": "string"
              },
              "type": "array"
            },
            "style": "form"
          },
          {
            "description": "Only changes whose `topic` is one of the values given. Repeat the key to mean 'or'; leave it out to match every `topic`.",
            "explode": true,
            "in": "query",
            "name": "topic",
            "required": false,
            "schema": {
              "items": {
                "type": "string"
              },
              "type": "array"
            },
            "style": "form"
          },
          {
            "description": "Only changes whose `entity_type` is one of the values given. Repeat the key to mean 'or'; leave it out to match every `entity_type`.",
            "explode": true,
            "in": "query",
            "name": "entity_type",
            "required": false,
            "schema": {
              "items": {
                "type": "string"
              },
              "type": "array"
            },
            "style": "form"
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/feed+json": {
                "schema": {
                  "$ref": "#/components/schemas/JsonFeed"
                }
              }
            },
            "description": "The matching changes, newest release first."
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "description": "the query names a key that is not one of the four filter dimensions. An unknown dimension is refused rather than ignored, because a subscriber who misspells one and is quietly sent everything has been widened without being told"
          }
        },
        "summary": "The published atom changes as a JSON Feed, narrowed by the query string",
        "tags": [
          "feeds"
        ]
      }
    },
    "/feed.xml": {
      "get": {
        "description": "The same items as the JSON feed, in the same order, with the same links, rendered as RSS 2.0 with the Atom self-link a validator asks for. RSS is a specified format and is not restated here; each item's `guid` is the feed item id, `link` is the diff manifest at the change, and `category` carries the change's facets as flat `name:value` labels.\n\nItem dates are RFC 822, which is what RSS takes, and are the same release build stamp the JSON feed spells as RFC 3339.",
        "operationId": "getRssFeed",
        "parameters": [
          {
            "description": "Only changes whose `jurisdiction` is one of the values given. Repeat the key to mean 'or'; leave it out to match every `jurisdiction`.",
            "explode": true,
            "in": "query",
            "name": "jurisdiction",
            "required": false,
            "schema": {
              "items": {
                "type": "string"
              },
              "type": "array"
            },
            "style": "form"
          },
          {
            "description": "Only changes whose `regime` is one of the values given. Repeat the key to mean 'or'; leave it out to match every `regime`.",
            "explode": true,
            "in": "query",
            "name": "regime",
            "required": false,
            "schema": {
              "items": {
                "type": "string"
              },
              "type": "array"
            },
            "style": "form"
          },
          {
            "description": "Only changes whose `topic` is one of the values given. Repeat the key to mean 'or'; leave it out to match every `topic`.",
            "explode": true,
            "in": "query",
            "name": "topic",
            "required": false,
            "schema": {
              "items": {
                "type": "string"
              },
              "type": "array"
            },
            "style": "form"
          },
          {
            "description": "Only changes whose `entity_type` is one of the values given. Repeat the key to mean 'or'; leave it out to match every `entity_type`.",
            "explode": true,
            "in": "query",
            "name": "entity_type",
            "required": false,
            "schema": {
              "items": {
                "type": "string"
              },
              "type": "array"
            },
            "style": "form"
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/rss+xml": {
                "schema": {
                  "$ref": "#/components/schemas/RssFeed"
                }
              }
            },
            "description": "The matching changes as an RSS channel."
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "description": "the query names a key that is not one of the four filter dimensions. An unknown dimension is refused rather than ignored, because a subscriber who misspells one and is quietly sent everything has been widened without being told"
          }
        },
        "summary": "The same entries as RSS 2.0, for everything that already reads RSS",
        "tags": [
          "feeds"
        ]
      }
    },
    "/log": {
      "get": {
        "description": "Every attempt ever made, oldest first, with the endpoint it went to, the status or the error it came back with, and when the delivery becomes due again. This is the retry's audit trail and the reason the attempts are a table rather than lines on a console.\n\nIt is not the access log. Requests to this service are not logged at all.",
        "operationId": "readDeliveryLog",
        "parameters": [
          {
            "description": "Only attempts at this delivery id. An id no delivery has is not an error; it matches nothing.",
            "in": "query",
            "name": "delivery",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AttemptList"
                }
              }
            },
            "description": "The attempt log, oldest first."
          }
        },
        "summary": "The service log: every attempt at every delivery",
        "tags": [
          "deliveries"
        ]
      }
    },
    "/publish": {
      "post": {
        "description": "Reads `regimes/<regime>/releases/<tag>/<tag>.diff.json` out of the checkout this service was started over, records its atom changes as feed entries, owes one signed payload to every subscription whose filter matches, and then attempts each of them inline — so a caller learns what happened rather than being told 'accepted' and left to poll.\n\nUnit changes in the manifest are not fanned out. They are the mechanism of a change rather than the change a downstream system acts on, and they stay one click away in the diff each item links to.\n\n**Announcing the same release twice is not news twice.** A delivery's id is derived from the subscription and the payload bytes, so a second publication of a release creates no second delivery and the response lists none.",
        "operationId": "publishRelease",
        "requestBody": {
          "content": {
            "application/json": {
              "example": {
                "regime": "fixreg",
                "tag": "2025.04"
              },
              "schema": {
                "$ref": "#/components/schemas/PublishRequest"
              }
            }
          },
          "description": "A JSON object; anything else, or nothing at all, is refused. **A member the operation does not read is ignored rather than refused**, which is the opposite of what the serving API does with an unknown field and is stated here because a caller who misspells one is answered rather than corrected. The whole body is read into memory before it is parsed, so put a proxy in front of anything that is not trusted to be small.",
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fanout"
                }
              }
            },
            "description": "What was announced, what it owes, and how the first attempt at each went."
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "description": "the body is not a JSON object or states no `regime` or `tag`; or that release is not built in this checkout, carries no `release-meta.yaml` or no diff manifest, or its manifest states a different release than the one asked for"
          }
        },
        "summary": "Announce a built release, and attempt every delivery it creates",
        "tags": [
          "deliveries"
        ]
      }
    },
    "/releases/{regime}/{tag}/diff.json": {
      "get": {
        "description": "Serves `regimes/<regime>/releases/<tag>/<tag>.diff.json` unchanged. This is why a feed item's link is a link: following one lands on the manifest the entry was read out of, at the change, rather than on a page about it.\n\nThe bytes are the release plane's and this service alters none of them, so the document's shape is the manifest's own — described here only as far as a client needs to find its way around it.",
        "operationId": "getReleaseDiffManifest",
        "parameters": [
          {
            "description": "The regime id, as the release directory spells it.",
            "in": "path",
            "name": "regime",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "The release tag, as the release directory spells it.",
            "in": "path",
            "name": "tag",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DiffManifest"
                }
              }
            },
            "description": "The manifest as the release shipped it."
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "description": "that release has not been published to this service, or its manifest is not where the publication recorded it. It is 400 rather than 404 because the path is one this service routes and the release named in it is the part that is wrong"
          }
        },
        "summary": "The release's own diff manifest, byte for byte as it shipped",
        "tags": [
          "deliveries"
        ]
      }
    },
    "/subscriptions": {
      "get": {
        "description": "Every registered subscription, oldest first. The signing secret is the one thing this service holds that is not already public and it is in no response: a secret a service echoes back is a secret one leaked request loses. Read it out of the store with `sqlite3` if an operator has to.",
        "operationId": "listSubscriptions",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SubscriptionList"
                }
              }
            },
            "description": "The registered subscriptions, secrets withheld."
          }
        },
        "summary": "What is registered — never the secrets",
        "tags": [
          "subscriptions"
        ]
      },
      "post": {
        "description": "Registers an endpoint to POST matching changes to. `endpoint` and `secret` are required; an `id` is minted when none is given, and `name` defaults to the endpoint.\n\n**The endpoint is checked here, not only at delivery.** It must be an http(s) URL, and cleartext `http` is admitted only to loopback, where a subscriber is a sidecar with no hop to wiretap. A delivery carries both what changed — which is metadata about what an organisation is watching — and the signature that says the bytes are this service's, so registering an endpoint every delivery to it would refuse is refused while somebody can still do something about it.",
        "operationId": "registerSubscription",
        "requestBody": {
          "content": {
            "application/json": {
              "example": {
                "endpoint": "https://subscriber.example.invalid/hooks/openregs",
                "filter": {
                  "entity_type": [
                    "operator"
                  ],
                  "regime": [
                    "fixreg"
                  ]
                },
                "name": "downstream-ci",
                "secret": "the-shared-secret-this-subscriber-verifies-with"
              },
              "schema": {
                "$ref": "#/components/schemas/SubscriptionRegistration"
              }
            }
          },
          "description": "A JSON object; anything else, or nothing at all, is refused. **A member the operation does not read is ignored rather than refused**, which is the opposite of what the serving API does with an unknown field and is stated here because a caller who misspells one is answered rather than corrected. The whole body is read into memory before it is parsed, so put a proxy in front of anything that is not trusted to be small.",
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SubscriptionCreated"
                }
              }
            },
            "description": "The subscription as registered, without its secret."
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "description": "the body is not a JSON object, states no `endpoint` or no `secret`, names a filter dimension that does not exist, gives an endpoint that is not an http(s) URL or is cleartext off loopback, or reuses an id that is already registered"
          }
        },
        "summary": "Register one webhook endpoint, its secret and the filter it wants",
        "tags": [
          "subscriptions"
        ]
      }
    },
    "/subscriptions/{subscription_id}": {
      "delete": {
        "description": "Removes the subscription. Deliveries already owed to it are **not** removed — the log has to keep saying what was owed and to whom — so a delivery still pending when its subscription goes away is refused by the next retry pass rather than sent somewhere.",
        "operationId": "unregisterSubscription",
        "parameters": [
          {
            "description": "The id the registration returned.",
            "in": "path",
            "name": "subscription_id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SubscriptionRemoved"
                }
              }
            },
            "description": "The subscription is gone; the id it had."
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "description": "no subscription of that id is registered"
          }
        },
        "summary": "Stop sending to one subscription",
        "tags": [
          "subscriptions"
        ]
      }
    }
  },
  "servers": [
    {
      "description": "Where `openregs feed serve` binds unless it is told otherwise: loopback, because a service holding subscription secrets and authenticating nobody must not be reachable from off the machine by default. A deployment sets --host and --base-url, and the base URL is what the links inside the feeds are written against.",
      "url": "http://127.0.0.1:8787"
    }
  ],
  "tags": [
    {
      "description": "Announcing a release, what it owes, and the log of every attempt at handing it over.",
      "name": "deliveries"
    },
    {
      "description": "Reading the published changes: the root document and the two feed formats.",
      "name": "feeds"
    },
    {
      "description": "Registering an endpoint to be told, and stopping.",
      "name": "subscriptions"
    }
  ],
  "webhooks": {
    "release.published": {
      "post": {
        "description": "**This is a request your endpoint receives, not one you make.** It is sent to the `endpoint` a subscription registered, carrying only the changes that subscription's filter matched.\n\n**Verify it.** `X-OpenRegs-Signature` is `sha256=<hex>`, an HMAC keyed with the subscription's secret over the exact bytes of the request body. Verify against the bytes that arrived rather than against a re-serialisation of the parsed document: any re-render is a chance for the two to differ.\n\n**It proves who sent the payload, not what the law says.** A handler's second step is `openregs pull`, which verifies the release's own signature chain before anything is used. The split is deliberate: this is a notification, and the evidence is the release.\n\n**At-least-once.** A retry is the same delivery — the same bytes, the same signature and the same `X-OpenRegs-Delivery` — so deduplicate on that id. Answer 2xx to accept; anything else, or no answer at all, is retried 4 more times at 60, 300, 1800, 7200 seconds and then given up on. A subscriber that missed every notification catches up by pulling the tag: releases are immutable and addressable forever.",
        "operationId": "receiveReleasePublished",
        "parameters": [
          {
            "description": "This delivery's id. Stable across retries, so it is what a subscriber deduplicates on.",
            "in": "header",
            "name": "X-OpenRegs-Delivery",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "The event that raised it. `release.published` today.",
            "in": "header",
            "name": "X-OpenRegs-Event",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "`sha256=<hex>` — the HMAC over the exact request body bytes.",
            "in": "header",
            "name": "X-OpenRegs-Signature",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Which subscription this delivery answers.",
            "in": "header",
            "name": "X-OpenRegs-Subscription",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookPayload"
              }
            }
          },
          "required": true
        },
        "responses": {
          "2XX": {
            "description": "Any 2xx accepts the delivery and ends it. A 3xx is not followed and is recorded as a failure: this service would otherwise reissue the POST as a GET with no body, so the payload would arrive nowhere while the signature header arrived somewhere the subscription never named. A subscriber that moved re-registers."
          }
        },
        "summary": "A release was published, and these are the changes you asked for"
      }
    }
  },
  "x-openregs-generator": "tooling/ci/dump_feed_openapi.py",
  "x-openregs-source": "tooling/openregs/feed/routes.py"
}
