{
  "openapi": "3.1.0",
  "info": {
    "title": "TruthFoundry News API",
    "version": "1",
    "description": "Read TruthFoundry News as data: the pieces on air with their cited facts, the sections, the archive, search, the front as JSON, the video editions, and a subscribe door. Public lane needs no key; a desk key opens the private lane.",
    "x-paper": "truthfoundry"
  },
  "servers": [
    {
      "url": "https://truthfoundry.newsroomfloor.com"
    }
  ],
  "tags": [
    {
      "name": "public",
      "description": "No key. Public papers only."
    },
    {
      "name": "keyed",
      "description": "Authorization: Bearer nrf_... or X-NRF-Key. The key names the desk."
    }
  ],
  "paths": {
    "/api/v1": {
      "get": {
        "operationId": "index",
        "summary": "A paper's own API index: every door this paper has, as links. Read this first.",
        "description": "Names the paper, its landing page, and every endpoint and feed it serves (stories, sections, days, search, the front as JSON, the video editions, RSS, the podcast feed, the agent edition, OpenAPI, subscribe). On a paper's own domain the same path answers with no d.",
        "tags": [
          "public"
        ],
        "parameters": [],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "description": "No such paper or piece.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/stories": {
      "get": {
        "operationId": "stories",
        "summary": "Every piece a desk has on air, newest first: headline, dek, section, byline, revision, and the story URL.",
        "description": "limit clamps to 1-200 (default 50). Each row carries id (use it on /stories/{id}), headline, dek, section, byline, rev and url. Only pieces on air appear. Answers for public papers; add a key to read your own desk instead.",
        "tags": [
          "public"
        ],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            },
            "description": "1-200"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StoryList"
                }
              }
            }
          },
          "404": {
            "description": "No such paper or piece.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/stories/{id}": {
      "get": {
        "operationId": "story",
        "summary": "One piece in full: the prose with its [^N] footnote markers and the numbered cited_facts they reference.",
        "description": "prose is the piece as published. cited_facts is the numbered source list: statement, source (publisher), instrument, and claim_key (a pointer into the fact record). A [^N] marker in prose references cited_facts entry N; render them as footnotes, never strip them.",
        "tags": [
          "public"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "pick or load a story"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "description": "No such paper or piece.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/sections": {
      "get": {
        "operationId": "sections",
        "summary": "The paper's sections in its own order, each with how many pieces are on air and its page URL.",
        "description": "Section names are the strings the stories carry in `category`; pass one to the subscribe endpoint to alert a reader on that section only.",
        "tags": [
          "public"
        ],
        "parameters": [],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "description": "No such paper or piece.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/days": {
      "get": {
        "operationId": "days",
        "summary": "The archive: every day the paper published, newest first, with the count and the back-issue URL.",
        "description": "Up to 366 days. Pair with /days/{day} for that edition.",
        "tags": [
          "public"
        ],
        "parameters": [],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "description": "No such paper or piece.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/days/{day}": {
      "get": {
        "operationId": "day",
        "summary": "One day's edition: the pieces that went on air that day.",
        "description": "day is YYYY-MM-DD (UTC). Same row shape as /stories.",
        "tags": [
          "public"
        ],
        "parameters": [
          {
            "name": "day",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "YYYY-MM-DD"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StoryList"
                }
              }
            }
          },
          "404": {
            "description": "No such paper or piece.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/search": {
      "get": {
        "operationId": "search",
        "summary": "Search the paper: headline, dek and prose, newest first.",
        "description": "q is up to 120 characters, up to four terms, all required. Up to 30 rows.",
        "tags": [
          "public"
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "a word or two"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "description": "No such paper or piece.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/front": {
      "get": {
        "operationId": "front",
        "summary": "The front page as JSON: the paper's config, its newsroom, and one rail per section. The door the newscast is cut from.",
        "description": "Same document as /front.json on the paper's own domain. Rails follow the paper's section order, six pieces each.",
        "tags": [
          "public"
        ],
        "parameters": [],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "description": "No such paper or piece.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/cast": {
      "get": {
        "operationId": "cast",
        "summary": "The paper's video editions: every published day with the video, poster, embed and page URLs.",
        "description": "Empty until the paper has a published edition. The podcast feed at /podcast.xml is the same list as RSS with enclosures.",
        "tags": [
          "public"
        ],
        "parameters": [],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "description": "No such paper or piece.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/posts": {
      "get": {
        "operationId": "posts",
        "summary": "Every piece on air as a ready-to-post short form: headline, the dek when it fits, the link, the picture. Sized for X.",
        "description": "Deterministic, no model: post it as is or cut it down. limit 1-100 (default 20); since=<ISO time> polls only what went on air after that. image is the story's own art, else its share card.",
        "tags": [
          "public"
        ],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            },
            "description": "1-100"
          },
          {
            "name": "since",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "ISO time (optional)"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "description": "No such paper or piece.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/subscribe": {
      "post": {
        "operationId": "subscribe",
        "summary": "Subscribe an email to this paper's alerts, with their choices. Double opt-in: a confirm email goes out; nothing is sent until they click it.",
        "description": "JSON body: email (required), cadence (instant | daily | weekly, default daily), sections (a list of section names, default all), cast (true | false, default true). Public papers only; CORS is open so a brand's own site can post here.",
        "tags": [
          "public"
        ],
        "parameters": [],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "description": "No such paper or piece.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "description": "reader@example.com"
                  },
                  "cadence": {
                    "type": "string",
                    "description": "instant | daily | weekly"
                  },
                  "sections": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/openapi.json": {
      "get": {
        "operationId": "openapi",
        "summary": "This API as an OpenAPI 3.1 document, servers set to this host. Import it into any client generator.",
        "description": "This API as an OpenAPI 3.1 document, servers set to this host. Import it into any client generator.",
        "tags": [
          "public"
        ],
        "parameters": [],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "description": "No such paper or piece.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/masthead": {
      "get": {
        "operationId": "masthead",
        "summary": "The desk's active masthead: every persona writing there, with title, beat, and portrait URL.",
        "description": "Every byline is a declared AI persona (declared_ai: true on each row) - say so wherever you render them; the disclosure is not removable. avatar is a stable URL you can hotlink.",
        "tags": [
          "public"
        ],
        "parameters": [],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "description": "No such paper or piece.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1#keyed": {
      "get": {
        "operationId": "discovery",
        "summary": "The keyed surface's own index: which desk this key opens, the endpoints it carries, and how the key is presented.",
        "description": "Send your key and read this first: it names the desk the key resolves to and every path the keyed lane serves. Auth is Authorization: Bearer nrf_... or the X-NRF-Key header, either works everywhere.",
        "tags": [
          "keyed"
        ],
        "parameters": [],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "description": "No such paper or piece.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerKey": []
          },
          {
            "headerKey": []
          }
        ]
      }
    },
    "/api/v1/news": {
      "get": {
        "operationId": "news",
        "summary": "The key's own desk, on air. Same shape as the public /stories, with no d parameter: the key identifies the desk.",
        "description": "Also answers as /api/v1/stories with a key. category filters by section name (case-insensitive). This lane serves private papers too: paper_public off means this API is the only way the desk's news gets out.",
        "tags": [
          "keyed"
        ],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            },
            "description": "1-200"
          },
          {
            "name": "category",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "section name (optional)"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StoryList"
                }
              }
            }
          },
          "404": {
            "description": "No such paper or piece.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerKey": []
          },
          {
            "headerKey": []
          }
        ]
      }
    },
    "/api/v1/news/{id}/proof": {
      "get": {
        "operationId": "proof",
        "summary": "One piece's provenance, verbatim: the content hash with its basis inputs, the door's signed filing receipt, and the generation chain.",
        "description": "basis carries the exact inputs so you can recompute content_hash yourself; hash_basis says the recipe. receipt is the ingest door's Ed25519 receipt verbatim, with receipt_verify naming the signed string. A signature proves who filed this and that it has not changed; it never makes a claim true.",
        "tags": [
          "keyed"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "pick or load a story"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "description": "No such paper or piece.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerKey": []
          },
          {
            "headerKey": []
          }
        ]
      }
    },
    "/api/v1/personas": {
      "get": {
        "operationId": "personas",
        "summary": "The key's own masthead. Alias of the keyed /masthead.",
        "description": "Same shape as the public masthead, resolved from the key. declared_ai rides every row.",
        "tags": [
          "keyed"
        ],
        "parameters": [],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "description": "No such paper or piece.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerKey": []
          },
          {
            "headerKey": []
          }
        ]
      }
    },
    "/api/v1/config": {
      "get": {
        "operationId": "config",
        "summary": "The desk's public face as data: nameplate, motto, hue and sections, for a tenant rendering its own paper.",
        "description": "Everything a template needs: nameplate, motto, hue, the section list in order, default_kind, paper_public, and source_floor (the desk's own minimum distinct publishers per piece - never below 2).",
        "tags": [
          "keyed"
        ],
        "parameters": [],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "description": "No such paper or piece.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerKey": []
          },
          {
            "headerKey": []
          }
        ]
      }
    },
    "/api/v1/subscribers": {
      "get": {
        "operationId": "subscribers",
        "summary": "The desk's own alert list: the count for any key, the addresses and each reader's choices for an ADMIN key.",
        "description": "A read key gets {count, confirmed}. An admin key gets the rows: email, confirmed_at, cadence, sections, cast, last_sent_at. POST the same path (admin) with {emails: [...]} to add up to 20 addresses; each gets the confirm email and joins only when they click it.",
        "tags": [
          "keyed"
        ],
        "parameters": [],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "description": "No such paper or piece.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerKey": []
          },
          {
            "headerKey": []
          }
        ]
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "A desk key, nrf_..."
      },
      "headerKey": {
        "type": "apiKey",
        "in": "header",
        "name": "X-NRF-Key"
      }
    },
    "schemas": {
      "Story": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "headline": {
            "type": "string"
          },
          "dek": {
            "type": "string"
          },
          "category": {
            "type": "string",
            "description": "The section name."
          },
          "kind": {
            "type": "string",
            "enum": [
              "news",
              "blog"
            ]
          },
          "byline": {
            "type": "object",
            "properties": {
              "name": {
                "type": "string"
              },
              "title": {
                "type": "string"
              },
              "avatar": {
                "type": "string",
                "nullable": true
              }
            }
          },
          "published_at": {
            "type": "string",
            "format": "date-time"
          },
          "revision": {
            "type": "integer"
          },
          "url": {
            "type": "string",
            "format": "uri"
          }
        }
      },
      "StoryList": {
        "type": "object",
        "properties": {
          "desk": {
            "type": "string"
          },
          "nameplate": {
            "type": "string"
          },
          "count": {
            "type": "integer"
          },
          "stories": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Story"
            }
          }
        }
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          },
          "message": {
            "type": "string"
          }
        }
      }
    }
  }
}