[{"data":1,"prerenderedAt":5490},["ShallowReactive",2],{"page-\u002Fgetting-started-with-python-apis-for-builders\u002Fdocumenting-apis-with-openapi\u002F":3,"faq-schema-\u002Fgetting-started-with-python-apis-for-builders\u002Fdocumenting-apis-with-openapi\u002F":5469},{"id":4,"title":5,"body":6,"description":5459,"extension":5460,"meta":5461,"navigation":518,"path":5465,"seo":5466,"stem":5467,"__hash__":5468},"content\u002Fgetting-started-with-python-apis-for-builders\u002Fdocumenting-apis-with-openapi\u002Findex.md","Documenting APIs with OpenAPI",{"type":7,"value":8,"toc":5445},"minimark",[9,13,28,40,43,206,211,222,326,329,458,466,470,481,1123,1126,1319,1342,1346,1361,1364,1987,2001,2012,2565,2674,2678,2689,3039,3066,3069,3164,3168,3171,3192,3911,3922,3938,4023,4027,4030,4291,4298,4378,4396,4400,4403,4615,4624,4628,4654,4671,4689,4703,4717,4721,4724,4809,4820,4827,5263,5267,5281,5353,5357,5363,5369,5387,5397,5403,5407,5441],[10,11,5],"h1",{"id":12},"documenting-apis-with-openapi",[14,15,16,17,22,23,27],"p",{},"Your OpenAPI document is the product surface a paying developer touches before they ever touch your endpoints, and a vague one costs you signups you never see. Part of the ",[18,19,21],"a",{"href":20},"\u002Fgetting-started-with-python-apis-for-builders\u002F","Getting Started with Python APIs for Builders"," guide, this page shows how to turn the schema that ",[18,24,26],{"href":25},"\u002Fgetting-started-with-python-apis-for-builders\u002Fsetting-up-fastapi\u002F","FastAPI"," generates for free into a document customers can read, trust, and feed straight into a code generator.",[14,29,30,31,35,36,39],{},"FastAPI already emits an OpenAPI 3.1 document at ",[32,33,34],"code",{},"\u002Fopenapi.json",". That default document is technically valid and commercially useless: operation IDs read like ",[32,37,38],{},"create_forecast_v1_forecasts__post",", every schema is named after a Python class, half the response codes are undocumented, and the authentication section is empty. A customer who generates a client from it gets method names nobody can guess and no hint about which header carries their key. The gap between \"valid spec\" and \"spec that closes a sale\" is about two hundred lines of configuration, and this guide walks all of it.",[14,41,42],{},"Treat the spec as a build artifact, not a runtime accident. You generate it, you version it, you diff it in CI, and you publish it to a stable URL. That single discipline shift is what separates an API that developers integrate in an afternoon from one that generates three support emails per signup.",[44,45,53,54,53,58,53,62,53,69,53,88,53,96,53,105,53,111,53,116,53,123,53,127,53,131,53,134,53,138,53,142,53,145,53,148,53,152,53,156,53,160,53,163,53,167,53,170,53,174,53,177,53,183,53,188,53,192,53,195,53,197,53,201,53,204],"svg",{"viewBox":46,"role":47,"ariaLabelledBy":48,"xmlns":51,"style":52},"0 0 760 250","img",[49,50],"oa-doc-pipe-t","oa-doc-pipe-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;height:auto;margin:1.5rem 0;font-family:var(--font-sans);","\n  ",[55,56,57],"title",{"id":49},"From route decorators to a generated customer SDK",[59,60,61],"desc",{"id":50},"Route code and Pydantic models feed the schema builder, which emits a versioned spec artifact that is published to docs renderers and consumed by client generators.",[63,64],"rect",{"x":65,"y":65,"width":66,"height":67,"fill":68},"0","760","250","var(--c-surface)",[70,71,72,73,53],"defs",{},"\n    ",[74,75,82,83,72],"marker",{"id":76,"viewBox":77,"refX":78,"refY":79,"markerWidth":80,"markerHeight":80,"orient":81},"oa-doc-pipe-arrow","0 0 10 10","9","5","7","auto-start-reverse","\n      ",[84,85],"path",{"d":86,"fill":87},"M0 0 L10 5 L0 10 z","var(--c-text-muted)",[89,90,95],"text",{"x":91,"y":92,"fill":93,"style":94},"380","34","var(--c-text)","text-anchor:middle;font-size:14;font-family:var(--font-sans);","Documentation build path",[63,97],{"x":98,"y":99,"width":100,"height":101,"rx":102,"fill":68,"stroke":103,"style":104},"12","62","128","66","10","var(--c-blue)","stroke-width:2;",[89,106,110],{"x":107,"y":108,"fill":93,"style":109},"76","90","text-anchor:middle;font-size:12;font-family:var(--font-sans);","Route code",[89,112,115],{"x":107,"y":113,"fill":87,"style":114},"110","text-anchor:middle;font-size:11;font-family:var(--font-sans);","decorators+models",[117,118],"line",{"x1":119,"y1":120,"x2":121,"y2":120,"stroke":87,"style":122},"140","95","162","stroke-width:2;marker-end:url(#oa-doc-pipe-arrow);",[63,124],{"x":125,"y":99,"width":100,"height":101,"rx":102,"fill":68,"stroke":126,"style":104},"164","var(--c-teal)",[89,128,130],{"x":129,"y":108,"fill":93,"style":109},"228","Schema build",[89,132,133],{"x":129,"y":113,"fill":87,"style":114},"get_openapi()",[117,135],{"x1":136,"y1":120,"x2":137,"y2":120,"stroke":87,"style":122},"292","314",[63,139],{"x":140,"y":99,"width":100,"height":101,"rx":102,"fill":68,"stroke":141,"style":104},"316","var(--c-yellow)",[89,143,144],{"x":91,"y":108,"fill":93,"style":109},"Spec artifact",[89,146,147],{"x":91,"y":113,"fill":87,"style":114},"openapi-v1.json",[117,149],{"x1":150,"y1":120,"x2":151,"y2":120,"stroke":87,"style":122},"444","466",[63,153],{"x":154,"y":99,"width":100,"height":101,"rx":102,"fill":68,"stroke":155,"style":104},"468","var(--c-coral)",[89,157,159],{"x":158,"y":108,"fill":93,"style":109},"532","Published docs",[89,161,162],{"x":158,"y":113,"fill":87,"style":114},"Swagger \u002F ReDoc",[117,164],{"x1":165,"y1":120,"x2":166,"y2":120,"stroke":87,"style":122},"596","618",[63,168],{"x":169,"y":99,"width":100,"height":101,"rx":102,"fill":68,"stroke":103,"style":104},"620",[89,171,173],{"x":172,"y":108,"fill":93,"style":109},"684","Customer SDK",[89,175,176],{"x":172,"y":113,"fill":87,"style":114},"generated client",[63,178],{"x":125,"y":121,"width":179,"height":99,"rx":102,"fill":180,"stroke":181,"style":182},"280","var(--c-surface-alt)","var(--c-border)","stroke-width:1.5;",[89,184,187],{"x":185,"y":186,"fill":93,"style":109},"304","186","CI gate: validate + diff",[89,189,191],{"x":185,"y":190,"fill":87,"style":114},"206","fails on undeclared breaking change",[117,193],{"x1":91,"y1":100,"x2":91,"y2":194,"stroke":87,"style":122},"160",[63,196],{"x":154,"y":121,"width":179,"height":99,"rx":102,"fill":180,"stroke":181,"style":182},[89,198,200],{"x":199,"y":186,"fill":93,"style":109},"608","Served from object storage",[89,202,203],{"x":199,"y":190,"fill":87,"style":114},"zero request cost at the edge",[117,205],{"x1":158,"y1":100,"x2":158,"y2":194,"stroke":87,"style":122},[207,208,210],"h2",{"id":209},"prerequisites","Prerequisites",[14,212,213,214,217,218,221],{},"You need Python 3.11 or newer, because the export tooling below uses ",[32,215,216],{},"tomllib"," from the standard library and a ",[32,219,220],{},"match"," statement for command dispatch. Pin the documentation-relevant packages explicitly; OpenAPI output changed shape across FastAPI minor releases, and an unpinned upgrade silently rewrites the artifact your customers diff against.",[223,224,229],"pre",{"className":225,"code":226,"language":227,"meta":228,"style":228},"language-bash shiki shiki-themes github-light github-dark","python -m venv .venv && . .venv\u002Fbin\u002Factivate\npip install \\\n  \"fastapi>=0.115,\u003C0.117\" \\\n  \"pydantic>=2.9,\u003C3\" \\\n  \"uvicorn[standard]>=0.34\" \\\n  \"httpx>=0.28\" \\\n  \"openapi-spec-validator>=0.7\" \\\n  \"pytest>=8.3\" \\\n  \"pytest-asyncio>=0.24\"\n","bash","",[32,230,231,260,272,280,288,296,304,312,320],{"__ignoreMap":228},[232,233,235,239,243,247,250,254,257],"span",{"class":117,"line":234},1,[232,236,238],{"class":237},"sScJk","python",[232,240,242],{"class":241},"sj4cs"," -m",[232,244,246],{"class":245},"sZZnC"," venv",[232,248,249],{"class":245}," .venv",[232,251,253],{"class":252},"sVt8B"," && ",[232,255,256],{"class":241},".",[232,258,259],{"class":245}," .venv\u002Fbin\u002Factivate\n",[232,261,263,266,269],{"class":117,"line":262},2,[232,264,265],{"class":237},"pip",[232,267,268],{"class":245}," install",[232,270,271],{"class":241}," \\\n",[232,273,275,278],{"class":117,"line":274},3,[232,276,277],{"class":245},"  \"fastapi>=0.115,\u003C0.117\"",[232,279,271],{"class":241},[232,281,283,286],{"class":117,"line":282},4,[232,284,285],{"class":245},"  \"pydantic>=2.9,\u003C3\"",[232,287,271],{"class":241},[232,289,291,294],{"class":117,"line":290},5,[232,292,293],{"class":245},"  \"uvicorn[standard]>=0.34\"",[232,295,271],{"class":241},[232,297,299,302],{"class":117,"line":298},6,[232,300,301],{"class":245},"  \"httpx>=0.28\"",[232,303,271],{"class":241},[232,305,307,310],{"class":117,"line":306},7,[232,308,309],{"class":245},"  \"openapi-spec-validator>=0.7\"",[232,311,271],{"class":241},[232,313,315,318],{"class":117,"line":314},8,[232,316,317],{"class":245},"  \"pytest>=8.3\"",[232,319,271],{"class":241},[232,321,323],{"class":117,"line":322},9,[232,324,325],{"class":245},"  \"pytest-asyncio>=0.24\"\n",[14,327,328],{},"Set the environment variables that drive every piece of metadata. Nothing in the code below hardcodes a title, a version, or a base URL, because you will run the same image in staging and production and the spec must describe whichever one it is serving.",[223,330,332],{"className":225,"code":331,"language":227,"meta":228,"style":228},"export API_TITLE=\"Forecast API\"\nexport API_VERSION=\"1.4.0\"\nexport API_PUBLIC_URL=\"https:\u002F\u002Fapi.example.com\"\nexport API_CONTACT_EMAIL=\"support@example.com\"\nexport API_KEY_HEADER=\"X-API-Key\"\nexport OPENAPI_URL=\"\u002Fopenapi.json\"\nexport DOCS_URL=\"\u002Fdocs\"\nexport REDOC_URL=\"\u002Fredoc\"\nexport SPEC_EXPORT_DIR=\".\u002Fspec\"\nexport ENVIRONMENT=\"production\"\n",[32,333,334,349,361,373,385,397,409,421,433,445],{"__ignoreMap":228},[232,335,336,340,343,346],{"class":117,"line":234},[232,337,339],{"class":338},"szBVR","export",[232,341,342],{"class":252}," API_TITLE",[232,344,345],{"class":338},"=",[232,347,348],{"class":245},"\"Forecast API\"\n",[232,350,351,353,356,358],{"class":117,"line":262},[232,352,339],{"class":338},[232,354,355],{"class":252}," API_VERSION",[232,357,345],{"class":338},[232,359,360],{"class":245},"\"1.4.0\"\n",[232,362,363,365,368,370],{"class":117,"line":274},[232,364,339],{"class":338},[232,366,367],{"class":252}," API_PUBLIC_URL",[232,369,345],{"class":338},[232,371,372],{"class":245},"\"https:\u002F\u002Fapi.example.com\"\n",[232,374,375,377,380,382],{"class":117,"line":282},[232,376,339],{"class":338},[232,378,379],{"class":252}," API_CONTACT_EMAIL",[232,381,345],{"class":338},[232,383,384],{"class":245},"\"support@example.com\"\n",[232,386,387,389,392,394],{"class":117,"line":290},[232,388,339],{"class":338},[232,390,391],{"class":252}," API_KEY_HEADER",[232,393,345],{"class":338},[232,395,396],{"class":245},"\"X-API-Key\"\n",[232,398,399,401,404,406],{"class":117,"line":298},[232,400,339],{"class":338},[232,402,403],{"class":252}," OPENAPI_URL",[232,405,345],{"class":338},[232,407,408],{"class":245},"\"\u002Fopenapi.json\"\n",[232,410,411,413,416,418],{"class":117,"line":306},[232,412,339],{"class":338},[232,414,415],{"class":252}," DOCS_URL",[232,417,345],{"class":338},[232,419,420],{"class":245},"\"\u002Fdocs\"\n",[232,422,423,425,428,430],{"class":117,"line":314},[232,424,339],{"class":338},[232,426,427],{"class":252}," REDOC_URL",[232,429,345],{"class":338},[232,431,432],{"class":245},"\"\u002Fredoc\"\n",[232,434,435,437,440,442],{"class":117,"line":322},[232,436,339],{"class":338},[232,438,439],{"class":252}," SPEC_EXPORT_DIR",[232,441,345],{"class":338},[232,443,444],{"class":245},"\".\u002Fspec\"\n",[232,446,448,450,453,455],{"class":117,"line":447},10,[232,449,339],{"class":338},[232,451,452],{"class":252}," ENVIRONMENT",[232,454,345],{"class":338},[232,456,457],{"class":245},"\"production\"\n",[14,459,460,461,465],{},"The baseline assumption is that you already have working routes and Pydantic v2 models. If your models are still loosely typed dictionaries, fix that first, because OpenAPI generation is entirely downstream of your type annotations. The rules covered in ",[18,462,464],{"href":463},"\u002Fgetting-started-with-python-apis-for-builders\u002Fparsing-json-responses\u002Fvalidating-json-with-pydantic-v2\u002F","validating JSON with Pydantic v2"," are the same rules that decide how readable your schema section turns out.",[207,467,469],{"id":468},"step-1-give-the-document-real-metadata","Step 1: Give the Document Real Metadata",[14,471,472,473,476,477,480],{},"The ",[32,474,475],{},"info"," block is the first thing a developer reads and the first thing a marketplace listing scrapes. Fill in every field. A ",[32,478,479],{},"description"," written in Markdown renders as the landing content of both Swagger UI and ReDoc, so this is where you put your authentication overview, your rate-limit policy, and a link to pricing. Keep it in a separate Markdown file so a non-engineer can edit it without touching Python.",[223,482,485],{"className":483,"code":484,"language":238,"meta":228,"style":228},"language-python shiki shiki-themes github-light github-dark","# app\u002Fopenapi_meta.py\nimport os\nfrom pathlib import Path\n\nfrom fastapi import FastAPI\nfrom fastapi.openapi.utils import get_openapi\n\nTAG_METADATA = [\n    {\n        \"name\": \"Forecasts\",\n        \"description\": \"Create and retrieve demand forecasts. Billed per forecast created.\",\n    },\n    {\n        \"name\": \"Usage\",\n        \"description\": \"Read your current billing period consumption and remaining quota.\",\n    },\n    {\n        \"name\": \"Health\",\n        \"description\": \"Unauthenticated liveness probes. Never billed, never rate limited.\",\n    },\n]\n\n\ndef _description() -> str:\n    path = os.getenv(\"API_DESCRIPTION_PATH\", \"docs\u002Foverview.md\")\n    candidate = Path(path)\n    return candidate.read_text(encoding=\"utf-8\") if candidate.is_file() else \"\"\n\n\ndef install_openapi(app: FastAPI) -> None:\n    def custom_openapi() -> dict:\n        if app.openapi_schema:\n            return app.openapi_schema\n\n        schema = get_openapi(\n            title=os.getenv(\"API_TITLE\", \"Untitled API\"),\n            version=os.getenv(\"API_VERSION\", \"0.0.0\"),\n            summary=os.getenv(\"API_SUMMARY\", \"Demand forecasting for commerce teams\"),\n            description=_description(),\n            routes=app.routes,\n            tags=TAG_METADATA,\n            servers=[{\"url\": os.getenv(\"API_PUBLIC_URL\", \"http:\u002F\u002Flocalhost:8000\")}],\n            contact={\n                \"name\": \"API Support\",\n                \"email\": os.getenv(\"API_CONTACT_EMAIL\", \"support@example.com\"),\n            },\n            license_info={\"name\": \"Commercial\", \"url\": os.getenv(\"API_TERMS_URL\", \"\")},\n        )\n        schema[\"info\"][\"x-environment\"] = os.getenv(\"ENVIRONMENT\", \"development\")\n        app.openapi_schema = schema\n        return schema\n\n    app.openapi = custom_openapi\n",[32,486,487,493,501,514,520,532,544,548,559,564,578,591,597,602,614,626,631,636,648,660,665,671,676,681,699,722,733,766,771,776,792,808,817,826,831,842,864,884,904,915,926,938,966,977,990,1008,1014,1050,1056,1088,1099,1107,1112],{"__ignoreMap":228},[232,488,489],{"class":117,"line":234},[232,490,492],{"class":491},"sJ8bj","# app\u002Fopenapi_meta.py\n",[232,494,495,498],{"class":117,"line":262},[232,496,497],{"class":338},"import",[232,499,500],{"class":252}," os\n",[232,502,503,506,509,511],{"class":117,"line":274},[232,504,505],{"class":338},"from",[232,507,508],{"class":252}," pathlib ",[232,510,497],{"class":338},[232,512,513],{"class":252}," Path\n",[232,515,516],{"class":117,"line":282},[232,517,519],{"emptyLinePlaceholder":518},true,"\n",[232,521,522,524,527,529],{"class":117,"line":290},[232,523,505],{"class":338},[232,525,526],{"class":252}," fastapi ",[232,528,497],{"class":338},[232,530,531],{"class":252}," FastAPI\n",[232,533,534,536,539,541],{"class":117,"line":298},[232,535,505],{"class":338},[232,537,538],{"class":252}," fastapi.openapi.utils ",[232,540,497],{"class":338},[232,542,543],{"class":252}," get_openapi\n",[232,545,546],{"class":117,"line":306},[232,547,519],{"emptyLinePlaceholder":518},[232,549,550,553,556],{"class":117,"line":314},[232,551,552],{"class":241},"TAG_METADATA",[232,554,555],{"class":338}," =",[232,557,558],{"class":252}," [\n",[232,560,561],{"class":117,"line":322},[232,562,563],{"class":252},"    {\n",[232,565,566,569,572,575],{"class":117,"line":447},[232,567,568],{"class":245},"        \"name\"",[232,570,571],{"class":252},": ",[232,573,574],{"class":245},"\"Forecasts\"",[232,576,577],{"class":252},",\n",[232,579,581,584,586,589],{"class":117,"line":580},11,[232,582,583],{"class":245},"        \"description\"",[232,585,571],{"class":252},[232,587,588],{"class":245},"\"Create and retrieve demand forecasts. Billed per forecast created.\"",[232,590,577],{"class":252},[232,592,594],{"class":117,"line":593},12,[232,595,596],{"class":252},"    },\n",[232,598,600],{"class":117,"line":599},13,[232,601,563],{"class":252},[232,603,605,607,609,612],{"class":117,"line":604},14,[232,606,568],{"class":245},[232,608,571],{"class":252},[232,610,611],{"class":245},"\"Usage\"",[232,613,577],{"class":252},[232,615,617,619,621,624],{"class":117,"line":616},15,[232,618,583],{"class":245},[232,620,571],{"class":252},[232,622,623],{"class":245},"\"Read your current billing period consumption and remaining quota.\"",[232,625,577],{"class":252},[232,627,629],{"class":117,"line":628},16,[232,630,596],{"class":252},[232,632,634],{"class":117,"line":633},17,[232,635,563],{"class":252},[232,637,639,641,643,646],{"class":117,"line":638},18,[232,640,568],{"class":245},[232,642,571],{"class":252},[232,644,645],{"class":245},"\"Health\"",[232,647,577],{"class":252},[232,649,651,653,655,658],{"class":117,"line":650},19,[232,652,583],{"class":245},[232,654,571],{"class":252},[232,656,657],{"class":245},"\"Unauthenticated liveness probes. Never billed, never rate limited.\"",[232,659,577],{"class":252},[232,661,663],{"class":117,"line":662},20,[232,664,596],{"class":252},[232,666,668],{"class":117,"line":667},21,[232,669,670],{"class":252},"]\n",[232,672,674],{"class":117,"line":673},22,[232,675,519],{"emptyLinePlaceholder":518},[232,677,679],{"class":117,"line":678},23,[232,680,519],{"emptyLinePlaceholder":518},[232,682,684,687,690,693,696],{"class":117,"line":683},24,[232,685,686],{"class":338},"def",[232,688,689],{"class":237}," _description",[232,691,692],{"class":252},"() -> ",[232,694,695],{"class":241},"str",[232,697,698],{"class":252},":\n",[232,700,702,705,707,710,713,716,719],{"class":117,"line":701},25,[232,703,704],{"class":252},"    path ",[232,706,345],{"class":338},[232,708,709],{"class":252}," os.getenv(",[232,711,712],{"class":245},"\"API_DESCRIPTION_PATH\"",[232,714,715],{"class":252},", ",[232,717,718],{"class":245},"\"docs\u002Foverview.md\"",[232,720,721],{"class":252},")\n",[232,723,725,728,730],{"class":117,"line":724},26,[232,726,727],{"class":252},"    candidate ",[232,729,345],{"class":338},[232,731,732],{"class":252}," Path(path)\n",[232,734,736,739,742,746,748,751,754,757,760,763],{"class":117,"line":735},27,[232,737,738],{"class":338},"    return",[232,740,741],{"class":252}," candidate.read_text(",[232,743,745],{"class":744},"s4XuR","encoding",[232,747,345],{"class":338},[232,749,750],{"class":245},"\"utf-8\"",[232,752,753],{"class":252},") ",[232,755,756],{"class":338},"if",[232,758,759],{"class":252}," candidate.is_file() ",[232,761,762],{"class":338},"else",[232,764,765],{"class":245}," \"\"\n",[232,767,769],{"class":117,"line":768},28,[232,770,519],{"emptyLinePlaceholder":518},[232,772,774],{"class":117,"line":773},29,[232,775,519],{"emptyLinePlaceholder":518},[232,777,779,781,784,787,790],{"class":117,"line":778},30,[232,780,686],{"class":338},[232,782,783],{"class":237}," install_openapi",[232,785,786],{"class":252},"(app: FastAPI) -> ",[232,788,789],{"class":241},"None",[232,791,698],{"class":252},[232,793,795,798,801,803,806],{"class":117,"line":794},31,[232,796,797],{"class":338},"    def",[232,799,800],{"class":237}," custom_openapi",[232,802,692],{"class":252},[232,804,805],{"class":241},"dict",[232,807,698],{"class":252},[232,809,811,814],{"class":117,"line":810},32,[232,812,813],{"class":338},"        if",[232,815,816],{"class":252}," app.openapi_schema:\n",[232,818,820,823],{"class":117,"line":819},33,[232,821,822],{"class":338},"            return",[232,824,825],{"class":252}," app.openapi_schema\n",[232,827,829],{"class":117,"line":828},34,[232,830,519],{"emptyLinePlaceholder":518},[232,832,834,837,839],{"class":117,"line":833},35,[232,835,836],{"class":252},"        schema ",[232,838,345],{"class":338},[232,840,841],{"class":252}," get_openapi(\n",[232,843,845,848,850,853,856,858,861],{"class":117,"line":844},36,[232,846,847],{"class":744},"            title",[232,849,345],{"class":338},[232,851,852],{"class":252},"os.getenv(",[232,854,855],{"class":245},"\"API_TITLE\"",[232,857,715],{"class":252},[232,859,860],{"class":245},"\"Untitled API\"",[232,862,863],{"class":252},"),\n",[232,865,867,870,872,874,877,879,882],{"class":117,"line":866},37,[232,868,869],{"class":744},"            version",[232,871,345],{"class":338},[232,873,852],{"class":252},[232,875,876],{"class":245},"\"API_VERSION\"",[232,878,715],{"class":252},[232,880,881],{"class":245},"\"0.0.0\"",[232,883,863],{"class":252},[232,885,887,890,892,894,897,899,902],{"class":117,"line":886},38,[232,888,889],{"class":744},"            summary",[232,891,345],{"class":338},[232,893,852],{"class":252},[232,895,896],{"class":245},"\"API_SUMMARY\"",[232,898,715],{"class":252},[232,900,901],{"class":245},"\"Demand forecasting for commerce teams\"",[232,903,863],{"class":252},[232,905,907,910,912],{"class":117,"line":906},39,[232,908,909],{"class":744},"            description",[232,911,345],{"class":338},[232,913,914],{"class":252},"_description(),\n",[232,916,918,921,923],{"class":117,"line":917},40,[232,919,920],{"class":744},"            routes",[232,922,345],{"class":338},[232,924,925],{"class":252},"app.routes,\n",[232,927,929,932,934,936],{"class":117,"line":928},41,[232,930,931],{"class":744},"            tags",[232,933,345],{"class":338},[232,935,552],{"class":241},[232,937,577],{"class":252},[232,939,941,944,946,949,952,955,958,960,963],{"class":117,"line":940},42,[232,942,943],{"class":744},"            servers",[232,945,345],{"class":338},[232,947,948],{"class":252},"[{",[232,950,951],{"class":245},"\"url\"",[232,953,954],{"class":252},": os.getenv(",[232,956,957],{"class":245},"\"API_PUBLIC_URL\"",[232,959,715],{"class":252},[232,961,962],{"class":245},"\"http:\u002F\u002Flocalhost:8000\"",[232,964,965],{"class":252},")}],\n",[232,967,969,972,974],{"class":117,"line":968},43,[232,970,971],{"class":744},"            contact",[232,973,345],{"class":338},[232,975,976],{"class":252},"{\n",[232,978,980,983,985,988],{"class":117,"line":979},44,[232,981,982],{"class":245},"                \"name\"",[232,984,571],{"class":252},[232,986,987],{"class":245},"\"API Support\"",[232,989,577],{"class":252},[232,991,993,996,998,1001,1003,1006],{"class":117,"line":992},45,[232,994,995],{"class":245},"                \"email\"",[232,997,954],{"class":252},[232,999,1000],{"class":245},"\"API_CONTACT_EMAIL\"",[232,1002,715],{"class":252},[232,1004,1005],{"class":245},"\"support@example.com\"",[232,1007,863],{"class":252},[232,1009,1011],{"class":117,"line":1010},46,[232,1012,1013],{"class":252},"            },\n",[232,1015,1017,1020,1022,1025,1028,1030,1033,1035,1037,1039,1042,1044,1047],{"class":117,"line":1016},47,[232,1018,1019],{"class":744},"            license_info",[232,1021,345],{"class":338},[232,1023,1024],{"class":252},"{",[232,1026,1027],{"class":245},"\"name\"",[232,1029,571],{"class":252},[232,1031,1032],{"class":245},"\"Commercial\"",[232,1034,715],{"class":252},[232,1036,951],{"class":245},[232,1038,954],{"class":252},[232,1040,1041],{"class":245},"\"API_TERMS_URL\"",[232,1043,715],{"class":252},[232,1045,1046],{"class":245},"\"\"",[232,1048,1049],{"class":252},")},\n",[232,1051,1053],{"class":117,"line":1052},48,[232,1054,1055],{"class":252},"        )\n",[232,1057,1059,1062,1065,1068,1071,1074,1076,1078,1081,1083,1086],{"class":117,"line":1058},49,[232,1060,1061],{"class":252},"        schema[",[232,1063,1064],{"class":245},"\"info\"",[232,1066,1067],{"class":252},"][",[232,1069,1070],{"class":245},"\"x-environment\"",[232,1072,1073],{"class":252},"] ",[232,1075,345],{"class":338},[232,1077,709],{"class":252},[232,1079,1080],{"class":245},"\"ENVIRONMENT\"",[232,1082,715],{"class":252},[232,1084,1085],{"class":245},"\"development\"",[232,1087,721],{"class":252},[232,1089,1091,1094,1096],{"class":117,"line":1090},50,[232,1092,1093],{"class":252},"        app.openapi_schema ",[232,1095,345],{"class":338},[232,1097,1098],{"class":252}," schema\n",[232,1100,1102,1105],{"class":117,"line":1101},51,[232,1103,1104],{"class":338},"        return",[232,1106,1098],{"class":252},[232,1108,1110],{"class":117,"line":1109},52,[232,1111,519],{"emptyLinePlaceholder":518},[232,1113,1115,1118,1120],{"class":117,"line":1114},53,[232,1116,1117],{"class":252},"    app.openapi ",[232,1119,345],{"class":338},[232,1121,1122],{"class":252}," custom_openapi\n",[14,1124,1125],{},"Wire it into the application factory alongside the documentation URLs. Exposing the interactive explorer in production is the right default for a commercial API — it is your best sales page — but you want the flexibility to turn it off in a private staging environment without a code change.",[223,1127,1129],{"className":483,"code":1128,"language":238,"meta":228,"style":228},"# app\u002Fmain.py\nimport os\n\nfrom fastapi import FastAPI\n\nfrom app.openapi_meta import install_openapi\nfrom app.routers import forecasts, health, usage\n\n\ndef create_app() -> FastAPI:\n    app = FastAPI(\n        openapi_url=os.getenv(\"OPENAPI_URL\", \"\u002Fopenapi.json\"),\n        docs_url=os.getenv(\"DOCS_URL\", \"\u002Fdocs\"),\n        redoc_url=os.getenv(\"REDOC_URL\", \"\u002Fredoc\"),\n    )\n    app.include_router(health.router)\n    app.include_router(forecasts.router)\n    app.include_router(usage.router)\n    install_openapi(app)\n    return app\n\n\napp = create_app()\n",[32,1130,1131,1136,1142,1146,1156,1160,1172,1184,1188,1192,1202,1212,1231,1250,1269,1274,1279,1284,1289,1294,1301,1305,1309],{"__ignoreMap":228},[232,1132,1133],{"class":117,"line":234},[232,1134,1135],{"class":491},"# app\u002Fmain.py\n",[232,1137,1138,1140],{"class":117,"line":262},[232,1139,497],{"class":338},[232,1141,500],{"class":252},[232,1143,1144],{"class":117,"line":274},[232,1145,519],{"emptyLinePlaceholder":518},[232,1147,1148,1150,1152,1154],{"class":117,"line":282},[232,1149,505],{"class":338},[232,1151,526],{"class":252},[232,1153,497],{"class":338},[232,1155,531],{"class":252},[232,1157,1158],{"class":117,"line":290},[232,1159,519],{"emptyLinePlaceholder":518},[232,1161,1162,1164,1167,1169],{"class":117,"line":298},[232,1163,505],{"class":338},[232,1165,1166],{"class":252}," app.openapi_meta ",[232,1168,497],{"class":338},[232,1170,1171],{"class":252}," install_openapi\n",[232,1173,1174,1176,1179,1181],{"class":117,"line":306},[232,1175,505],{"class":338},[232,1177,1178],{"class":252}," app.routers ",[232,1180,497],{"class":338},[232,1182,1183],{"class":252}," forecasts, health, usage\n",[232,1185,1186],{"class":117,"line":314},[232,1187,519],{"emptyLinePlaceholder":518},[232,1189,1190],{"class":117,"line":322},[232,1191,519],{"emptyLinePlaceholder":518},[232,1193,1194,1196,1199],{"class":117,"line":447},[232,1195,686],{"class":338},[232,1197,1198],{"class":237}," create_app",[232,1200,1201],{"class":252},"() -> FastAPI:\n",[232,1203,1204,1207,1209],{"class":117,"line":580},[232,1205,1206],{"class":252},"    app ",[232,1208,345],{"class":338},[232,1210,1211],{"class":252}," FastAPI(\n",[232,1213,1214,1217,1219,1221,1224,1226,1229],{"class":117,"line":593},[232,1215,1216],{"class":744},"        openapi_url",[232,1218,345],{"class":338},[232,1220,852],{"class":252},[232,1222,1223],{"class":245},"\"OPENAPI_URL\"",[232,1225,715],{"class":252},[232,1227,1228],{"class":245},"\"\u002Fopenapi.json\"",[232,1230,863],{"class":252},[232,1232,1233,1236,1238,1240,1243,1245,1248],{"class":117,"line":599},[232,1234,1235],{"class":744},"        docs_url",[232,1237,345],{"class":338},[232,1239,852],{"class":252},[232,1241,1242],{"class":245},"\"DOCS_URL\"",[232,1244,715],{"class":252},[232,1246,1247],{"class":245},"\"\u002Fdocs\"",[232,1249,863],{"class":252},[232,1251,1252,1255,1257,1259,1262,1264,1267],{"class":117,"line":604},[232,1253,1254],{"class":744},"        redoc_url",[232,1256,345],{"class":338},[232,1258,852],{"class":252},[232,1260,1261],{"class":245},"\"REDOC_URL\"",[232,1263,715],{"class":252},[232,1265,1266],{"class":245},"\"\u002Fredoc\"",[232,1268,863],{"class":252},[232,1270,1271],{"class":117,"line":616},[232,1272,1273],{"class":252},"    )\n",[232,1275,1276],{"class":117,"line":628},[232,1277,1278],{"class":252},"    app.include_router(health.router)\n",[232,1280,1281],{"class":117,"line":633},[232,1282,1283],{"class":252},"    app.include_router(forecasts.router)\n",[232,1285,1286],{"class":117,"line":638},[232,1287,1288],{"class":252},"    app.include_router(usage.router)\n",[232,1290,1291],{"class":117,"line":650},[232,1292,1293],{"class":252},"    install_openapi(app)\n",[232,1295,1296,1298],{"class":117,"line":662},[232,1297,738],{"class":338},[232,1299,1300],{"class":252}," app\n",[232,1302,1303],{"class":117,"line":667},[232,1304,519],{"emptyLinePlaceholder":518},[232,1306,1307],{"class":117,"line":673},[232,1308,519],{"emptyLinePlaceholder":518},[232,1310,1311,1314,1316],{"class":117,"line":678},[232,1312,1313],{"class":252},"app ",[232,1315,345],{"class":338},[232,1317,1318],{"class":252}," create_app()\n",[14,1320,1321,1322,1325,1326,1329,1330,1333,1334,1337,1338,1341],{},"Note the ordering: ",[32,1323,1324],{},"install_openapi"," runs after the routers are included, because ",[32,1327,1328],{},"get_openapi"," walks ",[32,1331,1332],{},"app.routes"," at call time and caches the result on ",[32,1335,1336],{},"app.openapi_schema",". If you install it first and then add a router, the cache is still empty so nothing breaks — but if any code calls ",[32,1339,1340],{},"app.openapi()"," during startup, every route registered afterwards vanishes from the document. Build the schema once, at the end.",[207,1343,1345],{"id":1344},"step-2-make-every-operation-self-describing","Step 2: Make Every Operation Self-Describing",[14,1347,1348,1349,1352,1353,1356,1357,1360],{},"A generated client is only as good as the operation IDs it derives method names from. FastAPI's default IDs concatenate the function name, the path, and the HTTP verb, which produces ",[32,1350,1351],{},"client.create_forecast_v1_forecasts_post()"," in a generated Python SDK. Set ",[32,1354,1355],{},"operation_id"," explicitly on every public route, in camelCase, and your customers get ",[32,1358,1359],{},"client.create_forecast()"," instead. This one change does more for perceived API quality than any amount of prose.",[14,1362,1363],{},"Document failure responses too. The 402 you return when a customer exhausts their quota is part of your contract, and a client generator that has never seen it will raise an unhandled exception in production.",[223,1365,1367],{"className":483,"code":1366,"language":238,"meta":228,"style":228},"# app\u002Frouters\u002Fforecasts.py\nimport os\nfrom typing import Annotated\n\nfrom fastapi import APIRouter, Body, Depends, status\n\nfrom app.models import Forecast, ForecastRequest, ProblemDetail\nfrom app.security import require_api_key\n\nrouter = APIRouter(\n    prefix=os.getenv(\"API_PREFIX\", \"\u002Fv1\"),\n    tags=[\"Forecasts\"],\n    dependencies=[Depends(require_api_key)],\n)\n\n\n@router.post(\n    \"\u002Fforecasts\",\n    response_model=Forecast,\n    status_code=status.HTTP_201_CREATED,\n    operation_id=\"createForecast\",\n    summary=\"Create a demand forecast\",\n    description=(\n        \"Runs the forecast model over the supplied history window and returns a \"\n        \"point estimate with an 80 percent prediction interval. Counts as one \"\n        \"billable unit against your plan.\"\n    ),\n    responses={\n        402: {\"model\": ProblemDetail, \"description\": \"Plan quota exhausted.\"},\n        422: {\"model\": ProblemDetail, \"description\": \"History window too short.\"},\n        429: {\"model\": ProblemDetail, \"description\": \"Rate limit exceeded.\"},\n    },\n)\nasync def create_forecast(\n    payload: Annotated[\n        ForecastRequest,\n        Body(\n            openapi_examples={\n                \"weekly_retail\": {\n                    \"summary\": \"Weekly retail demand\",\n                    \"description\": \"Twelve weeks of history, four week horizon.\",\n                    \"value\": {\n                        \"sku\": \"SKU-1043\",\n                        \"granularity\": \"weekly\",\n                        \"horizon\": 4,\n                        \"history\": [812, 790, 845, 901, 877, 860],\n                    },\n                },\n                \"sparse_series\": {\n                    \"summary\": \"Sparse slow-moving item\",\n                    \"description\": \"Intermittent demand; the model falls back to Croston.\",\n                    \"value\": {\n                        \"sku\": \"SKU-9920\",\n                        \"granularity\": \"weekly\",\n                        \"horizon\": 8,\n                        \"history\": [0, 0, 3, 0, 0, 1],\n                    },\n                },\n            }\n        ),\n    ],\n) -> Forecast:\n    return await run_forecast(payload)\n",[32,1368,1369,1374,1380,1392,1396,1407,1411,1423,1435,1439,1449,1468,1483,1493,1497,1501,1505,1513,1520,1530,1545,1557,1569,1578,1583,1588,1593,1598,1607,1632,1652,1672,1676,1680,1693,1698,1703,1708,1717,1725,1737,1749,1756,1768,1780,1792,1830,1835,1840,1847,1858,1869,1875,1886,1897,1909,1942,1947,1952,1958,1964,1970,1976],{"__ignoreMap":228},[232,1370,1371],{"class":117,"line":234},[232,1372,1373],{"class":491},"# app\u002Frouters\u002Fforecasts.py\n",[232,1375,1376,1378],{"class":117,"line":262},[232,1377,497],{"class":338},[232,1379,500],{"class":252},[232,1381,1382,1384,1387,1389],{"class":117,"line":274},[232,1383,505],{"class":338},[232,1385,1386],{"class":252}," typing ",[232,1388,497],{"class":338},[232,1390,1391],{"class":252}," Annotated\n",[232,1393,1394],{"class":117,"line":282},[232,1395,519],{"emptyLinePlaceholder":518},[232,1397,1398,1400,1402,1404],{"class":117,"line":290},[232,1399,505],{"class":338},[232,1401,526],{"class":252},[232,1403,497],{"class":338},[232,1405,1406],{"class":252}," APIRouter, Body, Depends, status\n",[232,1408,1409],{"class":117,"line":298},[232,1410,519],{"emptyLinePlaceholder":518},[232,1412,1413,1415,1418,1420],{"class":117,"line":306},[232,1414,505],{"class":338},[232,1416,1417],{"class":252}," app.models ",[232,1419,497],{"class":338},[232,1421,1422],{"class":252}," Forecast, ForecastRequest, ProblemDetail\n",[232,1424,1425,1427,1430,1432],{"class":117,"line":314},[232,1426,505],{"class":338},[232,1428,1429],{"class":252}," app.security ",[232,1431,497],{"class":338},[232,1433,1434],{"class":252}," require_api_key\n",[232,1436,1437],{"class":117,"line":322},[232,1438,519],{"emptyLinePlaceholder":518},[232,1440,1441,1444,1446],{"class":117,"line":447},[232,1442,1443],{"class":252},"router ",[232,1445,345],{"class":338},[232,1447,1448],{"class":252}," APIRouter(\n",[232,1450,1451,1454,1456,1458,1461,1463,1466],{"class":117,"line":580},[232,1452,1453],{"class":744},"    prefix",[232,1455,345],{"class":338},[232,1457,852],{"class":252},[232,1459,1460],{"class":245},"\"API_PREFIX\"",[232,1462,715],{"class":252},[232,1464,1465],{"class":245},"\"\u002Fv1\"",[232,1467,863],{"class":252},[232,1469,1470,1473,1475,1478,1480],{"class":117,"line":593},[232,1471,1472],{"class":744},"    tags",[232,1474,345],{"class":338},[232,1476,1477],{"class":252},"[",[232,1479,574],{"class":245},[232,1481,1482],{"class":252},"],\n",[232,1484,1485,1488,1490],{"class":117,"line":599},[232,1486,1487],{"class":744},"    dependencies",[232,1489,345],{"class":338},[232,1491,1492],{"class":252},"[Depends(require_api_key)],\n",[232,1494,1495],{"class":117,"line":604},[232,1496,721],{"class":252},[232,1498,1499],{"class":117,"line":616},[232,1500,519],{"emptyLinePlaceholder":518},[232,1502,1503],{"class":117,"line":628},[232,1504,519],{"emptyLinePlaceholder":518},[232,1506,1507,1510],{"class":117,"line":633},[232,1508,1509],{"class":237},"@router.post",[232,1511,1512],{"class":252},"(\n",[232,1514,1515,1518],{"class":117,"line":638},[232,1516,1517],{"class":245},"    \"\u002Fforecasts\"",[232,1519,577],{"class":252},[232,1521,1522,1525,1527],{"class":117,"line":650},[232,1523,1524],{"class":744},"    response_model",[232,1526,345],{"class":338},[232,1528,1529],{"class":252},"Forecast,\n",[232,1531,1532,1535,1537,1540,1543],{"class":117,"line":662},[232,1533,1534],{"class":744},"    status_code",[232,1536,345],{"class":338},[232,1538,1539],{"class":252},"status.",[232,1541,1542],{"class":241},"HTTP_201_CREATED",[232,1544,577],{"class":252},[232,1546,1547,1550,1552,1555],{"class":117,"line":667},[232,1548,1549],{"class":744},"    operation_id",[232,1551,345],{"class":338},[232,1553,1554],{"class":245},"\"createForecast\"",[232,1556,577],{"class":252},[232,1558,1559,1562,1564,1567],{"class":117,"line":673},[232,1560,1561],{"class":744},"    summary",[232,1563,345],{"class":338},[232,1565,1566],{"class":245},"\"Create a demand forecast\"",[232,1568,577],{"class":252},[232,1570,1571,1574,1576],{"class":117,"line":678},[232,1572,1573],{"class":744},"    description",[232,1575,345],{"class":338},[232,1577,1512],{"class":252},[232,1579,1580],{"class":117,"line":683},[232,1581,1582],{"class":245},"        \"Runs the forecast model over the supplied history window and returns a \"\n",[232,1584,1585],{"class":117,"line":701},[232,1586,1587],{"class":245},"        \"point estimate with an 80 percent prediction interval. Counts as one \"\n",[232,1589,1590],{"class":117,"line":724},[232,1591,1592],{"class":245},"        \"billable unit against your plan.\"\n",[232,1594,1595],{"class":117,"line":735},[232,1596,1597],{"class":252},"    ),\n",[232,1599,1600,1603,1605],{"class":117,"line":768},[232,1601,1602],{"class":744},"    responses",[232,1604,345],{"class":338},[232,1606,976],{"class":252},[232,1608,1609,1612,1615,1618,1621,1624,1626,1629],{"class":117,"line":773},[232,1610,1611],{"class":241},"        402",[232,1613,1614],{"class":252},": {",[232,1616,1617],{"class":245},"\"model\"",[232,1619,1620],{"class":252},": ProblemDetail, ",[232,1622,1623],{"class":245},"\"description\"",[232,1625,571],{"class":252},[232,1627,1628],{"class":245},"\"Plan quota exhausted.\"",[232,1630,1631],{"class":252},"},\n",[232,1633,1634,1637,1639,1641,1643,1645,1647,1650],{"class":117,"line":778},[232,1635,1636],{"class":241},"        422",[232,1638,1614],{"class":252},[232,1640,1617],{"class":245},[232,1642,1620],{"class":252},[232,1644,1623],{"class":245},[232,1646,571],{"class":252},[232,1648,1649],{"class":245},"\"History window too short.\"",[232,1651,1631],{"class":252},[232,1653,1654,1657,1659,1661,1663,1665,1667,1670],{"class":117,"line":794},[232,1655,1656],{"class":241},"        429",[232,1658,1614],{"class":252},[232,1660,1617],{"class":245},[232,1662,1620],{"class":252},[232,1664,1623],{"class":245},[232,1666,571],{"class":252},[232,1668,1669],{"class":245},"\"Rate limit exceeded.\"",[232,1671,1631],{"class":252},[232,1673,1674],{"class":117,"line":810},[232,1675,596],{"class":252},[232,1677,1678],{"class":117,"line":819},[232,1679,721],{"class":252},[232,1681,1682,1685,1688,1691],{"class":117,"line":828},[232,1683,1684],{"class":338},"async",[232,1686,1687],{"class":338}," def",[232,1689,1690],{"class":237}," create_forecast",[232,1692,1512],{"class":252},[232,1694,1695],{"class":117,"line":833},[232,1696,1697],{"class":252},"    payload: Annotated[\n",[232,1699,1700],{"class":117,"line":844},[232,1701,1702],{"class":252},"        ForecastRequest,\n",[232,1704,1705],{"class":117,"line":866},[232,1706,1707],{"class":252},"        Body(\n",[232,1709,1710,1713,1715],{"class":117,"line":886},[232,1711,1712],{"class":744},"            openapi_examples",[232,1714,345],{"class":338},[232,1716,976],{"class":252},[232,1718,1719,1722],{"class":117,"line":906},[232,1720,1721],{"class":245},"                \"weekly_retail\"",[232,1723,1724],{"class":252},": {\n",[232,1726,1727,1730,1732,1735],{"class":117,"line":917},[232,1728,1729],{"class":245},"                    \"summary\"",[232,1731,571],{"class":252},[232,1733,1734],{"class":245},"\"Weekly retail demand\"",[232,1736,577],{"class":252},[232,1738,1739,1742,1744,1747],{"class":117,"line":928},[232,1740,1741],{"class":245},"                    \"description\"",[232,1743,571],{"class":252},[232,1745,1746],{"class":245},"\"Twelve weeks of history, four week horizon.\"",[232,1748,577],{"class":252},[232,1750,1751,1754],{"class":117,"line":940},[232,1752,1753],{"class":245},"                    \"value\"",[232,1755,1724],{"class":252},[232,1757,1758,1761,1763,1766],{"class":117,"line":968},[232,1759,1760],{"class":245},"                        \"sku\"",[232,1762,571],{"class":252},[232,1764,1765],{"class":245},"\"SKU-1043\"",[232,1767,577],{"class":252},[232,1769,1770,1773,1775,1778],{"class":117,"line":979},[232,1771,1772],{"class":245},"                        \"granularity\"",[232,1774,571],{"class":252},[232,1776,1777],{"class":245},"\"weekly\"",[232,1779,577],{"class":252},[232,1781,1782,1785,1787,1790],{"class":117,"line":992},[232,1783,1784],{"class":245},"                        \"horizon\"",[232,1786,571],{"class":252},[232,1788,1789],{"class":241},"4",[232,1791,577],{"class":252},[232,1793,1794,1797,1800,1803,1805,1808,1810,1813,1815,1818,1820,1823,1825,1828],{"class":117,"line":1010},[232,1795,1796],{"class":245},"                        \"history\"",[232,1798,1799],{"class":252},": [",[232,1801,1802],{"class":241},"812",[232,1804,715],{"class":252},[232,1806,1807],{"class":241},"790",[232,1809,715],{"class":252},[232,1811,1812],{"class":241},"845",[232,1814,715],{"class":252},[232,1816,1817],{"class":241},"901",[232,1819,715],{"class":252},[232,1821,1822],{"class":241},"877",[232,1824,715],{"class":252},[232,1826,1827],{"class":241},"860",[232,1829,1482],{"class":252},[232,1831,1832],{"class":117,"line":1016},[232,1833,1834],{"class":252},"                    },\n",[232,1836,1837],{"class":117,"line":1052},[232,1838,1839],{"class":252},"                },\n",[232,1841,1842,1845],{"class":117,"line":1058},[232,1843,1844],{"class":245},"                \"sparse_series\"",[232,1846,1724],{"class":252},[232,1848,1849,1851,1853,1856],{"class":117,"line":1090},[232,1850,1729],{"class":245},[232,1852,571],{"class":252},[232,1854,1855],{"class":245},"\"Sparse slow-moving item\"",[232,1857,577],{"class":252},[232,1859,1860,1862,1864,1867],{"class":117,"line":1101},[232,1861,1741],{"class":245},[232,1863,571],{"class":252},[232,1865,1866],{"class":245},"\"Intermittent demand; the model falls back to Croston.\"",[232,1868,577],{"class":252},[232,1870,1871,1873],{"class":117,"line":1109},[232,1872,1753],{"class":245},[232,1874,1724],{"class":252},[232,1876,1877,1879,1881,1884],{"class":117,"line":1114},[232,1878,1760],{"class":245},[232,1880,571],{"class":252},[232,1882,1883],{"class":245},"\"SKU-9920\"",[232,1885,577],{"class":252},[232,1887,1889,1891,1893,1895],{"class":117,"line":1888},54,[232,1890,1772],{"class":245},[232,1892,571],{"class":252},[232,1894,1777],{"class":245},[232,1896,577],{"class":252},[232,1898,1900,1902,1904,1907],{"class":117,"line":1899},55,[232,1901,1784],{"class":245},[232,1903,571],{"class":252},[232,1905,1906],{"class":241},"8",[232,1908,577],{"class":252},[232,1910,1912,1914,1916,1918,1920,1922,1924,1927,1929,1931,1933,1935,1937,1940],{"class":117,"line":1911},56,[232,1913,1796],{"class":245},[232,1915,1799],{"class":252},[232,1917,65],{"class":241},[232,1919,715],{"class":252},[232,1921,65],{"class":241},[232,1923,715],{"class":252},[232,1925,1926],{"class":241},"3",[232,1928,715],{"class":252},[232,1930,65],{"class":241},[232,1932,715],{"class":252},[232,1934,65],{"class":241},[232,1936,715],{"class":252},[232,1938,1939],{"class":241},"1",[232,1941,1482],{"class":252},[232,1943,1945],{"class":117,"line":1944},57,[232,1946,1834],{"class":252},[232,1948,1950],{"class":117,"line":1949},58,[232,1951,1839],{"class":252},[232,1953,1955],{"class":117,"line":1954},59,[232,1956,1957],{"class":252},"            }\n",[232,1959,1961],{"class":117,"line":1960},60,[232,1962,1963],{"class":252},"        ),\n",[232,1965,1967],{"class":117,"line":1966},61,[232,1968,1969],{"class":252},"    ],\n",[232,1971,1973],{"class":117,"line":1972},62,[232,1974,1975],{"class":252},") -> Forecast:\n",[232,1977,1979,1981,1984],{"class":117,"line":1978},63,[232,1980,738],{"class":338},[232,1982,1983],{"class":338}," await",[232,1985,1986],{"class":252}," run_forecast(payload)\n",[14,1988,1989,1992,1993,1996,1997,2000],{},[32,1990,1991],{},"openapi_examples"," is the parameter to reach for, not ",[32,1994,1995],{},"example"," or ",[32,1998,1999],{},"examples",". It emits a named example map into the request body's media type object, which is what lets Swagger UI render a dropdown of scenarios a customer can execute directly. A single anonymous example gets ignored by half the tooling ecosystem; a named map is portable across every renderer and every generator.",[14,2002,2003,2004,2007,2008,2011],{},"Response examples belong on the model, using Pydantic v2's ",[32,2005,2006],{},"json_schema_extra",". Put the example on the model rather than the route so it shows up everywhere the model is referenced, including inside ",[32,2009,2010],{},"$ref"," chains that a route-level example never reaches.",[223,2013,2015],{"className":483,"code":2014,"language":238,"meta":228,"style":228},"# app\u002Fmodels.py\nfrom datetime import date\n\nfrom pydantic import BaseModel, ConfigDict, Field\n\n\nclass ForecastPoint(BaseModel):\n    period_start: date\n    value: float = Field(description=\"Point estimate in units.\", examples=[884.2])\n    lower: float = Field(description=\"Lower bound, 80 percent interval.\")\n    upper: float = Field(description=\"Upper bound, 80 percent interval.\")\n\n\nclass Forecast(BaseModel):\n    model_config = ConfigDict(\n        json_schema_extra={\n            \"examples\": [\n                {\n                    \"forecast_id\": \"fc_01HZX9K2\",\n                    \"sku\": \"SKU-1043\",\n                    \"model\": \"seasonal_naive\",\n                    \"points\": [\n                        {\n                            \"period_start\": \"2026-08-03\",\n                            \"value\": 884.2,\n                            \"lower\": 801.0,\n                            \"upper\": 967.4,\n                        }\n                    ],\n                    \"billable_units\": 1,\n                }\n            ]\n        }\n    )\n\n    forecast_id: str = Field(description=\"Stable identifier; safe to store.\")\n    sku: str\n    model: str = Field(description=\"Algorithm selected for this series.\")\n    points: list[ForecastPoint]\n    billable_units: int = Field(description=\"Units deducted from your plan quota.\")\n\n\nclass ProblemDetail(BaseModel):\n    model_config = ConfigDict(\n        json_schema_extra={\n            \"examples\": [\n                {\n                    \"type\": \"https:\u002F\u002Fexample.com\u002Ferrors\u002Fquota-exhausted\",\n                    \"title\": \"Plan quota exhausted\",\n                    \"status\": 402,\n                    \"detail\": \"You have used 10000 of 10000 forecasts this period.\",\n                }\n            ]\n        }\n    )\n\n    type: str\n    title: str\n    status: int\n    detail: str\n",[32,2016,2017,2022,2034,2038,2050,2054,2058,2075,2080,2114,2134,2154,2158,2162,2175,2185,2194,2202,2207,2219,2230,2242,2249,2254,2266,2277,2289,2301,2306,2311,2322,2327,2332,2337,2341,2345,2365,2373,2393,2398,2419,2423,2427,2440,2448,2456,2462,2466,2478,2490,2502,2514,2518,2522,2526,2530,2534,2543,2550,2558],{"__ignoreMap":228},[232,2018,2019],{"class":117,"line":234},[232,2020,2021],{"class":491},"# app\u002Fmodels.py\n",[232,2023,2024,2026,2029,2031],{"class":117,"line":262},[232,2025,505],{"class":338},[232,2027,2028],{"class":252}," datetime ",[232,2030,497],{"class":338},[232,2032,2033],{"class":252}," date\n",[232,2035,2036],{"class":117,"line":274},[232,2037,519],{"emptyLinePlaceholder":518},[232,2039,2040,2042,2045,2047],{"class":117,"line":282},[232,2041,505],{"class":338},[232,2043,2044],{"class":252}," pydantic ",[232,2046,497],{"class":338},[232,2048,2049],{"class":252}," BaseModel, ConfigDict, Field\n",[232,2051,2052],{"class":117,"line":290},[232,2053,519],{"emptyLinePlaceholder":518},[232,2055,2056],{"class":117,"line":298},[232,2057,519],{"emptyLinePlaceholder":518},[232,2059,2060,2063,2066,2069,2072],{"class":117,"line":306},[232,2061,2062],{"class":338},"class",[232,2064,2065],{"class":237}," ForecastPoint",[232,2067,2068],{"class":252},"(",[232,2070,2071],{"class":237},"BaseModel",[232,2073,2074],{"class":252},"):\n",[232,2076,2077],{"class":117,"line":314},[232,2078,2079],{"class":252},"    period_start: date\n",[232,2081,2082,2085,2088,2090,2093,2095,2097,2100,2102,2104,2106,2108,2111],{"class":117,"line":322},[232,2083,2084],{"class":252},"    value: ",[232,2086,2087],{"class":241},"float",[232,2089,555],{"class":338},[232,2091,2092],{"class":252}," Field(",[232,2094,479],{"class":744},[232,2096,345],{"class":338},[232,2098,2099],{"class":245},"\"Point estimate in units.\"",[232,2101,715],{"class":252},[232,2103,1999],{"class":744},[232,2105,345],{"class":338},[232,2107,1477],{"class":252},[232,2109,2110],{"class":241},"884.2",[232,2112,2113],{"class":252},"])\n",[232,2115,2116,2119,2121,2123,2125,2127,2129,2132],{"class":117,"line":447},[232,2117,2118],{"class":252},"    lower: ",[232,2120,2087],{"class":241},[232,2122,555],{"class":338},[232,2124,2092],{"class":252},[232,2126,479],{"class":744},[232,2128,345],{"class":338},[232,2130,2131],{"class":245},"\"Lower bound, 80 percent interval.\"",[232,2133,721],{"class":252},[232,2135,2136,2139,2141,2143,2145,2147,2149,2152],{"class":117,"line":580},[232,2137,2138],{"class":252},"    upper: ",[232,2140,2087],{"class":241},[232,2142,555],{"class":338},[232,2144,2092],{"class":252},[232,2146,479],{"class":744},[232,2148,345],{"class":338},[232,2150,2151],{"class":245},"\"Upper bound, 80 percent interval.\"",[232,2153,721],{"class":252},[232,2155,2156],{"class":117,"line":593},[232,2157,519],{"emptyLinePlaceholder":518},[232,2159,2160],{"class":117,"line":599},[232,2161,519],{"emptyLinePlaceholder":518},[232,2163,2164,2166,2169,2171,2173],{"class":117,"line":604},[232,2165,2062],{"class":338},[232,2167,2168],{"class":237}," Forecast",[232,2170,2068],{"class":252},[232,2172,2071],{"class":237},[232,2174,2074],{"class":252},[232,2176,2177,2180,2182],{"class":117,"line":616},[232,2178,2179],{"class":252},"    model_config ",[232,2181,345],{"class":338},[232,2183,2184],{"class":252}," ConfigDict(\n",[232,2186,2187,2190,2192],{"class":117,"line":628},[232,2188,2189],{"class":744},"        json_schema_extra",[232,2191,345],{"class":338},[232,2193,976],{"class":252},[232,2195,2196,2199],{"class":117,"line":633},[232,2197,2198],{"class":245},"            \"examples\"",[232,2200,2201],{"class":252},": [\n",[232,2203,2204],{"class":117,"line":638},[232,2205,2206],{"class":252},"                {\n",[232,2208,2209,2212,2214,2217],{"class":117,"line":650},[232,2210,2211],{"class":245},"                    \"forecast_id\"",[232,2213,571],{"class":252},[232,2215,2216],{"class":245},"\"fc_01HZX9K2\"",[232,2218,577],{"class":252},[232,2220,2221,2224,2226,2228],{"class":117,"line":662},[232,2222,2223],{"class":245},"                    \"sku\"",[232,2225,571],{"class":252},[232,2227,1765],{"class":245},[232,2229,577],{"class":252},[232,2231,2232,2235,2237,2240],{"class":117,"line":667},[232,2233,2234],{"class":245},"                    \"model\"",[232,2236,571],{"class":252},[232,2238,2239],{"class":245},"\"seasonal_naive\"",[232,2241,577],{"class":252},[232,2243,2244,2247],{"class":117,"line":673},[232,2245,2246],{"class":245},"                    \"points\"",[232,2248,2201],{"class":252},[232,2250,2251],{"class":117,"line":678},[232,2252,2253],{"class":252},"                        {\n",[232,2255,2256,2259,2261,2264],{"class":117,"line":683},[232,2257,2258],{"class":245},"                            \"period_start\"",[232,2260,571],{"class":252},[232,2262,2263],{"class":245},"\"2026-08-03\"",[232,2265,577],{"class":252},[232,2267,2268,2271,2273,2275],{"class":117,"line":701},[232,2269,2270],{"class":245},"                            \"value\"",[232,2272,571],{"class":252},[232,2274,2110],{"class":241},[232,2276,577],{"class":252},[232,2278,2279,2282,2284,2287],{"class":117,"line":724},[232,2280,2281],{"class":245},"                            \"lower\"",[232,2283,571],{"class":252},[232,2285,2286],{"class":241},"801.0",[232,2288,577],{"class":252},[232,2290,2291,2294,2296,2299],{"class":117,"line":735},[232,2292,2293],{"class":245},"                            \"upper\"",[232,2295,571],{"class":252},[232,2297,2298],{"class":241},"967.4",[232,2300,577],{"class":252},[232,2302,2303],{"class":117,"line":768},[232,2304,2305],{"class":252},"                        }\n",[232,2307,2308],{"class":117,"line":773},[232,2309,2310],{"class":252},"                    ],\n",[232,2312,2313,2316,2318,2320],{"class":117,"line":778},[232,2314,2315],{"class":245},"                    \"billable_units\"",[232,2317,571],{"class":252},[232,2319,1939],{"class":241},[232,2321,577],{"class":252},[232,2323,2324],{"class":117,"line":794},[232,2325,2326],{"class":252},"                }\n",[232,2328,2329],{"class":117,"line":810},[232,2330,2331],{"class":252},"            ]\n",[232,2333,2334],{"class":117,"line":819},[232,2335,2336],{"class":252},"        }\n",[232,2338,2339],{"class":117,"line":828},[232,2340,1273],{"class":252},[232,2342,2343],{"class":117,"line":833},[232,2344,519],{"emptyLinePlaceholder":518},[232,2346,2347,2350,2352,2354,2356,2358,2360,2363],{"class":117,"line":844},[232,2348,2349],{"class":252},"    forecast_id: ",[232,2351,695],{"class":241},[232,2353,555],{"class":338},[232,2355,2092],{"class":252},[232,2357,479],{"class":744},[232,2359,345],{"class":338},[232,2361,2362],{"class":245},"\"Stable identifier; safe to store.\"",[232,2364,721],{"class":252},[232,2366,2367,2370],{"class":117,"line":866},[232,2368,2369],{"class":252},"    sku: ",[232,2371,2372],{"class":241},"str\n",[232,2374,2375,2378,2380,2382,2384,2386,2388,2391],{"class":117,"line":886},[232,2376,2377],{"class":252},"    model: ",[232,2379,695],{"class":241},[232,2381,555],{"class":338},[232,2383,2092],{"class":252},[232,2385,479],{"class":744},[232,2387,345],{"class":338},[232,2389,2390],{"class":245},"\"Algorithm selected for this series.\"",[232,2392,721],{"class":252},[232,2394,2395],{"class":117,"line":906},[232,2396,2397],{"class":252},"    points: list[ForecastPoint]\n",[232,2399,2400,2403,2406,2408,2410,2412,2414,2417],{"class":117,"line":917},[232,2401,2402],{"class":252},"    billable_units: ",[232,2404,2405],{"class":241},"int",[232,2407,555],{"class":338},[232,2409,2092],{"class":252},[232,2411,479],{"class":744},[232,2413,345],{"class":338},[232,2415,2416],{"class":245},"\"Units deducted from your plan quota.\"",[232,2418,721],{"class":252},[232,2420,2421],{"class":117,"line":928},[232,2422,519],{"emptyLinePlaceholder":518},[232,2424,2425],{"class":117,"line":940},[232,2426,519],{"emptyLinePlaceholder":518},[232,2428,2429,2431,2434,2436,2438],{"class":117,"line":968},[232,2430,2062],{"class":338},[232,2432,2433],{"class":237}," ProblemDetail",[232,2435,2068],{"class":252},[232,2437,2071],{"class":237},[232,2439,2074],{"class":252},[232,2441,2442,2444,2446],{"class":117,"line":979},[232,2443,2179],{"class":252},[232,2445,345],{"class":338},[232,2447,2184],{"class":252},[232,2449,2450,2452,2454],{"class":117,"line":992},[232,2451,2189],{"class":744},[232,2453,345],{"class":338},[232,2455,976],{"class":252},[232,2457,2458,2460],{"class":117,"line":1010},[232,2459,2198],{"class":245},[232,2461,2201],{"class":252},[232,2463,2464],{"class":117,"line":1016},[232,2465,2206],{"class":252},[232,2467,2468,2471,2473,2476],{"class":117,"line":1052},[232,2469,2470],{"class":245},"                    \"type\"",[232,2472,571],{"class":252},[232,2474,2475],{"class":245},"\"https:\u002F\u002Fexample.com\u002Ferrors\u002Fquota-exhausted\"",[232,2477,577],{"class":252},[232,2479,2480,2483,2485,2488],{"class":117,"line":1058},[232,2481,2482],{"class":245},"                    \"title\"",[232,2484,571],{"class":252},[232,2486,2487],{"class":245},"\"Plan quota exhausted\"",[232,2489,577],{"class":252},[232,2491,2492,2495,2497,2500],{"class":117,"line":1090},[232,2493,2494],{"class":245},"                    \"status\"",[232,2496,571],{"class":252},[232,2498,2499],{"class":241},"402",[232,2501,577],{"class":252},[232,2503,2504,2507,2509,2512],{"class":117,"line":1101},[232,2505,2506],{"class":245},"                    \"detail\"",[232,2508,571],{"class":252},[232,2510,2511],{"class":245},"\"You have used 10000 of 10000 forecasts this period.\"",[232,2513,577],{"class":252},[232,2515,2516],{"class":117,"line":1109},[232,2517,2326],{"class":252},[232,2519,2520],{"class":117,"line":1114},[232,2521,2331],{"class":252},[232,2523,2524],{"class":117,"line":1888},[232,2525,2336],{"class":252},[232,2527,2528],{"class":117,"line":1899},[232,2529,1273],{"class":252},[232,2531,2532],{"class":117,"line":1911},[232,2533,519],{"emptyLinePlaceholder":518},[232,2535,2536,2539,2541],{"class":117,"line":1944},[232,2537,2538],{"class":241},"    type",[232,2540,571],{"class":252},[232,2542,2372],{"class":241},[232,2544,2545,2548],{"class":117,"line":1949},[232,2546,2547],{"class":252},"    title: ",[232,2549,2372],{"class":241},[232,2551,2552,2555],{"class":117,"line":1954},[232,2553,2554],{"class":252},"    status: ",[232,2556,2557],{"class":241},"int\n",[232,2559,2560,2563],{"class":117,"line":1960},[232,2561,2562],{"class":252},"    detail: ",[232,2564,2372],{"class":241},[44,2566,53,2571,53,2574,53,2577,53,2580,53,2587,53,2592,53,2596,53,2601,53,2605,53,2610,53,2613,53,2616,53,2619,53,2623,53,2625,53,2627,53,2630,53,2632,53,2636,53,2639,53,2641,53,2644,53,2647,53,2651,53,2654,53,2656,53,2659,53,2662,53,2666,53,2669,53,2671],{"viewBox":2567,"role":47,"ariaLabelledBy":2568,"xmlns":51,"style":52},"0 0 760 330",[2569,2570],"oa-doc-map-t","oa-doc-map-d",[55,2572,2573],{"id":2569},"How decorator arguments land in the OpenAPI document",[59,2575,2576],{"id":2570},"Each FastAPI route decorator argument on the left maps to a specific field of the generated OpenAPI operation object on the right.",[63,2578],{"x":65,"y":65,"width":66,"height":2579,"fill":68},"330",[70,2581,72,2582,53],{},[74,2583,82,2585,72],{"id":2584,"viewBox":77,"refX":78,"refY":79,"markerWidth":80,"markerHeight":80,"orient":81},"oa-doc-map-arrow",[84,2586],{"d":86,"fill":87},[89,2588,2591],{"x":125,"y":2589,"fill":93,"style":2590},"32","text-anchor:middle;font-size:13;font-family:var(--font-sans);","Route decorator argument",[89,2593,2595],{"x":2594,"y":2589,"fill":93,"style":2590},"580","OpenAPI 3.1 field",[63,2597],{"x":2598,"y":2599,"width":179,"height":2600,"rx":1906,"fill":68,"stroke":103,"style":104},"24","50","44",[89,2602,2604],{"x":125,"y":2603,"fill":93,"style":109},"77","operation_id=\"createForecast\"",[117,2606],{"x1":185,"y1":2607,"x2":2608,"y2":2607,"stroke":87,"style":2609},"72","438","stroke-width:2;marker-end:url(#oa-doc-map-arrow);",[63,2611],{"x":2612,"y":2599,"width":179,"height":2600,"rx":1906,"fill":180,"stroke":181,"style":182},"440",[89,2614,2615],{"x":2594,"y":2603,"fill":93,"style":109},"operationId — SDK method name",[63,2617],{"x":2598,"y":2618,"width":179,"height":2600,"rx":1906,"fill":68,"stroke":126,"style":104},"106",[89,2620,2622],{"x":125,"y":2621,"fill":93,"style":109},"133","summary=\"Create a forecast\"",[117,2624],{"x1":185,"y1":100,"x2":2608,"y2":100,"stroke":87,"style":2609},[63,2626],{"x":2612,"y":2618,"width":179,"height":2600,"rx":1906,"fill":180,"stroke":181,"style":182},[89,2628,2629],{"x":2594,"y":2621,"fill":93,"style":109},"summary — sidebar link text",[63,2631],{"x":2598,"y":121,"width":179,"height":2600,"rx":1906,"fill":68,"stroke":141,"style":104},[89,2633,2635],{"x":125,"y":2634,"fill":93,"style":109},"189","tags=[\"Forecasts\"]",[117,2637],{"x1":185,"y1":2638,"x2":2608,"y2":2638,"stroke":87,"style":2609},"184",[63,2640],{"x":2612,"y":121,"width":179,"height":2600,"rx":1906,"fill":180,"stroke":181,"style":182},[89,2642,2643],{"x":2594,"y":2634,"fill":93,"style":109},"tags — nav group and SDK class",[63,2645],{"x":2598,"y":2646,"width":179,"height":2600,"rx":1906,"fill":68,"stroke":155,"style":104},"218",[89,2648,2650],{"x":125,"y":2649,"fill":93,"style":109},"245","response_model=Forecast",[117,2652],{"x1":185,"y1":2653,"x2":2608,"y2":2653,"stroke":87,"style":2609},"240",[63,2655],{"x":2612,"y":2646,"width":179,"height":2600,"rx":1906,"fill":180,"stroke":181,"style":182},[89,2657,2658],{"x":2594,"y":2649,"fill":93,"style":109},"responses.201 — typed result",[63,2660],{"x":2598,"y":2661,"width":179,"height":2600,"rx":1906,"fill":68,"stroke":103,"style":104},"274",[89,2663,2665],{"x":125,"y":2664,"fill":93,"style":109},"301","responses={402: ProblemDetail}",[117,2667],{"x1":185,"y1":2668,"x2":2608,"y2":2668,"stroke":87,"style":2609},"296",[63,2670],{"x":2612,"y":2661,"width":179,"height":2600,"rx":1906,"fill":180,"stroke":181,"style":182},[89,2672,2673],{"x":2594,"y":2664,"fill":93,"style":109},"responses.402 — billing error",[207,2675,2677],{"id":2676},"step-3-document-authentication-so-it-actually-works","Step 3: Document Authentication So It Actually Works",[14,2679,2680,2681,2684,2685,2688],{},"An undocumented security scheme is the single biggest source of \"your API is broken\" support tickets. Customers cannot guess your header name, and a generated client will not send credentials at all unless the spec declares a security requirement. FastAPI populates ",[32,2682,2683],{},"components.securitySchemes"," automatically when you declare your auth dependency using a class from ",[32,2686,2687],{},"fastapi.security"," — do that rather than reading the header manually inside the route.",[223,2690,2692],{"className":483,"code":2691,"language":238,"meta":228,"style":228},"# app\u002Fsecurity.py\nimport hmac\nimport os\n\nfrom fastapi import Depends, HTTPException, status\nfrom fastapi.security import APIKeyHeader\nfrom typing import Annotated\n\napi_key_scheme = APIKeyHeader(\n    name=os.getenv(\"API_KEY_HEADER\", \"X-API-Key\"),\n    scheme_name=\"ApiKeyAuth\",\n    description=(\n        \"Send your secret key in this header on every request. \"\n        \"Keys are issued in the dashboard and can be rotated without downtime.\"\n    ),\n    auto_error=False,\n)\n\n\nasync def require_api_key(\n    presented: Annotated[str | None, Depends(api_key_scheme)],\n) -> str:\n    expected = os.getenv(\"API_KEY_LOOKUP_SALT\", \"\")\n    if presented is None or not expected:\n        raise HTTPException(\n            status_code=status.HTTP_401_UNAUTHORIZED,\n            detail=\"Missing API key.\",\n            headers={\"WWW-Authenticate\": \"ApiKey\"},\n        )\n    account = await resolve_account(hmac.new(expected.encode(), presented.encode(), \"sha256\").hexdigest())\n    if account is None:\n        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=\"Invalid API key.\")\n    return account\n",[32,2693,2694,2699,2706,2712,2716,2727,2739,2749,2753,2763,2782,2794,2802,2807,2812,2816,2828,2832,2836,2840,2851,2867,2876,2894,2916,2924,2938,2950,2969,2973,2991,3004,3032],{"__ignoreMap":228},[232,2695,2696],{"class":117,"line":234},[232,2697,2698],{"class":491},"# app\u002Fsecurity.py\n",[232,2700,2701,2703],{"class":117,"line":262},[232,2702,497],{"class":338},[232,2704,2705],{"class":252}," hmac\n",[232,2707,2708,2710],{"class":117,"line":274},[232,2709,497],{"class":338},[232,2711,500],{"class":252},[232,2713,2714],{"class":117,"line":282},[232,2715,519],{"emptyLinePlaceholder":518},[232,2717,2718,2720,2722,2724],{"class":117,"line":290},[232,2719,505],{"class":338},[232,2721,526],{"class":252},[232,2723,497],{"class":338},[232,2725,2726],{"class":252}," Depends, HTTPException, status\n",[232,2728,2729,2731,2734,2736],{"class":117,"line":298},[232,2730,505],{"class":338},[232,2732,2733],{"class":252}," fastapi.security ",[232,2735,497],{"class":338},[232,2737,2738],{"class":252}," APIKeyHeader\n",[232,2740,2741,2743,2745,2747],{"class":117,"line":306},[232,2742,505],{"class":338},[232,2744,1386],{"class":252},[232,2746,497],{"class":338},[232,2748,1391],{"class":252},[232,2750,2751],{"class":117,"line":314},[232,2752,519],{"emptyLinePlaceholder":518},[232,2754,2755,2758,2760],{"class":117,"line":322},[232,2756,2757],{"class":252},"api_key_scheme ",[232,2759,345],{"class":338},[232,2761,2762],{"class":252}," APIKeyHeader(\n",[232,2764,2765,2768,2770,2772,2775,2777,2780],{"class":117,"line":447},[232,2766,2767],{"class":744},"    name",[232,2769,345],{"class":338},[232,2771,852],{"class":252},[232,2773,2774],{"class":245},"\"API_KEY_HEADER\"",[232,2776,715],{"class":252},[232,2778,2779],{"class":245},"\"X-API-Key\"",[232,2781,863],{"class":252},[232,2783,2784,2787,2789,2792],{"class":117,"line":580},[232,2785,2786],{"class":744},"    scheme_name",[232,2788,345],{"class":338},[232,2790,2791],{"class":245},"\"ApiKeyAuth\"",[232,2793,577],{"class":252},[232,2795,2796,2798,2800],{"class":117,"line":593},[232,2797,1573],{"class":744},[232,2799,345],{"class":338},[232,2801,1512],{"class":252},[232,2803,2804],{"class":117,"line":599},[232,2805,2806],{"class":245},"        \"Send your secret key in this header on every request. \"\n",[232,2808,2809],{"class":117,"line":604},[232,2810,2811],{"class":245},"        \"Keys are issued in the dashboard and can be rotated without downtime.\"\n",[232,2813,2814],{"class":117,"line":616},[232,2815,1597],{"class":252},[232,2817,2818,2821,2823,2826],{"class":117,"line":628},[232,2819,2820],{"class":744},"    auto_error",[232,2822,345],{"class":338},[232,2824,2825],{"class":241},"False",[232,2827,577],{"class":252},[232,2829,2830],{"class":117,"line":633},[232,2831,721],{"class":252},[232,2833,2834],{"class":117,"line":638},[232,2835,519],{"emptyLinePlaceholder":518},[232,2837,2838],{"class":117,"line":650},[232,2839,519],{"emptyLinePlaceholder":518},[232,2841,2842,2844,2846,2849],{"class":117,"line":662},[232,2843,1684],{"class":338},[232,2845,1687],{"class":338},[232,2847,2848],{"class":237}," require_api_key",[232,2850,1512],{"class":252},[232,2852,2853,2856,2858,2861,2864],{"class":117,"line":667},[232,2854,2855],{"class":252},"    presented: Annotated[",[232,2857,695],{"class":241},[232,2859,2860],{"class":338}," |",[232,2862,2863],{"class":241}," None",[232,2865,2866],{"class":252},", Depends(api_key_scheme)],\n",[232,2868,2869,2872,2874],{"class":117,"line":673},[232,2870,2871],{"class":252},") -> ",[232,2873,695],{"class":241},[232,2875,698],{"class":252},[232,2877,2878,2881,2883,2885,2888,2890,2892],{"class":117,"line":678},[232,2879,2880],{"class":252},"    expected ",[232,2882,345],{"class":338},[232,2884,709],{"class":252},[232,2886,2887],{"class":245},"\"API_KEY_LOOKUP_SALT\"",[232,2889,715],{"class":252},[232,2891,1046],{"class":245},[232,2893,721],{"class":252},[232,2895,2896,2899,2902,2905,2907,2910,2913],{"class":117,"line":683},[232,2897,2898],{"class":338},"    if",[232,2900,2901],{"class":252}," presented ",[232,2903,2904],{"class":338},"is",[232,2906,2863],{"class":241},[232,2908,2909],{"class":338}," or",[232,2911,2912],{"class":338}," not",[232,2914,2915],{"class":252}," expected:\n",[232,2917,2918,2921],{"class":117,"line":701},[232,2919,2920],{"class":338},"        raise",[232,2922,2923],{"class":252}," HTTPException(\n",[232,2925,2926,2929,2931,2933,2936],{"class":117,"line":724},[232,2927,2928],{"class":744},"            status_code",[232,2930,345],{"class":338},[232,2932,1539],{"class":252},[232,2934,2935],{"class":241},"HTTP_401_UNAUTHORIZED",[232,2937,577],{"class":252},[232,2939,2940,2943,2945,2948],{"class":117,"line":735},[232,2941,2942],{"class":744},"            detail",[232,2944,345],{"class":338},[232,2946,2947],{"class":245},"\"Missing API key.\"",[232,2949,577],{"class":252},[232,2951,2952,2955,2957,2959,2962,2964,2967],{"class":117,"line":768},[232,2953,2954],{"class":744},"            headers",[232,2956,345],{"class":338},[232,2958,1024],{"class":252},[232,2960,2961],{"class":245},"\"WWW-Authenticate\"",[232,2963,571],{"class":252},[232,2965,2966],{"class":245},"\"ApiKey\"",[232,2968,1631],{"class":252},[232,2970,2971],{"class":117,"line":773},[232,2972,1055],{"class":252},[232,2974,2975,2978,2980,2982,2985,2988],{"class":117,"line":778},[232,2976,2977],{"class":252},"    account ",[232,2979,345],{"class":338},[232,2981,1983],{"class":338},[232,2983,2984],{"class":252}," resolve_account(hmac.new(expected.encode(), presented.encode(), ",[232,2986,2987],{"class":245},"\"sha256\"",[232,2989,2990],{"class":252},").hexdigest())\n",[232,2992,2993,2995,2998,3000,3002],{"class":117,"line":794},[232,2994,2898],{"class":338},[232,2996,2997],{"class":252}," account ",[232,2999,2904],{"class":338},[232,3001,2863],{"class":241},[232,3003,698],{"class":252},[232,3005,3006,3008,3011,3014,3016,3018,3020,3022,3025,3027,3030],{"class":117,"line":810},[232,3007,2920],{"class":338},[232,3009,3010],{"class":252}," HTTPException(",[232,3012,3013],{"class":744},"status_code",[232,3015,345],{"class":338},[232,3017,1539],{"class":252},[232,3019,2935],{"class":241},[232,3021,715],{"class":252},[232,3023,3024],{"class":744},"detail",[232,3026,345],{"class":338},[232,3028,3029],{"class":245},"\"Invalid API key.\"",[232,3031,721],{"class":252},[232,3033,3034,3036],{"class":117,"line":819},[232,3035,738],{"class":338},[232,3037,3038],{"class":252}," account\n",[14,3040,3041,3042,3045,3046,3048,3049,3052,3053,3056,3057,3061,3062,256],{},"Because the dependency is attached at router level, every operation under that router carries a ",[32,3043,3044],{},"security"," requirement in the document, and Swagger UI shows an Authorize button that persists the key across try-it-out calls. The ",[32,3047,479],{}," on the scheme renders inside that dialog, which is exactly where a confused customer looks first. If you issue bearer tokens instead of raw keys, swap ",[32,3050,3051],{},"APIKeyHeader"," for ",[32,3054,3055],{},"HTTPBearer"," — the trade-offs between the two are covered in ",[18,3058,3060],{"href":3059},"\u002Fgetting-started-with-python-apis-for-builders\u002Fhandling-api-authentication-in-python\u002Fjwt-vs-api-keys-for-python-apis\u002F","JWT vs API keys for Python APIs",", and the broader implementation lives in the ",[18,3063,3065],{"href":3064},"\u002Fgetting-started-with-python-apis-for-builders\u002Fhandling-api-authentication-in-python\u002F","API authentication guide",[14,3067,3068],{},"Leave your health endpoint outside the authenticated router. A probe that requires credentials is a probe your load balancer cannot use, and an unauthenticated liveness route in the document tells customers exactly which endpoint they can hit to confirm connectivity before debugging their key.",[44,3070,53,3075,53,3078,53,3081,53,3084,53,3091,53,3096,53,3099,53,3101,53,3104,53,3107,53,3111,53,3115,53,3117,53,3119,53,3124,53,3129,53,3132,53,3137,53,3139,53,3143,53,3147,53,3151,53,3156,53,3159],{"viewBox":3071,"role":47,"ariaLabelledBy":3072,"xmlns":51,"style":52},"0 0 760 300",[3073,3074],"oa-doc-seq-t","oa-doc-seq-d",[55,3076,3077],{"id":3073},"Path from published spec to an authenticated call",[59,3079,3080],{"id":3074},"A customer fetches the spec, generates a client that knows the security scheme, and the first authenticated request succeeds without a support ticket.",[63,3082],{"x":65,"y":65,"width":66,"height":3083,"fill":68},"300",[70,3085,72,3086,53],{},[74,3087,82,3089,72],{"id":3088,"viewBox":77,"refX":78,"refY":79,"markerWidth":80,"markerHeight":80,"orient":81},"oa-doc-seq-arrow",[84,3090],{"d":86,"fill":87},[63,3092],{"x":3093,"y":3094,"width":194,"height":3095,"rx":1906,"fill":68,"stroke":103,"style":104},"30","20","38",[89,3097,3098],{"x":113,"y":2600,"fill":93,"style":109},"Customer",[63,3100],{"x":3083,"y":3094,"width":194,"height":3095,"rx":1906,"fill":68,"stroke":126,"style":104},[89,3102,3103],{"x":91,"y":2600,"fill":93,"style":109},"Published spec",[63,3105],{"x":3106,"y":3094,"width":194,"height":3095,"rx":1906,"fill":68,"stroke":155,"style":104},"570",[89,3108,3110],{"x":3109,"y":2600,"fill":93,"style":109},"650","Your API",[117,3112],{"x1":113,"y1":3113,"x2":113,"y2":3114,"stroke":181,"style":104},"58","282",[117,3116],{"x1":91,"y1":3113,"x2":91,"y2":3114,"stroke":181,"style":104},[117,3118],{"x1":3109,"y1":3113,"x2":3109,"y2":3114,"stroke":181,"style":104},[117,3120],{"x1":113,"y1":3121,"x2":3122,"y2":3121,"stroke":87,"style":3123},"96","376","stroke-width:2;marker-end:url(#oa-doc-seq-arrow);",[89,3125,3128],{"x":3126,"y":3127,"fill":93,"style":109},"243","88","GET \u002Fopenapi.json",[117,3130],{"x1":91,"y1":119,"x2":3131,"y2":119,"stroke":87,"style":3123},"114",[89,3133,3136],{"x":3134,"y":3135,"fill":93,"style":109},"247","132","securitySchemes.ApiKeyAuth",[63,3138],{"x":3093,"y":194,"width":194,"height":2600,"rx":1906,"fill":180,"stroke":181,"style":182},[89,3140,3142],{"x":113,"y":3141,"fill":93,"style":114},"180","generate client",[89,3144,3146],{"x":113,"y":3145,"fill":87,"style":114},"196","header wired in",[117,3148],{"x1":113,"y1":3149,"x2":3150,"y2":3149,"stroke":87,"style":3123},"232","646",[89,3152,3155],{"x":3153,"y":3154,"fill":93,"style":109},"378","224","POST \u002Fv1\u002Fforecasts with X-API-Key",[117,3157],{"x1":3109,"y1":3158,"x2":3131,"y2":3158,"stroke":87,"style":3123},"272",[89,3160,3163],{"x":3161,"y":3162,"fill":93,"style":109},"382","264","201 Created — no support ticket",[207,3165,3167],{"id":3166},"step-4-export-a-versioned-spec-artifact","Step 4: Export a Versioned Spec Artifact",[14,3169,3170],{},"Serving the spec from a live process is convenient and insufficient. Customers need a URL that does not change when you deploy, marketplaces want a file they can ingest, and your CI needs a previous version to diff against. Export the document to disk on every build, name the file after the version, and commit it.",[14,3172,3173,3174,3177,3178,3180,3181,3184,3185,3187,3188,3191],{},"This script reads the version from ",[32,3175,3176],{},"pyproject.toml"," with ",[32,3179,216],{},", writes a version-stamped file plus a ",[32,3182,3183],{},"latest"," pointer, and dispatches subcommands with a ",[32,3186,220],{}," statement. The ",[32,3189,3190],{},"check"," mode is what you run in CI: it regenerates the spec, compares it against the committed copy, and fails the build if they diverge, so nobody ships a contract change without noticing.",[223,3193,3195],{"className":483,"code":3194,"language":238,"meta":228,"style":228},"# scripts\u002Fexport_spec.py\nfrom __future__ import annotations\n\nimport json\nimport os\nimport sys\nimport tomllib\nfrom pathlib import Path\n\nfrom openapi_spec_validator import validate\n\nfrom app.main import create_app\n\n\ndef project_version() -> str:\n    manifest = Path(os.getenv(\"PYPROJECT_PATH\", \"pyproject.toml\"))\n    if not manifest.is_file():\n        return os.getenv(\"API_VERSION\", \"0.0.0\")\n    data = tomllib.loads(manifest.read_text(encoding=\"utf-8\"))\n    return data.get(\"project\", {}).get(\"version\", \"0.0.0\")\n\n\ndef render() -> tuple[str, str]:\n    version = project_version()\n    os.environ.setdefault(\"API_VERSION\", version)\n    schema = create_app().openapi()\n    validate(schema)\n    return version, json.dumps(schema, indent=2, sort_keys=True) + \"\\n\"\n\n\ndef target_paths(version: str) -> tuple[Path, Path]:\n    out_dir = Path(os.getenv(\"SPEC_EXPORT_DIR\", \".\u002Fspec\"))\n    out_dir.mkdir(parents=True, exist_ok=True)\n    return out_dir \u002F f\"openapi-{version}.json\", out_dir \u002F \"openapi-latest.json\"\n\n\ndef main(argv: list[str]) -> int:\n    version, rendered = render()\n    stamped, latest = target_paths(version)\n    match argv:\n        case [\"export\"]:\n            stamped.write_text(rendered, encoding=\"utf-8\")\n            latest.write_text(rendered, encoding=\"utf-8\")\n            print(f\"wrote {stamped} ({len(rendered)} bytes)\")\n            return 0\n        case [\"check\"]:\n            if not latest.is_file():\n                print(\"no committed spec to compare against\", file=sys.stderr)\n                return 1\n            if latest.read_text(encoding=\"utf-8\") != rendered:\n                print(\"spec drift: run `python -m scripts.export_spec export`\", file=sys.stderr)\n                return 1\n            print(\"spec matches committed artifact\")\n            return 0\n        case _:\n            print(\"usage: export_spec.py [export|check]\", file=sys.stderr)\n            return 2\n\n\nif __name__ == \"__main__\":\n    raise SystemExit(main(sys.argv[1:]))\n",[32,3196,3197,3202,3215,3219,3226,3232,3239,3246,3256,3260,3272,3276,3288,3292,3296,3309,3330,3339,3353,3371,3393,3397,3401,3420,3430,3440,3450,3455,3494,3498,3502,3517,3536,3559,3594,3598,3602,3621,3631,3641,3649,3662,3675,3688,3724,3731,3742,3752,3772,3780,3801,3818,3824,3835,3841,3848,3865,3872,3876,3880,3895],{"__ignoreMap":228},[232,3198,3199],{"class":117,"line":234},[232,3200,3201],{"class":491},"# scripts\u002Fexport_spec.py\n",[232,3203,3204,3206,3209,3212],{"class":117,"line":262},[232,3205,505],{"class":338},[232,3207,3208],{"class":241}," __future__",[232,3210,3211],{"class":338}," import",[232,3213,3214],{"class":252}," annotations\n",[232,3216,3217],{"class":117,"line":274},[232,3218,519],{"emptyLinePlaceholder":518},[232,3220,3221,3223],{"class":117,"line":282},[232,3222,497],{"class":338},[232,3224,3225],{"class":252}," json\n",[232,3227,3228,3230],{"class":117,"line":290},[232,3229,497],{"class":338},[232,3231,500],{"class":252},[232,3233,3234,3236],{"class":117,"line":298},[232,3235,497],{"class":338},[232,3237,3238],{"class":252}," sys\n",[232,3240,3241,3243],{"class":117,"line":306},[232,3242,497],{"class":338},[232,3244,3245],{"class":252}," tomllib\n",[232,3247,3248,3250,3252,3254],{"class":117,"line":314},[232,3249,505],{"class":338},[232,3251,508],{"class":252},[232,3253,497],{"class":338},[232,3255,513],{"class":252},[232,3257,3258],{"class":117,"line":322},[232,3259,519],{"emptyLinePlaceholder":518},[232,3261,3262,3264,3267,3269],{"class":117,"line":447},[232,3263,505],{"class":338},[232,3265,3266],{"class":252}," openapi_spec_validator ",[232,3268,497],{"class":338},[232,3270,3271],{"class":252}," validate\n",[232,3273,3274],{"class":117,"line":580},[232,3275,519],{"emptyLinePlaceholder":518},[232,3277,3278,3280,3283,3285],{"class":117,"line":593},[232,3279,505],{"class":338},[232,3281,3282],{"class":252}," app.main ",[232,3284,497],{"class":338},[232,3286,3287],{"class":252}," create_app\n",[232,3289,3290],{"class":117,"line":599},[232,3291,519],{"emptyLinePlaceholder":518},[232,3293,3294],{"class":117,"line":604},[232,3295,519],{"emptyLinePlaceholder":518},[232,3297,3298,3300,3303,3305,3307],{"class":117,"line":616},[232,3299,686],{"class":338},[232,3301,3302],{"class":237}," project_version",[232,3304,692],{"class":252},[232,3306,695],{"class":241},[232,3308,698],{"class":252},[232,3310,3311,3314,3316,3319,3322,3324,3327],{"class":117,"line":628},[232,3312,3313],{"class":252},"    manifest ",[232,3315,345],{"class":338},[232,3317,3318],{"class":252}," Path(os.getenv(",[232,3320,3321],{"class":245},"\"PYPROJECT_PATH\"",[232,3323,715],{"class":252},[232,3325,3326],{"class":245},"\"pyproject.toml\"",[232,3328,3329],{"class":252},"))\n",[232,3331,3332,3334,3336],{"class":117,"line":633},[232,3333,2898],{"class":338},[232,3335,2912],{"class":338},[232,3337,3338],{"class":252}," manifest.is_file():\n",[232,3340,3341,3343,3345,3347,3349,3351],{"class":117,"line":638},[232,3342,1104],{"class":338},[232,3344,709],{"class":252},[232,3346,876],{"class":245},[232,3348,715],{"class":252},[232,3350,881],{"class":245},[232,3352,721],{"class":252},[232,3354,3355,3358,3360,3363,3365,3367,3369],{"class":117,"line":650},[232,3356,3357],{"class":252},"    data ",[232,3359,345],{"class":338},[232,3361,3362],{"class":252}," tomllib.loads(manifest.read_text(",[232,3364,745],{"class":744},[232,3366,345],{"class":338},[232,3368,750],{"class":245},[232,3370,3329],{"class":252},[232,3372,3373,3375,3378,3381,3384,3387,3389,3391],{"class":117,"line":662},[232,3374,738],{"class":338},[232,3376,3377],{"class":252}," data.get(",[232,3379,3380],{"class":245},"\"project\"",[232,3382,3383],{"class":252},", {}).get(",[232,3385,3386],{"class":245},"\"version\"",[232,3388,715],{"class":252},[232,3390,881],{"class":245},[232,3392,721],{"class":252},[232,3394,3395],{"class":117,"line":667},[232,3396,519],{"emptyLinePlaceholder":518},[232,3398,3399],{"class":117,"line":673},[232,3400,519],{"emptyLinePlaceholder":518},[232,3402,3403,3405,3408,3411,3413,3415,3417],{"class":117,"line":678},[232,3404,686],{"class":338},[232,3406,3407],{"class":237}," render",[232,3409,3410],{"class":252},"() -> tuple[",[232,3412,695],{"class":241},[232,3414,715],{"class":252},[232,3416,695],{"class":241},[232,3418,3419],{"class":252},"]:\n",[232,3421,3422,3425,3427],{"class":117,"line":683},[232,3423,3424],{"class":252},"    version ",[232,3426,345],{"class":338},[232,3428,3429],{"class":252}," project_version()\n",[232,3431,3432,3435,3437],{"class":117,"line":701},[232,3433,3434],{"class":252},"    os.environ.setdefault(",[232,3436,876],{"class":245},[232,3438,3439],{"class":252},", version)\n",[232,3441,3442,3445,3447],{"class":117,"line":724},[232,3443,3444],{"class":252},"    schema ",[232,3446,345],{"class":338},[232,3448,3449],{"class":252}," create_app().openapi()\n",[232,3451,3452],{"class":117,"line":735},[232,3453,3454],{"class":252},"    validate(schema)\n",[232,3456,3457,3459,3462,3465,3467,3470,3472,3475,3477,3480,3482,3485,3488,3491],{"class":117,"line":768},[232,3458,738],{"class":338},[232,3460,3461],{"class":252}," version, json.dumps(schema, ",[232,3463,3464],{"class":744},"indent",[232,3466,345],{"class":338},[232,3468,3469],{"class":241},"2",[232,3471,715],{"class":252},[232,3473,3474],{"class":744},"sort_keys",[232,3476,345],{"class":338},[232,3478,3479],{"class":241},"True",[232,3481,753],{"class":252},[232,3483,3484],{"class":338},"+",[232,3486,3487],{"class":245}," \"",[232,3489,3490],{"class":241},"\\n",[232,3492,3493],{"class":245},"\"\n",[232,3495,3496],{"class":117,"line":773},[232,3497,519],{"emptyLinePlaceholder":518},[232,3499,3500],{"class":117,"line":778},[232,3501,519],{"emptyLinePlaceholder":518},[232,3503,3504,3506,3509,3512,3514],{"class":117,"line":794},[232,3505,686],{"class":338},[232,3507,3508],{"class":237}," target_paths",[232,3510,3511],{"class":252},"(version: ",[232,3513,695],{"class":241},[232,3515,3516],{"class":252},") -> tuple[Path, Path]:\n",[232,3518,3519,3522,3524,3526,3529,3531,3534],{"class":117,"line":810},[232,3520,3521],{"class":252},"    out_dir ",[232,3523,345],{"class":338},[232,3525,3318],{"class":252},[232,3527,3528],{"class":245},"\"SPEC_EXPORT_DIR\"",[232,3530,715],{"class":252},[232,3532,3533],{"class":245},"\".\u002Fspec\"",[232,3535,3329],{"class":252},[232,3537,3538,3541,3544,3546,3548,3550,3553,3555,3557],{"class":117,"line":819},[232,3539,3540],{"class":252},"    out_dir.mkdir(",[232,3542,3543],{"class":744},"parents",[232,3545,345],{"class":338},[232,3547,3479],{"class":241},[232,3549,715],{"class":252},[232,3551,3552],{"class":744},"exist_ok",[232,3554,345],{"class":338},[232,3556,3479],{"class":241},[232,3558,721],{"class":252},[232,3560,3561,3563,3566,3569,3572,3575,3577,3580,3583,3586,3589,3591],{"class":117,"line":828},[232,3562,738],{"class":338},[232,3564,3565],{"class":252}," out_dir ",[232,3567,3568],{"class":338},"\u002F",[232,3570,3571],{"class":338}," f",[232,3573,3574],{"class":245},"\"openapi-",[232,3576,1024],{"class":241},[232,3578,3579],{"class":252},"version",[232,3581,3582],{"class":241},"}",[232,3584,3585],{"class":245},".json\"",[232,3587,3588],{"class":252},", out_dir ",[232,3590,3568],{"class":338},[232,3592,3593],{"class":245}," \"openapi-latest.json\"\n",[232,3595,3596],{"class":117,"line":833},[232,3597,519],{"emptyLinePlaceholder":518},[232,3599,3600],{"class":117,"line":844},[232,3601,519],{"emptyLinePlaceholder":518},[232,3603,3604,3606,3609,3612,3614,3617,3619],{"class":117,"line":866},[232,3605,686],{"class":338},[232,3607,3608],{"class":237}," main",[232,3610,3611],{"class":252},"(argv: list[",[232,3613,695],{"class":241},[232,3615,3616],{"class":252},"]) -> ",[232,3618,2405],{"class":241},[232,3620,698],{"class":252},[232,3622,3623,3626,3628],{"class":117,"line":886},[232,3624,3625],{"class":252},"    version, rendered ",[232,3627,345],{"class":338},[232,3629,3630],{"class":252}," render()\n",[232,3632,3633,3636,3638],{"class":117,"line":906},[232,3634,3635],{"class":252},"    stamped, latest ",[232,3637,345],{"class":338},[232,3639,3640],{"class":252}," target_paths(version)\n",[232,3642,3643,3646],{"class":117,"line":917},[232,3644,3645],{"class":338},"    match",[232,3647,3648],{"class":252}," argv:\n",[232,3650,3651,3654,3657,3660],{"class":117,"line":928},[232,3652,3653],{"class":338},"        case",[232,3655,3656],{"class":252}," [",[232,3658,3659],{"class":245},"\"export\"",[232,3661,3419],{"class":252},[232,3663,3664,3667,3669,3671,3673],{"class":117,"line":940},[232,3665,3666],{"class":252},"            stamped.write_text(rendered, ",[232,3668,745],{"class":744},[232,3670,345],{"class":338},[232,3672,750],{"class":245},[232,3674,721],{"class":252},[232,3676,3677,3680,3682,3684,3686],{"class":117,"line":968},[232,3678,3679],{"class":252},"            latest.write_text(rendered, ",[232,3681,745],{"class":744},[232,3683,345],{"class":338},[232,3685,750],{"class":245},[232,3687,721],{"class":252},[232,3689,3690,3693,3695,3698,3701,3703,3706,3708,3711,3714,3717,3719,3722],{"class":117,"line":979},[232,3691,3692],{"class":241},"            print",[232,3694,2068],{"class":252},[232,3696,3697],{"class":338},"f",[232,3699,3700],{"class":245},"\"wrote ",[232,3702,1024],{"class":241},[232,3704,3705],{"class":252},"stamped",[232,3707,3582],{"class":241},[232,3709,3710],{"class":245}," (",[232,3712,3713],{"class":241},"{len",[232,3715,3716],{"class":252},"(rendered)",[232,3718,3582],{"class":241},[232,3720,3721],{"class":245}," bytes)\"",[232,3723,721],{"class":252},[232,3725,3726,3728],{"class":117,"line":992},[232,3727,822],{"class":338},[232,3729,3730],{"class":241}," 0\n",[232,3732,3733,3735,3737,3740],{"class":117,"line":1010},[232,3734,3653],{"class":338},[232,3736,3656],{"class":252},[232,3738,3739],{"class":245},"\"check\"",[232,3741,3419],{"class":252},[232,3743,3744,3747,3749],{"class":117,"line":1016},[232,3745,3746],{"class":338},"            if",[232,3748,2912],{"class":338},[232,3750,3751],{"class":252}," latest.is_file():\n",[232,3753,3754,3757,3759,3762,3764,3767,3769],{"class":117,"line":1052},[232,3755,3756],{"class":241},"                print",[232,3758,2068],{"class":252},[232,3760,3761],{"class":245},"\"no committed spec to compare against\"",[232,3763,715],{"class":252},[232,3765,3766],{"class":744},"file",[232,3768,345],{"class":338},[232,3770,3771],{"class":252},"sys.stderr)\n",[232,3773,3774,3777],{"class":117,"line":1058},[232,3775,3776],{"class":338},"                return",[232,3778,3779],{"class":241}," 1\n",[232,3781,3782,3784,3787,3789,3791,3793,3795,3798],{"class":117,"line":1090},[232,3783,3746],{"class":338},[232,3785,3786],{"class":252}," latest.read_text(",[232,3788,745],{"class":744},[232,3790,345],{"class":338},[232,3792,750],{"class":245},[232,3794,753],{"class":252},[232,3796,3797],{"class":338},"!=",[232,3799,3800],{"class":252}," rendered:\n",[232,3802,3803,3805,3807,3810,3812,3814,3816],{"class":117,"line":1101},[232,3804,3756],{"class":241},[232,3806,2068],{"class":252},[232,3808,3809],{"class":245},"\"spec drift: run `python -m scripts.export_spec export`\"",[232,3811,715],{"class":252},[232,3813,3766],{"class":744},[232,3815,345],{"class":338},[232,3817,3771],{"class":252},[232,3819,3820,3822],{"class":117,"line":1109},[232,3821,3776],{"class":338},[232,3823,3779],{"class":241},[232,3825,3826,3828,3830,3833],{"class":117,"line":1114},[232,3827,3692],{"class":241},[232,3829,2068],{"class":252},[232,3831,3832],{"class":245},"\"spec matches committed artifact\"",[232,3834,721],{"class":252},[232,3836,3837,3839],{"class":117,"line":1888},[232,3838,822],{"class":338},[232,3840,3730],{"class":241},[232,3842,3843,3845],{"class":117,"line":1899},[232,3844,3653],{"class":338},[232,3846,3847],{"class":252}," _:\n",[232,3849,3850,3852,3854,3857,3859,3861,3863],{"class":117,"line":1911},[232,3851,3692],{"class":241},[232,3853,2068],{"class":252},[232,3855,3856],{"class":245},"\"usage: export_spec.py [export|check]\"",[232,3858,715],{"class":252},[232,3860,3766],{"class":744},[232,3862,345],{"class":338},[232,3864,3771],{"class":252},[232,3866,3867,3869],{"class":117,"line":1944},[232,3868,822],{"class":338},[232,3870,3871],{"class":241}," 2\n",[232,3873,3874],{"class":117,"line":1949},[232,3875,519],{"emptyLinePlaceholder":518},[232,3877,3878],{"class":117,"line":1954},[232,3879,519],{"emptyLinePlaceholder":518},[232,3881,3882,3884,3887,3890,3893],{"class":117,"line":1960},[232,3883,756],{"class":338},[232,3885,3886],{"class":241}," __name__",[232,3888,3889],{"class":338}," ==",[232,3891,3892],{"class":245}," \"__main__\"",[232,3894,698],{"class":252},[232,3896,3897,3900,3903,3906,3908],{"class":117,"line":1966},[232,3898,3899],{"class":338},"    raise",[232,3901,3902],{"class":241}," SystemExit",[232,3904,3905],{"class":252},"(main(sys.argv[",[232,3907,1939],{"class":241},[232,3909,3910],{"class":252},":]))\n",[14,3912,3913,3914,3917,3918,3921],{},"Sorting keys is not cosmetic. Python dictionary ordering is stable but the order FastAPI builds ",[32,3915,3916],{},"components.schemas"," in depends on route registration order, so an unsorted dump produces enormous phantom diffs the moment you reorder an ",[32,3919,3920],{},"include_router"," call. Sorted output makes the diff show only genuine contract changes, which is the whole point of committing the file.",[14,3923,3924,3925,3928,3929,3932,3933,3937],{},"Keep every stamped file. When a customer opens a ticket saying your response shape changed, you can hand them the exact diff between ",[32,3926,3927],{},"openapi-1.3.0.json"," and ",[32,3930,3931],{},"openapi-1.4.0.json"," in under a minute. That artifact history is also the foundation for the deprecation workflow described in ",[18,3934,3936],{"href":3935},"\u002Fscaling-and-operating-production-python-apis\u002Fversioning-and-evolving-public-apis\u002F","versioning and evolving public APIs",", and it decides how painful a future major bump will be.",[44,3939,53,3944,53,3947,53,3950,53,3952,53,3957,53,3960,53,3964,53,3968,53,3972,53,3974,53,3977,53,3980,53,3982,53,3985,53,3988,53,3991,53,3993,53,3996,53,3999,53,4002,53,4005,53,4009,53,4013,53,4018],{"viewBox":3940,"role":47,"ariaLabelledBy":3941,"xmlns":51,"style":52},"0 0 760 240",[3942,3943],"oa-doc-tl-t","oa-doc-tl-d",[55,3945,3946],{"id":3942},"Spec artifact history across four releases",[59,3948,3949],{"id":3943},"Additive releases pass the CI diff gate automatically while a breaking change forces a major version and a deprecation window.",[63,3951],{"x":65,"y":65,"width":66,"height":2653,"fill":68},[117,3953],{"x1":3954,"y1":100,"x2":3955,"y2":100,"stroke":181,"style":3956},"40","720","stroke-width:3;",[3958,3959],"circle",{"cx":113,"cy":100,"r":78,"fill":103},[89,3961,3963],{"x":113,"y":3962,"fill":93,"style":109},"104","v1.2.0",[89,3965,3967],{"x":113,"y":3966,"fill":87,"style":114},"156","new tag added",[89,3969,3971],{"x":113,"y":3970,"fill":87,"style":114},"174","gate: pass",[3958,3973],{"cx":3083,"cy":100,"r":78,"fill":126},[89,3975,3976],{"x":3083,"y":3962,"fill":93,"style":109},"v1.3.0",[89,3978,3979],{"x":3083,"y":3966,"fill":87,"style":114},"optional field",[89,3981,3971],{"x":3083,"y":3970,"fill":87,"style":114},[3958,3983],{"cx":3984,"cy":100,"r":78,"fill":141},"490",[89,3986,3987],{"x":3984,"y":3962,"fill":93,"style":109},"v1.4.0",[89,3989,3990],{"x":3984,"y":3966,"fill":87,"style":114},"402 documented",[89,3992,3971],{"x":3984,"y":3970,"fill":87,"style":114},[3958,3994],{"cx":3995,"cy":100,"r":78,"fill":155},"668",[89,3997,3998],{"x":3995,"y":3962,"fill":93,"style":109},"v2.0.0",[89,4000,4001],{"x":3995,"y":3966,"fill":87,"style":114},"field removed",[89,4003,4004],{"x":3995,"y":3970,"fill":87,"style":114},"gate: blocked",[63,4006],{"x":4007,"y":3145,"width":4008,"height":2589,"rx":1906,"fill":180,"stroke":181,"style":182},"470","252",[89,4010,4012],{"x":165,"y":4011,"fill":93,"style":109},"217","requires 90-day deprecation notice",[89,4014,4017],{"x":3954,"y":4015,"fill":93,"style":4016},"46","text-anchor:start;font-size:13;font-family:var(--font-sans);","Committed spec artifacts, oldest to newest",[89,4019,4022],{"x":3954,"y":4020,"fill":87,"style":4021},"68","text-anchor:start;font-size:11;font-family:var(--font-sans);","CI regenerates and diffs on every pull request",[207,4024,4026],{"id":4025},"step-5-publish-the-spec-so-customers-can-generate-clients","Step 5: Publish the Spec So Customers Can Generate Clients",[14,4028,4029],{},"Publishing means three things: a stable URL, correct content type, and permissive CORS so a browser-based playground on someone else's domain can fetch it. Mount the exported directory as static files rather than regenerating the document per request, and put a long cache header on the version-stamped files since they never change.",[223,4031,4033],{"className":483,"code":4032,"language":238,"meta":228,"style":228},"# app\u002Fpublish.py\nimport os\nfrom pathlib import Path\n\nfrom fastapi import FastAPI\nfrom fastapi.middleware.cors import CORSMiddleware\nfrom fastapi.staticfiles import StaticFiles\n\n\ndef mount_spec(app: FastAPI) -> None:\n    spec_dir = Path(os.getenv(\"SPEC_EXPORT_DIR\", \".\u002Fspec\"))\n    spec_dir.mkdir(parents=True, exist_ok=True)\n    app.add_middleware(\n        CORSMiddleware,\n        allow_origins=os.getenv(\"SPEC_CORS_ORIGINS\", \"*\").split(\",\"),\n        allow_methods=[\"GET\"],\n        allow_headers=[\"*\"],\n        max_age=int(os.getenv(\"SPEC_CORS_MAX_AGE\", \"86400\")),\n    )\n    app.mount(\n        os.getenv(\"SPEC_MOUNT_PATH\", \"\u002Fspec\"),\n        StaticFiles(directory=spec_dir),\n        name=\"spec\",\n    )\n",[32,4034,4035,4040,4046,4056,4060,4070,4082,4094,4098,4102,4115,4132,4153,4158,4163,4188,4202,4215,4238,4242,4247,4262,4275,4287],{"__ignoreMap":228},[232,4036,4037],{"class":117,"line":234},[232,4038,4039],{"class":491},"# app\u002Fpublish.py\n",[232,4041,4042,4044],{"class":117,"line":262},[232,4043,497],{"class":338},[232,4045,500],{"class":252},[232,4047,4048,4050,4052,4054],{"class":117,"line":274},[232,4049,505],{"class":338},[232,4051,508],{"class":252},[232,4053,497],{"class":338},[232,4055,513],{"class":252},[232,4057,4058],{"class":117,"line":282},[232,4059,519],{"emptyLinePlaceholder":518},[232,4061,4062,4064,4066,4068],{"class":117,"line":290},[232,4063,505],{"class":338},[232,4065,526],{"class":252},[232,4067,497],{"class":338},[232,4069,531],{"class":252},[232,4071,4072,4074,4077,4079],{"class":117,"line":298},[232,4073,505],{"class":338},[232,4075,4076],{"class":252}," fastapi.middleware.cors ",[232,4078,497],{"class":338},[232,4080,4081],{"class":252}," CORSMiddleware\n",[232,4083,4084,4086,4089,4091],{"class":117,"line":306},[232,4085,505],{"class":338},[232,4087,4088],{"class":252}," fastapi.staticfiles ",[232,4090,497],{"class":338},[232,4092,4093],{"class":252}," StaticFiles\n",[232,4095,4096],{"class":117,"line":314},[232,4097,519],{"emptyLinePlaceholder":518},[232,4099,4100],{"class":117,"line":322},[232,4101,519],{"emptyLinePlaceholder":518},[232,4103,4104,4106,4109,4111,4113],{"class":117,"line":447},[232,4105,686],{"class":338},[232,4107,4108],{"class":237}," mount_spec",[232,4110,786],{"class":252},[232,4112,789],{"class":241},[232,4114,698],{"class":252},[232,4116,4117,4120,4122,4124,4126,4128,4130],{"class":117,"line":580},[232,4118,4119],{"class":252},"    spec_dir ",[232,4121,345],{"class":338},[232,4123,3318],{"class":252},[232,4125,3528],{"class":245},[232,4127,715],{"class":252},[232,4129,3533],{"class":245},[232,4131,3329],{"class":252},[232,4133,4134,4137,4139,4141,4143,4145,4147,4149,4151],{"class":117,"line":593},[232,4135,4136],{"class":252},"    spec_dir.mkdir(",[232,4138,3543],{"class":744},[232,4140,345],{"class":338},[232,4142,3479],{"class":241},[232,4144,715],{"class":252},[232,4146,3552],{"class":744},[232,4148,345],{"class":338},[232,4150,3479],{"class":241},[232,4152,721],{"class":252},[232,4154,4155],{"class":117,"line":599},[232,4156,4157],{"class":252},"    app.add_middleware(\n",[232,4159,4160],{"class":117,"line":604},[232,4161,4162],{"class":252},"        CORSMiddleware,\n",[232,4164,4165,4168,4170,4172,4175,4177,4180,4183,4186],{"class":117,"line":616},[232,4166,4167],{"class":744},"        allow_origins",[232,4169,345],{"class":338},[232,4171,852],{"class":252},[232,4173,4174],{"class":245},"\"SPEC_CORS_ORIGINS\"",[232,4176,715],{"class":252},[232,4178,4179],{"class":245},"\"*\"",[232,4181,4182],{"class":252},").split(",[232,4184,4185],{"class":245},"\",\"",[232,4187,863],{"class":252},[232,4189,4190,4193,4195,4197,4200],{"class":117,"line":628},[232,4191,4192],{"class":744},"        allow_methods",[232,4194,345],{"class":338},[232,4196,1477],{"class":252},[232,4198,4199],{"class":245},"\"GET\"",[232,4201,1482],{"class":252},[232,4203,4204,4207,4209,4211,4213],{"class":117,"line":633},[232,4205,4206],{"class":744},"        allow_headers",[232,4208,345],{"class":338},[232,4210,1477],{"class":252},[232,4212,4179],{"class":245},[232,4214,1482],{"class":252},[232,4216,4217,4220,4222,4224,4227,4230,4232,4235],{"class":117,"line":638},[232,4218,4219],{"class":744},"        max_age",[232,4221,345],{"class":338},[232,4223,2405],{"class":241},[232,4225,4226],{"class":252},"(os.getenv(",[232,4228,4229],{"class":245},"\"SPEC_CORS_MAX_AGE\"",[232,4231,715],{"class":252},[232,4233,4234],{"class":245},"\"86400\"",[232,4236,4237],{"class":252},")),\n",[232,4239,4240],{"class":117,"line":650},[232,4241,1273],{"class":252},[232,4243,4244],{"class":117,"line":662},[232,4245,4246],{"class":252},"    app.mount(\n",[232,4248,4249,4252,4255,4257,4260],{"class":117,"line":667},[232,4250,4251],{"class":252},"        os.getenv(",[232,4253,4254],{"class":245},"\"SPEC_MOUNT_PATH\"",[232,4256,715],{"class":252},[232,4258,4259],{"class":245},"\"\u002Fspec\"",[232,4261,863],{"class":252},[232,4263,4264,4267,4270,4272],{"class":117,"line":673},[232,4265,4266],{"class":252},"        StaticFiles(",[232,4268,4269],{"class":744},"directory",[232,4271,345],{"class":338},[232,4273,4274],{"class":252},"spec_dir),\n",[232,4276,4277,4280,4282,4285],{"class":117,"line":678},[232,4278,4279],{"class":744},"        name",[232,4281,345],{"class":338},[232,4283,4284],{"class":245},"\"spec\"",[232,4286,577],{"class":252},[232,4288,4289],{"class":117,"line":683},[232,4290,1273],{"class":252},[14,4292,4293,4294,4297],{},"Once ",[32,4295,4296],{},"\u002Fspec\u002Fopenapi-latest.json"," is live, a customer generates a working client in one command. Publish these exact invocations in your documentation overview; the fastest way to convert a trial into a paying integration is to remove every step between reading and calling.",[223,4299,4301],{"className":225,"code":4300,"language":227,"meta":228,"style":228},"# Python client\npipx run openapi-python-client generate \\\n  --url \"$API_PUBLIC_URL\u002Fspec\u002Fopenapi-latest.json\"\n\n# TypeScript client\nnpx @hey-api\u002Fopenapi-ts \\\n  --input \"$API_PUBLIC_URL\u002Fspec\u002Fopenapi-latest.json\" \\\n  --output .\u002Fsrc\u002Fgenerated\n",[32,4302,4303,4308,4324,4337,4341,4346,4356,4370],{"__ignoreMap":228},[232,4304,4305],{"class":117,"line":234},[232,4306,4307],{"class":491},"# Python client\n",[232,4309,4310,4313,4316,4319,4322],{"class":117,"line":262},[232,4311,4312],{"class":237},"pipx",[232,4314,4315],{"class":245}," run",[232,4317,4318],{"class":245}," openapi-python-client",[232,4320,4321],{"class":245}," generate",[232,4323,271],{"class":241},[232,4325,4326,4329,4331,4334],{"class":117,"line":274},[232,4327,4328],{"class":241},"  --url",[232,4330,3487],{"class":245},[232,4332,4333],{"class":252},"$API_PUBLIC_URL",[232,4335,4336],{"class":245},"\u002Fspec\u002Fopenapi-latest.json\"\n",[232,4338,4339],{"class":117,"line":282},[232,4340,519],{"emptyLinePlaceholder":518},[232,4342,4343],{"class":117,"line":290},[232,4344,4345],{"class":491},"# TypeScript client\n",[232,4347,4348,4351,4354],{"class":117,"line":298},[232,4349,4350],{"class":237},"npx",[232,4352,4353],{"class":245}," @hey-api\u002Fopenapi-ts",[232,4355,271],{"class":241},[232,4357,4358,4361,4363,4365,4368],{"class":117,"line":306},[232,4359,4360],{"class":241},"  --input",[232,4362,3487],{"class":245},[232,4364,4333],{"class":252},[232,4366,4367],{"class":245},"\u002Fspec\u002Fopenapi-latest.json\"",[232,4369,271],{"class":241},[232,4371,4372,4375],{"class":117,"line":314},[232,4373,4374],{"class":241},"  --output",[232,4376,4377],{"class":245}," .\u002Fsrc\u002Fgenerated\n",[14,4379,4380,4381,4385,4386,4390,4391,4395],{},"For a richer reading experience than the bundled explorer, render the same artifact into a standalone documentation site. The comparison in ",[18,4382,4384],{"href":4383},"\u002Fgetting-started-with-python-apis-for-builders\u002Fdocumenting-apis-with-openapi\u002Fredoc-vs-swagger-ui\u002F","ReDoc vs Swagger UI"," covers which renderer suits a commercial audience, and if you are building a full self-serve onboarding flow around it, ",[18,4387,4389],{"href":4388},"\u002Fbuilding-monetizing-api-driven-micro-saas\u002Fdeploying-apis-to-render-or-vercel\u002Fcreating-a-developer-portal-for-your-api\u002F","creating a developer portal for your API"," picks up where this page stops. Marketplaces consume the same file: ",[18,4392,4394],{"href":4393},"\u002Fbuilding-monetizing-api-driven-micro-saas\u002Fbuilding-api-marketplaces\u002Flisting-your-api-on-rapidapi\u002F","listing your API on RapidAPI"," is mostly a matter of uploading a clean spec.",[207,4397,4399],{"id":4398},"configuration-reference","Configuration Reference",[14,4401,4402],{},"Every knob below reads from the environment, so one image serves staging and production with different metadata. The defaults are development-safe; the recommendations assume a public commercial API.",[4404,4405,4406,4422],"table",{},[4407,4408,4409],"thead",{},[4410,4411,4412,4416,4419],"tr",{},[4413,4414,4415],"th",{},"Variable",[4413,4417,4418],{},"Default",[4413,4420,4421],{},"Production recommendation",[4423,4424,4425,4441,4458,4473,4488,4503,4518,4532,4547,4562,4577,4594],"tbody",{},[4410,4426,4427,4433,4438],{},[4428,4429,4430],"td",{},[32,4431,4432],{},"API_TITLE",[4428,4434,4435],{},[32,4436,4437],{},"Untitled API",[4428,4439,4440],{},"Your product name, exactly as billed",[4410,4442,4443,4448,4453],{},[4428,4444,4445],{},[32,4446,4447],{},"API_VERSION",[4428,4449,4450],{},[32,4451,4452],{},"0.0.0",[4428,4454,4455,4456],{},"Semantic version from ",[32,4457,3176],{},[4410,4459,4460,4465,4470],{},[4428,4461,4462],{},[32,4463,4464],{},"API_PUBLIC_URL",[4428,4466,4467],{},[32,4468,4469],{},"http:\u002F\u002Flocalhost:8000",[4428,4471,4472],{},"Public HTTPS origin, no trailing slash",[4410,4474,4475,4480,4485],{},[4428,4476,4477],{},[32,4478,4479],{},"API_DESCRIPTION_PATH",[4428,4481,4482],{},[32,4483,4484],{},"docs\u002Foverview.md",[4428,4486,4487],{},"Markdown file shipped in the image",[4410,4489,4490,4495,4500],{},[4428,4491,4492],{},[32,4493,4494],{},"API_CONTACT_EMAIL",[4428,4496,4497],{},[32,4498,4499],{},"support@example.com",[4428,4501,4502],{},"A monitored support inbox",[4410,4504,4505,4510,4515],{},[4428,4506,4507],{},[32,4508,4509],{},"API_KEY_HEADER",[4428,4511,4512],{},[32,4513,4514],{},"X-API-Key",[4428,4516,4517],{},"Keep stable; renaming breaks clients",[4410,4519,4520,4525,4529],{},[4428,4521,4522],{},[32,4523,4524],{},"OPENAPI_URL",[4428,4526,4527],{},[32,4528,34],{},[4428,4530,4531],{},"Leave enabled for discoverability",[4410,4533,4534,4539,4544],{},[4428,4535,4536],{},[32,4537,4538],{},"DOCS_URL",[4428,4540,4541],{},[32,4542,4543],{},"\u002Fdocs",[4428,4545,4546],{},"Enabled; it is your best sales page",[4410,4548,4549,4554,4559],{},[4428,4550,4551],{},[32,4552,4553],{},"REDOC_URL",[4428,4555,4556],{},[32,4557,4558],{},"\u002Fredoc",[4428,4560,4561],{},"Enabled for long-form reading",[4410,4563,4564,4569,4574],{},[4428,4565,4566],{},[32,4567,4568],{},"SPEC_EXPORT_DIR",[4428,4570,4571],{},[32,4572,4573],{},".\u002Fspec",[4428,4575,4576],{},"Baked into the image at build time",[4410,4578,4579,4584,4589],{},[4428,4580,4581],{},[32,4582,4583],{},"SPEC_CORS_ORIGINS",[4428,4585,4586],{},[32,4587,4588],{},"*",[4428,4590,4591,4593],{},[32,4592,4588],{}," for the spec route only",[4410,4595,4596,4601,4606],{},[4428,4597,4598],{},[32,4599,4600],{},"ENVIRONMENT",[4428,4602,4603],{},[32,4604,4605],{},"development",[4428,4607,4608,4611,4612],{},[32,4609,4610],{},"production",", surfaced as ",[32,4613,4614],{},"x-environment",[14,4616,4617,4618,4620,4621,4623],{},"Two entries deserve elaboration. ",[32,4619,4583],{}," should stay wide open for the spec mount because third-party tools fetch it from arbitrary origins, but it must not be the same policy your authenticated API routes use — apply the permissive middleware to a dedicated sub-application if your main routes need a restricted origin list. And ",[32,4622,4509],{}," looks like a harmless setting right up until you change it, at which point every generated client in the wild starts sending credentials your service ignores. Treat it as frozen once the first customer integrates.",[207,4625,4627],{"id":4626},"gotchas-and-failure-modes","Gotchas and Failure Modes",[14,4629,4630,4634,4635,4637,4638,4640,4641,4643,4644,4646,4647,4649,4650,4653],{},[4631,4632,4633],"strong",{},"Schema cached before all routers were included."," The symptom is an endpoint that responds correctly to curl but is missing from ",[32,4636,34],{}," entirely. The cause is that something called ",[32,4639,1340],{}," — often a health check, a test fixture, or middleware — before the last ",[32,4642,3920],{}," ran, and the result was cached on ",[32,4645,1336],{},". The fix is to build the schema only after full router registration and never call ",[32,4648,1340],{}," during startup. If you genuinely need to invalidate, set ",[32,4651,4652],{},"app.openapi_schema = None"," and let the next call rebuild it.",[14,4655,4656,4659,4660,4663,4664,4667,4668,4670],{},[4631,4657,4658],{},"Duplicate operation IDs from reused function names."," The symptom is a code generator that emits a single method where two endpoints should exist, or a warning about a non-unique ",[32,4661,4662],{},"operationId",". It happens when two routers both define ",[32,4665,4666],{},"async def list_items"," and you generate IDs from function names. Set ",[32,4669,1355],{}," explicitly on every public route, and add a test that asserts the set of operation IDs has the same length as the list — that assertion has caught this bug in every codebase I have shipped.",[14,4672,4673,4676,4677,4680,4681,4684,4685,4688],{},[4631,4674,4675],{},"Pydantic models that serialize differently than they document."," The symptom is a customer reporting a field your spec says is required but which arrives as ",[32,4678,4679],{},"null",". It comes from using ",[32,4682,4683],{},"response_model_exclude_none=True"," or a custom serializer while the schema still marks the field required. Keep the model honest instead: mark genuinely optional fields as ",[32,4686,4687],{},"X | None = None"," so the generated schema matches the bytes on the wire. A spec that lies is worse than no spec, because clients generated from it fail at runtime rather than at compile time.",[14,4690,4691,4694,4695,4698,4699,256],{},[4631,4692,4693],{},"Undocumented error responses breaking generated clients."," The symptom is a customer whose SDK raises an unexpected-status exception the first time they hit a quota wall. FastAPI documents only the success response and a 422 unless you populate ",[32,4696,4697],{},"responses",". List every status code you can return — 401, 402, 409, 429 — with a model. Related failures around credentials specifically are worth studying in ",[18,4700,4702],{"href":4701},"\u002Fgetting-started-with-python-apis-for-builders\u002Fparsing-json-responses\u002Fdebugging-401-unauthorized-api-errors\u002F","debugging 401 unauthorized API errors",[14,4704,4705,4708,4709,4712,4713,4716],{},[4631,4706,4707],{},"Leaking internal routes into the public document."," The symptom is a curious customer poking at ",[32,4710,4711],{},"\u002Finternal\u002Freplay-events",". Any route you do not want in the contract needs ",[32,4714,4715],{},"include_in_schema=False"," on the decorator. Better still, mount internal tooling as a separate ASGI application on a different port so it cannot appear in the public spec by accident.",[207,4718,4720],{"id":4719},"verification","Verification",[14,4722,4723],{},"Prove the document is real before you tell customers about it. Start the service, then check that the metadata, the security scheme, and the operation IDs are all where you expect.",[223,4725,4727],{"className":225,"code":4726,"language":227,"meta":228,"style":228},"uvicorn app.main:app --port \"${PORT:-8000}\" &\ncurl -s \"http:\u002F\u002Flocalhost:${PORT:-8000}\u002Fopenapi.json\" \\\n  | python -c 'import json,sys; d=json.load(sys.stdin); \\\nprint(d[\"openapi\"], d[\"info\"][\"title\"], d[\"info\"][\"version\"]); \\\nprint(sorted(d[\"components\"][\"securitySchemes\"])); \\\nprint(sorted(o[\"operationId\"] for p in d[\"paths\"].values() for o in p.values()))'\n",[32,4728,4729,4758,4780,4794,4799,4804],{"__ignoreMap":228},[232,4730,4731,4734,4737,4740,4743,4746,4749,4752,4755],{"class":117,"line":234},[232,4732,4733],{"class":237},"uvicorn",[232,4735,4736],{"class":245}," app.main:app",[232,4738,4739],{"class":241}," --port",[232,4741,4742],{"class":245}," \"${",[232,4744,4745],{"class":252},"PORT",[232,4747,4748],{"class":338},":-",[232,4750,4751],{"class":252},"8000",[232,4753,4754],{"class":245},"}\"",[232,4756,4757],{"class":252}," &\n",[232,4759,4760,4763,4766,4769,4771,4773,4775,4778],{"class":117,"line":262},[232,4761,4762],{"class":237},"curl",[232,4764,4765],{"class":241}," -s",[232,4767,4768],{"class":245}," \"http:\u002F\u002Flocalhost:${",[232,4770,4745],{"class":252},[232,4772,4748],{"class":338},[232,4774,4751],{"class":252},[232,4776,4777],{"class":245},"}\u002Fopenapi.json\"",[232,4779,271],{"class":241},[232,4781,4782,4785,4788,4791],{"class":117,"line":274},[232,4783,4784],{"class":338},"  |",[232,4786,4787],{"class":237}," python",[232,4789,4790],{"class":241}," -c",[232,4792,4793],{"class":245}," 'import json,sys; d=json.load(sys.stdin); \\\n",[232,4795,4796],{"class":117,"line":282},[232,4797,4798],{"class":245},"print(d[\"openapi\"], d[\"info\"][\"title\"], d[\"info\"][\"version\"]); \\\n",[232,4800,4801],{"class":117,"line":290},[232,4802,4803],{"class":245},"print(sorted(d[\"components\"][\"securitySchemes\"])); \\\n",[232,4805,4806],{"class":117,"line":298},[232,4807,4808],{"class":245},"print(sorted(o[\"operationId\"] for p in d[\"paths\"].values() for o in p.values()))'\n",[14,4810,4811,4812,4815,4816,4819],{},"You should see ",[32,4813,4814],{},"3.1.0 Forecast API 1.4.0",", then ",[32,4817,4818],{},"['ApiKeyAuth']",", then a list of clean camelCase identifiers. Anything named after a Python function is a route you forgot to label.",[14,4821,4822,4823,256],{},"Lock it in with a test so the contract cannot regress silently. This suite runs in under a second and belongs in the same CI job as the drift check; the wider testing approach is covered in ",[18,4824,4826],{"href":4825},"\u002Fscaling-and-operating-production-python-apis\u002Ftesting-python-apis-with-pytest\u002F","testing Python APIs with pytest",[223,4828,4830],{"className":483,"code":4829,"language":238,"meta":228,"style":228},"# tests\u002Ftest_openapi_contract.py\nimport os\n\nimport pytest\nfrom openapi_spec_validator import validate\n\nfrom app.main import create_app\n\n\n@pytest.fixture(scope=\"module\")\ndef schema() -> dict:\n    os.environ.setdefault(\"API_TITLE\", \"Forecast API\")\n    return create_app().openapi()\n\n\ndef test_document_is_valid(schema: dict) -> None:\n    validate(schema)\n    assert schema[\"openapi\"].startswith(\"3.1\")\n\n\ndef test_operation_ids_are_unique_and_clean(schema: dict) -> None:\n    ids = [op[\"operationId\"] for path in schema[\"paths\"].values() for op in path.values()]\n    assert len(ids) == len(set(ids))\n    assert all(\"_\" not in oid for oid in ids), \"set operation_id explicitly\"\n\n\ndef test_security_scheme_is_documented(schema: dict) -> None:\n    schemes = schema[\"components\"][\"securitySchemes\"]\n    assert \"ApiKeyAuth\" in schemes\n    assert schemes[\"ApiKeyAuth\"][\"in\"] == \"header\"\n\n\ndef test_billing_errors_are_documented(schema: dict) -> None:\n    post = schema[\"paths\"][\"\u002Fv1\u002Fforecasts\"][\"post\"]\n    assert {\"402\", \"429\"} \u003C= set(post[\"responses\"])\n",[32,4831,4832,4837,4843,4847,4854,4864,4868,4878,4882,4886,4903,4916,4929,4935,4939,4943,4961,4965,4984,4988,4992,5009,5051,5074,5106,5110,5114,5131,5150,5162,5183,5187,5191,5208,5231],{"__ignoreMap":228},[232,4833,4834],{"class":117,"line":234},[232,4835,4836],{"class":491},"# tests\u002Ftest_openapi_contract.py\n",[232,4838,4839,4841],{"class":117,"line":262},[232,4840,497],{"class":338},[232,4842,500],{"class":252},[232,4844,4845],{"class":117,"line":274},[232,4846,519],{"emptyLinePlaceholder":518},[232,4848,4849,4851],{"class":117,"line":282},[232,4850,497],{"class":338},[232,4852,4853],{"class":252}," pytest\n",[232,4855,4856,4858,4860,4862],{"class":117,"line":290},[232,4857,505],{"class":338},[232,4859,3266],{"class":252},[232,4861,497],{"class":338},[232,4863,3271],{"class":252},[232,4865,4866],{"class":117,"line":298},[232,4867,519],{"emptyLinePlaceholder":518},[232,4869,4870,4872,4874,4876],{"class":117,"line":306},[232,4871,505],{"class":338},[232,4873,3282],{"class":252},[232,4875,497],{"class":338},[232,4877,3287],{"class":252},[232,4879,4880],{"class":117,"line":314},[232,4881,519],{"emptyLinePlaceholder":518},[232,4883,4884],{"class":117,"line":322},[232,4885,519],{"emptyLinePlaceholder":518},[232,4887,4888,4891,4893,4896,4898,4901],{"class":117,"line":447},[232,4889,4890],{"class":237},"@pytest.fixture",[232,4892,2068],{"class":252},[232,4894,4895],{"class":744},"scope",[232,4897,345],{"class":338},[232,4899,4900],{"class":245},"\"module\"",[232,4902,721],{"class":252},[232,4904,4905,4907,4910,4912,4914],{"class":117,"line":580},[232,4906,686],{"class":338},[232,4908,4909],{"class":237}," schema",[232,4911,692],{"class":252},[232,4913,805],{"class":241},[232,4915,698],{"class":252},[232,4917,4918,4920,4922,4924,4927],{"class":117,"line":593},[232,4919,3434],{"class":252},[232,4921,855],{"class":245},[232,4923,715],{"class":252},[232,4925,4926],{"class":245},"\"Forecast API\"",[232,4928,721],{"class":252},[232,4930,4931,4933],{"class":117,"line":599},[232,4932,738],{"class":338},[232,4934,3449],{"class":252},[232,4936,4937],{"class":117,"line":604},[232,4938,519],{"emptyLinePlaceholder":518},[232,4940,4941],{"class":117,"line":616},[232,4942,519],{"emptyLinePlaceholder":518},[232,4944,4945,4947,4950,4953,4955,4957,4959],{"class":117,"line":628},[232,4946,686],{"class":338},[232,4948,4949],{"class":237}," test_document_is_valid",[232,4951,4952],{"class":252},"(schema: ",[232,4954,805],{"class":241},[232,4956,2871],{"class":252},[232,4958,789],{"class":241},[232,4960,698],{"class":252},[232,4962,4963],{"class":117,"line":633},[232,4964,3454],{"class":252},[232,4966,4967,4970,4973,4976,4979,4982],{"class":117,"line":638},[232,4968,4969],{"class":338},"    assert",[232,4971,4972],{"class":252}," schema[",[232,4974,4975],{"class":245},"\"openapi\"",[232,4977,4978],{"class":252},"].startswith(",[232,4980,4981],{"class":245},"\"3.1\"",[232,4983,721],{"class":252},[232,4985,4986],{"class":117,"line":650},[232,4987,519],{"emptyLinePlaceholder":518},[232,4989,4990],{"class":117,"line":662},[232,4991,519],{"emptyLinePlaceholder":518},[232,4993,4994,4996,4999,5001,5003,5005,5007],{"class":117,"line":667},[232,4995,686],{"class":338},[232,4997,4998],{"class":237}," test_operation_ids_are_unique_and_clean",[232,5000,4952],{"class":252},[232,5002,805],{"class":241},[232,5004,2871],{"class":252},[232,5006,789],{"class":241},[232,5008,698],{"class":252},[232,5010,5011,5014,5016,5019,5022,5024,5027,5030,5033,5035,5038,5041,5043,5046,5048],{"class":117,"line":673},[232,5012,5013],{"class":252},"    ids ",[232,5015,345],{"class":338},[232,5017,5018],{"class":252}," [op[",[232,5020,5021],{"class":245},"\"operationId\"",[232,5023,1073],{"class":252},[232,5025,5026],{"class":338},"for",[232,5028,5029],{"class":252}," path ",[232,5031,5032],{"class":338},"in",[232,5034,4972],{"class":252},[232,5036,5037],{"class":245},"\"paths\"",[232,5039,5040],{"class":252},"].values() ",[232,5042,5026],{"class":338},[232,5044,5045],{"class":252}," op ",[232,5047,5032],{"class":338},[232,5049,5050],{"class":252}," path.values()]\n",[232,5052,5053,5055,5058,5061,5064,5066,5068,5071],{"class":117,"line":678},[232,5054,4969],{"class":338},[232,5056,5057],{"class":241}," len",[232,5059,5060],{"class":252},"(ids) ",[232,5062,5063],{"class":338},"==",[232,5065,5057],{"class":241},[232,5067,2068],{"class":252},[232,5069,5070],{"class":241},"set",[232,5072,5073],{"class":252},"(ids))\n",[232,5075,5076,5078,5081,5083,5086,5088,5091,5094,5096,5098,5100,5103],{"class":117,"line":683},[232,5077,4969],{"class":338},[232,5079,5080],{"class":241}," all",[232,5082,2068],{"class":252},[232,5084,5085],{"class":245},"\"_\"",[232,5087,2912],{"class":338},[232,5089,5090],{"class":338}," in",[232,5092,5093],{"class":252}," oid ",[232,5095,5026],{"class":338},[232,5097,5093],{"class":252},[232,5099,5032],{"class":338},[232,5101,5102],{"class":252}," ids), ",[232,5104,5105],{"class":245},"\"set operation_id explicitly\"\n",[232,5107,5108],{"class":117,"line":701},[232,5109,519],{"emptyLinePlaceholder":518},[232,5111,5112],{"class":117,"line":724},[232,5113,519],{"emptyLinePlaceholder":518},[232,5115,5116,5118,5121,5123,5125,5127,5129],{"class":117,"line":735},[232,5117,686],{"class":338},[232,5119,5120],{"class":237}," test_security_scheme_is_documented",[232,5122,4952],{"class":252},[232,5124,805],{"class":241},[232,5126,2871],{"class":252},[232,5128,789],{"class":241},[232,5130,698],{"class":252},[232,5132,5133,5136,5138,5140,5143,5145,5148],{"class":117,"line":768},[232,5134,5135],{"class":252},"    schemes ",[232,5137,345],{"class":338},[232,5139,4972],{"class":252},[232,5141,5142],{"class":245},"\"components\"",[232,5144,1067],{"class":252},[232,5146,5147],{"class":245},"\"securitySchemes\"",[232,5149,670],{"class":252},[232,5151,5152,5154,5157,5159],{"class":117,"line":773},[232,5153,4969],{"class":338},[232,5155,5156],{"class":245}," \"ApiKeyAuth\"",[232,5158,5090],{"class":338},[232,5160,5161],{"class":252}," schemes\n",[232,5163,5164,5166,5169,5171,5173,5176,5178,5180],{"class":117,"line":778},[232,5165,4969],{"class":338},[232,5167,5168],{"class":252}," schemes[",[232,5170,2791],{"class":245},[232,5172,1067],{"class":252},[232,5174,5175],{"class":245},"\"in\"",[232,5177,1073],{"class":252},[232,5179,5063],{"class":338},[232,5181,5182],{"class":245}," \"header\"\n",[232,5184,5185],{"class":117,"line":794},[232,5186,519],{"emptyLinePlaceholder":518},[232,5188,5189],{"class":117,"line":810},[232,5190,519],{"emptyLinePlaceholder":518},[232,5192,5193,5195,5198,5200,5202,5204,5206],{"class":117,"line":819},[232,5194,686],{"class":338},[232,5196,5197],{"class":237}," test_billing_errors_are_documented",[232,5199,4952],{"class":252},[232,5201,805],{"class":241},[232,5203,2871],{"class":252},[232,5205,789],{"class":241},[232,5207,698],{"class":252},[232,5209,5210,5213,5215,5217,5219,5221,5224,5226,5229],{"class":117,"line":828},[232,5211,5212],{"class":252},"    post ",[232,5214,345],{"class":338},[232,5216,4972],{"class":252},[232,5218,5037],{"class":245},[232,5220,1067],{"class":252},[232,5222,5223],{"class":245},"\"\u002Fv1\u002Fforecasts\"",[232,5225,1067],{"class":252},[232,5227,5228],{"class":245},"\"post\"",[232,5230,670],{"class":252},[232,5232,5233,5235,5238,5241,5243,5246,5249,5252,5255,5258,5261],{"class":117,"line":833},[232,5234,4969],{"class":338},[232,5236,5237],{"class":252}," {",[232,5239,5240],{"class":245},"\"402\"",[232,5242,715],{"class":252},[232,5244,5245],{"class":245},"\"429\"",[232,5247,5248],{"class":252},"} ",[232,5250,5251],{"class":338},"\u003C=",[232,5253,5254],{"class":241}," set",[232,5256,5257],{"class":252},"(post[",[232,5259,5260],{"class":245},"\"responses\"",[232,5262,2113],{"class":252},[207,5264,5266],{"id":5265},"cost-and-performance-at-scale","Cost and Performance at Scale",[14,5268,5269,5270,5272,5273,5275,5276,5280],{},"Documentation looks free until you serve it from the same process that serves billable traffic. Generating an OpenAPI document is expensive — walking routes and building JSON Schema for a fifty-operation API takes roughly 200 to 400 milliseconds on a shared vCPU, and it allocates several megabytes. FastAPI caches the result on ",[32,5271,1336],{}," so this happens once per worker, which sounds fine until you remember that a rolling deploy with eight workers pays that cost eight times during the exact window when your health checks are impatient. The document also grows linearly at roughly 4.7 KB per operation, so a hundred-operation API ships a 470 KB JSON payload; served uncompressed from a single small dyno, a bot crawling your docs page every minute costs more egress than your paying customers do. Enable gzip and the same payload drops to about 11 percent of that. Better still, serve the exported static file from object storage or your CDN and leave the dynamic ",[32,5274,34],{}," route as a convenience only — the marginal cost of a spec fetch then rounds to zero and stops competing with billable requests for worker time. The commercial return is easier to measure than most infrastructure spend: every hour of integration support you avoid is an hour back, and a clean spec that lets a prospect generate a client in ninety seconds converts materially better than a page of prose. If you are modelling this properly, fold documentation egress into the same spreadsheet you use for ",[18,5277,5279],{"href":5278},"\u002Fbuilding-monetizing-api-driven-micro-saas\u002Fdesigning-api-pricing-tiers\u002Fcalculating-cost-per-api-request\u002F","calculating cost per API request",", because at low volume it is genuinely a line item rather than a rounding error.",[44,5282,53,5287,53,5290,53,5293,53,5296,53,5299,53,5303,53,5308,53,5311,53,5316,53,5320,53,5322,53,5326,53,5330,53,5333,53,5338,53,5342,53,5344,53,5349],{"viewBox":5283,"role":47,"ariaLabelledBy":5284,"xmlns":51,"style":52},"0 0 720 260",[5285,5286],"oa-doc-size-t","oa-doc-size-d",[55,5288,5289],{"id":5285},"Spec payload size against operation count",[59,5291,5292],{"id":5286},"Uncompressed OpenAPI document size grows at about 4.7 kilobytes per documented operation, reaching 470 kilobytes at one hundred operations.",[63,5294],{"x":65,"y":65,"width":3955,"height":5295,"fill":68},"260",[89,5297,5298],{"x":2598,"y":3093,"fill":93,"style":4016},"Uncompressed spec size by operation count",[117,5300],{"x1":5301,"y1":5302,"x2":5301,"y2":129,"stroke":181,"style":104},"150","48",[89,5304,5307],{"x":5305,"y":107,"fill":87,"style":5306},"138","text-anchor:end;font-size:11;font-family:var(--font-sans);","10 ops",[63,5309],{"x":5301,"y":3113,"width":5310,"height":3093,"rx":1789,"fill":103},"45",[89,5312,5315],{"x":5313,"y":5314,"fill":93,"style":4021},"207","78","48 KB",[89,5317,5319],{"x":5305,"y":5318,"fill":87,"style":5306},"122","25 ops",[63,5321],{"x":5301,"y":3962,"width":113,"height":3093,"rx":1789,"fill":126},[89,5323,5325],{"x":3158,"y":5324,"fill":93,"style":4021},"124","118 KB",[89,5327,5329],{"x":5305,"y":5328,"fill":87,"style":5306},"168","50 ops",[63,5331],{"x":5301,"y":5301,"width":5332,"height":3093,"rx":1789,"fill":141},"221",[89,5334,5337],{"x":5335,"y":5336,"fill":93,"style":4021},"383","170","236 KB",[89,5339,5341],{"x":5305,"y":5340,"fill":87,"style":5306},"214","100 ops",[63,5343],{"x":5301,"y":3145,"width":2612,"height":3093,"rx":1789,"fill":155},[89,5345,5348],{"x":5346,"y":5347,"fill":93,"style":4021},"602","216","470 KB",[89,5350,5352],{"x":2598,"y":5351,"fill":87,"style":4021},"248","Gzip reduces each bar to roughly 11 percent of the size shown.",[207,5354,5356],{"id":5355},"faq","FAQ",[14,5358,5359,5362],{},[4631,5360,5361],{},"Should I expose the interactive docs UI on a paid production API?","\nYes, and it should be the first link in your marketing. A prospect who can execute a real request against your API inside thirty seconds of landing on your site converts far better than one who has to read a PDF and write a curl command. The Authorize dialog only accepts a key the visitor already owns, so it exposes nothing your normal auth does not already gate. The only reason to disable it is a private internal API where the endpoint list itself is sensitive, and even then a separate ASGI application on a restricted port is a better answer than turning documentation off.",[14,5364,5365,5368],{},[4631,5366,5367],{},"How much does serving the spec cost at a million docs requests a month?","\nAlmost nothing if you do it right and surprisingly much if you do not. A 470 KB uncompressed document served a million times is roughly 470 GB of egress, which lands between forty and one hundred dollars a month on typical platform pricing and consumes worker time you are paying for anyway. Gzip cuts that to about 52 GB. Serving the exported static artifact from object storage behind a CDN drops the effective cost under two dollars and removes the CPU contention entirely, which is why the export step in this guide is not optional at scale.",[14,5370,5371,5374,5375,5378,5379,5382,5383,256],{},[4631,5372,5373],{},"Does changing an operation ID break existing customers?","\nNot their HTTP calls, but it absolutely breaks their generated SDKs. An operation ID is the method name in every generated client, so renaming ",[32,5376,5377],{},"createForecast"," to ",[32,5380,5381],{},"createDemandForecast"," means the next time a customer regenerates, their code stops compiling. Treat operation IDs with the same care as URL paths: choose them once, before the first external customer, and change them only in a major version alongside the guidance in ",[18,5384,5386],{"href":5385},"\u002Fscaling-and-operating-production-python-apis\u002Fversioning-and-evolving-public-apis\u002Fdeprecating-an-api-endpoint-without-breaking-customers\u002F","deprecating an API endpoint without breaking customers",[14,5388,5389,5392,5393,256],{},[4631,5390,5391],{},"Do I need a separate spec per API version?","\nYes, and it is cheaper than it sounds. Export one artifact per released version and keep them all reachable, because a customer stuck on v1 needs v1 documentation, not a document describing endpoints they cannot call. If you version by URL prefix you can generate both from one application by filtering routes; if you version by header the split is harder and the trade-offs are laid out in ",[18,5394,5396],{"href":5395},"\u002Fscaling-and-operating-production-python-apis\u002Fversioning-and-evolving-public-apis\u002Furl-versioning-vs-header-versioning\u002F","URL versioning vs header versioning",[14,5398,5399,5402],{},[4631,5400,5401],{},"What is the fastest documentation improvement I can ship this week?","\nAdd named request examples to your three highest-traffic endpoints and document every non-2xx status code they return. Examples turn the try-it-out button from a form a visitor has to fill in into one they can execute immediately, and documented error codes stop generated clients from crashing on the first quota rejection. Both changes take an afternoon, require no architectural work, and cut integration support volume more than any redesign of your docs site will.",[207,5404,5406],{"id":5405},"related","Related",[5408,5409,5410,5418,5423,5429,5435],"ul",{},[5411,5412,5413,5417],"li",{},[18,5414,5416],{"href":5415},"\u002Fgetting-started-with-python-apis-for-builders\u002Fdocumenting-apis-with-openapi\u002Fcustomizing-fastapi-openapi-schema\u002F","Customizing the FastAPI OpenAPI Schema"," — go deeper on rewriting the generated document, injecting extensions, and renaming schema components.",[5411,5419,5420,5422],{},[18,5421,4384],{"href":4383}," — pick the renderer that fits a paying developer audience, with load-time numbers for each.",[5411,5424,5425,5428],{},[18,5426,5427],{"href":25},"Setting Up FastAPI"," — the project structure and application factory this guide assumes you already have.",[5411,5430,5431,5434],{},[18,5432,5433],{"href":3064},"Handling API Authentication in Python"," — implement the security scheme your spec now advertises.",[5411,5436,5437,5440],{},[18,5438,5439],{"href":3935},"Versioning and Evolving Public APIs"," — what to do once a committed spec artifact tells you a change is breaking.",[5442,5443,5444],"style",{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":228,"searchDepth":262,"depth":262,"links":5446},[5447,5448,5449,5450,5451,5452,5453,5454,5455,5456,5457,5458],{"id":209,"depth":262,"text":210},{"id":468,"depth":262,"text":469},{"id":1344,"depth":262,"text":1345},{"id":2676,"depth":262,"text":2677},{"id":3166,"depth":262,"text":3167},{"id":4025,"depth":262,"text":4026},{"id":4398,"depth":262,"text":4399},{"id":4626,"depth":262,"text":4627},{"id":4719,"depth":262,"text":4720},{"id":5265,"depth":262,"text":5266},{"id":5355,"depth":262,"text":5356},{"id":5405,"depth":262,"text":5406},"Turn a FastAPI service into customer-ready OpenAPI docs: schema metadata, rich examples, tags, auth documentation, versioned spec export, and client generation.","md",{"pageTitle":5462,"type":5463,"datePublished":5464,"dateModified":5464},"Documenting APIs with OpenAPI: FastAPI Specs That Sell","guide","2026-07-23","\u002Fgetting-started-with-python-apis-for-builders\u002Fdocumenting-apis-with-openapi",{"title":5,"description":5459},"getting-started-with-python-apis-for-builders\u002Fdocumenting-apis-with-openapi\u002Findex","IGILbfJVJ932u4lWbKChSrSGURGuqFJJh17LdYySxvE",{"@context":5470,"@type":5471,"mainEntity":5472},"https:\u002F\u002Fschema.org","FAQPage",[5473,5478,5481,5484,5487],{"@type":5474,"name":5361,"acceptedAnswer":5475},"Question",{"@type":5476,"text":5477},"Answer","Yes, and it should be the first link in your marketing. A prospect who can execute a real request against your API inside thirty seconds of landing on your site converts far better than one who has to read a PDF and write a curl command. The Authorize dialog only accepts a key the visitor already owns, so it exposes nothing your normal auth does not already gate. The only reason to disable it is a private internal API where the endpoint list itself is sensitive, and even then a separate ASGI application on a restricted port is a better answer than turning documentation off.",{"@type":5474,"name":5367,"acceptedAnswer":5479},{"@type":5476,"text":5480},"Almost nothing if you do it right and surprisingly much if you do not. A 470 KB uncompressed document served a million times is roughly 470 GB of egress, which lands between forty and one hundred dollars a month on typical platform pricing and consumes worker time you are paying for anyway. Gzip cuts that to about 52 GB. Serving the exported static artifact from object storage behind a CDN drops the effective cost under two dollars and removes the CPU contention entirely, which is why the export step in this guide is not optional at scale.",{"@type":5474,"name":5373,"acceptedAnswer":5482},{"@type":5476,"text":5483},"Not their HTTP calls, but it absolutely breaks their generated SDKs. An operation ID is the method name in every generated client, so renaming createForecast to createDemandForecast means the next time a customer regenerates, their code stops compiling. Treat operation IDs with the same care as URL paths: choose them once, before the first external customer, and change them only in a major version alongside the guidance in deprecating an API endpoint without breaking customers.",{"@type":5474,"name":5391,"acceptedAnswer":5485},{"@type":5476,"text":5486},"Yes, and it is cheaper than it sounds. Export one artifact per released version and keep them all reachable, because a customer stuck on v1 needs v1 documentation, not a document describing endpoints they cannot call. If you version by URL prefix you can generate both from one application by filtering routes; if you version by header the split is harder and the trade-offs are laid out in URL versioning vs header versioning.",{"@type":5474,"name":5401,"acceptedAnswer":5488},{"@type":5476,"text":5489},"Add named request examples to your three highest-traffic endpoints and document every non-2xx status code they return. Examples turn the try-it-out button from a form a visitor has to fill in into one they can execute immediately, and documented error codes stop generated clients from crashing on the first quota rejection. Both changes take an afternoon, require no architectural work, and cut integration support volume more than any redesign of your docs site will.",1784887027766]