{
  "openapi": "3.1.0",
  "x-generated-by": "Generated by scripts/generators/build-openapi.mjs from registry/routers.json. Do not hand-edit.",
  "info": {
    "title": "NotifHQ API",
    "version": "1.0.0",
    "summary": "One API for governed customer communications.",
    "description": "Send, receive, govern, search and prove every customer communication.\n\nThe surface is deliberately small; the machine underneath is not. A `POST /v1/notify` with three fields runs contract resolution, schema validation, classification, policy evaluation, preference and consent resolution, template selection, routing, dispatch, evidence capture, analytics and webhook fan-out.\n\n**Four states, never collapsed.** Requested is not sent. Sent is not delivered. Delivered is not engaged. Every response, event and metric keeps them apart, because the moment they merge you can no longer answer the only question that matters when something goes wrong.\n\n**No transport detail crosses this boundary.** You see `channel: email` and `provider: managed`. Which infrastructure carries the message is ours to change without breaking you.",
    "contact": {
      "name": "NotifHQ Support",
      "url": "https://notifhq.com/support",
      "email": "support@notifhq.com"
    },
    "license": {
      "name": "Proprietary",
      "url": "https://notifhq.com/terms"
    }
  },
  "servers": [
    {
      "url": "https://api.notifhq.com",
      "description": "Production"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "tags": [
    {
      "name": "core",
      "description": "Included in every plan."
    },
    {
      "name": "alerting",
      "description": "Requires the alerting rail. Absent from the document served to a workspace without it."
    },
    {
      "name": "compliance",
      "description": "Requires the compliance rail. Absent from the document served to a workspace without it."
    },
    {
      "name": "document_intake",
      "description": "Requires the document_intake rail. Absent from the document served to a workspace without it."
    },
    {
      "name": "inbound_email",
      "description": "Requires the inbound_email rail. Absent from the document served to a workspace without it."
    },
    {
      "name": "invoice_intake",
      "description": "Requires the invoice_intake rail. Absent from the document served to a workspace without it."
    },
    {
      "name": "notification",
      "description": "Requires the notification rail. Absent from the document served to a workspace without it."
    },
    {
      "name": "search",
      "description": "Requires the search rail. Absent from the document served to a workspace without it."
    },
    {
      "name": "team_inbox",
      "description": "Requires the team_inbox rail. Absent from the document served to a workspace without it."
    }
  ],
  "paths": {
    "/v1/observations": {
      "post": {
        "summary": "Report a condition true or false",
        "operationId": "recordObservation",
        "description": "The write path of the whole physical rail, and almost all of the work it does is deciding NOT to send a message.\n\nReport every reading. Suppression is the platform's job, not yours: debounce holds a cold-store door open during a delivery below the threshold that would raise, repeat_after stops one unresolved fault producing a message per reading, and flap detection says once that a signal is untrustworthy rather than fifty times that a process is failing.\n\n`holds: false` is how an alert clears, and it matters as much as the true ones: counting only the true reports would make a signal oscillating every thirty seconds look like a steady fault.\n\nThe response always carries `because`. A suppressed occurrence that cannot explain itself is indistinguishable from a dropped one.",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "condition",
                  "subject"
                ],
                "properties": {
                  "condition": {
                    "type": "string",
                    "description": "The condition's external_id."
                  },
                  "subject": {
                    "type": "string",
                    "description": "The subject's external_id."
                  },
                  "holds": {
                    "type": "boolean",
                    "default": true,
                    "description": "false reports that the condition has ended."
                  },
                  "observed_at": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "detail": {
                    "type": "object",
                    "description": "The reading: measured value, threshold, fault code."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Considered. The body says what happened and why.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "outcome": {
                      "enum": [
                        "raise",
                        "fold",
                        "held",
                        "below_threshold",
                        "clear",
                        "ignored"
                      ]
                    },
                    "because": {
                      "type": "string"
                    },
                    "alert": {
                      "type": [
                        "object",
                        "null"
                      ]
                    },
                    "notified": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The condition is scoped to a class this subject is not.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "No such condition or subject in this workspace.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "tags": [
          "alerting"
        ],
        "x-rail": "alerting",
        "x-rail-note": "Absent from the document served to a workspace without this rail; the endpoint returns 404 rail_not_installed."
      }
    },
    "/v1/alerts": {
      "get": {
        "summary": "The alert board",
        "operationId": "listAlerts",
        "description": "Worst first, then newest — the order an operations board is read in.",
        "parameters": [
          {
            "name": "state",
            "in": "query",
            "required": false,
            "description": "raised, acknowledged, cleared or closed. The board defaults to what is still open.",
            "schema": {
              "type": "string",
              "example": "raised"
            }
          },
          {
            "name": "severity",
            "in": "query",
            "required": false,
            "description": "critical, high, medium or low. Narrowing to critical is how an out-of-hours view is built.",
            "schema": {
              "type": "string",
              "example": "critical"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Alerts."
          }
        },
        "tags": [
          "alerting"
        ],
        "x-rail": "alerting",
        "x-rail-note": "Absent from the document served to a workspace without this rail; the endpoint returns 404 rail_not_installed."
      }
    },
    "/v1/alerts/{id}": {
      "get": {
        "summary": "Get an alert",
        "operationId": "getAlert",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "responses": {
          "200": {
            "description": "The alert."
          },
          "404": {
            "description": "No such alert.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "tags": [
          "alerting"
        ],
        "x-rail": "alerting",
        "x-rail-note": "Absent from the document served to a workspace without this rail; the endpoint returns 404 rail_not_installed."
      }
    },
    "/v1/alerts/{id}/acknowledge": {
      "post": {
        "summary": "Acknowledge an alert",
        "operationId": "acknowledgeAlert",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "description": "Says somebody has it. This stops the escalation and NOTHING else: the condition may still hold, and the alert stays open until the source clears it or a person closes it. Treating acknowledgement as resolution is how a board shows green while a freezer is still warming.",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "by": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Acknowledged."
          },
          "400": {
            "description": "The alert cannot move there from where it is.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "tags": [
          "alerting"
        ],
        "x-rail": "alerting",
        "x-rail-note": "Absent from the document served to a workspace without this rail; the endpoint returns 404 rail_not_installed."
      }
    },
    "/v1/alerts/{id}/close": {
      "post": {
        "summary": "Close an alert",
        "operationId": "closeAlert",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "description": "A person ending it without the source clearing it — a false positive, a known fault, equipment out of service. Never merged with cleared: 'the condition went false' and 'somebody decided to stop looking at it' are opposite answers when a regulator asks what happened to an excursion.",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "by": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Closed."
          },
          "400": {
            "description": "The alert cannot move there from where it is.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "tags": [
          "alerting"
        ],
        "x-rail": "alerting",
        "x-rail-note": "Absent from the document served to a workspace without this rail; the endpoint returns 404 rail_not_installed."
      }
    },
    "/v1/alerts/{id}/escalation": {
      "get": {
        "summary": "What the escalation would do next",
        "operationId": "getAlertEscalation",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "description": "Who is next and when, read-only. Exhaustion is its own outcome rather than silence, because an alert nobody ever answered is the most important thing an operations lead can be told.",
        "responses": {
          "200": {
            "description": "The next step.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "kind": {
                      "enum": [
                        "notify",
                        "exhausted",
                        "waiting",
                        "stopped"
                      ]
                    },
                    "because": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        },
        "tags": [
          "alerting"
        ],
        "x-rail": "alerting",
        "x-rail-note": "Absent from the document served to a workspace without this rail; the endpoint returns 404 rail_not_installed."
      }
    },
    "/v1/analytics/summary": {
      "get": {
        "summary": "Headline metrics",
        "operationId": "analyticsSummary",
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AnalyticsSummary"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "Lower bound on the window, inclusive. An ISO date or timestamp.",
            "schema": {
              "type": "string",
              "example": "2026-01-01"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "Upper bound on the window, exclusive.",
            "schema": {
              "type": "string",
              "example": "2026-02-01"
            }
          }
        ],
        "tags": [
          "core"
        ]
      }
    },
    "/v1/analytics/delivery": {
      "get": {
        "summary": "Delivery metrics",
        "operationId": "analyticsDelivery",
        "responses": {
          "200": {
            "description": "OK."
          }
        },
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "Lower bound on the window, inclusive. An ISO date or timestamp.",
            "schema": {
              "type": "string",
              "example": "2026-01-01"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "Upper bound on the window, exclusive.",
            "schema": {
              "type": "string",
              "example": "2026-02-01"
            }
          }
        ],
        "tags": [
          "core"
        ]
      }
    },
    "/v1/analytics/engagement": {
      "get": {
        "summary": "Engagement metrics",
        "description": "Reports the OPEN OBSERVATION rate, not an 'open rate'. A pixel fetch is an observation about a mail client, not a fact about a human.",
        "operationId": "analyticsEngagement",
        "responses": {
          "200": {
            "description": "OK."
          }
        },
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "Lower bound on the window, inclusive. An ISO date or timestamp.",
            "schema": {
              "type": "string",
              "example": "2026-01-01"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "Upper bound on the window, exclusive.",
            "schema": {
              "type": "string",
              "example": "2026-02-01"
            }
          }
        ],
        "tags": [
          "core"
        ]
      }
    },
    "/v1/analytics/templates": {
      "get": {
        "summary": "Template performance",
        "operationId": "analyticsTemplates",
        "responses": {
          "200": {
            "description": "OK."
          }
        },
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "Lower bound on the window, inclusive. An ISO date or timestamp.",
            "schema": {
              "type": "string",
              "example": "2026-01-01"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "Upper bound on the window, exclusive.",
            "schema": {
              "type": "string",
              "example": "2026-02-01"
            }
          }
        ],
        "tags": [
          "core"
        ]
      }
    },
    "/v1/analytics/preferences": {
      "get": {
        "summary": "Preference and opt-out trend",
        "operationId": "analyticsPreferences",
        "responses": {
          "200": {
            "description": "OK."
          }
        },
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "Lower bound on the window, inclusive. An ISO date or timestamp.",
            "schema": {
              "type": "string",
              "example": "2026-01-01"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "Upper bound on the window, exclusive.",
            "schema": {
              "type": "string",
              "example": "2026-02-01"
            }
          }
        ],
        "tags": [
          "core"
        ]
      }
    },
    "/v1/analytics/tickets": {
      "get": {
        "summary": "Ticket metrics",
        "operationId": "analyticsTickets",
        "responses": {
          "200": {
            "description": "OK."
          }
        },
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "Lower bound on the window, inclusive. An ISO date or timestamp.",
            "schema": {
              "type": "string",
              "example": "2026-01-01"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "Upper bound on the window, exclusive.",
            "schema": {
              "type": "string",
              "example": "2026-02-01"
            }
          }
        ],
        "tags": [
          "core"
        ]
      }
    },
    "/v1/analytics/invoices": {
      "get": {
        "summary": "Invoice intake metrics",
        "operationId": "analyticsInvoices",
        "responses": {
          "200": {
            "description": "OK."
          }
        },
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "Lower bound on the window, inclusive. An ISO date or timestamp.",
            "schema": {
              "type": "string",
              "example": "2026-01-01"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "Upper bound on the window, exclusive.",
            "schema": {
              "type": "string",
              "example": "2026-02-01"
            }
          }
        ],
        "tags": [
          "core"
        ]
      }
    },
    "/v1/analytics/timeseries": {
      "get": {
        "summary": "Counts over time",
        "description": "Requested, sent and failed bucketed by hour or day. A dashboard that shows only a total cannot answer 'when did it start', which is the first question anybody asks about a spike.",
        "operationId": "analyticsTimeseries",
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "Lower bound on the window, inclusive.",
            "schema": {
              "type": "string",
              "example": "2026-01-01"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "Upper bound on the window, exclusive.",
            "schema": {
              "type": "string",
              "example": "2026-02-01"
            }
          },
          {
            "name": "bucket",
            "in": "query",
            "required": false,
            "description": "How the window is grouped: day, week or month.",
            "schema": {
              "type": "string",
              "example": "day"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK."
          }
        },
        "tags": [
          "core"
        ]
      }
    },
    "/v1/analytics/issues": {
      "get": {
        "summary": "What needs attention",
        "description": "The things a person should act on: suppressing bounces, failing webhook endpoints, domains whose verification has decayed, and notifications that failed. Derived from live state — an issues list that has to be dismissed is a second inbox nobody reads.",
        "operationId": "analyticsIssues",
        "responses": {
          "200": {
            "description": "OK."
          }
        },
        "tags": [
          "core"
        ]
      }
    },
    "/v1/apps": {
      "get": {
        "summary": "List applications",
        "operationId": "listApps",
        "responses": {
          "200": {
            "description": "OK."
          }
        },
        "tags": [
          "core"
        ]
      },
      "post": {
        "summary": "Create an application",
        "operationId": "createApp",
        "responses": {
          "201": {
            "description": "Created."
          }
        },
        "tags": [
          "core"
        ]
      }
    },
    "/v1/apps/{id}": {
      "get": {
        "summary": "Retrieve an application",
        "operationId": "getApp",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "responses": {
          "200": {
            "description": "OK."
          }
        },
        "tags": [
          "core"
        ]
      }
    },
    "/v1/api-keys": {
      "get": {
        "summary": "List API keys",
        "operationId": "listApiKeys",
        "responses": {
          "200": {
            "description": "OK."
          }
        },
        "tags": [
          "core"
        ]
      },
      "post": {
        "summary": "Create an API key",
        "description": "Scoped to a workspace AND an environment AND a capability set. Shown once.",
        "operationId": "createApiKey",
        "responses": {
          "201": {
            "description": "Created."
          }
        },
        "tags": [
          "core"
        ]
      }
    },
    "/v1/api-keys/{id}": {
      "delete": {
        "summary": "Revoke an API key",
        "operationId": "revokeApiKey",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "responses": {
          "204": {
            "description": "Revoked."
          }
        },
        "tags": [
          "core"
        ]
      }
    },
    "/v1/api-keys/{id}/rotate": {
      "post": {
        "summary": "Replace a key without an outage",
        "description": "Mints a successor carrying the same scopes and holder, records which key it replaced, and leaves the old one working for a grace window so the holder can move over. The columns for this existed from the first migration and nothing ever wrote them: rotation was two columns and no capability, while the registry said a holder could replace a credential without an outage. The secret is shown once.",
        "operationId": "rotateApiKey",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "responses": {
          "201": {
            "description": "The successor, once."
          },
          "404": {
            "description": "No such key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "tags": [
          "core"
        ]
      }
    },
    "/v1/api-keys/revoke": {
      "post": {
        "summary": "Revoke every key held by a thing, or at a place",
        "description": "The two requests that actually arrive in an incident — a device is stolen, a site is decommissioned — and neither could be asked while the only reference to a device was a free-text agent_id. Revokes by subject, by location, or by both. Idempotent.",
        "operationId": "revokeApiKeysBy",
        "responses": {
          "200": {
            "description": "How many were revoked."
          },
          "400": {
            "description": "Name a subject or a location.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "tags": [
          "core"
        ]
      }
    },
    "/v1/billing/subscription": {
      "get": {
        "summary": "Retrieve the workspace's subscription",
        "description": "The plan being charged for, its status and when the period ends. 501 when no billing provider is configured — never a plausible answer about money.",
        "operationId": "getBillingSubscription",
        "responses": {
          "200": {
            "description": "OK."
          },
          "501": {
            "description": "No billing provider is configured."
          }
        },
        "tags": [
          "core"
        ]
      }
    },
    "/v1/billing/usage": {
      "get": {
        "summary": "Metered usage this period",
        "description": "Notifications counted against the plan's included volume, and what was reported to the meter. Answers from this platform's own evidence rather than from the provider: metering that disagrees with evidence is a billing defect, and evidence wins.",
        "operationId": "getBillingUsage",
        "responses": {
          "200": {
            "description": "OK."
          }
        },
        "tags": [
          "core"
        ]
      }
    },
    "/v1/billing/portal": {
      "post": {
        "summary": "Open a billing portal session",
        "description": "Returns a short-lived URL to the provider's hosted portal, where a plan or a card is changed. 501 when no billing provider is configured.",
        "operationId": "createBillingPortalSession",
        "responses": {
          "200": {
            "description": "OK."
          },
          "501": {
            "description": "No billing provider is configured."
          }
        },
        "tags": [
          "core"
        ]
      }
    },
    "/v1/compliance/profile": {
      "get": {
        "summary": "Read the compliance profile",
        "operationId": "getComplianceProfile",
        "responses": {
          "200": {
            "description": "OK."
          }
        },
        "tags": [
          "compliance"
        ],
        "x-rail": "compliance",
        "x-rail-note": "Absent from the document served to a workspace without this rail; the endpoint returns 404 rail_not_installed."
      },
      "patch": {
        "summary": "Update the compliance profile",
        "operationId": "updateComplianceProfile",
        "responses": {
          "200": {
            "description": "OK."
          }
        },
        "tags": [
          "compliance"
        ],
        "x-rail": "compliance",
        "x-rail-note": "Absent from the document served to a workspace without this rail; the endpoint returns 404 rail_not_installed."
      }
    },
    "/v1/compliance/policies": {
      "get": {
        "summary": "List compliance policies",
        "operationId": "listCompliancePolicies",
        "responses": {
          "200": {
            "description": "OK."
          }
        },
        "tags": [
          "compliance"
        ],
        "x-rail": "compliance",
        "x-rail-note": "Absent from the document served to a workspace without this rail; the endpoint returns 404 rail_not_installed."
      },
      "patch": {
        "summary": "Update compliance policies",
        "operationId": "updateCompliancePolicies",
        "responses": {
          "200": {
            "description": "OK."
          }
        },
        "tags": [
          "compliance"
        ],
        "x-rail": "compliance",
        "x-rail-note": "Absent from the document served to a workspace without this rail; the endpoint returns 404 rail_not_installed."
      }
    },
    "/v1/compliance/preprocess/test": {
      "post": {
        "summary": "Run the Preprocessing Rail against a sample",
        "description": "The same rail a real send uses, so a customer can see exactly what would happen to their payload before committing to it.",
        "operationId": "testPreprocess",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PreprocessResult"
                }
              }
            }
          }
        },
        "tags": [
          "compliance"
        ],
        "x-rail": "compliance",
        "x-rail-note": "Absent from the document served to a workspace without this rail; the endpoint returns 404 rail_not_installed."
      }
    },
    "/v1/compliance/violations": {
      "get": {
        "summary": "List violations",
        "description": "Excerpts are redacted to their shape. A violation record that quoted the value would recreate the problem it reports.",
        "operationId": "listViolations",
        "responses": {
          "200": {
            "description": "OK."
          }
        },
        "parameters": [
          {
            "name": "rule_id",
            "in": "query",
            "required": false,
            "description": "Only violations of one declared policy rule.",
            "schema": {
              "type": "string",
              "example": "pci_pan_in_payload"
            }
          },
          {
            "name": "outcome",
            "in": "query",
            "required": false,
            "description": "What the policy plane decided: blocked, flagged or redacted.",
            "schema": {
              "type": "string",
              "example": "blocked"
            }
          }
        ],
        "tags": [
          "compliance"
        ],
        "x-rail": "compliance",
        "x-rail-note": "Absent from the document served to a workspace without this rail; the endpoint returns 404 rail_not_installed."
      }
    },
    "/v1/compliance/exports": {
      "post": {
        "summary": "Generate a hash-sealed compliance export",
        "operationId": "createComplianceExport",
        "responses": {
          "202": {
            "description": "Queued."
          }
        },
        "tags": [
          "compliance"
        ],
        "x-rail": "compliance",
        "x-rail-note": "Absent from the document served to a workspace without this rail; the endpoint returns 404 rail_not_installed."
      }
    },
    "/v1/dsar/exports": {
      "post": {
        "summary": "Request a subject access export",
        "operationId": "createDsarExport",
        "responses": {
          "202": {
            "description": "Queued."
          }
        },
        "tags": [
          "compliance"
        ],
        "x-rail": "compliance",
        "x-rail-note": "Absent from the document served to a workspace without this rail; the endpoint returns 404 rail_not_installed."
      }
    },
    "/v1/dsar/deletions": {
      "post": {
        "summary": "Request an erasure",
        "description": "Executed across every plane — operational, analytics, search, object storage and comms state — and each writes its own completion evidence. Completion in one plane is not completion.",
        "operationId": "createDsarDeletion",
        "responses": {
          "202": {
            "description": "Queued."
          }
        },
        "tags": [
          "compliance"
        ],
        "x-rail": "compliance",
        "x-rail-note": "Absent from the document served to a workspace without this rail; the endpoint returns 404 rail_not_installed."
      }
    },
    "/v1/conditions": {
      "get": {
        "summary": "List conditions",
        "operationId": "listConditions",
        "responses": {
          "200": {
            "description": "Conditions, with their suppression and tiers."
          }
        },
        "tags": [
          "alerting"
        ],
        "x-rail": "alerting",
        "x-rail-note": "Absent from the document served to a workspace without this rail; the endpoint returns 404 rail_not_installed."
      },
      "post": {
        "summary": "Declare a condition",
        "operationId": "createCondition",
        "description": "The rule, not an instance of it. Severity is what decides whether an alert from it wakes anybody — not the fact that a machine emitted it, because a refrigeration failure and a mileage threshold come off the same trailer and only one of them should.\n\nSuppression is the part worth care. Without it a camera on a busy yard produces hundreds of messages a day and the channel is unread within a week — and the freezer failure that matters arrives in a stream the recipient has learned to swipe away.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "external_id",
                  "name",
                  "severity"
                ],
                "properties": {
                  "external_id": {
                    "type": "string"
                  },
                  "name": {
                    "type": "string"
                  },
                  "description": {
                    "type": "string"
                  },
                  "severity": {
                    "type": "string",
                    "description": "critical, high, medium or low."
                  },
                  "applies_to_class": {
                    "type": "string",
                    "description": "Scope to one class of thing. A temperature excursion is meaningless on a camera."
                  },
                  "suppression": {
                    "type": "object",
                    "description": "Strategy to parameter: debounce, occurrence_threshold, repeat_after, aggregate, flap_detection."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Declared."
          },
          "400": {
            "description": "Unknown severity, class, or suppression strategy.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "That external_id is already a condition here.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "tags": [
          "alerting"
        ],
        "x-rail": "alerting",
        "x-rail-note": "Absent from the document served to a workspace without this rail; the endpoint returns 404 rail_not_installed."
      }
    },
    "/v1/conditions/{id}": {
      "get": {
        "summary": "Get a condition",
        "operationId": "getCondition",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "responses": {
          "200": {
            "description": "The condition."
          },
          "404": {
            "description": "No such condition.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "tags": [
          "alerting"
        ],
        "x-rail": "alerting",
        "x-rail-note": "Absent from the document served to a workspace without this rail; the endpoint returns 404 rail_not_installed."
      },
      "patch": {
        "summary": "Amend a condition",
        "operationId": "updateCondition",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "description": {
                    "type": "string"
                  },
                  "severity": {
                    "type": "string"
                  },
                  "suppression": {
                    "type": "object"
                  },
                  "enabled": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Amended."
          }
        },
        "tags": [
          "alerting"
        ],
        "x-rail": "alerting",
        "x-rail-note": "Absent from the document served to a workspace without this rail; the endpoint returns 404 rail_not_installed."
      }
    },
    "/v1/conditions/{id}/escalations": {
      "post": {
        "summary": "Add an escalation tier",
        "operationId": "addEscalationTier",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "description": "Who to reach when nobody has acknowledged, and how long to wait first. Tiers are rows rather than a list inside the condition, because a recipient named inside a JSON blob is a reference nothing can follow and an erasure cannot reach.\n\naccepts_out_of_hours is what makes on-call consent rather than override: only a tier carrying it may be reached inside its recipients' quiet hours, because somebody agreed in advance to be woken.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "tier",
                  "recipient_id"
                ],
                "properties": {
                  "tier": {
                    "type": "integer",
                    "minimum": 0,
                    "description": "0 is reached immediately."
                  },
                  "after_seconds": {
                    "type": "integer",
                    "minimum": 0
                  },
                  "recipient_id": {
                    "type": "string"
                  },
                  "accepts_out_of_hours": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The condition, with its tiers."
          },
          "400": {
            "description": "No such recipient, or a missing tier.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "tags": [
          "alerting"
        ],
        "x-rail": "alerting",
        "x-rail-note": "Absent from the document served to a workspace without this rail; the endpoint returns 404 rail_not_installed."
      }
    },
    "/v1/conditions/{id}/escalations/{escalation_id}": {
      "delete": {
        "summary": "Remove an escalation tier",
        "operationId": "removeEscalationTier",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          },
          {
            "name": "escalation_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Removed."
          }
        },
        "tags": [
          "alerting"
        ],
        "x-rail": "alerting",
        "x-rail-note": "Absent from the document served to a workspace without this rail; the endpoint returns 404 rail_not_installed."
      }
    },
    "/v1/recipients/{id}/consents": {
      "get": {
        "summary": "List consents",
        "operationId": "listConsents",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "responses": {
          "200": {
            "description": "OK."
          }
        },
        "tags": [
          "core"
        ]
      },
      "post": {
        "summary": "Record a consent",
        "description": "Requires a lawful basis, a scope and a source. A consent with no evidence is an assertion, and an assertion is not a defence.",
        "operationId": "recordConsent",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "responses": {
          "201": {
            "description": "Recorded."
          }
        },
        "tags": [
          "core"
        ]
      }
    },
    "/v1/contracts": {
      "get": {
        "summary": "List contracts",
        "operationId": "listContracts",
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/NotificationContract"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "notification_type",
            "in": "query",
            "required": false,
            "description": "Contracts governing one notification type.",
            "schema": {
              "type": "string",
              "example": "order.shipped"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "draft, published or archived.",
            "schema": {
              "type": "string",
              "example": "published"
            }
          }
        ],
        "tags": [
          "core"
        ]
      },
      "post": {
        "summary": "Create a contract",
        "operationId": "createContract",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/NotificationContract"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created as draft.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotificationContract"
                }
              }
            }
          }
        },
        "tags": [
          "core"
        ]
      }
    },
    "/v1/contracts/{id}": {
      "get": {
        "summary": "Retrieve a contract",
        "operationId": "getContract",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotificationContract"
                }
              }
            }
          }
        },
        "tags": [
          "core"
        ]
      }
    },
    "/v1/contracts/{id}/publish": {
      "post": {
        "summary": "Publish a contract version",
        "description": "A published contract is immutable. A change creates a new version, and both coexist so a notification can record which it instantiated.",
        "operationId": "publishContract",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "responses": {
          "200": {
            "description": "Published.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotificationContract"
                }
              }
            }
          }
        },
        "tags": [
          "core"
        ]
      }
    },
    "/v1/contracts/{id}/archive": {
      "post": {
        "summary": "Archive a contract",
        "description": "Stops new instantiation. It never invalidates notifications already sent under it — evidence outlives configuration.",
        "operationId": "archiveContract",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "responses": {
          "200": {
            "description": "Archived.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotificationContract"
                }
              }
            }
          }
        },
        "tags": [
          "core"
        ]
      }
    },
    "/v1/deliveries": {
      "get": {
        "summary": "List delivery attempts",
        "operationId": "listDeliveries",
        "responses": {
          "200": {
            "description": "OK."
          }
        },
        "parameters": [
          {
            "name": "notification_id",
            "in": "query",
            "required": false,
            "description": "Every attempt made for one notification.",
            "schema": {
              "type": "string",
              "example": "ntf_06G4..."
            }
          },
          {
            "name": "state",
            "in": "query",
            "required": false,
            "description": "Where the attempt got to: queued, sent, delivered, bounced, failed.",
            "schema": {
              "type": "string",
              "example": "bounced"
            }
          },
          {
            "name": "channel",
            "in": "query",
            "required": false,
            "description": "email, sms, push, in_app, webhook or chat.",
            "schema": {
              "type": "string",
              "example": "email"
            }
          }
        ],
        "tags": [
          "core"
        ]
      }
    },
    "/v1/deliveries/{id}": {
      "get": {
        "summary": "Retrieve a delivery attempt",
        "operationId": "getDelivery",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "responses": {
          "200": {
            "description": "OK."
          }
        },
        "tags": [
          "core"
        ]
      }
    },
    "/v1/notification-types": {
      "get": {
        "summary": "List dictionary entries",
        "operationId": "listNotificationTypes",
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/DictionaryEntry"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "tags": [
          "core"
        ]
      },
      "post": {
        "summary": "Declare a notification type",
        "operationId": "createNotificationType",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DictionaryEntry"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DictionaryEntry"
                }
              }
            }
          }
        },
        "tags": [
          "core"
        ]
      }
    },
    "/v1/notification-types/{key}": {
      "get": {
        "summary": "Retrieve a dictionary entry",
        "operationId": "getNotificationType",
        "parameters": [
          {
            "name": "key",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DictionaryEntry"
                }
              }
            }
          }
        },
        "tags": [
          "core"
        ]
      },
      "patch": {
        "summary": "Update a dictionary entry",
        "operationId": "updateNotificationType",
        "parameters": [
          {
            "name": "key",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DictionaryEntry"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DictionaryEntry"
                }
              }
            }
          }
        },
        "tags": [
          "core"
        ]
      }
    },
    "/v1/documents": {
      "get": {
        "summary": "List documents",
        "operationId": "listDocuments",
        "responses": {
          "200": {
            "description": "OK."
          }
        },
        "parameters": [
          {
            "name": "state",
            "in": "query",
            "required": false,
            "description": "Where intake got to: received, classified, review_required, ocr_required.",
            "schema": {
              "type": "string",
              "example": "review_required"
            }
          },
          {
            "name": "document_class",
            "in": "query",
            "required": false,
            "description": "One of the classes registry/document-classes.json declares.",
            "schema": {
              "type": "string",
              "example": "invoice"
            }
          }
        ],
        "tags": [
          "document_intake"
        ],
        "x-rail": "document_intake",
        "x-rail-note": "Absent from the document served to a workspace without this rail; the endpoint returns 404 rail_not_installed."
      },
      "post": {
        "summary": "Upload a document",
        "operationId": "uploadDocument",
        "responses": {
          "201": {
            "description": "Stored with an immutable source hash."
          }
        },
        "tags": [
          "document_intake"
        ],
        "x-rail": "document_intake",
        "x-rail-note": "Absent from the document served to a workspace without this rail; the endpoint returns 404 rail_not_installed."
      }
    },
    "/v1/documents/{id}": {
      "get": {
        "summary": "Retrieve a document",
        "operationId": "getDocument",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Document"
                }
              }
            }
          }
        },
        "tags": [
          "document_intake"
        ],
        "x-rail": "document_intake",
        "x-rail-note": "Absent from the document served to a workspace without this rail; the endpoint returns 404 rail_not_installed."
      }
    },
    "/v1/documents/{id}/process": {
      "post": {
        "summary": "Run the Document Rail — not available",
        "description": "Document classification and extraction are not built. This returns 501 rather than accepting work nothing carries out. The document itself is stored, hashed and auditable by POST /v1/documents.",
        "operationId": "processDocument",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "responses": {
          "501": {
            "description": "Document processing is not built.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "tags": [
          "document_intake"
        ],
        "x-rail": "document_intake",
        "x-rail-note": "Absent from the document served to a workspace without this rail; the endpoint returns 404 rail_not_installed."
      }
    },
    "/v1/documents/{id}/review": {
      "post": {
        "summary": "Record a human review",
        "operationId": "reviewDocument",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "responses": {
          "200": {
            "description": "OK."
          }
        },
        "tags": [
          "document_intake"
        ],
        "x-rail": "document_intake",
        "x-rail-note": "Absent from the document served to a workspace without this rail; the endpoint returns 404 rail_not_installed."
      }
    },
    "/v1/domains": {
      "get": {
        "summary": "List sending domains",
        "operationId": "listDomains",
        "responses": {
          "200": {
            "description": "OK."
          }
        },
        "tags": [
          "core"
        ]
      },
      "post": {
        "summary": "Add a sending domain",
        "operationId": "createDomain",
        "responses": {
          "201": {
            "description": "Created, awaiting DNS."
          }
        },
        "tags": [
          "core"
        ]
      }
    },
    "/v1/domains/{id}": {
      "get": {
        "summary": "Retrieve a domain",
        "operationId": "getDomain",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Domain"
                }
              }
            }
          }
        },
        "tags": [
          "core"
        ]
      },
      "delete": {
        "summary": "Remove a domain",
        "operationId": "deleteDomain",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "responses": {
          "204": {
            "description": "Removed."
          }
        },
        "tags": [
          "core"
        ]
      }
    },
    "/v1/domains/{id}/verify": {
      "post": {
        "summary": "Re-check SPF, DKIM and DMARC",
        "description": "Verification decays after 30 days. An unverified domain is unverified, not assumed good.",
        "operationId": "verifyDomain",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Domain"
                }
              }
            }
          }
        },
        "tags": [
          "core"
        ]
      }
    },
    "/v1/evidence-packs": {
      "get": {
        "summary": "List a workspace's evidence packs, in chain order",
        "description": "Dense and ascending. A gap in the sequence is a missing pack, which is the cheapest check an auditor can run and the first one they should.",
        "operationId": "listEvidencePacks",
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/EvidencePackSummary"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "tags": [
          "notification"
        ],
        "x-rail": "notification",
        "x-rail-note": "Absent from the document served to a workspace without this rail; the endpoint returns 404 rail_not_installed."
      }
    },
    "/v1/evidence-packs/verify": {
      "get": {
        "summary": "Walk the workspace's pack chain and report where it breaks",
        "description": "The check an auditor would otherwise have to write. Every pack's signature against the published key, every link against the pack before it, the sequence against its own density, and the signed chain head against what is actually here — which is the only thing that catches packs removed from the END, since the ones that remain verify perfectly. It reports the first break rather than a verdict, because 'invalid' without a position is not actionable, and it reports 'unverified' rather than a verdict when it could not check at all. Running it here is a convenience, not the proof: the same walk runs offline from the packs and the key alone.",
        "operationId": "verifyEvidenceChain",
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChainVerification"
                }
              }
            }
          }
        },
        "tags": [
          "notification"
        ],
        "x-rail": "notification",
        "x-rail-note": "Absent from the document served to a workspace without this rail; the endpoint returns 404 rail_not_installed."
      }
    },
    "/v1/evidence-packs/{id}": {
      "get": {
        "summary": "Retrieve one sealed evidence pack",
        "description": "Everything one notification produced, sealed when it completed and signed. The pack carries the exact bytes that were signed and the steps to check them, so it verifies offline with no login and no cooperation from us — which is the point, because the moment it matters is the moment that cooperation is worth least.",
        "operationId": "getEvidencePack",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EvidencePack"
                }
              }
            }
          },
          "404": {
            "description": "No such pack.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "tags": [
          "notification"
        ],
        "x-rail": "notification",
        "x-rail-note": "Absent from the document served to a workspace without this rail; the endpoint returns 404 rail_not_installed."
      }
    },
    "/v1/inbound/email": {
      "post": {
        "summary": "Deliver an inbound email to the rail",
        "description": "For sources that are not Cloudflare's own mail handler — a forwarding rule, another provider's inbound webhook, a migration. The rail resolves it exactly as it resolves mail arriving directly, so there is one resolution path and not two that drift.",
        "operationId": "receiveInboundEmail",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/InboundEmail"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InboundReceipt"
                }
              }
            }
          }
        },
        "tags": [
          "inbound_email"
        ],
        "x-rail": "inbound_email",
        "x-rail-note": "Absent from the document served to a workspace without this rail; the endpoint returns 404 rail_not_installed."
      }
    },
    "/v1/messages": {
      "get": {
        "summary": "List messages across threads",
        "description": "Inbound, outbound and internal in one place, each carrying how it was matched and how sure that match was. A message resolved at low confidence is visible as such rather than silently filed.",
        "operationId": "listMessages",
        "parameters": [
          {
            "name": "direction",
            "in": "query",
            "required": false,
            "description": "inbound or outbound.",
            "schema": {
              "type": "string",
              "example": "inbound"
            }
          },
          {
            "name": "resolution_method",
            "in": "query",
            "required": false,
            "description": "How the reply was matched to a recipient.",
            "schema": {
              "type": "string",
              "example": "header"
            }
          },
          {
            "name": "unresolved",
            "in": "query",
            "required": false,
            "description": "Only replies nothing could be matched to — the queue a person works.",
            "schema": {
              "type": "string",
              "example": "true"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Message"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "tags": [
          "inbound_email"
        ],
        "x-rail": "inbound_email",
        "x-rail-note": "Absent from the document served to a workspace without this rail; the endpoint returns 404 rail_not_installed."
      }
    },
    "/v1/threads": {
      "get": {
        "summary": "List threads",
        "operationId": "listThreads",
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Thread"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "tags": [
          "inbound_email"
        ],
        "x-rail": "inbound_email",
        "x-rail-note": "Absent from the document served to a workspace without this rail; the endpoint returns 404 rail_not_installed."
      }
    },
    "/v1/threads/{id}": {
      "get": {
        "summary": "Retrieve a thread and its messages",
        "operationId": "getThread",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Thread"
                }
              }
            }
          },
          "404": {
            "description": "Not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "tags": [
          "inbound_email"
        ],
        "x-rail": "inbound_email",
        "x-rail-note": "Absent from the document served to a workspace without this rail; the endpoint returns 404 rail_not_installed."
      }
    },
    "/v1/invoices": {
      "get": {
        "summary": "List invoices",
        "operationId": "listInvoices",
        "responses": {
          "200": {
            "description": "OK."
          }
        },
        "parameters": [
          {
            "name": "state",
            "in": "query",
            "required": false,
            "description": "Where the invoice got to in intake.",
            "schema": {
              "type": "string",
              "example": "classified"
            }
          },
          {
            "name": "supplier",
            "in": "query",
            "required": false,
            "description": "Exact supplier name as extracted.",
            "schema": {
              "type": "string",
              "example": "Acme Ltd"
            }
          }
        ],
        "tags": [
          "invoice_intake"
        ],
        "x-rail": "invoice_intake",
        "x-rail-note": "Absent from the document served to a workspace without this rail; the endpoint returns 404 rail_not_installed."
      },
      "post": {
        "summary": "Submit an invoice document",
        "operationId": "createInvoice",
        "responses": {
          "201": {
            "description": "Created."
          }
        },
        "tags": [
          "invoice_intake"
        ],
        "x-rail": "invoice_intake",
        "x-rail-note": "Absent from the document served to a workspace without this rail; the endpoint returns 404 rail_not_installed."
      }
    },
    "/v1/invoices/{id}": {
      "get": {
        "summary": "Retrieve a canonical invoice",
        "operationId": "getInvoice",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Invoice"
                }
              }
            }
          }
        },
        "tags": [
          "invoice_intake"
        ],
        "x-rail": "invoice_intake",
        "x-rail-note": "Absent from the document served to a workspace without this rail; the endpoint returns 404 rail_not_installed."
      }
    },
    "/v1/invoices/{id}/approve": {
      "post": {
        "summary": "Approve an invoice",
        "description": "Records a human approval. NotifHQ produces a canonical invoice and hands it on; it never approves a payment.",
        "operationId": "approveInvoice",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "responses": {
          "200": {
            "description": "OK."
          }
        },
        "tags": [
          "invoice_intake"
        ],
        "x-rail": "invoice_intake",
        "x-rail-note": "Absent from the document served to a workspace without this rail; the endpoint returns 404 rail_not_installed."
      }
    },
    "/v1/invoices/{id}/reject": {
      "post": {
        "summary": "Reject an invoice",
        "operationId": "rejectInvoice",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "responses": {
          "200": {
            "description": "OK."
          }
        },
        "tags": [
          "invoice_intake"
        ],
        "x-rail": "invoice_intake",
        "x-rail-note": "Absent from the document served to a workspace without this rail; the endpoint returns 404 rail_not_installed."
      }
    },
    "/v1/jobs": {
      "get": {
        "summary": "List asynchronous jobs",
        "description": "The work this workspace has asked for and what became of it. Filter by 'kind' or 'state'.",
        "operationId": "listJobs",
        "responses": {
          "200": {
            "description": "OK."
          }
        },
        "parameters": [
          {
            "name": "kind",
            "in": "query",
            "required": false,
            "description": "One declared job type.",
            "schema": {
              "type": "string",
              "example": "retention_sweep"
            }
          },
          {
            "name": "state",
            "in": "query",
            "required": false,
            "description": "queued, running, succeeded or failed.",
            "schema": {
              "type": "string",
              "example": "failed"
            }
          }
        ],
        "tags": [
          "core"
        ]
      }
    },
    "/v1/jobs/{id}": {
      "get": {
        "summary": "Retrieve one job",
        "description": "State, the request as it was made, and — once the work has produced one — the evidence id holding the result.",
        "operationId": "getJob",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "responses": {
          "200": {
            "description": "OK."
          },
          "404": {
            "description": "Not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "tags": [
          "core"
        ]
      }
    },
    "/v1/journeys": {
      "get": {
        "summary": "List the journeys this workspace can run",
        "description": "Declared centrally and the same for everybody: a journey is the shape of a sequence, not a workspace's copy of one. Which ones matter to you follows from your sector.",
        "operationId": "listJourneys",
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Journey"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "tags": [
          "notification"
        ],
        "x-rail": "notification",
        "x-rail-note": "Absent from the document served to a workspace without this rail; the endpoint returns 404 rail_not_installed."
      }
    },
    "/v1/journeys/{id}/runs": {
      "post": {
        "summary": "Start a journey for one subject and one recipient",
        "description": "Opens a run at the journey's initial state and sends whatever that state sends. Idempotent per subject and recipient: starting the same appointment twice returns the run that already exists rather than doubling every reminder.",
        "operationId": "startJourneyRun",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/StartJourneyRun"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "A run already existed for this subject and recipient.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JourneyRun"
                }
              }
            }
          },
          "201": {
            "description": "Started.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JourneyRun"
                }
              }
            }
          },
          "404": {
            "description": "No such journey.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "tags": [
          "notification"
        ],
        "x-rail": "notification",
        "x-rail-note": "Absent from the document served to a workspace without this rail; the endpoint returns 404 rail_not_installed."
      }
    },
    "/v1/journey-runs": {
      "get": {
        "summary": "List journey runs",
        "description": "What is in flight, and what each is waiting for. A run with no next_due_at is waiting on an event rather than a clock, which is a state worth being able to see.",
        "operationId": "listJourneyRuns",
        "parameters": [
          {
            "name": "journey_id",
            "in": "query",
            "required": false,
            "description": "Runs of one journey.",
            "schema": {
              "type": "string",
              "example": "jny_06G4..."
            }
          },
          {
            "name": "state",
            "in": "query",
            "required": false,
            "description": "Where the run is: active, waiting, completed, cancelled.",
            "schema": {
              "type": "string",
              "example": "waiting"
            }
          },
          {
            "name": "overdue",
            "in": "query",
            "required": false,
            "description": "Only runs past the step deadline their journey declares.",
            "schema": {
              "type": "string",
              "example": "true"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/JourneyRun"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "tags": [
          "notification"
        ],
        "x-rail": "notification",
        "x-rail-note": "Absent from the document served to a workspace without this rail; the endpoint returns 404 rail_not_installed."
      }
    },
    "/v1/journey-runs/{id}": {
      "get": {
        "summary": "Retrieve one journey run",
        "operationId": "getJourneyRun",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JourneyRun"
                }
              }
            }
          },
          "404": {
            "description": "Not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "tags": [
          "notification"
        ],
        "x-rail": "notification",
        "x-rail-note": "Absent from the document served to a workspace without this rail; the endpoint returns 404 rail_not_installed."
      }
    },
    "/v1/journey-runs/{id}/events": {
      "post": {
        "summary": "Tell a run that something happened",
        "description": "The world moved: the appointment was rescheduled, the claim was decided, the customer confirmed the card. The run advances if the event maps to a transition from where it is, and cancellation wins over everything — including a reminder that was due in the same moment.",
        "operationId": "recordJourneyEvent",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "event"
                ],
                "properties": {
                  "event": {
                    "type": "string",
                    "description": "The event type, as the journey declares it."
                  },
                  "subject": {
                    "type": "object",
                    "description": "Fields that changed. A rescheduled appointment carries the new appointment_at, and every clock counting from it moves with it."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JourneyRun"
                }
              }
            }
          },
          "404": {
            "description": "Not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "tags": [
          "notification"
        ],
        "x-rail": "notification",
        "x-rail-note": "Absent from the document served to a workspace without this rail; the endpoint returns 404 rail_not_installed."
      }
    },
    "/v1/locations": {
      "get": {
        "summary": "List places",
        "operationId": "listLocations",
        "parameters": [
          {
            "name": "kind",
            "in": "query",
            "required": false,
            "description": "One level of the place hierarchy, so a map can ask for sites without every bay inside them.",
            "schema": {
              "type": "string",
              "example": "site"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Places."
          }
        },
        "tags": [
          "alerting"
        ],
        "x-rail": "alerting",
        "x-rail-note": "Absent from the document served to a workspace without this rail; the endpoint returns 404 rail_not_installed."
      },
      "post": {
        "summary": "Declare a place",
        "operationId": "createLocation",
        "description": "A place is contained by another, and the containment is checked rather than trusted: a site cannot be put inside a zone. Every upward walk for a timezone or a jurisdiction depends on the tree terminating.\n\nA kind that carries a working day must state its IANA timezone. That is what 'local time' and 'out of hours' mean for everything inside it, and it is not the timezone on the profile of whoever receives the message.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "external_id",
                  "name",
                  "kind"
                ],
                "properties": {
                  "external_id": {
                    "type": "string"
                  },
                  "name": {
                    "type": "string"
                  },
                  "kind": {
                    "type": "string"
                  },
                  "parent_id": {
                    "type": "string"
                  },
                  "timezone": {
                    "type": "string"
                  },
                  "jurisdiction": {
                    "type": "string"
                  },
                  "latitude": {
                    "type": "number"
                  },
                  "longitude": {
                    "type": "number"
                  },
                  "geofence_radius_m": {
                    "type": "number"
                  },
                  "attributes": {
                    "type": "object"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Declared."
          },
          "400": {
            "description": "The kind is unknown, the parent may not contain it, or a timezone is required and absent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "402": {
            "description": "The workspace already holds every governed subject its plan includes. A subject and a place both count toward that one allowance, because an estate is both; the body names the plan, the limit and the count, and a plan change raises it. 402 rather than 403, because this works once they pay for it — a 403 says they may not and sends the caller looking for a permission that is not the problem.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "That external_id is already a place here.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "tags": [
          "alerting"
        ],
        "x-rail": "alerting",
        "x-rail-note": "Absent from the document served to a workspace without this rail; the endpoint returns 404 rail_not_installed."
      }
    },
    "/v1/locations/{id}": {
      "get": {
        "summary": "Get a place",
        "operationId": "getLocation",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "responses": {
          "200": {
            "description": "The place."
          },
          "404": {
            "description": "No such place.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "tags": [
          "alerting"
        ],
        "x-rail": "alerting",
        "x-rail-note": "Absent from the document served to a workspace without this rail; the endpoint returns 404 rail_not_installed."
      },
      "patch": {
        "summary": "Amend a place",
        "operationId": "updateLocation",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "parent_id": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "timezone": {
                    "type": "string"
                  },
                  "jurisdiction": {
                    "type": "string"
                  },
                  "latitude": {
                    "type": "number"
                  },
                  "longitude": {
                    "type": "number"
                  },
                  "geofence_radius_m": {
                    "type": "number"
                  },
                  "attributes": {
                    "type": "object"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Amended."
          },
          "400": {
            "description": "The containment is not permitted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "tags": [
          "alerting"
        ],
        "x-rail": "alerting",
        "x-rail-note": "Absent from the document served to a workspace without this rail; the endpoint returns 404 rail_not_installed."
      }
    },
    "/v1/members": {
      "get": {
        "summary": "List members",
        "description": "Who is in the workspace and what role each holds. A role is the whole of what a person may do here — the scope set behind it is declared once in registry/surfaces.json rather than assembled per person.",
        "operationId": "listMembers",
        "responses": {
          "200": {
            "description": "OK."
          }
        },
        "tags": [
          "core"
        ]
      }
    },
    "/v1/members/{id}": {
      "delete": {
        "summary": "Remove a member",
        "description": "Removes the membership, never the user: the same person may hold membership in other workspaces, and deleting them would revoke access they were not removed from.",
        "operationId": "removeMember",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "responses": {
          "204": {
            "description": "Removed."
          },
          "409": {
            "description": "The last owner cannot be removed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "tags": [
          "core"
        ]
      }
    },
    "/v1/members/invitations": {
      "get": {
        "summary": "List invitations",
        "operationId": "listInvitations",
        "responses": {
          "200": {
            "description": "OK."
          }
        },
        "tags": [
          "core"
        ]
      },
      "post": {
        "summary": "Invite someone to the workspace",
        "description": "Issues a single-use invitation and sends it through the comms rail. The role is fixed here, by the person granting access — an invitee cannot choose their own on the way in.",
        "operationId": "inviteMember",
        "responses": {
          "201": {
            "description": "Invited."
          },
          "409": {
            "description": "That address is already a member, or already has a live invitation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "tags": [
          "core"
        ]
      }
    },
    "/v1/members/invitations/{id}": {
      "delete": {
        "summary": "Revoke an invitation",
        "description": "Revoked and expired are recorded separately: one is a decision and the other is time passing.",
        "operationId": "revokeInvitation",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "responses": {
          "204": {
            "description": "Revoked."
          }
        },
        "tags": [
          "core"
        ]
      }
    },
    "/v1/notifications": {
      "get": {
        "summary": "List notifications",
        "operationId": "listNotifications",
        "parameters": [
          {
            "name": "recipient_external_id",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "from",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "maximum": 200,
              "default": 50
            }
          },
          {
            "name": "type",
            "in": "query",
            "required": false,
            "description": "One notification type, exactly as declared.",
            "schema": {
              "type": "string",
              "example": "order.shipped"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Where the notification got to: queued, sent, delivered, failed or suppressed.",
            "schema": {
              "type": "string",
              "example": "failed"
            }
          },
          {
            "name": "correlation_id",
            "in": "query",
            "required": false,
            "description": "The caller's own id for a business event, so every notification raised by one thing can be found together.",
            "schema": {
              "type": "string",
              "example": "order_9134"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Notification"
                      }
                    },
                    "has_more": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          }
        },
        "tags": [
          "core"
        ]
      }
    },
    "/v1/notifications/{id}": {
      "get": {
        "summary": "Retrieve a notification",
        "operationId": "getNotification",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Notification"
                }
              }
            }
          },
          "404": {
            "description": "Not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "tags": [
          "core"
        ]
      }
    },
    "/v1/notifications/{id}/cancel": {
      "post": {
        "summary": "Cancel a queued notification",
        "operationId": "cancelNotification",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "responses": {
          "200": {
            "description": "Cancelled.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Notification"
                }
              }
            }
          },
          "409": {
            "description": "Already dispatched.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "tags": [
          "core"
        ]
      }
    },
    "/v1/notifications/{id}/deliveries": {
      "get": {
        "summary": "List the delivery attempts for this notification",
        "description": "One row per attempt per channel, with its own state, timing and error. Attempts are separate from the notification: three retries on one channel are three rows, not one status that keeps changing.",
        "operationId": "listNotificationDeliveries",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Delivery"
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "tags": [
          "core"
        ]
      }
    },
    "/v1/notifications/{id}/events": {
      "get": {
        "summary": "List the raw events recorded for this notification",
        "description": "The event rows themselves, before the timeline renders them into prose. The timeline is the readable view; this is what it was built from, for anyone reconciling against their own copy of the event stream.",
        "operationId": "listNotificationEvents",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Event"
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "tags": [
          "core"
        ]
      }
    },
    "/v1/notifications/{id}/payload": {
      "get": {
        "summary": "Retrieve what is retained of the request",
        "description": "The payload itself is purged on the schedule the retention policy sets, so this returns what survives that purge: the contract and version it was validated against, the hash that proves what was submitted, and the fields the contract declared. A hash outlives the data it describes, which is the point — it can still answer 'was this the payload' after the payload is gone.",
        "operationId": "getNotificationPayload",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotificationPayload"
                }
              }
            }
          },
          "404": {
            "description": "Not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "tags": [
          "core"
        ]
      }
    },
    "/v1/notifications/{id}/policy": {
      "get": {
        "summary": "Retrieve the policy decision behind this notification",
        "description": "The decision, the policies evaluated, the reason codes and the obligations it attached. A suppression with no retrievable decision is an outcome nobody can defend, so the decision is stored beside the notification rather than reconstructed on demand.",
        "operationId": "getNotificationPolicy",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PolicyDecision"
                }
              }
            }
          },
          "404": {
            "description": "Not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "tags": [
          "core"
        ]
      }
    },
    "/v1/notifications/{id}/resend": {
      "post": {
        "summary": "Send again as a new notification",
        "description": "Creates a NEW notification referencing the original. It never mutates the original: history is immutable.",
        "operationId": "resendNotification",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "responses": {
          "202": {
            "description": "Accepted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotifyResponse"
                }
              }
            }
          }
        },
        "tags": [
          "core"
        ]
      }
    },
    "/v1/notifications/{id}/template": {
      "get": {
        "summary": "Retrieve the template versions that rendered this",
        "description": "Which template, at which version, produced what each channel actually sent — with the hash of the rendered subject and body. A template edited since is a different version, so the record here does not drift when somebody changes the current draft.",
        "operationId": "getNotificationTemplate",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotificationTemplates"
                }
              }
            }
          },
          "404": {
            "description": "Not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "tags": [
          "core"
        ]
      }
    },
    "/v1/notifications/{id}/timeline": {
      "get": {
        "summary": "Retrieve the evidence timeline",
        "description": "The strongest surface in the product. Every state transition with its evidence, in order — the answer to 'I never received it' that does not require reading a transport's logs.",
        "operationId": "getNotificationTimeline",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Timeline"
                }
              }
            }
          }
        },
        "tags": [
          "core"
        ]
      }
    },
    "/v1/notifications/{id}/webhooks": {
      "get": {
        "summary": "List webhook deliveries triggered by this notification",
        "description": "Every attempt to tell your systems what happened here, with its response status and error. Answers 'your webhook never fired' without either side reading the other's logs.",
        "operationId": "listNotificationWebhooks",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/WebhookDelivery"
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "tags": [
          "core"
        ]
      }
    },
    "/v1/notify": {
      "post": {
        "summary": "Send a notification",
        "description": "The whole product behind three fields. Behind this call: dictionary lookup, contract resolution, schema validation, classification, policy evaluation, preference resolution, template selection, routing, adapter dispatch, evidence capture, analytics emission and webhook fan-out.\n\n`accepted` means accepted. It is not a delivery claim — requested, sent, delivered and engaged are four distinct states and this response reports the first.",
        "operationId": "notify",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Scoped to {workspace, environment, operation, key}. A repeat within the retention window returns the ORIGINAL result with `Idempotent-Replay: true`, never a second effect."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/NotifyRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotifyResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request or payload failed validation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The policy plane denied the send. The body names the reason codes and the decision.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "No published contract for this notification type.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "A declared rate limit was reached.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "tags": [
          "core"
        ]
      }
    },
    "/v1/notify/bulk": {
      "post": {
        "summary": "Send up to 1,000 notifications",
        "description": "Each entry is evaluated independently: one denied recipient does not fail the batch, and each result carries its own status and reason codes.",
        "operationId": "notifyBulk",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "notifications"
                ],
                "properties": {
                  "notifications": {
                    "type": "array",
                    "maxItems": 1000,
                    "items": {
                      "$ref": "#/components/schemas/NotifyRequest"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "results": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/NotifyResponse"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "tags": [
          "core"
        ]
      }
    },
    "/v1/otp/challenges": {
      "post": {
        "summary": "Issue a one-time passcode",
        "description": "Generates a code, sends it through the notification rail as auth.identity.otp, and stores only a salted hash of it. The code appears in the response to nobody: it goes to the recipient, and a caller that could read it back would make the whole exercise theatre.",
        "operationId": "createOtpChallenge",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateOtpChallenge"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Issued.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OtpChallenge"
                }
              }
            }
          },
          "404": {
            "description": "No such recipient.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "tags": [
          "notification"
        ],
        "x-rail": "notification",
        "x-rail-note": "Absent from the document served to a workspace without this rail; the endpoint returns 404 rail_not_installed."
      }
    },
    "/v1/otp/challenges/{id}/verify": {
      "post": {
        "summary": "Check a passcode against a challenge",
        "description": "Compares in constant time, counts the attempt whether or not it succeeded, and refuses a challenge that is expired, already used, or out of attempts. The failure reasons are deliberately uniform to a caller who should not be able to tell a wrong code from a missing challenge.",
        "operationId": "verifyOtpChallenge",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "code"
                ],
                "properties": {
                  "code": {
                    "type": "string",
                    "description": "The digits the recipient entered."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Checked. Read `verified` — a 200 is not a pass.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OtpVerification"
                }
              }
            }
          },
          "404": {
            "description": "No such challenge.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "tags": [
          "notification"
        ],
        "x-rail": "notification",
        "x-rail-note": "Absent from the document served to a workspace without this rail; the endpoint returns 404 rail_not_installed."
      }
    },
    "/v1/packs": {
      "get": {
        "summary": "List packs",
        "operationId": "listPacks",
        "responses": {
          "200": {
            "description": "OK."
          }
        },
        "tags": [
          "core"
        ]
      }
    },
    "/v1/packs/{id}": {
      "get": {
        "summary": "Retrieve a pack",
        "operationId": "getPack",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "responses": {
          "200": {
            "description": "OK."
          }
        },
        "tags": [
          "core"
        ]
      }
    },
    "/v1/packs/{id}/install": {
      "post": {
        "summary": "Install a pack",
        "description": "Deterministic: the same pack version installed twice into the same workspace produces the same manifest and no second set of objects.",
        "operationId": "installPack",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "responses": {
          "200": {
            "description": "Installed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PackInstallation"
                }
              }
            }
          }
        },
        "tags": [
          "core"
        ]
      }
    },
    "/v1/packs/{id}/upgrade": {
      "post": {
        "summary": "Upgrade an installed pack",
        "operationId": "upgradePack",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "responses": {
          "200": {
            "description": "Upgraded."
          }
        },
        "tags": [
          "core"
        ]
      }
    },
    "/v1/packs/{id}/uninstall": {
      "post": {
        "summary": "Uninstall a pack",
        "description": "Removes pack-owned configuration. Never removes notifications, evidence, tickets or invoices — history is not a pack's to delete.",
        "operationId": "uninstallPack",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "responses": {
          "200": {
            "description": "Uninstalled."
          }
        },
        "tags": [
          "core"
        ]
      }
    },
    "/v1/me/notifications": {
      "get": {
        "summary": "List the signed-in recipient's own notifications",
        "description": "The recipient's view of what was sent to them, for an in-app inbox. Resolved from the widget token's recipient, never from a parameter — an endpoint that takes a recipient id is an endpoint that enumerates recipients.",
        "operationId": "listMyNotifications",
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/RecipientNotification"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "tags": [
          "core"
        ]
      }
    },
    "/v1/me/notifications/{id}/read": {
      "post": {
        "summary": "Mark one of the recipient's own notifications read",
        "description": "Read state belongs to the recipient, not to the send. It is recorded as engagement evidence like any other observation, and never collapsed into delivery (§0.5).",
        "operationId": "markMyNotificationRead",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecipientNotification"
                }
              }
            }
          },
          "404": {
            "description": "Not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "tags": [
          "core"
        ]
      }
    },
    "/v1/me/preferences": {
      "get": {
        "summary": "Read the calling recipient's own preferences",
        "description": "Authenticated by a widget token, never an API key.",
        "operationId": "getMyPreferences",
        "responses": {
          "200": {
            "description": "OK."
          }
        },
        "tags": [
          "core"
        ]
      },
      "patch": {
        "summary": "Update the calling recipient's own preferences",
        "operationId": "updateMyPreferences",
        "responses": {
          "200": {
            "description": "OK."
          }
        },
        "tags": [
          "core"
        ]
      }
    },
    "/v1/recipients/{id}/preferences": {
      "get": {
        "summary": "Read preferences",
        "operationId": "getPreferences",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "responses": {
          "200": {
            "description": "OK."
          }
        },
        "tags": [
          "core"
        ]
      },
      "patch": {
        "summary": "Update preferences",
        "operationId": "updatePreferences",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "responses": {
          "200": {
            "description": "OK."
          }
        },
        "tags": [
          "core"
        ]
      }
    },
    "/v1/widget-tokens": {
      "post": {
        "summary": "Mint a widget token",
        "description": "Short-lived and scoped to ONE recipient. Never place a workspace API key in a browser: a widget that would need the key needs a server endpoint instead.",
        "operationId": "createWidgetToken",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WidgetTokenRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WidgetToken"
                }
              }
            }
          }
        },
        "tags": [
          "core"
        ]
      }
    },
    "/v1/recipients": {
      "get": {
        "summary": "List recipients",
        "operationId": "listRecipients",
        "responses": {
          "200": {
            "description": "OK."
          }
        },
        "parameters": [
          {
            "name": "external_id",
            "in": "query",
            "required": false,
            "description": "The caller's own id for a person. Exact match, never a search: this is a lookup.",
            "schema": {
              "type": "string",
              "example": "cust_9134"
            }
          }
        ],
        "tags": [
          "core"
        ]
      },
      "post": {
        "summary": "Create or update a recipient",
        "operationId": "upsertRecipient",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Recipient"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Recipient"
                }
              }
            }
          }
        },
        "tags": [
          "core"
        ]
      }
    },
    "/v1/recipients/{id}": {
      "get": {
        "summary": "Retrieve a recipient",
        "operationId": "getRecipient",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Recipient"
                }
              }
            }
          }
        },
        "tags": [
          "core"
        ]
      },
      "patch": {
        "summary": "Update a recipient",
        "operationId": "updateRecipient",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "responses": {
          "200": {
            "description": "OK."
          }
        },
        "tags": [
          "core"
        ]
      },
      "delete": {
        "summary": "Erase a recipient",
        "description": "Executes an erasure across every plane. The row survives as a tombstone so evidence stays coherent; every addressable field is gone.",
        "operationId": "eraseRecipient",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "responses": {
          "202": {
            "description": "Erasure accepted."
          }
        },
        "tags": [
          "core"
        ]
      }
    },
    "/v1/schemas": {
      "get": {
        "summary": "List payload schemas",
        "operationId": "listSchemas",
        "responses": {
          "200": {
            "description": "OK."
          }
        },
        "parameters": [
          {
            "name": "notification_type",
            "in": "query",
            "required": false,
            "description": "Payload schemas for one notification type.",
            "schema": {
              "type": "string",
              "example": "order.shipped"
            }
          }
        ],
        "tags": [
          "core"
        ]
      },
      "post": {
        "summary": "Create a payload schema",
        "operationId": "createSchema",
        "responses": {
          "201": {
            "description": "Created."
          }
        },
        "tags": [
          "core"
        ]
      }
    },
    "/v1/schemas/{id}": {
      "get": {
        "summary": "Retrieve a payload schema",
        "operationId": "getSchema",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "responses": {
          "200": {
            "description": "OK."
          }
        },
        "tags": [
          "core"
        ]
      }
    },
    "/v1/schemas/{id}/validate": {
      "post": {
        "summary": "Validate a payload without sending",
        "description": "Runs the Preprocessing Rail and returns what would be dropped, classified, redacted or blocked. The same rail a real send uses.",
        "operationId": "validatePayload",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PreprocessResult"
                }
              }
            }
          }
        },
        "tags": [
          "core"
        ]
      }
    },
    "/v1/search": {
      "post": {
        "summary": "Search across canonical objects",
        "description": "Results are authorized AFTER the query and BEFORE the response. An index that returns rows the caller may not see is a data breach with extra steps.",
        "operationId": "search",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SearchRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SearchResponse"
                }
              }
            }
          }
        },
        "tags": [
          "search"
        ],
        "x-rail": "search",
        "x-rail-note": "Absent from the document served to a workspace without this rail; the endpoint returns 404 rail_not_installed."
      }
    },
    "/v1/saved-searches": {
      "get": {
        "summary": "List saved searches",
        "operationId": "listSavedSearches",
        "responses": {
          "200": {
            "description": "OK."
          }
        },
        "tags": [
          "search"
        ],
        "x-rail": "search",
        "x-rail-note": "Absent from the document served to a workspace without this rail; the endpoint returns 404 rail_not_installed."
      },
      "post": {
        "summary": "Save a search",
        "operationId": "createSavedSearch",
        "responses": {
          "201": {
            "description": "Created."
          }
        },
        "tags": [
          "search"
        ],
        "x-rail": "search",
        "x-rail-note": "Absent from the document served to a workspace without this rail; the endpoint returns 404 rail_not_installed."
      }
    },
    "/v1/saved-searches/{id}": {
      "delete": {
        "summary": "Delete a saved search",
        "operationId": "deleteSavedSearch",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted."
          }
        },
        "tags": [
          "search"
        ],
        "x-rail": "search",
        "x-rail-note": "Absent from the document served to a workspace without this rail; the endpoint returns 404 rail_not_installed."
      }
    },
    "/v1/subjects": {
      "get": {
        "summary": "List tracked things",
        "operationId": "listSubjects",
        "parameters": [
          {
            "name": "location_id",
            "in": "query",
            "required": false,
            "description": "Everything tracked at one place. The query an operator runs when a site calls.",
            "schema": {
              "type": "string",
              "example": "loc_01JQ8ZK4M7XN2P5R9TVB3CDEFG"
            }
          },
          {
            "name": "entity_class",
            "in": "query",
            "required": false,
            "description": "One sort of thing — every probe, every camera. What a maintenance round is planned from.",
            "schema": {
              "type": "string",
              "example": "sensor"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Subjects."
          }
        },
        "tags": [
          "alerting"
        ],
        "x-rail": "alerting",
        "x-rail-note": "Absent from the document served to a workspace without this rail; the endpoint returns 404 rail_not_installed."
      },
      "post": {
        "summary": "Declare a tracked thing",
        "operationId": "createSubject",
        "description": "What a notification is ABOUT, as distinct from who receives it. A freezer has no preferences; a depot manager has no location worth alerting on.\n\nSupply entity_class alone and the kind is inferred. Supply both and disagree and the request is refused rather than one being silently preferred: a row whose kind and class contradict each other decides the wrong planes for the rest of its life.\n\nA device that is also ADDRESSED — a printer taking jobs, a controller taking setpoints — names its recipient row through recipient_id rather than this object trying to be both.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "external_id",
                  "name"
                ],
                "properties": {
                  "external_id": {
                    "type": "string"
                  },
                  "name": {
                    "type": "string"
                  },
                  "entity_kind": {
                    "type": "string"
                  },
                  "entity_class": {
                    "type": "string"
                  },
                  "location_id": {
                    "type": "string"
                  },
                  "parent_id": {
                    "type": "string"
                  },
                  "recipient_id": {
                    "type": "string"
                  },
                  "attributes": {
                    "type": "object"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Declared."
          },
          "400": {
            "description": "Unknown kind or class, a kind that is never a subject, or a kind and class that disagree.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "402": {
            "description": "The workspace already holds every governed subject its plan includes. A subject and a place both count toward that one allowance, because an estate is both; the body names the plan, the limit and the count, and a plan change raises it. 402 rather than 403, because this works once they pay for it — a 403 says they may not and sends the caller looking for a permission that is not the problem.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "That external_id is already a subject here.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "tags": [
          "alerting"
        ],
        "x-rail": "alerting",
        "x-rail-note": "Absent from the document served to a workspace without this rail; the endpoint returns 404 rail_not_installed."
      }
    },
    "/v1/subjects/{id}": {
      "get": {
        "summary": "Get a tracked thing",
        "operationId": "getSubject",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "responses": {
          "200": {
            "description": "The subject."
          },
          "404": {
            "description": "No such subject.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "tags": [
          "alerting"
        ],
        "x-rail": "alerting",
        "x-rail-note": "Absent from the document served to a workspace without this rail; the endpoint returns 404 rail_not_installed."
      },
      "patch": {
        "summary": "Amend a tracked thing",
        "operationId": "updateSubject",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "location_id": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "parent_id": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "recipient_id": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "attributes": {
                    "type": "object"
                  },
                  "retired": {
                    "type": "boolean",
                    "description": "Equipment leaving service. A tombstone, never a delete, so alerts raised against it stay coherent."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Amended."
          }
        },
        "tags": [
          "alerting"
        ],
        "x-rail": "alerting",
        "x-rail-note": "Absent from the document served to a workspace without this rail; the endpoint returns 404 rail_not_installed."
      }
    },
    "/v1/subjects/{id}/positions": {
      "get": {
        "summary": "Position history",
        "operationId": "listPositions",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "responses": {
          "200": {
            "description": "Fixes, newest first."
          }
        },
        "tags": [
          "alerting"
        ],
        "x-rail": "alerting",
        "x-rail-note": "Absent from the document served to a workspace without this rail; the endpoint returns 404 rail_not_installed."
      },
      "post": {
        "summary": "Report a position",
        "operationId": "reportPosition",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "description": "accuracy_m is required and has no default. A position with no stated accuracy cannot be acted on: 'outside the geofence' from a source good to a kilometre is not a fact, and raising on it produces the false alarms that teach people to ignore the channel.\n\nobserved_at is when the fix was taken, not when it arrived. A device out of coverage reports an hour late, and deciding on the arrival time puts a vehicle where it was rather than where it is.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "latitude",
                  "longitude",
                  "accuracy_m",
                  "source"
                ],
                "properties": {
                  "latitude": {
                    "type": "number"
                  },
                  "longitude": {
                    "type": "number"
                  },
                  "accuracy_m": {
                    "type": "number",
                    "description": "Metres."
                  },
                  "source": {
                    "type": "string",
                    "description": "gnss, gnss_rtk, cellular, wifi, beacon or declared."
                  },
                  "observed_at": {
                    "type": "string",
                    "format": "date-time"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Recorded."
          },
          "400": {
            "description": "Coordinates, accuracy or a declared source are missing.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "tags": [
          "alerting"
        ],
        "x-rail": "alerting",
        "x-rail-note": "Absent from the document served to a workspace without this rail; the endpoint returns 404 rail_not_installed."
      }
    },
    "/v1/templates": {
      "get": {
        "summary": "List templates",
        "operationId": "listTemplates",
        "responses": {
          "200": {
            "description": "OK."
          }
        },
        "parameters": [
          {
            "name": "notification_type",
            "in": "query",
            "required": false,
            "description": "Templates for one notification type.",
            "schema": {
              "type": "string",
              "example": "order.shipped"
            }
          },
          {
            "name": "channel",
            "in": "query",
            "required": false,
            "description": "email, sms, push, in_app, webhook or chat.",
            "schema": {
              "type": "string",
              "example": "email"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "draft, published or archived.",
            "schema": {
              "type": "string",
              "example": "published"
            }
          }
        ],
        "tags": [
          "core"
        ]
      },
      "post": {
        "summary": "Create a template",
        "operationId": "createTemplate",
        "responses": {
          "201": {
            "description": "Created as draft."
          }
        },
        "tags": [
          "core"
        ]
      }
    },
    "/v1/templates/draft": {
      "post": {
        "summary": "Draft template copy",
        "description": "Propose a subject and body for a notification type, referencing only the fields that type declares. Advisory: the result is a starting point for a person to edit, never something sent. The same request returns the same draft — the first is stored under a hash of the model, prompt version, generation settings and inputs, and every identical request after it is served from that store without a model call.",
        "operationId": "draftTemplate",
        "responses": {
          "200": {
            "description": "OK."
          },
          "501": {
            "description": "No drafting provider is configured."
          }
        },
        "tags": [
          "core"
        ]
      }
    },
    "/v1/templates/generate": {
      "post": {
        "summary": "Draft a template",
        "description": "Returns a DRAFT. Generated content traverses validation, review and approval like any other draft — drafting is a stage inside the Template Rail, never a bypass around it.",
        "operationId": "generateTemplate",
        "responses": {
          "200": {
            "description": "A draft, never a published template."
          }
        },
        "tags": [
          "core"
        ]
      }
    },
    "/v1/templates/{id}": {
      "get": {
        "summary": "Retrieve a template",
        "operationId": "getTemplate",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Template"
                }
              }
            }
          },
          "404": {
            "description": "Not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "tags": [
          "core"
        ]
      },
      "patch": {
        "summary": "Edit a draft template",
        "description": "Drafts only. An approved or published version is refused with 409: approval is of a checksum, so content that changes underneath one is unreviewed copy carrying an audit trail that says a human signed it off. Revise it into a new draft instead.",
        "operationId": "updateTemplate",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TemplateContent"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Template"
                }
              }
            }
          },
          "404": {
            "description": "Not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "Not a draft.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "tags": [
          "core"
        ]
      }
    },
    "/v1/templates/{id}/approve": {
      "post": {
        "summary": "Approve a template version",
        "description": "Records approval of THIS checksum. Publication is refused if the content changes afterwards.",
        "operationId": "approveTemplate",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "responses": {
          "200": {
            "description": "Approved."
          }
        },
        "tags": [
          "core"
        ]
      }
    },
    "/v1/templates/{id}/archive": {
      "post": {
        "summary": "Archive a template",
        "operationId": "archiveTemplate",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "responses": {
          "200": {
            "description": "Archived."
          }
        },
        "tags": [
          "core"
        ]
      }
    },
    "/v1/templates/{id}/preview": {
      "post": {
        "summary": "Render a preview with sample parameters",
        "description": "Rendered through the same substitution a real send uses. Pass `draft` to preview content that is not saved yet — an editor that previews the saved version while you type is previewing something else.",
        "operationId": "previewTemplate",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "params": {
                    "type": "object",
                    "description": "Sample values for the template's variables."
                  },
                  "draft": {
                    "$ref": "#/components/schemas/TemplateContent",
                    "description": "Unsaved content to render instead of what is stored."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TemplatePreview"
                }
              }
            }
          }
        },
        "tags": [
          "core"
        ]
      }
    },
    "/v1/templates/{id}/publish": {
      "post": {
        "summary": "Publish an approved template",
        "operationId": "publishTemplate",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "responses": {
          "200": {
            "description": "Published."
          },
          "403": {
            "description": "Not approved, or the content changed after approval.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "tags": [
          "core"
        ]
      }
    },
    "/v1/templates/{id}/revise": {
      "post": {
        "summary": "Open the next draft version of a template",
        "description": "Copies this version's content into a new draft at the next version number. Approved and published templates are never edited in place — an approval is of a checksum, and content that changes under an approval is unreviewed copy with an audit trail that says otherwise.",
        "operationId": "reviseTemplate",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "responses": {
          "201": {
            "description": "Created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Template"
                }
              }
            }
          },
          "404": {
            "description": "Not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "tags": [
          "core"
        ]
      }
    },
    "/v1/tickets": {
      "get": {
        "summary": "List tickets",
        "operationId": "listTickets",
        "responses": {
          "200": {
            "description": "OK."
          }
        },
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "open, pending, resolved or closed.",
            "schema": {
              "type": "string",
              "example": "open"
            }
          },
          {
            "name": "assignee_id",
            "in": "query",
            "required": false,
            "description": "Tickets assigned to one member. The API has no opinion about who is asking, so 'mine' is a caller naming themselves.",
            "schema": {
              "type": "string",
              "example": "mem_06G4..."
            }
          },
          {
            "name": "priority",
            "in": "query",
            "required": false,
            "description": "low, normal, high or urgent.",
            "schema": {
              "type": "string",
              "example": "high"
            }
          },
          {
            "name": "labels",
            "in": "query",
            "required": false,
            "description": "Canonical labels from registry/labels.json, comma-separated. EVERY one must be present, never any: 'purpose:marketing AND severity:critical' is the question people mean, and OR quietly widens a filter written to narrow one. A label this platform does not declare is refused with 422, never ignored — a dropped filter returns the whole workspace and looks exactly like a filter that worked.",
            "schema": {
              "type": "string",
              "example": "severity:critical,purpose:marketing"
            }
          },
          {
            "name": "tags",
            "in": "query",
            "required": false,
            "description": "Free-form tags, comma-separated, combined the same way. A tag wearing a canonical prefix is refused: the facets would believe it.",
            "schema": {
              "type": "string",
              "example": "escalated,refund-batch-7"
            }
          }
        ],
        "tags": [
          "team_inbox"
        ],
        "x-rail": "team_inbox",
        "x-rail-note": "Absent from the document served to a workspace without this rail; the endpoint returns 404 rail_not_installed."
      },
      "post": {
        "summary": "Create a ticket",
        "operationId": "createTicket",
        "responses": {
          "201": {
            "description": "Created."
          }
        },
        "tags": [
          "team_inbox"
        ],
        "x-rail": "team_inbox",
        "x-rail-note": "Absent from the document served to a workspace without this rail; the endpoint returns 404 rail_not_installed."
      }
    },
    "/v1/tickets/queues": {
      "get": {
        "summary": "List the inbox queues with their live counts",
        "description": "One queue per ticket state, taken from the state machine rather than written here, plus `unassigned` — the question an inbox is opened to answer and the one cut no state answers. Counts are computed on each call: a queue count cached anywhere is a number that tells somebody the backlog is clear when it is not.",
        "operationId": "listTicketQueues",
        "parameters": [
          {
            "name": "assignee_id",
            "in": "query",
            "required": false,
            "description": "Adds a 'mine' queue counted for the person named. Without it the queues are the workspace's backlog.",
            "schema": {
              "type": "string",
              "example": "mem_06G4..."
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/TicketQueue"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "tags": [
          "team_inbox"
        ],
        "x-rail": "team_inbox",
        "x-rail-note": "Absent from the document served to a workspace without this rail; the endpoint returns 404 rail_not_installed."
      }
    },
    "/v1/tickets/{id}": {
      "get": {
        "summary": "Retrieve a ticket with its communication evidence",
        "operationId": "getTicket",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ticket"
                }
              }
            }
          }
        },
        "tags": [
          "team_inbox"
        ],
        "x-rail": "team_inbox",
        "x-rail-note": "Absent from the document served to a workspace without this rail; the endpoint returns 404 rail_not_installed."
      },
      "patch": {
        "summary": "Update a ticket",
        "operationId": "updateTicket",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "responses": {
          "200": {
            "description": "OK."
          }
        },
        "tags": [
          "team_inbox"
        ],
        "x-rail": "team_inbox",
        "x-rail-note": "Absent from the document served to a workspace without this rail; the endpoint returns 404 rail_not_installed."
      }
    },
    "/v1/tickets/{id}/assign": {
      "post": {
        "summary": "Assign a ticket",
        "operationId": "assignTicket",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "responses": {
          "200": {
            "description": "OK."
          }
        },
        "tags": [
          "team_inbox"
        ],
        "x-rail": "team_inbox",
        "x-rail-note": "Absent from the document served to a workspace without this rail; the endpoint returns 404 rail_not_installed."
      }
    },
    "/v1/tickets/{id}/note": {
      "post": {
        "summary": "Add an internal note",
        "operationId": "noteTicket",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "responses": {
          "201": {
            "description": "Created."
          }
        },
        "tags": [
          "team_inbox"
        ],
        "x-rail": "team_inbox",
        "x-rail-note": "Absent from the document served to a workspace without this rail; the endpoint returns 404 rail_not_installed."
      }
    },
    "/v1/tickets/{id}/reopen": {
      "post": {
        "summary": "Reopen a ticket",
        "operationId": "reopenTicket",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "responses": {
          "200": {
            "description": "OK."
          }
        },
        "tags": [
          "team_inbox"
        ],
        "x-rail": "team_inbox",
        "x-rail-note": "Absent from the document served to a workspace without this rail; the endpoint returns 404 rail_not_installed."
      }
    },
    "/v1/tickets/{id}/reply": {
      "post": {
        "summary": "Reply on a ticket",
        "description": "The reply is dispatched through the Comms Rail like any other outbound message.",
        "operationId": "replyTicket",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "responses": {
          "202": {
            "description": "Accepted."
          }
        },
        "tags": [
          "team_inbox"
        ],
        "x-rail": "team_inbox",
        "x-rail-note": "Absent from the document served to a workspace without this rail; the endpoint returns 404 rail_not_installed."
      }
    },
    "/v1/tickets/{id}/resolve": {
      "post": {
        "summary": "Resolve a ticket",
        "operationId": "resolveTicket",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "responses": {
          "200": {
            "description": "OK."
          }
        },
        "tags": [
          "team_inbox"
        ],
        "x-rail": "team_inbox",
        "x-rail-note": "Absent from the document served to a workspace without this rail; the endpoint returns 404 rail_not_installed."
      }
    },
    "/v1/webhooks/endpoints": {
      "get": {
        "summary": "List webhook endpoints",
        "operationId": "listWebhookEndpoints",
        "responses": {
          "200": {
            "description": "OK."
          }
        },
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "active, paused or disabled.",
            "schema": {
              "type": "string",
              "example": "active"
            }
          }
        ],
        "tags": [
          "core"
        ]
      },
      "post": {
        "summary": "Create a webhook endpoint",
        "operationId": "createWebhookEndpoint",
        "responses": {
          "201": {
            "description": "Created. The signing secret is shown once."
          }
        },
        "tags": [
          "core"
        ]
      }
    },
    "/v1/webhooks/endpoints/{id}": {
      "patch": {
        "summary": "Update a webhook endpoint",
        "operationId": "updateWebhookEndpoint",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "responses": {
          "200": {
            "description": "OK."
          }
        },
        "tags": [
          "core"
        ]
      },
      "delete": {
        "summary": "Delete a webhook endpoint",
        "operationId": "deleteWebhookEndpoint",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted."
          }
        },
        "tags": [
          "core"
        ]
      }
    },
    "/v1/webhooks/deliveries": {
      "get": {
        "summary": "List webhook deliveries",
        "operationId": "listWebhookDeliveries",
        "responses": {
          "200": {
            "description": "OK."
          }
        },
        "parameters": [
          {
            "name": "endpoint_id",
            "in": "query",
            "required": false,
            "description": "Attempts made to one endpoint.",
            "schema": {
              "type": "string",
              "example": "whe_06G4..."
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "pending, delivered or failed.",
            "schema": {
              "type": "string",
              "example": "failed"
            }
          }
        ],
        "tags": [
          "core"
        ]
      }
    },
    "/v1/webhooks/deliveries/{id}/replay": {
      "post": {
        "summary": "Replay a webhook delivery",
        "operationId": "replayWebhookDelivery",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "responses": {
          "202": {
            "description": "Queued."
          }
        },
        "tags": [
          "core"
        ]
      }
    },
    "/v1/workspaces": {
      "get": {
        "summary": "List workspaces",
        "operationId": "listWorkspaces",
        "responses": {
          "200": {
            "description": "OK."
          }
        },
        "tags": [
          "core"
        ]
      },
      "post": {
        "summary": "Create a workspace",
        "operationId": "createWorkspace",
        "responses": {
          "201": {
            "description": "Created."
          }
        },
        "tags": [
          "core"
        ]
      }
    },
    "/v1/workspaces/{id}": {
      "get": {
        "summary": "Retrieve a workspace",
        "operationId": "getWorkspace",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "responses": {
          "200": {
            "description": "OK."
          }
        },
        "tags": [
          "core"
        ]
      },
      "patch": {
        "summary": "Update a workspace",
        "operationId": "updateWorkspace",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "responses": {
          "200": {
            "description": "OK."
          }
        },
        "tags": [
          "core"
        ]
      }
    },
    "/v1/workspaces/{id}/signature": {
      "get": {
        "summary": "Retrieve the workspace's sending signature",
        "operationId": "getWorkspaceSignature",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "responses": {
          "200": {
            "description": "OK.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WorkspaceSignature"
                }
              }
            }
          },
          "404": {
            "description": "The workspace has never set one, so it cannot send."
          }
        },
        "tags": [
          "core"
        ]
      },
      "put": {
        "summary": "Set the workspace's sending signature",
        "operationId": "setWorkspaceSignature",
        "description": "Supersedes the version in force rather than overwriting it: what the footer said on the day a message was sent stays answerable.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WorkspaceSignature"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Set.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WorkspaceSignature"
                }
              }
            }
          },
          "400": {
            "description": "A required field is missing; the response names which."
          }
        },
        "tags": [
          "core"
        ]
      }
    },
    "/v1/workspaces/{id}/rails": {
      "get": {
        "summary": "List every rail and this workspace's standing against it",
        "operationId": "listWorkspaceRails",
        "description": "Every rail, including ones this workspace cannot have — each carrying whether it is installed, whether it can be, and why not. A list of only what is installable drops the question somebody actually opens a ticket about.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          }
        ],
        "responses": {
          "200": {
            "description": "OK."
          }
        },
        "tags": [
          "core"
        ]
      }
    },
    "/v1/workspaces/{id}/rails/{rail}": {
      "post": {
        "summary": "Install a rail",
        "operationId": "installWorkspaceRail",
        "description": "Idempotent: installing one already installed answers 200 and writes no second audit record, because a retry after a timeout is the ordinary case and answering 409 would make the safe thing look like an error.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          },
          {
            "name": "rail",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "A rail id as registry/rails.json declares it, such as 'document_intake'."
          }
        ],
        "responses": {
          "200": {
            "description": "Installed, or already was."
          },
          "404": {
            "description": "The rail is not built, not offered to this plan, or not a rail at all. 404 rather than 403: which rails exist and which a plan includes is the product roster (§21.4)."
          }
        },
        "tags": [
          "core"
        ]
      },
      "delete": {
        "summary": "Uninstall a rail",
        "operationId": "uninstallWorkspaceRail",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The resource identifier."
          },
          {
            "name": "rail",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "A rail id as registry/rails.json declares it, such as 'document_intake'."
          }
        ],
        "responses": {
          "200": {
            "description": "Removed."
          },
          "400": {
            "description": "A core rail. Every other rail runs on the core, so removing it leaves a workspace that authenticates a key and then answers 404 to everything it exists to do."
          },
          "404": {
            "description": "Not a rail, or not installed here."
          }
        },
        "tags": [
          "core"
        ]
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "A workspace API key, scoped to one environment. A development key cannot cause a production delivery. Never place this key in a browser — mint a widget token instead."
      }
    },
    "schemas": {
      "Problem": {
        "type": "object",
        "required": [
          "error",
          "message",
          "reason_codes",
          "docs"
        ],
        "description": "Every error names a reason code. An error that cannot is a bug we have not diagnosed.",
        "properties": {
          "error": {
            "type": "string"
          },
          "message": {
            "type": "string"
          },
          "reason_codes": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "decision_id": {
            "type": "string",
            "description": "Present when the policy plane produced the outcome. Fetchable for the full evaluation."
          },
          "docs": {
            "type": "string",
            "format": "uri"
          }
        }
      },
      "NotifyRequest": {
        "type": "object",
        "required": [
          "type",
          "recipient",
          "payload"
        ],
        "properties": {
          "type": {
            "type": "string",
            "pattern": "^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*){2}$",
            "examples": [
              "commerce.order.shipped"
            ]
          },
          "recipient": {
            "type": "object",
            "required": [
              "external_id"
            ],
            "properties": {
              "external_id": {
                "type": "string"
              }
            }
          },
          "payload": {
            "type": "object",
            "description": "Validated against the contract's schema. Undeclared fields are dropped before anything stores them."
          },
          "options": {
            "type": "object",
            "properties": {
              "correlation_id": {
                "type": "string"
              },
              "locale": {
                "type": "string",
                "default": "en-GB"
              }
            }
          }
        }
      },
      "NotifyResponse": {
        "type": "object",
        "required": [
          "notification_id",
          "status"
        ],
        "properties": {
          "notification_id": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "accepted",
              "suppressed",
              "deferred"
            ],
            "description": "`accepted` is not a delivery claim."
          },
          "suppression_reason": {
            "type": "string"
          },
          "defer_until": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Notification": {
        "$comment": "Generated from publicNotification in src/routers/notification.ts, typed from schema/notification.v1.json.",
        "type": "object",
        "properties": {
          "notification_id": {
            "type": "string",
            "pattern": "^ntf_[0-9A-HJKMNP-TV-Z]{26}$",
            "x-kind": "identity"
          },
          "notification_type": {
            "type": "string",
            "pattern": "^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*){2}$",
            "description": "Canonical, never an alias. An alias resolves at the gateway and never appears here (§3.3).",
            "x-kind": "opaque"
          },
          "status": {
            "enum": [
              "requested",
              "validated",
              "normalized",
              "authorized",
              "suppressed",
              "queued",
              "rendered",
              "dispatched",
              "sent",
              "delivered",
              "failed",
              "expired",
              "cancelled"
            ]
          },
          "suppression_reason": {
            "type": [
              "string",
              "null"
            ],
            "description": "Set when status is suppressed. Must exist in registry/suppression-reasons.json.",
            "x-kind": "text"
          },
          "requested_at": {
            "type": "string",
            "format": "date-time"
          },
          "correlation_id": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 255,
            "x-kind": "opaque"
          },
          "environment": {
            "type": "string",
            "enum": [
              "development",
              "staging",
              "production"
            ],
            "description": "Which world a thing belongs to. An API key's environment IS the request's environment: no header can widen it, so a development key cannot cause a production delivery (§0.10, §13.5)."
          },
          "actor_kind": {
            "type": "string",
            "enum": [
              "human",
              "service",
              "agent"
            ],
            "description": "Who caused this send, unpacked from provenance. An operator asking whether a model sent something should not have to open a database."
          },
          "agent_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "The agent identity behind the send, where one held the key."
          }
        }
      },
      "NotificationContract": {
        "$comment": "Generated from present in src/routers/contract.ts, typed from schema/notification-contract.v1.json.",
        "type": "object",
        "properties": {
          "contract_id": {
            "type": "string",
            "pattern": "^nct_[0-9A-HJKMNP-TV-Z]{26}$",
            "x-kind": "identity"
          },
          "namespace": {
            "type": "string",
            "pattern": "^[a-z][a-z0-9_]*\\.[a-z][a-z0-9_]*$",
            "description": "The <domain>.<object> half of the notification type.",
            "x-kind": "opaque"
          },
          "notification_type": {
            "type": "string",
            "pattern": "^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*){2}$",
            "description": "Must exist in the Notification Dictionary (DICTIONARY FIRST).",
            "x-kind": "opaque"
          },
          "version": {
            "type": "string",
            "pattern": "^\\d+\\.\\d+\\.\\d+$",
            "x-kind": "opaque"
          },
          "document": {
            "type": "string",
            "description": "The contract itself: channels, purpose, policies and classification. Stored whole so a notification can record the exact document it instantiated.",
            "x-kind": "opaque"
          },
          "status": {
            "enum": [
              "draft",
              "active",
              "archived"
            ]
          },
          "published_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "checksum": {
            "type": "string",
            "pattern": "^[0-9a-f]{64}$",
            "x-kind": "opaque"
          }
        }
      },
      "Recipient": {
        "$comment": "Generated from present in src/routers/recipient.ts, typed from schema/recipient.v1.json.",
        "type": "object",
        "properties": {
          "recipient_id": {
            "type": "string",
            "pattern": "^rcp_[0-9A-HJKMNP-TV-Z]{26}$",
            "x-kind": "identity"
          },
          "external_id": {
            "type": "string",
            "minLength": 1,
            "maxLength": 255,
            "description": "The workspace's own identifier. Unique within workspace.",
            "x-kind": "identity"
          },
          "entity_kind": {
            "type": "string",
            "enum": [
              "human",
              "legal_entity",
              "software_agent",
              "device"
            ],
            "description": "What this recipient IS. Decides whether consent applies, whether preferences and quiet hours are read, whether a legal footer belongs on what it receives, and whether its identifiers are personal data. Defaults to 'human', because every recipient predating this field is a person and treating one as a machine switches off all four for somebody owed them."
          },
          "entity_class": {
            "type": "string",
            "enum": [
              "person",
              "company",
              "public_body",
              "charity",
              "agent",
              "bot",
              "sensor",
              "machine",
              "controller",
              "robot",
              "autonomous_vehicle",
              "vehicle",
              "printer_3d",
              "industrial_equipment",
              "access_control",
              "camera",
              "appliance",
              "hardware",
              "patient_monitor",
              "therapy_device",
              "implant",
              "diagnostic_system",
              "medical_equipment"
            ],
            "description": "What sort of thing it is within its kind. Descriptive; decides nothing."
          },
          "roles": {
            "type": "array",
            "items": {
              "type": "string",
              "x-kind": "opaque"
            },
            "description": "Stakeholder roles from registry/stakeholders.json."
          },
          "locale": {
            "type": "string",
            "pattern": "^[a-z]{2}(-[A-Z]{2})?$",
            "x-kind": "opaque"
          },
          "timezone": {
            "type": "string",
            "description": "IANA zone. Required for quiet hours to mean anything.",
            "x-kind": "opaque"
          },
          "jurisdiction": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^[A-Z]{2}$",
            "x-kind": "opaque"
          },
          "attributes": {
            "type": "object",
            "description": "Minimised, classified workspace attributes used for routing and rendering. Not a CRM record (§2.4).",
            "x-open": "The customer's own facts about a recipient, used for segmentation and personalisation. NotifHQ never interprets them, so it never constrains them."
          },
          "labels": {
            "type": "array",
            "items": {
              "type": "string",
              "x-kind": "opaque"
            }
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "erased_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Set by an erasure. The row survives as a tombstone so evidence remains coherent; every addressable field is gone."
          }
        }
      },
      "Domain": {
        "$comment": "Generated from present in src/routers/domain.ts, typed from schema/domain.v1.json.",
        "type": "object",
        "properties": {
          "domain_id": {
            "type": "string",
            "pattern": "^dom_[0-9A-HJKMNP-TV-Z]{26}$",
            "x-kind": "identity"
          },
          "domain": {
            "type": "string",
            "format": "hostname"
          },
          "capabilities": {
            "type": "object",
            "additionalProperties": false,
            "properties": {
              "outbound_email": {
                "type": "boolean",
                "default": false
              },
              "inbound_email": {
                "type": "boolean",
                "default": false
              },
              "preferences_domain": {
                "type": "boolean",
                "default": false
              }
            }
          },
          "spf": {
            "type": "string",
            "description": "SPF check state at last verification. Partial verification is not verification: a domain is active only when all three pass.",
            "x-kind": "opaque"
          },
          "dkim": {
            "type": "string",
            "description": "DKIM check state at last verification.",
            "x-kind": "opaque"
          },
          "dmarc": {
            "type": "string",
            "description": "DMARC check state at last verification.",
            "x-kind": "opaque"
          },
          "bounce_subdomain": {
            "type": [
              "string",
              "null"
            ],
            "format": "hostname",
            "description": "Where outbound MX, SPF and DKIM are provisioned. Never the apex — that would break inbound routing (§10.6)."
          },
          "status": {
            "enum": [
              "pending",
              "dns_pending",
              "verifying",
              "active",
              "degraded",
              "suspended"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "activated_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "last_verified_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Decays after 30 days (DECAY-WINDOWS.md). A domain unverified past its window is unverified, not assumed good."
          },
          "verification_stale": {
            "type": "boolean",
            "description": "Whether the last verification is older than the TTL. Computed at read time, because a domain verified once is not verified forever."
          },
          "verification_ttl_days": {
            "type": "integer",
            "description": "How long a verification stands before it must be repeated."
          },
          "required_records": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "purpose",
                "type",
                "name",
                "value"
              ],
              "description": "One DNS record the customer publishes. Issued by the provider that holds the DKIM key, so the selector is real — verification used to look for one at notifhq._domainkey.<domain> that nothing here could supply, which is why no domain could ever activate.",
              "properties": {
                "purpose": {
                  "enum": [
                    "spf",
                    "dkim",
                    "dmarc",
                    "bounce",
                    "tracking",
                    "receiving"
                  ]
                },
                "type": {
                  "enum": [
                    "TXT",
                    "MX",
                    "CNAME"
                  ]
                },
                "name": {
                  "type": "string",
                  "description": "Fully qualified, so it can be pasted as-is.",
                  "x-kind": "text"
                },
                "value": {
                  "type": "string",
                  "x-kind": "opaque"
                },
                "priority": {
                  "type": "integer",
                  "description": "MX only."
                }
              }
            },
            "description": "What the customer must publish. This was the missing half: they were shown spf, dkim and dmarc all reading 'pending' and were never told which records would clear them, so the domain could not be completed and — because the rail holds a workspace to its domain's state — every email from that workspace was suppressed indefinitely."
          },
          "records_note": {
            "type": "string",
            "description": "How to use the records above, and that all three checks must pass."
          }
        }
      },
      "Ticket": {
        "$comment": "Generated from present in src/routers/ticket.ts, typed from schema/ticket.v1.json.",
        "type": "object",
        "properties": {
          "ticket_id": {
            "type": "string",
            "pattern": "^tkt_[0-9A-HJKMNP-TV-Z]{26}$",
            "x-kind": "identity"
          },
          "thread_id": {
            "type": "string",
            "pattern": "^thr_[0-9A-HJKMNP-TV-Z]{26}$",
            "x-kind": "identity"
          },
          "recipient_id": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^rcp_[0-9A-HJKMNP-TV-Z]{26}$",
            "x-kind": "identity"
          },
          "subject": {
            "type": "string",
            "maxLength": 500,
            "x-kind": "text"
          },
          "status": {
            "enum": [
              "open",
              "waiting",
              "resolved"
            ]
          },
          "assignee_id": {
            "type": [
              "string",
              "null"
            ],
            "x-kind": "identity"
          },
          "labels": {
            "type": "array",
            "items": {
              "type": "string",
              "x-kind": "opaque"
            }
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string",
              "x-kind": "opaque"
            }
          },
          "priority": {
            "enum": [
              "low",
              "normal",
              "high",
              "urgent"
            ],
            "default": "normal"
          },
          "linked_notifications": {
            "type": "array",
            "items": {
              "type": "string",
              "pattern": "^ntf_[0-9A-HJKMNP-TV-Z]{26}$",
              "x-kind": "opaque"
            },
            "description": "The evidence link. This is the whole reason this rail exists: 'I never received the payment email' is answered from the timeline, not from a provider's log."
          },
          "linked_documents": {
            "type": "array",
            "items": {
              "type": "string",
              "pattern": "^doc_[0-9A-HJKMNP-TV-Z]{26}$",
              "x-kind": "opaque"
            }
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "first_response_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "resolved_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "reopened_count": {
            "type": "integer",
            "minimum": 0,
            "default": 0
          }
        }
      },
      "Document": {
        "$comment": "Generated from present in src/routers/document.ts, typed from schema/document.v1.json.",
        "type": "object",
        "properties": {
          "document_id": {
            "type": "string",
            "pattern": "^doc_[0-9A-HJKMNP-TV-Z]{26}$",
            "x-kind": "identity"
          },
          "source": {
            "enum": [
              "inbound_email",
              "api_upload",
              "connector",
              "widget"
            ]
          },
          "source_ref": {
            "type": [
              "string",
              "null"
            ],
            "description": "The message or request the document arrived on.",
            "x-kind": "opaque"
          },
          "filename": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 500,
            "x-kind": "opaque"
          },
          "content_hash": {
            "type": "string",
            "pattern": "^[0-9a-f]{64}$",
            "x-kind": "opaque"
          },
          "content_type": {
            "type": "string",
            "x-kind": "opaque"
          },
          "byte_size": {
            "type": "integer",
            "minimum": 0
          },
          "document_class": {
            "type": [
              "string",
              "null"
            ],
            "description": "invoice, receipt, contract, identity, statement, unknown.",
            "x-kind": "opaque"
          },
          "class_confidence": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 0,
            "maximum": 1
          },
          "classification_detail": {
            "type": [
              "object",
              "null"
            ],
            "additionalProperties": false,
            "description": "What the classifier concluded beyond the class itself. Presented as an object because the three answer one question together — how confident, how far ahead of the runner-up, and under which model — and a reader given only the first cannot tell a clear invoice from a coin toss.",
            "properties": {
              "margin": {
                "type": [
                  "number",
                  "null"
                ],
                "minimum": -1,
                "maximum": 2,
                "description": "How far the best class beat the second. Decides whether a person is asked to look, never whether the class is right."
              },
              "model": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The embedding model the class was judged under. A model change makes earlier confidences incomparable with later ones.",
                "x-kind": "opaque"
              },
              "review_reason": {
                "type": [
                  "string",
                  "null"
                ],
                "enum": [
                  "below_threshold",
                  "ambiguous",
                  "no_text_layer",
                  null
                ],
                "description": "Why a person has to decide, or null where none was asked."
              }
            }
          },
          "extraction": {
            "type": [
              "object",
              "null"
            ],
            "additionalProperties": false,
            "properties": {
              "method": {
                "enum": [
                  "native_text",
                  "ocr",
                  "vision",
                  "none"
                ],
                "description": "The Text Extractor Registry dispatches to one of these. The abstraction is why scan and photo handling can arrive later without an architectural change (§17.5)."
              },
              "extractor": {
                "type": "string",
                "x-kind": "opaque"
              },
              "version": {
                "type": "string",
                "x-kind": "opaque"
              },
              "text_hash": {
                "type": [
                  "string",
                  "null"
                ],
                "pattern": "^[0-9a-f]{64}$",
                "x-kind": "opaque"
              },
              "page_count": {
                "type": [
                  "integer",
                  "null"
                ],
                "minimum": 0
              },
              "confidence": {
                "type": [
                  "number",
                  "null"
                ],
                "minimum": 0,
                "maximum": 1
              },
              "has_text": {
                "type": "boolean",
                "description": "Whether text was obtained, never the text itself. The document's content belongs to the workspace that uploaded it, and returning it from every read would copy it into logs, caches and anything that stores a response body (§1.12)."
              }
            }
          },
          "classification": {
            "type": "array",
            "items": {
              "type": "string",
              "x-kind": "opaque"
            }
          },
          "state": {
            "enum": [
              "received",
              "classified",
              "ocr_required",
              "text_extracted",
              "parsed",
              "normalized",
              "review_required",
              "accepted",
              "rejected"
            ]
          },
          "canonical_object_ref": {
            "type": [
              "string",
              "null"
            ],
            "description": "What the document became — an invoice id, for example.",
            "x-kind": "opaque"
          },
          "rejected_reason": {
            "type": [
              "string",
              "null"
            ],
            "x-kind": "text"
          },
          "received_at": {
            "type": "string",
            "format": "date-time"
          },
          "processed_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "Invoice": {
        "$comment": "Generated from present in src/routers/invoice.ts, typed from schema/invoice.v1.json.",
        "type": "object",
        "properties": {
          "invoice_id": {
            "type": "string",
            "pattern": "^inv_[0-9A-HJKMNP-TV-Z]{26}$",
            "x-kind": "identity"
          },
          "supplier": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "name"
            ],
            "properties": {
              "name": {
                "type": "string",
                "minLength": 1,
                "x-kind": "text"
              },
              "tax_id": {
                "type": [
                  "string",
                  "null"
                ],
                "x-kind": "opaque"
              },
              "address_text": {
                "type": [
                  "string",
                  "null"
                ],
                "x-kind": "text"
              },
              "matched_ref": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "A workspace supplier record this was matched to, if any. Null means unmatched, not unknown-and-guessed.",
                "x-kind": "opaque"
              }
            }
          },
          "invoice_number": {
            "type": "string",
            "minLength": 1,
            "x-kind": "opaque"
          },
          "po_number": {
            "type": [
              "string",
              "null"
            ],
            "x-kind": "opaque"
          },
          "issue_date": {
            "type": "string",
            "format": "date"
          },
          "due_date": {
            "type": [
              "string",
              "null"
            ],
            "format": "date"
          },
          "currency": {
            "type": "string",
            "pattern": "^[A-Z]{3}$",
            "x-kind": "opaque"
          },
          "subtotal_minor": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Minor units. Floating-point money is a defect waiting for a rounding complaint."
          },
          "tax_minor": {
            "type": [
              "integer",
              "null"
            ]
          },
          "total_minor": {
            "type": "integer"
          },
          "line_items": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "description",
                "total_minor"
              ],
              "properties": {
                "description": {
                  "type": "string",
                  "x-kind": "text"
                },
                "quantity": {
                  "type": [
                    "number",
                    "null"
                  ]
                },
                "unit_price_minor": {
                  "type": [
                    "integer",
                    "null"
                  ]
                },
                "tax_rate": {
                  "type": [
                    "number",
                    "null"
                  ]
                },
                "total_minor": {
                  "type": "integer"
                },
                "confidence": {
                  "type": [
                    "number",
                    "null"
                  ],
                  "minimum": 0,
                  "maximum": 1
                }
              }
            }
          },
          "confidence": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "overall"
            ],
            "properties": {
              "overall": {
                "type": "number",
                "minimum": 0,
                "maximum": 1
              },
              "per_field": {
                "type": "object",
                "additionalProperties": {
                  "type": "number",
                  "minimum": 0,
                  "maximum": 1
                }
              }
            },
            "description": "Confidence below the workspace threshold routes to invoice.review_required rather than being accepted quietly (§17.6)."
          },
          "source_document_ref": {
            "type": "string",
            "pattern": "^doc_[0-9A-HJKMNP-TV-Z]{26}$",
            "x-kind": "opaque"
          },
          "duplicate_of": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^inv_[0-9A-HJKMNP-TV-Z]{26}$",
            "description": "Set when supplier + invoice number + total matched an already-accepted invoice.",
            "x-kind": "opaque"
          },
          "state": {
            "enum": [
              "received",
              "parsed",
              "normalized",
              "review_required",
              "approval_required",
              "approved",
              "rejected",
              "duplicate"
            ]
          },
          "approved_by": {
            "type": [
              "string",
              "null"
            ],
            "x-kind": "opaque"
          },
          "approved_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "rejected_reason": {
            "type": [
              "string",
              "null"
            ],
            "x-kind": "text"
          }
        }
      },
      "PackInstallation": {
        "$comment": "Generated from present in src/routers/pack.ts, typed from schema/pack-installation.v1.json.",
        "type": "object",
        "properties": {
          "pack_id": {
            "type": "string",
            "x-kind": "opaque"
          },
          "title": {
            "type": "string",
            "description": "From the pack itself, not the installation row."
          },
          "kind": {
            "type": "string",
            "enum": [
              "sector",
              "functional",
              "compliance"
            ],
            "description": "What the pack is. Taken from registry/packs.json — capability was invented here and functional, which every pack actually uses, was dropped."
          },
          "version": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "namespaces": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "counts": {
            "type": "object",
            "additionalProperties": {
              "type": "integer"
            },
            "description": "How much of each kind the pack installs."
          },
          "installation": {
            "type": [
              "object",
              "null"
            ],
            "additionalProperties": true,
            "description": "The installation row, or null where this workspace has not installed it."
          }
        }
      },
      "DictionaryEntry": {
        "$comment": "Generated from present in src/routers/dictionary.ts, typed from schema/registry.dictionary.v1.json.",
        "type": "object",
        "properties": {
          "notification_type_id": {
            "type": "string",
            "description": "The stored row's identifier, which the registry itself has no column for."
          },
          "key": {
            "type": "string",
            "pattern": "^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*){2}$",
            "x-kind": "identity"
          },
          "canonical_name": {
            "type": "string",
            "x-kind": "text"
          },
          "namespace": {
            "type": "string",
            "x-kind": "opaque"
          },
          "purpose": {
            "type": "string",
            "enum": [
              "transactional",
              "operational",
              "support",
              "legal",
              "marketing"
            ],
            "description": "Why a message is being sent. It decides which consent basis applies, so it is a compliance fact rather than a label."
          },
          "importance": {
            "enum": [
              "critical",
              "high",
              "normal",
              "low"
            ]
          },
          "description": {
            "type": "string",
            "x-kind": "text"
          },
          "recommended_channels": {
            "type": "array",
            "items": {
              "type": "string",
              "x-kind": "ref",
              "x-ref": "registry/channels.json#channels[].id"
            },
            "minItems": 1
          },
          "allowed_channels": {
            "type": "array",
            "items": {
              "type": "string",
              "x-kind": "ref",
              "x-ref": "registry/channels.json#channels[].id"
            },
            "minItems": 1
          },
          "required_fields": {
            "type": "array",
            "items": {
              "type": "string",
              "x-kind": "opaque"
            }
          },
          "optional_fields": {
            "type": "array",
            "items": {
              "type": "string",
              "x-kind": "opaque"
            }
          },
          "preference_override": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "allowed"
            ],
            "properties": {
              "allowed": {
                "type": "boolean"
              }
            }
          },
          "classification": {
            "type": "array",
            "items": {
              "type": "string",
              "x-kind": "opaque"
            }
          },
          "ttl_seconds": {
            "type": "integer",
            "minimum": 1
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "TemplateContent": {
        "type": "object",
        "description": "The parts of a template that the checksum covers. Anything outside this is metadata and does not invalidate an approval.",
        "properties": {
          "subject": {
            "type": "string"
          },
          "preheader": {
            "type": "string"
          },
          "html": {
            "type": "string"
          },
          "text": {
            "type": "string"
          },
          "variables": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The parameters this template expects. Declared rather than inferred, so a typo in a placeholder is a missing variable rather than a silent blank."
          },
          "link_refs": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "Template": {
        "$comment": "Generated from present in src/routers/template.ts, typed from schema/template.v1.json.",
        "type": "object",
        "properties": {
          "template_id": {
            "type": "string",
            "pattern": "^tpl_[0-9A-HJKMNP-TV-Z]{26}$",
            "x-kind": "identity"
          },
          "notification_type": {
            "type": "string",
            "pattern": "^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*){2}$",
            "x-kind": "opaque"
          },
          "channel": {
            "type": "string",
            "enum": [
              "email",
              "sms",
              "push",
              "in_app",
              "webhook",
              "chat"
            ],
            "description": "A way of reaching somebody. Declared in registry/channels.json; this mirrors it for schema references."
          },
          "locale": {
            "type": "string",
            "pattern": "^[a-z]{2}(-[A-Z]{2})?$",
            "x-kind": "opaque"
          },
          "version": {
            "type": "integer",
            "minimum": 1
          },
          "subject": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 500,
            "x-kind": "text"
          },
          "preheader": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 500,
            "x-kind": "opaque"
          },
          "html": {
            "type": [
              "string",
              "null"
            ],
            "x-kind": "opaque"
          },
          "text": {
            "type": [
              "string",
              "null"
            ],
            "x-kind": "text"
          },
          "variables": {
            "type": "array",
            "items": {
              "type": "string",
              "pattern": "^[a-zA-Z0-9_]+$",
              "x-kind": "opaque"
            },
            "description": "Every ${name} appearing in the body. A variable not declared here fails mechanical validation."
          },
          "link_refs": {
            "type": "array",
            "items": {
              "type": "string",
              "x-kind": "opaque"
            },
            "description": "Every outbound link, registered. An unregistered URL in the body is a validation failure (§10.3)."
          },
          "status": {
            "enum": [
              "draft",
              "in_review",
              "approved",
              "published",
              "archived"
            ]
          },
          "created_by": {
            "enum": [
              "human",
              "ai",
              "ai+human",
              "pack"
            ],
            "description": "AI-drafted content is marked as such for its whole life. Provenance does not get laundered by an approval."
          },
          "checksum": {
            "type": "string",
            "pattern": "^[0-9a-f]{64}$",
            "x-kind": "opaque"
          },
          "approved_checksum": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^[0-9a-f]{64}$",
            "description": "The checksum a human actually approved. If it differs from checksum, the content changed after approval and publication is refused.",
            "x-kind": "opaque"
          },
          "approved_by": {
            "type": [
              "string",
              "null"
            ],
            "x-kind": "opaque"
          },
          "approved_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "published_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "approval_current": {
            "type": "boolean",
            "description": "Whether the approved checksum still matches the content. False means a human approved different bytes from the ones now stored."
          }
        }
      },
      "TemplatePreview": {
        "type": "object",
        "properties": {
          "template_id": {
            "type": "string"
          },
          "channel": {
            "type": "string"
          },
          "locale": {
            "type": "string"
          },
          "subject": {
            "type": "string"
          },
          "preheader": {
            "type": "string"
          },
          "html": {
            "type": "string"
          },
          "text": {
            "type": "string"
          },
          "declared_variables": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "supplied_parameters": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "missing_parameters": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Declared but not supplied. Named rather than rendered blank, because a blank in a preview looks like an empty value rather than a missing one."
          },
          "unused_parameters": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Supplied but not declared. Usually a renamed placeholder."
          },
          "referenced_variables": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Parsed from the source, not taken from the declaration — the two disagreeing is how a template ships broken."
          },
          "undeclared_references": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Referenced by the source but never declared. Substitution throws on a real send, so this is an error the author should see while writing."
          },
          "unreferenced_variables": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Declared but never referenced. Harmless at send time, almost always a renamed placeholder left behind."
          },
          "is_draft_preview": {
            "type": "boolean",
            "description": "True when unsaved content was rendered rather than what is stored."
          }
        }
      },
      "TicketQueue": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "describes": {
            "type": "string"
          },
          "filter": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "The query this queue applies to listTickets. Returned rather than assumed, so a client selects a queue by asking for it rather than by re-deriving the filter."
          },
          "count": {
            "type": "integer"
          }
        }
      },
      "RecipientNotification": {
        "type": "object",
        "description": "A notification as its own recipient sees it. Carries no workspace internals, no delivery detail and no provider: the reader is the customer's customer.",
        "properties": {
          "notification_id": {
            "type": "string"
          },
          "notification_type": {
            "type": "string"
          },
          "subject": {
            "type": "string"
          },
          "body_text": {
            "type": "string"
          },
          "sent_at": {
            "type": "string",
            "format": "date-time"
          },
          "read_at": {
            "type": "string",
            "format": "date-time",
            "description": "Null until the recipient reads it. Never inferred from delivery."
          }
        }
      },
      "InboundEmail": {
        "type": "object",
        "required": [
          "to",
          "from",
          "subject"
        ],
        "properties": {
          "to": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Every address it was addressed to, including cc. The reply address we planted may be in any of them."
          },
          "from": {
            "type": "string"
          },
          "subject": {
            "type": "string"
          },
          "text": {
            "type": "string"
          },
          "html": {
            "type": "string"
          },
          "message_id": {
            "type": "string"
          },
          "references": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "In-Reply-To and References, in order."
          },
          "received_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "InboundReceipt": {
        "type": "object",
        "properties": {
          "message_id": {
            "type": "string"
          },
          "thread_id": {
            "type": "string"
          },
          "ticket_id": {
            "type": "string"
          },
          "notification_id": {
            "type": "string",
            "description": "What it answers, where that could be established without guessing."
          },
          "resolution_method": {
            "type": "string"
          },
          "resolution_confidence": {
            "type": "number"
          },
          "explanation": {
            "type": "string",
            "description": "Why it resolved the way it did, in words, for whoever reads this later."
          },
          "started_new_thread": {
            "type": "boolean",
            "description": "True where confidence was too low to join an existing one. Merging on a guess shows one customer another customer's conversation."
          }
        }
      },
      "Message": {
        "type": "object",
        "properties": {
          "message_id": {
            "type": "string"
          },
          "thread_id": {
            "type": "string"
          },
          "direction": {
            "type": "string",
            "enum": [
              "inbound",
              "outbound",
              "internal"
            ]
          },
          "channel": {
            "type": "string"
          },
          "from_hint": {
            "type": "string",
            "description": "Recognisable, not contactable."
          },
          "subject": {
            "type": "string"
          },
          "body_text": {
            "type": "string"
          },
          "resolution_method": {
            "type": "string"
          },
          "resolution_confidence": {
            "type": "number"
          },
          "notification_ref": {
            "type": "string"
          },
          "received_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Thread": {
        "type": "object",
        "properties": {
          "thread_id": {
            "type": "string"
          },
          "channel": {
            "type": "string"
          },
          "subject": {
            "type": "string"
          },
          "ticket_id": {
            "type": "string"
          },
          "message_count": {
            "type": "integer"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "last_message_at": {
            "type": "string",
            "format": "date-time"
          },
          "messages": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Message"
            }
          }
        }
      },
      "CreateOtpChallenge": {
        "type": "object",
        "required": [
          "recipient"
        ],
        "properties": {
          "recipient": {
            "type": "object",
            "required": [
              "external_id"
            ],
            "properties": {
              "external_id": {
                "type": "string"
              }
            }
          },
          "digits": {
            "type": "integer",
            "minimum": 4,
            "maximum": 8,
            "default": 4
          },
          "ttl_seconds": {
            "type": "integer",
            "minimum": 60,
            "maximum": 1800,
            "default": 300,
            "description": "How long the code is good for. Short by default: a passcode that lives for an hour is a password."
          }
        }
      },
      "OtpChallenge": {
        "type": "object",
        "description": "What the caller gets back. Never the code — that went to the recipient.",
        "properties": {
          "challenge_id": {
            "type": "string"
          },
          "digits": {
            "type": "integer"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          },
          "attempts_remaining": {
            "type": "integer"
          },
          "notification_id": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "OtpVerification": {
        "type": "object",
        "properties": {
          "verified": {
            "type": "boolean",
            "description": "The only field that decides anything. A 200 means the check ran, not that it passed."
          },
          "reason": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "incorrect",
              "expired",
              "already_used",
              "no_attempts_remaining",
              null
            ],
            "description": "Why it failed. Uniform enough that a caller cannot distinguish a wrong code from a challenge that never existed."
          },
          "attempts_remaining": {
            "type": "integer"
          }
        }
      },
      "ChainVerification": {
        "type": "object",
        "properties": {
          "integrity_verified": {
            "type": "boolean",
            "description": "True only when every pack verified, every link held, no sequence number was missing, and the signed head agrees with what is here."
          },
          "status": {
            "type": "string",
            "enum": [
              "intact",
              "broken",
              "unverified"
            ],
            "description": "Three answers, not two. 'Checked and fine', 'checked and found a problem', and 'could not check' are different things, and collapsing the third into either of the others is how a verifier that proves nothing comes to read as reassurance."
          },
          "packs_checked": {
            "type": "integer"
          },
          "first_break": {
            "type": [
              "object",
              "null"
            ],
            "additionalProperties": true,
            "description": "Where it stops being trustworthy, and why. Null unless status is 'broken'. A verdict without a position tells an auditor nothing they can act on."
          },
          "unverifiable": {
            "type": [
              "object",
              "null"
            ],
            "additionalProperties": true,
            "description": "Why no check could run — no published key, or no signed head. Null unless status is 'unverified'."
          },
          "verified_against_kid": {
            "type": [
              "string",
              "null"
            ],
            "description": "The key the signatures were checked with, so the result names what it trusted."
          }
        }
      },
      "EvidencePack": {
        "type": "object",
        "description": "A sealed, self-verifying record. Adopted from KYE's signed packs, plus the chain link KYE lacks.",
        "properties": {
          "body": {
            "type": "object",
            "additionalProperties": true,
            "description": "The facts, including previous_pack_hash. Canonical JSON of this is what was signed."
          },
          "signed_body_canonical": {
            "type": "string",
            "description": "The exact bytes signed. Carried rather than re-derived: asking a verifier to reproduce canonical JSON is where offline verification fails, and a key-order disagreement reads identically to a forgery."
          },
          "signature": {
            "type": "object",
            "additionalProperties": true
          },
          "verification_instructions": {
            "type": "object",
            "additionalProperties": true,
            "description": "How to check it, including where the public key lives."
          }
        }
      },
      "EvidencePackSummary": {
        "type": "object",
        "properties": {
          "pack_id": {
            "type": "string"
          },
          "sequence": {
            "type": "integer",
            "description": "Dense from 1. A gap is a missing pack."
          },
          "notification_id": {
            "type": "string"
          },
          "previous_pack_hash": {
            "type": "string"
          },
          "pack_hash": {
            "type": "string"
          },
          "sealed_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Journey": {
        "$comment": "Generated from presentJourney in src/routers/journey.ts, typed from schema/registry.journeys.v1.json.",
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^[a-z][a-z0-9_]*$",
            "x-kind": "identity"
          },
          "sectors": {
            "type": "array",
            "minItems": 1,
            "items": {
              "type": "string",
              "x-kind": "ref",
              "x-ref": "registry/sectors.json#sectors[].id"
            },
            "description": "Which sectors run it. A journey belonging to several is normal — a subscription behaves the same in SaaS and media — and duplicating it per sector is how two copies start disagreeing."
          },
          "subject": {
            "type": "string",
            "description": "What the journey is ABOUT — an appointment, a claim, an order. One run of the journey exists per subject per recipient, which is what stops two orders sharing one state.",
            "x-kind": "opaque"
          },
          "describes": {
            "type": "string",
            "minLength": 40,
            "x-kind": "text"
          },
          "states": {
            "type": "array",
            "minItems": 2,
            "items": {
              "type": "object",
              "required": [
                "name",
                "kind",
                "describes"
              ],
              "additionalProperties": false,
              "properties": {
                "name": {
                  "type": "string",
                  "pattern": "^[a-z][a-z0-9_]*$",
                  "x-kind": "identity"
                },
                "kind": {
                  "type": "string",
                  "enum": [
                    "initial",
                    "transient",
                    "waiting",
                    "terminal"
                  ],
                  "description": "waiting means the journey is parked on somebody else. An initial state may also hold a clock: a card hold is both where the journey starts and the wait that matters, so sla_hours is allowed on any state and required on a waiting one."
                },
                "notify": {
                  "type": "object",
                  "required": [
                    "type",
                    "recipient_role"
                  ],
                  "additionalProperties": false,
                  "description": "What is sent on entering this state. Absent means the state is reached silently, which is a real answer: not every step in a journey is worth a message, and a product that notifies on all of them trains people to ignore it.",
                  "properties": {
                    "type": {
                      "type": "string",
                      "description": "A notification type from the dictionary.",
                      "x-kind": "ref",
                      "x-ref": "registry/dictionary.json#entries[].key"
                    },
                    "recipient_role": {
                      "type": "string",
                      "description": "Who hears about it. A stakeholder role, because the person a claim decision goes to is not always the person who pays for the policy.",
                      "x-kind": "ref",
                      "x-ref": "registry/stakeholders.json#classes[].roles[].id"
                    }
                  }
                },
                "sla_hours": {
                  "type": "number",
                  "exclusiveMinimum": 0,
                  "description": "How long this state may hold before it is a problem. On a waiting state this is the clock a regulator cares about."
                },
                "describes": {
                  "type": "string",
                  "minLength": 20,
                  "x-kind": "text"
                }
              }
            }
          },
          "transitions": {
            "type": "array",
            "minItems": 1,
            "items": {
              "type": "object",
              "required": [
                "from",
                "to",
                "trigger"
              ],
              "additionalProperties": false,
              "properties": {
                "from": {
                  "type": "string",
                  "x-kind": "ref",
                  "x-ref": "registry/journeys.json#journeys[].states[].name"
                },
                "to": {
                  "type": "string",
                  "x-kind": "ref",
                  "x-ref": "registry/journeys.json#journeys[].states[].name"
                },
                "trigger": {
                  "type": "string",
                  "enum": [
                    "event",
                    "before_subject_time",
                    "after_state_entry",
                    "sla_breach"
                  ],
                  "description": "before_subject_time counts backwards from a field on the subject — 48 hours before the appointment. after_state_entry counts forwards from arriving here — three days after the claim was logged. They are different clocks and conflating them is how a reminder fires after the thing it reminds about."
                },
                "subject_field": {
                  "type": "string",
                  "description": "The field counted from, for before_subject_time.",
                  "x-kind": "opaque"
                },
                "offset_hours": {
                  "type": "number",
                  "description": "How far, in hours. Positive is a magnitude; the trigger decides the direction."
                },
                "on_event": {
                  "type": "string",
                  "description": "The event type that fires it, for an event trigger.",
                  "x-kind": "opaque"
                },
                "guard": {
                  "type": "string",
                  "description": "A condition that must hold, written against the journey's own state. Declared rather than coded so a sector is still a row.",
                  "x-kind": "text"
                },
                "describes": {
                  "type": "string",
                  "minLength": 20,
                  "x-kind": "text"
                }
              }
            }
          },
          "cancels_on": {
            "type": "array",
            "description": "Events that end the journey wherever it has got to, and the state they land in. Part of the machine rather than beside it: a reminder arriving after a cancellation tells somebody to attend something that is not happening, and that only gets caught if cancelling is a transition like any other.",
            "items": {
              "type": "object",
              "required": [
                "on_event",
                "to"
              ],
              "additionalProperties": false,
              "properties": {
                "on_event": {
                  "type": "string",
                  "x-kind": "opaque"
                },
                "to": {
                  "type": "string",
                  "description": "The terminal state it lands in.",
                  "x-kind": "ref",
                  "x-ref": "registry/journeys.json#journeys[].states[].name"
                }
              }
            }
          },
          "resets_on": {
            "type": "array",
            "description": "Events that restart the journey from wherever it had got to. A reschedule is not a step forward from one particular state — it invalidates every clock counting from the old subject time, whichever of them has already fired. Declared journey-wide for the same reason cancellation is: listing it per state means forgetting a state, and the forgotten one sends a reminder for an appointment that moved.",
            "items": {
              "type": "object",
              "required": [
                "on_event",
                "to"
              ],
              "additionalProperties": false,
              "properties": {
                "on_event": {
                  "type": "string",
                  "x-kind": "opaque"
                },
                "to": {
                  "type": "string",
                  "description": "The state to re-enter. Its outgoing timed transitions are recomputed against the new subject.",
                  "x-kind": "opaque"
                },
                "describes": {
                  "type": "string",
                  "x-kind": "text"
                }
              }
            }
          }
        }
      },
      "JourneyRun": {
        "$comment": "Generated from present in src/routers/journey.ts, typed from schema/journey_run.v1.json.",
        "type": "object",
        "properties": {
          "run_id": {
            "type": "string",
            "x-kind": "identity"
          },
          "workspace_id": {
            "type": "string",
            "x-kind": "opaque"
          },
          "journey_id": {
            "type": "string",
            "description": "A journey declared in registry/journeys.json.",
            "x-kind": "opaque"
          },
          "subject_id": {
            "type": "string",
            "description": "The thing this is about — one appointment, one claim, one card hold. One run exists per subject per recipient, because two orders sharing a state would double every message.",
            "x-kind": "opaque"
          },
          "recipient_id": {
            "type": "string",
            "x-kind": "opaque"
          },
          "state": {
            "type": "string",
            "description": "Which state of the journey the run is in now.",
            "x-kind": "opaque"
          },
          "entered_at": {
            "type": "string",
            "format": "date-time",
            "description": "When it entered that state. Distinct from started_at, because a clock counting forward from arriving is not the same clock as one counting from the beginning."
          },
          "subject": {
            "type": "object",
            "description": "The fields the journey counts from, such as appointment_at. Held on the run because a reminder has to move when the appointment moves, and the run is the only thing that knows both.",
            "x-open": "A snapshot of the subject as the journey saw it when the run began, so a later change to the subject does not rewrite what the run decided.",
            "default": {}
          },
          "counters": {
            "type": "object",
            "additionalProperties": {
              "type": "integer"
            },
            "description": "What a guard reads. A chase that has already fired twice is the difference between a reminder and pressure.",
            "default": {}
          },
          "next_due_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When something is next due. Null means the run waits on an event rather than a clock — a real state, and not an idle one."
          },
          "started_at": {
            "type": "string",
            "format": "date-time"
          },
          "cancelled_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Set when an event ended the journey early. Kept apart from completed_at: a claim withdrawn and a claim settled are different facts, and collapsing them loses which one happened."
          },
          "completed_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "StartJourneyRun": {
        "type": "object",
        "required": [
          "subject_id",
          "recipient"
        ],
        "properties": {
          "subject_id": {
            "type": "string",
            "description": "The thing this run is about. One run exists per subject per recipient."
          },
          "recipient": {
            "type": "object",
            "required": [
              "external_id"
            ],
            "properties": {
              "external_id": {
                "type": "string"
              }
            }
          },
          "subject": {
            "type": "object",
            "description": "The fields the journey counts from, such as appointment_at. Without them a clock that counts back from the subject has nothing to count from."
          }
        }
      },
      "NotificationPayload": {
        "type": "object",
        "description": "What is retained of a request after the retention policy has run. Absent payload is stated, never implied by an empty object.",
        "properties": {
          "notification_id": {
            "type": "string"
          },
          "contract_ref": {
            "type": "string"
          },
          "contract_version": {
            "type": "string"
          },
          "payload_hash": {
            "type": "string",
            "description": "Of the payload as submitted, after undeclared fields were dropped."
          },
          "retained": {
            "type": "boolean",
            "description": "false once the retention policy has purged the body."
          },
          "fields": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The field names the contract declares, so the shape is knowable after the values are gone."
          },
          "payload": {
            "type": "object",
            "description": "Present only while retained is true."
          },
          "hash_algorithm": {
            "type": "string",
            "description": "How payload_hash was computed, so it can be checked rather than trusted."
          },
          "hash_recipe": {
            "type": "string"
          },
          "retention_note": {
            "type": "string"
          }
        }
      },
      "NotificationTemplates": {
        "type": "object",
        "properties": {
          "notification_id": {
            "type": "string"
          },
          "data": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "channel": {
                  "type": "string"
                },
                "template_id": {
                  "type": "string"
                },
                "template_version": {
                  "type": "integer"
                },
                "notification_type": {
                  "type": "string"
                },
                "locale": {
                  "type": "string"
                },
                "subject": {
                  "type": "string",
                  "description": "The template's current subject line, for orientation. The hashes below are what was sent."
                },
                "rendered_subject_hash": {
                  "type": "string"
                },
                "rendered_body_hash": {
                  "type": "string"
                },
                "template_status": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "PolicyDecision": {
        "type": "object",
        "properties": {
          "decision_id": {
            "type": "string"
          },
          "decision": {
            "type": "string"
          },
          "action": {
            "type": "string"
          },
          "evaluated_policies": {
            "type": "array",
            "description": "Every policy consulted, including the ones that did not apply. A decision that lists only the rule that fired cannot show what else was considered.",
            "items": {
              "type": "object",
              "properties": {
                "policy_id": {
                  "type": "string"
                },
                "version": {
                  "type": "string"
                },
                "point": {
                  "type": "string"
                },
                "verdict": {
                  "type": "string",
                  "enum": [
                    "allow",
                    "deny",
                    "defer",
                    "not_applicable"
                  ]
                }
              }
            }
          },
          "obligations": {
            "type": "array",
            "description": "What the decision requires of whatever acts on it — purge the raw payload, record a receipt. An obligation with nothing enforcing it is a comment.",
            "items": {
              "type": "object",
              "properties": {
                "code": {
                  "type": "string"
                },
                "enforced_by": {
                  "type": "string",
                  "description": "The component that carries it out. An obligation nothing is named to enforce is a comment."
                },
                "parameters": {
                  "type": "object",
                  "additionalProperties": true,
                  "description": "Parameters for this obligation. The shape is the obligation's own — a redaction names fields, a delay names a duration."
                }
              }
            }
          },
          "reason_codes": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "defer_until": {
            "type": "string",
            "format": "date-time"
          },
          "decided_at": {
            "type": "string",
            "format": "date-time"
          },
          "evaluation_ms": {
            "type": "integer"
          },
          "cached": {
            "type": "boolean"
          }
        }
      },
      "WebhookDelivery": {
        "type": "object",
        "properties": {
          "delivery_id": {
            "type": "string"
          },
          "endpoint_url": {
            "type": "string",
            "format": "uri"
          },
          "event_id": {
            "type": "string"
          },
          "event_type": {
            "type": "string"
          },
          "attempt": {
            "type": "integer"
          },
          "status": {
            "type": "string"
          },
          "response_status": {
            "type": "integer"
          },
          "requested_at": {
            "type": "string",
            "format": "date-time"
          },
          "completed_at": {
            "type": "string",
            "format": "date-time"
          },
          "next_attempt_at": {
            "type": "string",
            "format": "date-time"
          },
          "error_detail": {
            "type": "string"
          }
        }
      },
      "Event": {
        "type": "object",
        "properties": {
          "event_id": {
            "type": "string"
          },
          "event_type": {
            "type": "string"
          },
          "event_version": {
            "type": "string"
          },
          "occurred_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the world produced the signal."
          },
          "recorded_at": {
            "type": "string",
            "format": "date-time",
            "description": "When we wrote it down. Kept apart from occurred_at so lag stays measurable."
          },
          "sequence": {
            "type": "integer"
          },
          "correlation_id": {
            "type": "string"
          },
          "data": {
            "type": "object",
            "additionalProperties": true,
            "description": "The event's payload. Its shape follows the event family, declared in registry/event-families.json."
          },
          "evidence_refs": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "Timeline": {
        "type": "object",
        "properties": {
          "notification_id": {
            "type": "string"
          },
          "entries": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "at": {
                  "type": "string",
                  "format": "date-time"
                },
                "event": {
                  "type": "string"
                },
                "detail": {
                  "type": "string"
                },
                "evidence_refs": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                }
              }
            }
          }
        }
      },
      "PreprocessResult": {
        "$comment": "Generated from presentPreprocess in src/routers/compliance.ts, typed from schema/notification.v1.json.",
        "type": "object",
        "properties": {
          "outcome": {
            "type": "string",
            "enum": [
              "allow",
              "block",
              "quarantine"
            ]
          },
          "canonical": {
            "type": "object",
            "additionalProperties": true,
            "description": "The payload as it would be persisted, after normalisation."
          },
          "search_projection": {
            "type": "object",
            "additionalProperties": true,
            "description": "What of the payload becomes searchable. Everything else is not indexed."
          },
          "dropped_fields": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Undeclared fields, removed before persistence."
          },
          "classifications": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "detection": {
            "type": [
              "object",
              "null"
            ],
            "additionalProperties": true,
            "description": "What the detection rules found, where they found anything."
          },
          "payload_hash": {
            "type": [
              "string",
              "null"
            ],
            "description": "Hash of the canonical payload. Survives the payload itself, which retention purges, so a replay can still be proven to match.",
            "x-kind": "opaque"
          },
          "errors": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "WidgetTokenRequest": {
        "type": "object",
        "required": [
          "external_user_id",
          "widgets",
          "scopes"
        ],
        "properties": {
          "external_user_id": {
            "type": "string"
          },
          "widgets": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "scopes": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "ttl": {
            "type": "integer",
            "default": 600,
            "maximum": 3600
          }
        }
      },
      "WidgetToken": {
        "type": "object",
        "properties": {
          "token": {
            "type": "string"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "WorkspaceSignature": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "legal_name",
          "website_url",
          "jurisdiction"
        ],
        "description": "NotifHQ's own mail carries PLATFORM_SIGNATURE. A customer's mail must carry THEIRS — not ours, and not nothing. src/rails/notification.ts passed a null signature on purpose (appending our copyright to someone else's email claims their content as ours), and the half nobody wrote was the other one: a workspace could send with no identifying footer at all. A recipient who cannot tell who wrote to them, or where to reach them, is the case PECR's identification rule exists for, and what satisfies it is a legal name, a jurisdiction and somewhere to go. This platform is B2B2C: the sender is a business whose own customer is being written to, and such a sender has a website long before it has a mail room. The postal address is optional for that reason and stays declared, because CAN-SPAM §7704(a)(5) still demands one for COMMERCIAL mail to US recipients — a case the sender knows they are in and this platform cannot detect. It — and the liability lands on the platform that carried it.",
        "properties": {
          "legal_name": {
            "type": "string",
            "description": "Who is actually sending. The workspace's display name is a product label and may be a brand, a team or an environment; the legal name is the entity a recipient can act against."
          },
          "website_url": {
            "type": "string",
            "description": "Where a recipient goes to find out who wrote to them. A live site carrying the sender's own identity answers 'who are these people' faster than an address on a line nobody reads, and it is the one contact detail a B2B2C sender reliably has: the business whose customer is being written to has a website long before it has a mail room."
          },
          "jurisdiction": {
            "type": "string",
            "description": "Which law the sender answers to. Already held per workspace for residency; restated here because the footer must say it and the two must not drift."
          },
          "postal_address": {
            "type": [
              "string",
              "null"
            ],
            "description": "Registered postal address"
          },
          "privacy_url": {
            "type": [
              "string",
              "null"
            ],
            "description": "Privacy policy URL"
          },
          "terms_url": {
            "type": [
              "string",
              "null"
            ],
            "description": "Terms URL"
          },
          "support_email": {
            "type": [
              "string",
              "null"
            ],
            "description": "Support address"
          }
        }
      },
      "SearchRequest": {
        "type": "object",
        "required": [
          "query"
        ],
        "properties": {
          "query": {
            "type": "string"
          },
          "types": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "notification",
                "delivery",
                "recipient",
                "ticket",
                "message",
                "template",
                "document",
                "invoice",
                "audit"
              ]
            }
          },
          "filters": {
            "type": "object",
            "additionalProperties": false,
            "properties": {
              "labels": {
                "description": "Canonical labels, from registry/labels.json. Matched against entity_labels, which is a row per label and indexed — the JSON columns these replaced could not serve this query at all, which is why the product never offered it. Narrows: ticket, recipient.",
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "tags": {
                "description": "Free-form tags, from entity_tags. Searchable and never faceted: a facet over an open set is noise (registry/labels.json). Tickets only — a recipient carries canonical labels and no tags, and saying otherwise would offer a narrowing that can only ever return nothing, which reads as 'there are none' rather than 'this was never recorded'. Narrows: ticket.",
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "since": {
                "description": "Lower bound on the object's own time column, inclusive. The column is the one the object already declares as order_by, so a filter can never sort by one clock and bound by another. Narrows: notification, delivery, recipient, ticket, message, template, document, invoice, audit.",
                "type": "string",
                "format": "date-time"
              },
              "until": {
                "description": "Upper bound on the same column, exclusive. Narrows: notification, delivery, recipient, ticket, message, template, document, invoice, audit.",
                "type": "string",
                "format": "date-time"
              }
            }
          },
          "limit": {
            "type": "integer",
            "default": 25,
            "maximum": 100
          }
        }
      },
      "SearchResponse": {
        "type": "object",
        "properties": {
          "results": {
            "type": "array",
            "items": {
              "$comment": "Generated from presentHit in src/routers/search.ts, typed from schema/registry.dictionary.v1.json.",
              "type": "object",
              "properties": {
                "type": {
                  "type": "string",
                  "description": "Which searchable table this hit came from."
                },
                "id": {
                  "type": "string",
                  "description": "The row's own identifier, aliased from each table's primary key."
                },
                "title": {
                  "type": "string",
                  "description": "The one column worth reading first, aliased per table."
                },
                "state": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "description": "The row's status, where it has one."
                },
                "at": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "format": "date-time",
                  "description": "When it happened. What the combined list sorts by."
                }
              }
            }
          },
          "compiled_filters": {
            "type": "object",
            "description": "The canonical filters the query compiled to. A search whose filters cannot be shown is not shipped."
          },
          "total": {
            "type": "integer"
          }
        }
      },
      "AnalyticsSummary": {
        "type": "object",
        "properties": {
          "requested": {
            "type": "integer"
          },
          "sent": {
            "type": "integer"
          },
          "delivered": {
            "type": "integer"
          },
          "suppressed": {
            "type": "integer"
          },
          "failed": {
            "type": "integer"
          },
          "open_observation_rate": {
            "type": "number",
            "description": "Not an 'open rate'. A pixel fetch is an observation about a client."
          },
          "click_rate": {
            "type": "number"
          },
          "p50_latency_ms": {
            "type": "integer"
          },
          "p95_latency_ms": {
            "type": "integer"
          },
          "p99_latency_ms": {
            "type": "integer"
          }
        }
      }
    }
  },
  "x-plans": [
    {
      "id": "growth",
      "rails": [
        "notification",
        "preprocessing",
        "template",
        "preference",
        "delivery",
        "analytics"
      ]
    },
    {
      "id": "scale",
      "rails": [
        "notification",
        "preprocessing",
        "template",
        "preference",
        "delivery",
        "analytics",
        "search"
      ]
    },
    {
      "id": "business",
      "rails": [
        "notification",
        "preprocessing",
        "template",
        "preference",
        "delivery",
        "analytics",
        "search",
        "inbound_email",
        "team_inbox",
        "workflow"
      ]
    },
    {
      "id": "high_volume",
      "rails": [
        "notification",
        "preprocessing",
        "template",
        "preference",
        "delivery",
        "analytics",
        "search",
        "inbound_email",
        "team_inbox",
        "workflow",
        "compliance",
        "routing"
      ]
    },
    {
      "id": "platform",
      "rails": [
        "notification",
        "preprocessing",
        "template",
        "preference",
        "delivery",
        "analytics"
      ]
    }
  ]
}
