Documenting APIs with OpenAPI
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 Getting Started with Python APIs for Builders guide, this page shows how to turn the schema that FastAPI generates for free into a document customers can read, trust, and feed straight into a code generator.
FastAPI already emits an OpenAPI 3.1 document at /openapi.json. That default document is technically valid and commercially useless: operation IDs read like 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.
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.
Prerequisites
You need Python 3.11 or newer, because the export tooling below uses tomllib from the standard library and a 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.
python -m venv .venv && . .venv/bin/activate
pip install \
"fastapi>=0.115,<0.117" \
"pydantic>=2.9,<3" \
"uvicorn[standard]>=0.34" \
"httpx>=0.28" \
"openapi-spec-validator>=0.7" \
"pytest>=8.3" \
"pytest-asyncio>=0.24"
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.
export API_TITLE="Forecast API"
export API_VERSION="1.4.0"
export API_PUBLIC_URL="https://api.example.com"
export API_CONTACT_EMAIL="support@example.com"
export API_KEY_HEADER="X-API-Key"
export OPENAPI_URL="/openapi.json"
export DOCS_URL="/docs"
export REDOC_URL="/redoc"
export SPEC_EXPORT_DIR="./spec"
export ENVIRONMENT="production"
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 validating JSON with Pydantic v2 are the same rules that decide how readable your schema section turns out.
Step 1: Give the Document Real Metadata
The info block is the first thing a developer reads and the first thing a marketplace listing scrapes. Fill in every field. A 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.
# app/openapi_meta.py
import os
from pathlib import Path
from fastapi import FastAPI
from fastapi.openapi.utils import get_openapi
TAG_METADATA = [
{
"name": "Forecasts",
"description": "Create and retrieve demand forecasts. Billed per forecast created.",
},
{
"name": "Usage",
"description": "Read your current billing period consumption and remaining quota.",
},
{
"name": "Health",
"description": "Unauthenticated liveness probes. Never billed, never rate limited.",
},
]
def _description() -> str:
path = os.getenv("API_DESCRIPTION_PATH", "docs/overview.md")
candidate = Path(path)
return candidate.read_text(encoding="utf-8") if candidate.is_file() else ""
def install_openapi(app: FastAPI) -> None:
def custom_openapi() -> dict:
if app.openapi_schema:
return app.openapi_schema
schema = get_openapi(
title=os.getenv("API_TITLE", "Untitled API"),
version=os.getenv("API_VERSION", "0.0.0"),
summary=os.getenv("API_SUMMARY", "Demand forecasting for commerce teams"),
description=_description(),
routes=app.routes,
tags=TAG_METADATA,
servers=[{"url": os.getenv("API_PUBLIC_URL", "http://localhost:8000")}],
contact={
"name": "API Support",
"email": os.getenv("API_CONTACT_EMAIL", "support@example.com"),
},
license_info={"name": "Commercial", "url": os.getenv("API_TERMS_URL", "")},
)
schema["info"]["x-environment"] = os.getenv("ENVIRONMENT", "development")
app.openapi_schema = schema
return schema
app.openapi = custom_openapi
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.
# app/main.py
import os
from fastapi import FastAPI
from app.openapi_meta import install_openapi
from app.routers import forecasts, health, usage
def create_app() -> FastAPI:
app = FastAPI(
openapi_url=os.getenv("OPENAPI_URL", "/openapi.json"),
docs_url=os.getenv("DOCS_URL", "/docs"),
redoc_url=os.getenv("REDOC_URL", "/redoc"),
)
app.include_router(health.router)
app.include_router(forecasts.router)
app.include_router(usage.router)
install_openapi(app)
return app
app = create_app()
Note the ordering: install_openapi runs after the routers are included, because get_openapi walks app.routes at call time and caches the result on 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 app.openapi() during startup, every route registered afterwards vanishes from the document. Build the schema once, at the end.
Step 2: Make Every Operation Self-Describing
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 client.create_forecast_v1_forecasts_post() in a generated Python SDK. Set operation_id explicitly on every public route, in camelCase, and your customers get client.create_forecast() instead. This one change does more for perceived API quality than any amount of prose.
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.
# app/routers/forecasts.py
import os
from typing import Annotated
from fastapi import APIRouter, Body, Depends, status
from app.models import Forecast, ForecastRequest, ProblemDetail
from app.security import require_api_key
router = APIRouter(
prefix=os.getenv("API_PREFIX", "/v1"),
tags=["Forecasts"],
dependencies=[Depends(require_api_key)],
)
@router.post(
"/forecasts",
response_model=Forecast,
status_code=status.HTTP_201_CREATED,
operation_id="createForecast",
summary="Create a demand forecast",
description=(
"Runs the forecast model over the supplied history window and returns a "
"point estimate with an 80 percent prediction interval. Counts as one "
"billable unit against your plan."
),
responses={
402: {"model": ProblemDetail, "description": "Plan quota exhausted."},
422: {"model": ProblemDetail, "description": "History window too short."},
429: {"model": ProblemDetail, "description": "Rate limit exceeded."},
},
)
async def create_forecast(
payload: Annotated[
ForecastRequest,
Body(
openapi_examples={
"weekly_retail": {
"summary": "Weekly retail demand",
"description": "Twelve weeks of history, four week horizon.",
"value": {
"sku": "SKU-1043",
"granularity": "weekly",
"horizon": 4,
"history": [812, 790, 845, 901, 877, 860],
},
},
"sparse_series": {
"summary": "Sparse slow-moving item",
"description": "Intermittent demand; the model falls back to Croston.",
"value": {
"sku": "SKU-9920",
"granularity": "weekly",
"horizon": 8,
"history": [0, 0, 3, 0, 0, 1],
},
},
}
),
],
) -> Forecast:
return await run_forecast(payload)
openapi_examples is the parameter to reach for, not example or 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.
Response examples belong on the model, using Pydantic v2's json_schema_extra. Put the example on the model rather than the route so it shows up everywhere the model is referenced, including inside $ref chains that a route-level example never reaches.
# app/models.py
from datetime import date
from pydantic import BaseModel, ConfigDict, Field
class ForecastPoint(BaseModel):
period_start: date
value: float = Field(description="Point estimate in units.", examples=[884.2])
lower: float = Field(description="Lower bound, 80 percent interval.")
upper: float = Field(description="Upper bound, 80 percent interval.")
class Forecast(BaseModel):
model_config = ConfigDict(
json_schema_extra={
"examples": [
{
"forecast_id": "fc_01HZX9K2",
"sku": "SKU-1043",
"model": "seasonal_naive",
"points": [
{
"period_start": "2026-08-03",
"value": 884.2,
"lower": 801.0,
"upper": 967.4,
}
],
"billable_units": 1,
}
]
}
)
forecast_id: str = Field(description="Stable identifier; safe to store.")
sku: str
model: str = Field(description="Algorithm selected for this series.")
points: list[ForecastPoint]
billable_units: int = Field(description="Units deducted from your plan quota.")
class ProblemDetail(BaseModel):
model_config = ConfigDict(
json_schema_extra={
"examples": [
{
"type": "https://example.com/errors/quota-exhausted",
"title": "Plan quota exhausted",
"status": 402,
"detail": "You have used 10000 of 10000 forecasts this period.",
}
]
}
)
type: str
title: str
status: int
detail: str
Step 3: Document Authentication So It Actually Works
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 components.securitySchemes automatically when you declare your auth dependency using a class from fastapi.security — do that rather than reading the header manually inside the route.
# app/security.py
import hmac
import os
from fastapi import Depends, HTTPException, status
from fastapi.security import APIKeyHeader
from typing import Annotated
api_key_scheme = APIKeyHeader(
name=os.getenv("API_KEY_HEADER", "X-API-Key"),
scheme_name="ApiKeyAuth",
description=(
"Send your secret key in this header on every request. "
"Keys are issued in the dashboard and can be rotated without downtime."
),
auto_error=False,
)
async def require_api_key(
presented: Annotated[str | None, Depends(api_key_scheme)],
) -> str:
expected = os.getenv("API_KEY_LOOKUP_SALT", "")
if presented is None or not expected:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Missing API key.",
headers={"WWW-Authenticate": "ApiKey"},
)
account = await resolve_account(hmac.new(expected.encode(), presented.encode(), "sha256").hexdigest())
if account is None:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid API key.")
return account
Because the dependency is attached at router level, every operation under that router carries a security requirement in the document, and Swagger UI shows an Authorize button that persists the key across try-it-out calls. The description 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 APIKeyHeader for HTTPBearer — the trade-offs between the two are covered in JWT vs API keys for Python APIs, and the broader implementation lives in the API authentication guide.
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.
Step 4: Export a Versioned Spec Artifact
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.
This script reads the version from pyproject.toml with tomllib, writes a version-stamped file plus a latest pointer, and dispatches subcommands with a match statement. The 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.
# scripts/export_spec.py
from __future__ import annotations
import json
import os
import sys
import tomllib
from pathlib import Path
from openapi_spec_validator import validate
from app.main import create_app
def project_version() -> str:
manifest = Path(os.getenv("PYPROJECT_PATH", "pyproject.toml"))
if not manifest.is_file():
return os.getenv("API_VERSION", "0.0.0")
data = tomllib.loads(manifest.read_text(encoding="utf-8"))
return data.get("project", {}).get("version", "0.0.0")
def render() -> tuple[str, str]:
version = project_version()
os.environ.setdefault("API_VERSION", version)
schema = create_app().openapi()
validate(schema)
return version, json.dumps(schema, indent=2, sort_keys=True) + "\n"
def target_paths(version: str) -> tuple[Path, Path]:
out_dir = Path(os.getenv("SPEC_EXPORT_DIR", "./spec"))
out_dir.mkdir(parents=True, exist_ok=True)
return out_dir / f"openapi-{version}.json", out_dir / "openapi-latest.json"
def main(argv: list[str]) -> int:
version, rendered = render()
stamped, latest = target_paths(version)
match argv:
case ["export"]:
stamped.write_text(rendered, encoding="utf-8")
latest.write_text(rendered, encoding="utf-8")
print(f"wrote {stamped} ({len(rendered)} bytes)")
return 0
case ["check"]:
if not latest.is_file():
print("no committed spec to compare against", file=sys.stderr)
return 1
if latest.read_text(encoding="utf-8") != rendered:
print("spec drift: run `python -m scripts.export_spec export`", file=sys.stderr)
return 1
print("spec matches committed artifact")
return 0
case _:
print("usage: export_spec.py [export|check]", file=sys.stderr)
return 2
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))
Sorting keys is not cosmetic. Python dictionary ordering is stable but the order FastAPI builds components.schemas in depends on route registration order, so an unsorted dump produces enormous phantom diffs the moment you reorder an include_router call. Sorted output makes the diff show only genuine contract changes, which is the whole point of committing the file.
Keep every stamped file. When a customer opens a ticket saying your response shape changed, you can hand them the exact diff between openapi-1.3.0.json and openapi-1.4.0.json in under a minute. That artifact history is also the foundation for the deprecation workflow described in versioning and evolving public APIs, and it decides how painful a future major bump will be.
Step 5: Publish the Spec So Customers Can Generate Clients
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.
# app/publish.py
import os
from pathlib import Path
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
def mount_spec(app: FastAPI) -> None:
spec_dir = Path(os.getenv("SPEC_EXPORT_DIR", "./spec"))
spec_dir.mkdir(parents=True, exist_ok=True)
app.add_middleware(
CORSMiddleware,
allow_origins=os.getenv("SPEC_CORS_ORIGINS", "*").split(","),
allow_methods=["GET"],
allow_headers=["*"],
max_age=int(os.getenv("SPEC_CORS_MAX_AGE", "86400")),
)
app.mount(
os.getenv("SPEC_MOUNT_PATH", "/spec"),
StaticFiles(directory=spec_dir),
name="spec",
)
Once /spec/openapi-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.
# Python client
pipx run openapi-python-client generate \
--url "$API_PUBLIC_URL/spec/openapi-latest.json"
# TypeScript client
npx @hey-api/openapi-ts \
--input "$API_PUBLIC_URL/spec/openapi-latest.json" \
--output ./src/generated
For a richer reading experience than the bundled explorer, render the same artifact into a standalone documentation site. The comparison in ReDoc vs Swagger UI covers which renderer suits a commercial audience, and if you are building a full self-serve onboarding flow around it, creating a developer portal for your API picks up where this page stops. Marketplaces consume the same file: listing your API on RapidAPI is mostly a matter of uploading a clean spec.
Configuration Reference
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.
| Variable | Default | Production recommendation |
|---|---|---|
API_TITLE | Untitled API | Your product name, exactly as billed |
API_VERSION | 0.0.0 | Semantic version from pyproject.toml |
API_PUBLIC_URL | http://localhost:8000 | Public HTTPS origin, no trailing slash |
API_DESCRIPTION_PATH | docs/overview.md | Markdown file shipped in the image |
API_CONTACT_EMAIL | support@example.com | A monitored support inbox |
API_KEY_HEADER | X-API-Key | Keep stable; renaming breaks clients |
OPENAPI_URL | /openapi.json | Leave enabled for discoverability |
DOCS_URL | /docs | Enabled; it is your best sales page |
REDOC_URL | /redoc | Enabled for long-form reading |
SPEC_EXPORT_DIR | ./spec | Baked into the image at build time |
SPEC_CORS_ORIGINS | * | * for the spec route only |
ENVIRONMENT | development | production, surfaced as x-environment |
Two entries deserve elaboration. SPEC_CORS_ORIGINS 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 API_KEY_HEADER 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.
Gotchas and Failure Modes
Schema cached before all routers were included. The symptom is an endpoint that responds correctly to curl but is missing from /openapi.json entirely. The cause is that something called app.openapi() — often a health check, a test fixture, or middleware — before the last include_router ran, and the result was cached on app.openapi_schema. The fix is to build the schema only after full router registration and never call app.openapi() during startup. If you genuinely need to invalidate, set app.openapi_schema = None and let the next call rebuild it.
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 operationId. It happens when two routers both define async def list_items and you generate IDs from function names. Set operation_id 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.
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 null. It comes from using 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 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.
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 responses. List every status code you can return — 401, 402, 409, 429 — with a model. Related failures around credentials specifically are worth studying in debugging 401 unauthorized API errors.
Leaking internal routes into the public document. The symptom is a curious customer poking at /internal/replay-events. Any route you do not want in the contract needs 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.
Verification
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.
uvicorn app.main:app --port "${PORT:-8000}" &
curl -s "http://localhost:${PORT:-8000}/openapi.json" \
| python -c 'import json,sys; d=json.load(sys.stdin); \
print(d["openapi"], d["info"]["title"], d["info"]["version"]); \
print(sorted(d["components"]["securitySchemes"])); \
print(sorted(o["operationId"] for p in d["paths"].values() for o in p.values()))'
You should see 3.1.0 Forecast API 1.4.0, then ['ApiKeyAuth'], then a list of clean camelCase identifiers. Anything named after a Python function is a route you forgot to label.
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 testing Python APIs with pytest.
# tests/test_openapi_contract.py
import os
import pytest
from openapi_spec_validator import validate
from app.main import create_app
@pytest.fixture(scope="module")
def schema() -> dict:
os.environ.setdefault("API_TITLE", "Forecast API")
return create_app().openapi()
def test_document_is_valid(schema: dict) -> None:
validate(schema)
assert schema["openapi"].startswith("3.1")
def test_operation_ids_are_unique_and_clean(schema: dict) -> None:
ids = [op["operationId"] for path in schema["paths"].values() for op in path.values()]
assert len(ids) == len(set(ids))
assert all("_" not in oid for oid in ids), "set operation_id explicitly"
def test_security_scheme_is_documented(schema: dict) -> None:
schemes = schema["components"]["securitySchemes"]
assert "ApiKeyAuth" in schemes
assert schemes["ApiKeyAuth"]["in"] == "header"
def test_billing_errors_are_documented(schema: dict) -> None:
post = schema["paths"]["/v1/forecasts"]["post"]
assert {"402", "429"} <= set(post["responses"])
Cost and Performance at Scale
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 app.openapi_schema 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 /openapi.json 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 calculating cost per API request, because at low volume it is genuinely a line item rather than a rounding error.
FAQ
Should I expose the interactive docs UI on a paid production API? 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.
How much does serving the spec cost at a million docs requests a month? 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.
Does changing an operation ID break existing customers?
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.
Do I need a separate spec per API version? 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.
What is the fastest documentation improvement I can ship this week? 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.
Related
- Customizing the FastAPI OpenAPI Schema — go deeper on rewriting the generated document, injecting extensions, and renaming schema components.
- ReDoc vs Swagger UI — pick the renderer that fits a paying developer audience, with load-time numbers for each.
- Setting Up FastAPI — the project structure and application factory this guide assumes you already have.
- Handling API Authentication in Python — implement the security scheme your spec now advertises.
- Versioning and Evolving Public APIs — what to do once a committed spec artifact tells you a change is breaking.