{
  "openapi": "3.1.0",
  "info": {
    "title": "TermShelf Public Delivery API",
    "version": "1.0.0",
    "license": {
      "name": "Proprietary",
      "url": "https://termshelf.com/terms"
    },
    "summary": "Read-only delivery of published legal-text artifacts as JSON, HTML and PDF.",
    "description": "The Public Delivery API serves published TermShelf legal-text artifacts for a\n`(site, locale, market, profile, document_type)` tuple. Three artifact forms\nare exposed at the same path prefix and only the trailing segment differs:\n\n- `/v1/delivery/sites/{siteID}/documents/{typeCode}`        → `application/json`\n- `/v1/delivery/sites/{siteID}/documents/{typeCode}/html`   → `text/html; charset=utf-8`\n- `/v1/delivery/sites/{siteID}/documents/{typeCode}/pdf`    → `application/pdf`\n\nAll endpoints are anonymous-safe and read from the live delivery projection only.\nDrafts, in-review revisions and superseded versions are never returned.\n\nThree independent gates apply:\n\n- **Per-IP rate limit** — applied by the server's in-process token bucket. Exhausting\n  the IP budget returns `429` with a flat error body (`{\"error\":\"rate_limited\", ...}`)\n  that is intentionally distinct from per-workspace quota errors.\n- **Plan-aware entitlement gating** — JSON / HTML hang on `feature.public_delivery`,\n  PDF additionally on `feature.pdf_delivery`, and `?version=N` on `feature.version_pinning`.\n  Denied requests respond `403 entitlement_required`. The wire body intentionally carries\n  no plan name, no upgrade hint and no budget value.\n- **Per-workspace monthly delivery quota** — enforced backend-side. Exhausting the\n  monthly budget returns `429 quota_exceeded`. PDF requests count against both the\n  umbrella delivery metric and a dedicated PDF metric.\n\nJSON / HTML are availability-first: a transient resolver or counter-store outage\nserves the request rather than dropping legacy traffic. PDF is fail-closed for the\nsame outages and returns `403` (entitlement evidence missing) or `503` (store\nmisconfigured / quota counter unreachable).\n\n`effective_at` is supported. A request that sets `effective_at=<RFC3339>` is\nresolved against the projection's publication history and returns the version\nthat was live at that instant. `version` and `effective_at` are mutually\nexclusive; both share the `feature.version_pinning` entitlement gate because\nboth ask the API to serve a non-default historical version.\n",
    "contact": {
      "name": "TermShelf",
      "url": "https://termshelf.de"
    }
  },
  "security": [],
  "servers": [
    {
      "url": "/",
      "description": "Relative server URL. Bind the spec to the deployment host that serves\n`/v1/delivery/...`. No public production host is hard-coded here so the\nsame spec can be loaded from local development, staging and production\nwithout edits.\n"
    }
  ],
  "tags": [
    {
      "name": "Delivery",
      "description": "Public read-only delivery of published legal-text artifacts."
    }
  ],
  "paths": {
    "/v1/delivery/sites/{siteID}/documents/{typeCode}": {
      "get": {
        "tags": [
          "Delivery"
        ],
        "operationId": "getDocumentJSON",
        "summary": "Get the live document as a structured JSON envelope.",
        "description": "Returns the live delivery projection row for the requested tuple as a\nstable JSON envelope. The response body is shaped for consumers that\nrender with their own templates (own e-mail templates, own checkout\nflows, own apps).\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/SiteID"
          },
          {
            "$ref": "#/components/parameters/TypeCode"
          },
          {
            "$ref": "#/components/parameters/Locale"
          },
          {
            "$ref": "#/components/parameters/Market"
          },
          {
            "$ref": "#/components/parameters/Profile"
          },
          {
            "$ref": "#/components/parameters/Version"
          },
          {
            "$ref": "#/components/parameters/EffectiveAt"
          },
          {
            "$ref": "#/components/parameters/APIVersionPin"
          },
          {
            "$ref": "#/components/parameters/IfNoneMatch"
          }
        ],
        "responses": {
          "200": {
            "description": "Live delivery row encoded as JSON.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Cache-Control": {
                "$ref": "#/components/headers/CacheControlJSONHTML"
              },
              "Last-Modified": {
                "$ref": "#/components/headers/LastModified"
              },
              "X-Termshelf-Document-Version": {
                "$ref": "#/components/headers/XTermshelfDocumentVersion"
              },
              "X-Termshelf-Published-At": {
                "$ref": "#/components/headers/XTermshelfPublishedAt"
              },
              "X-Correlation-Id": {
                "$ref": "#/components/headers/XCorrelationId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeliveryDocument"
                },
                "examples": {
                  "privacy": {
                    "$ref": "#/components/examples/DeliveryDocumentExample"
                  }
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "403": {
            "$ref": "#/components/responses/EntitlementRequired"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "409": {
            "$ref": "#/components/responses/VersionMismatch"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        }
      }
    },
    "/v1/delivery/sites/{siteID}/documents/{typeCode}/html": {
      "get": {
        "tags": [
          "Delivery"
        ],
        "operationId": "getDocumentHTML",
        "summary": "Get the live document as a sanitized HTML fragment.",
        "description": "Returns the same projection row as `getDocumentJSON`, rendered as a\nself-contained HTML fragment wrapped in `<article class=\"ts-document\">`.\n\nThe fragment is **not** a full page — it carries no `<head>`, `<body>`\nor layout chrome and is intended for direct embedding in pages, app\ntemplates, transactional e-mails or Checkout flows. All authoring text\nand attribute values are HTML-escaped server-side; the API never emits\nan authoring-supplied HTML blob as-is and never inlines `<script>`.\n\nCache and conditional-request semantics are identical to the JSON\nendpoint and the two share the same `ETag`.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/SiteID"
          },
          {
            "$ref": "#/components/parameters/TypeCode"
          },
          {
            "$ref": "#/components/parameters/Locale"
          },
          {
            "$ref": "#/components/parameters/Market"
          },
          {
            "$ref": "#/components/parameters/Profile"
          },
          {
            "$ref": "#/components/parameters/Version"
          },
          {
            "$ref": "#/components/parameters/EffectiveAt"
          },
          {
            "$ref": "#/components/parameters/APIVersionPin"
          },
          {
            "$ref": "#/components/parameters/IfNoneMatch"
          }
        ],
        "responses": {
          "200": {
            "description": "HTML fragment for the live delivery row.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Cache-Control": {
                "$ref": "#/components/headers/CacheControlJSONHTML"
              },
              "Last-Modified": {
                "$ref": "#/components/headers/LastModified"
              },
              "X-Termshelf-Document-Version": {
                "$ref": "#/components/headers/XTermshelfDocumentVersion"
              },
              "X-Termshelf-Published-At": {
                "$ref": "#/components/headers/XTermshelfPublishedAt"
              },
              "X-Correlation-Id": {
                "$ref": "#/components/headers/XCorrelationId"
              }
            },
            "content": {
              "text/html": {
                "schema": {
                  "type": "string",
                  "description": "Sanitized HTML fragment. Outer element is always\n`<article class=\"ts-document\" ...>`. Block elements use\nfixed `ts-block ts-block--<kind>` classes; unknown block\nkinds fall back to a `ts-block--unknown` wrapper with\nescaped text.\n"
                },
                "example": "<article class=\"ts-document\"\n         data-document-type-code=\"privacy\"\n         data-document-slug=\"privacy-policy\"\n         data-document-version=\"3\"\n         data-locale=\"de-DE\"\n         lang=\"de-DE\"\n         data-market=\"DE\"\n         data-site-profile=\"B2C\">\n  <section class=\"ts-section\" data-section-key=\"main\">\n    <h2 class=\"ts-section__title\">Main</h2>\n    <div class=\"ts-block ts-block--paragraph\" data-block-key=\"intro\">\n      <p>Hello.</p>\n    </div>\n  </section>\n</article>\n"
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "403": {
            "$ref": "#/components/responses/EntitlementRequired"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "409": {
            "$ref": "#/components/responses/VersionMismatch"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          }
        }
      }
    },
    "/v1/delivery/sites/{siteID}/documents/{typeCode}/pdf": {
      "get": {
        "tags": [
          "Delivery"
        ],
        "operationId": "getDocumentPDF",
        "summary": "Get the live document as a PDF artifact.",
        "description": "Streams the cached PDF file for the live projection row. PDFs are\ngenerated upstream by the publish pipeline and are immutable per\ndocument version. The endpoint is suitable for attaching the PDF to\ntransactional e-mails (order confirmations, contract mails, sign-up\nflows) or for direct download.\n\nPDF is a premium artifact and is fail-closed: missing entitlement\nevidence, missing quota evidence, or a misconfigured documents store\nall deny the response rather than streaming bytes speculatively.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/SiteID"
          },
          {
            "$ref": "#/components/parameters/TypeCode"
          },
          {
            "$ref": "#/components/parameters/Locale"
          },
          {
            "$ref": "#/components/parameters/Market"
          },
          {
            "$ref": "#/components/parameters/Profile"
          },
          {
            "$ref": "#/components/parameters/Version"
          },
          {
            "$ref": "#/components/parameters/EffectiveAt"
          },
          {
            "$ref": "#/components/parameters/APIVersionPin"
          },
          {
            "$ref": "#/components/parameters/IfNoneMatch"
          }
        ],
        "responses": {
          "200": {
            "description": "Cached PDF artifact for the live delivery row.",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Cache-Control": {
                "$ref": "#/components/headers/CacheControlPDF"
              },
              "Content-Disposition": {
                "$ref": "#/components/headers/ContentDispositionPDF"
              },
              "Content-Length": {
                "$ref": "#/components/headers/ContentLength"
              },
              "X-Termshelf-Document-Version": {
                "$ref": "#/components/headers/XTermshelfDocumentVersion"
              },
              "X-Correlation-Id": {
                "$ref": "#/components/headers/XCorrelationId"
              }
            },
            "content": {
              "application/pdf": {
                "schema": {
                  "type": "string",
                  "format": "binary",
                  "description": "PDF byte stream."
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "403": {
            "$ref": "#/components/responses/EntitlementRequired"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "409": {
            "$ref": "#/components/responses/VersionMismatch"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/Internal"
          },
          "503": {
            "$ref": "#/components/responses/PDFUnavailable"
          }
        }
      }
    }
  },
  "components": {
    "parameters": {
      "SiteID": {
        "name": "siteID",
        "in": "path",
        "required": true,
        "description": "Backoffice site identifier. The Public API never exposes a workspace\nid directly; the site → workspace binding is encoded on every\nprojection row.\n",
        "schema": {
          "type": "integer",
          "format": "int64",
          "minimum": 1
        },
        "example": 42
      },
      "TypeCode": {
        "name": "typeCode",
        "in": "path",
        "required": true,
        "description": "Stable document type code (e.g. `privacy`, `terms`, `imprint`).",
        "schema": {
          "type": "string",
          "minLength": 1
        },
        "example": "privacy"
      },
      "Locale": {
        "name": "locale",
        "in": "query",
        "required": false,
        "description": "Locale code such as `de-DE`. Must be omitted when the projection\nrow has `locale_code = NULL`. When the row is locale-bound, the\nvalue must match exactly.\n",
        "schema": {
          "type": "string"
        },
        "example": "de-DE"
      },
      "Market": {
        "name": "market",
        "in": "query",
        "required": false,
        "description": "Market code such as `DE`. Same nullability rule as `locale`.\n",
        "schema": {
          "type": "string"
        },
        "example": "DE"
      },
      "Profile": {
        "name": "profile",
        "in": "query",
        "required": false,
        "description": "Site profile code such as `B2C`. Same nullability rule as `locale`.\n",
        "schema": {
          "type": "string"
        },
        "example": "B2C"
      },
      "Version": {
        "name": "version",
        "in": "query",
        "required": false,
        "description": "Pin a specific live document version. Must be a positive integer.\nWhen the live row's `version_number` differs from the pinned value\nthe API returns `409 version_mismatch` with `error.live_version`.\n\nPlan gating: `?version=N` requires the `feature.version_pinning`\nentitlement on every artifact endpoint. Requests without that\nentitlement (or when the resolver cannot confirm it) return\n`403 entitlement_required`.\n\nMutually exclusive with `effective_at`. Supplying both returns\n`400 invalid_request`.\n",
        "schema": {
          "type": "integer",
          "minimum": 1
        },
        "example": 3
      },
      "EffectiveAt": {
        "name": "effective_at",
        "in": "query",
        "required": false,
        "description": "Resolve the document version that was live for this delivery\ntuple at the supplied instant. The value MUST be an RFC 3339\ndate-time with an explicit timezone (e.g. `2026-05-02T10:15:00Z`\nor `2026-05-02T12:15:00+02:00`); the server normalises it to\nUTC.\n\nLookup semantics:\n\n- The tuple's published rows form a chain. The row whose\n  live-window contains the timestamp is returned. The same\n  ETag, `X-Termshelf-Document-Version` and\n  `X-Termshelf-Published-At` headers are emitted as for a\n  live request, scoped to the historical row.\n- Drafts and `stale` projections are never returned —\n  `effective_at` is past-only and published-only.\n- A timestamp before the tuple's first publication returns\n  `404 not_found`.\n- A timestamp after the latest publication (but not in the\n  future) returns the current live row.\n- For PDF, the historical artifact for the resolved version\n  is streamed; if no cached PDF exists for that version, the\n  endpoint returns `404 not_found` rather than falling back\n  to the live PDF.\n\nValidation:\n\n- Malformed values return `400 invalid_request`. Timestamps\n  MUST be RFC 3339 with an explicit timezone; the server\n  normalises them to UTC before comparison.\n- Timestamps that lie more than 5 seconds in the future\n  return `400 invalid_request`. The 5-second window absorbs\n  benign clock skew between the caller and the server; values\n  past that window are rejected.\n- `effective_at` and `version` express different lookup\n  semantics and are mutually exclusive on the wire. Supplying\n  both returns `400 invalid_request`.\n\nPlan gating: `effective_at` requires the\n`feature.version_pinning` entitlement on every artifact\nendpoint. Requests without that entitlement (or when the\nresolver cannot confirm it) return `403 entitlement_required`.\n",
        "schema": {
          "type": "string",
          "format": "date-time"
        },
        "example": "2026-05-02T10:15:00+00:00"
      },
      "APIVersionPin": {
        "name": "api",
        "in": "query",
        "required": false,
        "description": "Optional explicit API-contract pin. Only `v1` is accepted in this\nrevision; any other value returns `400 unsupported_version`.\n",
        "schema": {
          "type": "string",
          "enum": [
            "v1"
          ]
        },
        "example": "v1"
      },
      "IfNoneMatch": {
        "name": "If-None-Match",
        "in": "header",
        "required": false,
        "description": "Conditional GET validator. When the supplied value equals the row's\nETag (or is `*`), the API returns `304 Not Modified` with `ETag`\nand `Cache-Control` headers and no body.\n",
        "schema": {
          "type": "string"
        },
        "example": "\"v3-abc123\""
      }
    },
    "headers": {
      "ETag": {
        "description": "Strong validator derived from the document version number and the\ncontent hash, e.g. `\"v3-abc123\"`. Stable across rebuilds of the\nsame content. JSON, HTML and PDF responses share the same value\nfor the same live row.\n",
        "schema": {
          "type": "string"
        },
        "example": "\"v3-abc123\""
      },
      "CacheControlJSONHTML": {
        "description": "Cache directive for JSON / HTML responses.",
        "schema": {
          "type": "string"
        },
        "example": "public, max-age=60, stale-while-revalidate=30"
      },
      "CacheControlPDF": {
        "description": "Cache directive for PDF responses (longer TTL, identical semantics).",
        "schema": {
          "type": "string"
        },
        "example": "public, max-age=300, stale-while-revalidate=60"
      },
      "LastModified": {
        "description": "Latest `built_at` timestamp of the projection row, formatted per\nRFC 7232. Set on JSON / HTML responses; not set on PDF responses.\n",
        "schema": {
          "type": "string"
        },
        "example": "Tue, 21 Apr 2026 09:30:10 GMT"
      },
      "XTermshelfDocumentVersion": {
        "description": "Numeric document version of the row served (or, on `409`, the live version).",
        "schema": {
          "type": "integer"
        },
        "example": 3
      },
      "XTermshelfPublishedAt": {
        "description": "ISO-8601 (RFC 3339) timestamp of the document's first publication\nfor this delivery tuple. Set on JSON / HTML responses; not set on\nPDF responses.\n",
        "schema": {
          "type": "string",
          "format": "date-time"
        },
        "example": "2026-04-10T08:00:00+00:00"
      },
      "ContentDispositionPDF": {
        "description": "Forces download in browsers and provides a deterministic filename\n(`<workspace>-<doc>-<locale>-v<N>.pdf`).\n",
        "schema": {
          "type": "string"
        },
        "example": "attachment; filename=\"acme-privacy-de-DE-v3.pdf\""
      },
      "ContentLength": {
        "description": "PDF byte size.",
        "schema": {
          "type": "integer",
          "format": "int64"
        },
        "example": 184320
      },
      "XCorrelationId": {
        "description": "Per-request correlation id, set by middleware. Useful when filing\na support ticket — operators can grep logs for the value.\n",
        "schema": {
          "type": "string"
        }
      }
    },
    "responses": {
      "NotModified": {
        "description": "Conditional GET match. The body is empty; only `ETag` and\n`Cache-Control` are guaranteed on this response.\n",
        "headers": {
          "ETag": {
            "$ref": "#/components/headers/ETag"
          },
          "Cache-Control": {
            "$ref": "#/components/headers/CacheControlJSONHTML"
          }
        }
      },
      "BadRequest": {
        "description": "Malformed request — invalid `siteID`, missing `typeCode`, malformed\n`?version` (non-integer or non-positive), unsupported `?api`\nvalue, or an invalid `?effective_at` (not RFC 3339 with timezone,\nin the future, or supplied together with `?version`). Errors with\ncode `unsupported_version` indicate the `?api` case; all other\nshapes use `invalid_request`.\n",
        "headers": {
          "Cache-Control": {
            "schema": {
              "type": "string"
            },
            "example": "no-store"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "examples": {
              "invalid_request": {
                "value": {
                  "error": {
                    "code": "invalid_request",
                    "message": "invalid version parameter"
                  }
                }
              },
              "unsupported_version": {
                "value": {
                  "error": {
                    "code": "unsupported_version",
                    "message": "unsupported api version"
                  }
                }
              }
            }
          }
        }
      },
      "EntitlementRequired": {
        "description": "The resolved plan does not entitle the requested artifact form, or\na `?version=N` / `?effective_at=` request was supplied without the\nversion-pinning entitlement, or PDF was requested without\naffirmative entitlement evidence (resolver outage or unknown\nsite → workspace).\n\nThe wire body intentionally carries no plan name, no plan code and\nno upgrade hint.\n",
        "headers": {
          "Cache-Control": {
            "schema": {
              "type": "string"
            },
            "example": "no-store"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "examples": {
              "entitlement_required": {
                "value": {
                  "error": {
                    "code": "entitlement_required",
                    "message": "this delivery artifact is not available for the requested site"
                  }
                }
              }
            }
          }
        }
      },
      "NotFound": {
        "description": "No projection row matches the requested tuple. For `effective_at`\nrequests this also covers timestamps before the tuple's first\npublication. For the PDF endpoint, this additionally covers the\ncase where the row resolves but no cached PDF exists for the\nselected version (the endpoint never falls back to a different\nversion's PDF).\n",
        "headers": {
          "Cache-Control": {
            "schema": {
              "type": "string"
            },
            "example": "no-store"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "examples": {
              "not_found": {
                "value": {
                  "error": {
                    "code": "not_found",
                    "message": "no live delivery record for the requested tuple"
                  }
                }
              }
            }
          }
        }
      },
      "MethodNotAllowed": {
        "description": "Non-`GET` method on a delivery path.",
        "headers": {
          "Cache-Control": {
            "schema": {
              "type": "string"
            },
            "example": "no-store"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "examples": {
              "method_not_allowed": {
                "value": {
                  "error": {
                    "code": "method_not_allowed",
                    "message": "method not allowed"
                  }
                }
              }
            }
          }
        }
      },
      "VersionMismatch": {
        "description": "`?version=N` was supplied but does not match the currently live\nversion. The body extends the standard error envelope with a\n`live_version` integer so a pinned consumer can re-pin\ndeterministically. `X-Termshelf-Document-Version` carries the\nlive version too.\n",
        "headers": {
          "Cache-Control": {
            "schema": {
              "type": "string"
            },
            "example": "no-store"
          },
          "X-Termshelf-Document-Version": {
            "$ref": "#/components/headers/XTermshelfDocumentVersion"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/VersionMismatchError"
            },
            "examples": {
              "version_mismatch": {
                "value": {
                  "error": {
                    "code": "version_mismatch",
                    "message": "requested version does not match the currently live version",
                    "live_version": 4
                  }
                }
              }
            }
          }
        }
      },
      "TooManyRequests": {
        "description": "Either the per-IP rate limit (token bucket, additive to plan-level\ngating) or the per-workspace monthly delivery quota was exceeded.\n\nThese two cases use **different body shapes** because they are\nproduced by different layers of the stack:\n\n- The IP rate limiter (middleware) emits a flat envelope\n  (`{\"error\":\"rate_limited\",\"message\":\"...\"}`) and a `Retry-After`\n  header.\n- The monthly delivery / PDF quota (handler) emits the standard\n  structured envelope with `error.code = \"quota_exceeded\"`. PDF\n  requests count against both the umbrella `delivery.api.requests`\n  metric and a dedicated `delivery.pdf.requests` metric.\n\nThe quota body intentionally carries no plan name, no budget\nvalue and no upgrade hint.\n",
        "headers": {
          "Cache-Control": {
            "schema": {
              "type": "string"
            },
            "example": "no-store"
          },
          "Retry-After": {
            "description": "Seconds to wait before retrying. Set on rate-limited responses.",
            "schema": {
              "type": "string"
            },
            "example": "1"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "oneOf": [
                {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                {
                  "$ref": "#/components/schemas/RateLimitedError"
                }
              ]
            },
            "examples": {
              "quota_exceeded": {
                "value": {
                  "error": {
                    "code": "quota_exceeded",
                    "message": "delivery quota exceeded for the requested site"
                  }
                }
              },
              "rate_limited": {
                "value": {
                  "error": "rate_limited",
                  "message": "too many requests"
                }
              }
            }
          }
        }
      },
      "Internal": {
        "description": "Unexpected server error while reading the projection or rendering\nthe response. The body never leaks authoring-state detail; consult\n`X-Correlation-Id` for ops follow-up.\n",
        "headers": {
          "Cache-Control": {
            "schema": {
              "type": "string"
            },
            "example": "no-store"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "examples": {
              "internal": {
                "value": {
                  "error": {
                    "code": "internal",
                    "message": "internal error"
                  }
                }
              }
            }
          }
        }
      },
      "PDFUnavailable": {
        "description": "PDF endpoint only. Returned when the documents store is not wired\nin this deployment (e.g. a development environment without a\ndocuments root) or when the quota counter store is unreachable\nfor a PDF request — PDF is fail-closed for both. The error code\nis reused from the standard envelope (`internal`); there is no\ndedicated `service_unavailable` code.\n",
        "headers": {
          "Cache-Control": {
            "schema": {
              "type": "string"
            },
            "example": "no-store"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "examples": {
              "pdf_unavailable": {
                "value": {
                  "error": {
                    "code": "internal",
                    "message": "pdf delivery not configured"
                  }
                }
              }
            }
          }
        }
      }
    },
    "schemas": {
      "DeliveryDocument": {
        "type": "object",
        "description": "Stable v1 envelope returned by the JSON endpoint.",
        "required": [
          "schema_version",
          "api_version",
          "document",
          "target",
          "version",
          "sections",
          "meta"
        ],
        "properties": {
          "schema_version": {
            "type": "integer",
            "description": "Body shape version. Bumping this is a breaking change.",
            "example": 1
          },
          "api_version": {
            "type": "string",
            "description": "API-contract version pinned to the URL prefix.",
            "example": "v1"
          },
          "document": {
            "$ref": "#/components/schemas/DocumentEnvelope"
          },
          "target": {
            "$ref": "#/components/schemas/TargetEnvelope"
          },
          "version": {
            "$ref": "#/components/schemas/VersionEnvelope"
          },
          "sections": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Section"
            }
          },
          "meta": {
            "$ref": "#/components/schemas/MetaEnvelope"
          }
        }
      },
      "DocumentEnvelope": {
        "type": "object",
        "required": [
          "type_code",
          "slug",
          "title",
          "summary"
        ],
        "properties": {
          "type_code": {
            "type": "string",
            "example": "privacy"
          },
          "slug": {
            "type": "string",
            "example": "privacy-policy"
          },
          "title": {
            "type": "string",
            "example": "Privacy Policy"
          },
          "summary": {
            "type": [
              "string",
              "null"
            ],
            "example": null
          }
        }
      },
      "TargetEnvelope": {
        "type": "object",
        "required": [
          "site_id",
          "site_slug",
          "locale_code",
          "market_code",
          "site_profile_code"
        ],
        "properties": {
          "site_id": {
            "type": "integer",
            "format": "int64",
            "example": 42
          },
          "site_slug": {
            "type": "string",
            "example": "main-site"
          },
          "locale_code": {
            "type": [
              "string",
              "null"
            ],
            "example": "de-DE"
          },
          "market_code": {
            "type": [
              "string",
              "null"
            ],
            "example": "DE"
          },
          "site_profile_code": {
            "type": [
              "string",
              "null"
            ],
            "example": "B2C"
          }
        }
      },
      "VersionEnvelope": {
        "type": "object",
        "required": [
          "number",
          "captured_at",
          "published_at"
        ],
        "properties": {
          "number": {
            "type": "integer",
            "example": 3
          },
          "captured_at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-04-20T12:00:00+00:00"
          },
          "published_at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-04-21T09:30:00+00:00"
          }
        }
      },
      "MetaEnvelope": {
        "type": "object",
        "required": [
          "etag",
          "built_at",
          "first_published_at"
        ],
        "description": "Delivery-facing metadata that consumer caches can rely on. Mirrors\nthe response headers but lives inside the body so a consumer that\nonly stores the JSON payload still has access to the freshness\nsignals.\n",
        "properties": {
          "etag": {
            "type": "string",
            "example": "\"v3-abc123\""
          },
          "built_at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-04-21T09:30:10+00:00"
          },
          "first_published_at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-04-10T08:00:00+00:00"
          }
        }
      },
      "Section": {
        "type": "object",
        "required": [
          "key",
          "title",
          "position",
          "blocks"
        ],
        "properties": {
          "key": {
            "type": "string",
            "example": "main"
          },
          "title": {
            "type": [
              "string",
              "null"
            ],
            "example": "Main"
          },
          "position": {
            "type": "integer",
            "example": 0
          },
          "blocks": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Block"
            }
          }
        }
      },
      "Block": {
        "type": "object",
        "required": [
          "key",
          "kind",
          "position",
          "payload"
        ],
        "description": "Authoring block. `kind` controls how the HTML renderer maps the\npayload (`heading`, `paragraph`, `list`, `note` are recognised;\nother values render as `ts-block--unknown` with escaped text).\n",
        "properties": {
          "key": {
            "type": "string",
            "example": "intro"
          },
          "kind": {
            "type": "string",
            "example": "paragraph"
          },
          "position": {
            "type": "integer",
            "example": 0
          },
          "payload": {
            "type": "object",
            "description": "Untyped payload. Shape depends on `kind` and is intentionally\nforwarded as-is so new authoring block kinds flow through\nwithout a Public API redeploy.\n",
            "additionalProperties": true,
            "example": {
              "text": "Hello."
            }
          }
        }
      },
      "ErrorResponse": {
        "type": "object",
        "description": "Default error envelope emitted by all handler-level error paths.\nSee `RateLimitedError` for the exception used by the IP rate\nlimiter.\n",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string",
                "description": "Stable, compatibility-policy-controlled code.",
                "enum": [
                  "invalid_request",
                  "unsupported_version",
                  "entitlement_required",
                  "not_found",
                  "method_not_allowed",
                  "version_mismatch",
                  "quota_exceeded",
                  "internal"
                ],
                "example": "not_found"
              },
              "message": {
                "type": "string",
                "description": "Short human-readable description. Never carries plan or authoring detail.",
                "example": "no live delivery record for the requested tuple"
              }
            }
          }
        }
      },
      "VersionMismatchError": {
        "type": "object",
        "description": "Extends the default error envelope with a `live_version` integer\nfor the `409 version_mismatch` response.\n",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message",
              "live_version"
            ],
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "version_mismatch"
                ]
              },
              "message": {
                "type": "string",
                "example": "requested version does not match the currently live version"
              },
              "live_version": {
                "type": "integer",
                "description": "The version currently served on the live projection row.",
                "example": 4
              }
            }
          }
        }
      },
      "RateLimitedError": {
        "type": "object",
        "description": "Flat envelope emitted by the IP rate-limit middleware (only). The\nshape differs from `ErrorResponse` because the middleware sits\nahead of the delivery handlers.\n",
        "required": [
          "error",
          "message"
        ],
        "properties": {
          "error": {
            "type": "string",
            "enum": [
              "rate_limited"
            ]
          },
          "message": {
            "type": "string",
            "example": "too many requests"
          }
        }
      }
    },
    "examples": {
      "DeliveryDocumentExample": {
        "summary": "Example JSON envelope.",
        "value": {
          "schema_version": 1,
          "api_version": "v1",
          "document": {
            "type_code": "privacy",
            "slug": "privacy-policy",
            "title": "Privacy Policy",
            "summary": null
          },
          "target": {
            "site_id": 42,
            "site_slug": "main-site",
            "locale_code": "de-DE",
            "market_code": "DE",
            "site_profile_code": "B2C"
          },
          "version": {
            "number": 3,
            "captured_at": "2026-04-20T12:00:00+00:00",
            "published_at": "2026-04-21T09:30:00+00:00"
          },
          "sections": [
            {
              "key": "main",
              "title": "Main",
              "position": 0,
              "blocks": [
                {
                  "key": "intro",
                  "kind": "paragraph",
                  "position": 0,
                  "payload": {
                    "text": "Hello."
                  }
                }
              ]
            }
          ],
          "meta": {
            "etag": "\"v3-abc123\"",
            "built_at": "2026-04-21T09:30:10+00:00",
            "first_published_at": "2026-04-10T08:00:00+00:00"
          }
        }
      }
    }
  }
}