# Tags help organize operations into logical groups
tags:
  - name: burger
  - name: order
```

The most straightforward way to add tags is to edit each operation and add a list of tags. This example highlights the tags list:

```python
from fastapi import FastAPI

app = FastAPI()

@app.get(
    "/burger/{burger_id}",
    tags=["burger"],
)
def read_burger(burger_id: int):
    return {
        "burger_id": burger_id,
    }
```

### Tag metadata

You can add metadata to your tags to further improve the developer experience.

FastAPI accepts a parameter called `openapi_tags`, which we can use to add metadata, such as a description and a list of external documentation links.

Here's how to add metadata to tags:

```python
from fastapi import FastAPI

tags_metadata = [
    {
        "name": "burger",
        "description": "Operations related to burgers",
        "externalDocs": {
            "description": "Burger external docs",
            "url": "https://en.wikipedia.org/wiki/Hamburger",
        },
    },
    {
        "name": "order",
        "description": "Operations related to orders",
    },
]

app = FastAPI(
    openapi_tags=tags_metadata,
)

@app.get(
    "/burger/{burger_id}",
    tags=["burger"],
)
def read_burger(burger_id: int):
    return {
        "burger_id": burger_id,
    }
```

When we add metadata to tags, FastAPI adds a top-level `tags` section to our OpenAPI document:

```yaml
tags:
  - description: Operations related to burgers
    externalDocs:
      description: Burger external docs
      url: https://en.wikipedia.org/wiki/Hamburger
    name: burger
  - description: Operations related to orders
    name: order
```

Each tagged path in our OpenAPI document also gets a list of tags:

```yaml
paths:
  /burger/{burger_id}:
    get:
      description: Read a burger
      operationId: readBurger
      summary: Read Burger
      tags:
        - burger
      # ...
```

### Operation ID customization

When FastAPI outputs an OpenAPI document, it generates a unique OpenAPI `operationId` for each path. By default, this unique ID is generated by the FastAPI `generate_unique_id` function:

```python
def generate_unique_id(route: "APIRoute") -> str:
    operation_id = route.name + route.path_format
    operation_id = re.sub(r"\W", "_", operation_id)
    assert route.methods
    operation_id = operation_id + "_" + list(route.methods)[0].lower()
    return operation_id
```

This can often lead to cumbersome and unintuitive names. To improve usability, we have two methods of customizing these generated strings:

1. Using a custom `generate_unique_id_function`
2. Specifying `operation_id` per operation

The preferred method is to use a custom function when you generate unique IDs for paths.

The example below is an illustrative function that doesn't generate guaranteed-unique IDs and doesn't handle method names without an underscore. However, it demonstrates how you can add a function that generates IDs based on an operation's method name:

```python
from fastapi import FastAPI

def convert_snake_case_to_camel_case(string: str) -> str:
    """Convert snake case to camel case"""

    words = string.split("_")
    return words[0] + "".join(word.title() for word in words[1:])


def custom_generate_unique_id_function(route: APIRoute) -> str:
    """Custom function to generate unique id for each endpoint"""

    return convert_snake_case_to_camel_case(route.name)


app = FastAPI(
    generate_unique_id_function=custom_generate_unique_id_function,
)
```

With FastAPI, you can also specify the `operationId` per operation. For our example, we'll add a new parameter called `operation_id` to the operation decorator:

```python
from fastapi import FastAPI

app = FastAPI()

@app.get(
    "/burger/{burger_id}",
    operation_id="readBurger",
)
def read_burger(burger_id: int):
    pass
```

### Webhooks

Starting with OpenAPI version 3.1.0, it is possible to specify webhooks for your application in OpenAPI.

Here's how to add a webhook to FastAPI:

```python
from fastapi import FastAPI

app = FastAPI()


class Order(BaseModel):
    id: int
    note: str


@app.webhooks.post(
    "order-status-changed",
    operation_id="webhookOrderStatusChanged",
)
def webhook_order_status_changed(body: Order):
    """
    When an order status is changed, this webhook will be triggered.

    The server will send a `POST` request with the order details to the webhook URL.
    """
    pass
```

FastAPI generates the following top-level `webhooks` section in `openapi.yaml`:

```yaml
webhooks:
  order-status-changed:
    post:
      description:
        "When an order status is changed, this webhook will be triggered.


        The server will send a `POST` request with the order details to the webhook
        URL."
      operationId: webhookOrderStatusChanged
      requestBody:
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/Order"
        required: true
      responses:
        "200":
          content:
            application/json:
              schema: {}
          description: Successful Response
        "422":
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HTTPValidationError"
          description: Validation Error
      summary: Webhook Order Status Changed
```

## Speakeasy integration

Now that we have a customized OpenAPI document, we can use Speakeasy to generate SDKs based on it. You can generate an SDK by running the `gen.sh` bash script, which runs the `speakeasy generate` command. Alternatively, you can use the Speakeasy quick start command for a guided SDK setup:

```bash filename="Terminal"
speakeasy quickstart
```

Following the prompts, provide the OpenAPI document location, name your SDK, and select the SDK language.
In the terminal, you'll see the steps taken by Speakeasy to create the SDK:

```
│   └─Source: APItizing Burgers API - success
│     └─Validating Document - success
│     └─Diagnosing OpenAPI - success
│     └─Tracking OpenAPI Changes - success
│       └─Snapshotting OpenAPI Revision - success
│       └─Storing OpenAPI Revision - success
│   └─Validating gen.yaml - success
│   └─Generating Python SDK - success
│     └─Setup Environment - success
│     └─Load and Validate Document - success
│     └─Generate SDK - success
│     └─Compile SDK - success
│     └─Setup Environment - success
│     └─Load and Validate Document - success
│     └─Generate SDK - success
│   └─Generating Code Samples - success
│     └─Snapshotting Code Samples - success
│       └─Snapshotting Code Samples - success
│       └─Uploading Code Samples - success
```

Speakeasy [validates](/docs/concepts#validation) the OpenAPI document to check that it's ready for code generation. Validation issues will be printed in the terminal and the generated SDK will be saved as a folder in your project.
Speakeasy also suggests improvements for your SDK using [Speakeasy Suggest](/docs/prep-openapi/maintenance), which is an AI-powered tool in Speakeasy Studio. You can see suggestions by opening the link to your Speakeasy Studio workspace in the terminal.

Let's take a look at how the information we detailed in the OpenAPI document affects how Speakeasy generates SDKs.

Adding the local server information leads to the following generated output in the `openapi.yaml` file:

```yaml