Svix Blog
Published on

Generating OpenAPI Webhooks Definitions

Authors

Cover image

Svix is the enterprise ready webhooks sending service. With Svix, you can build a secure, reliable, and scalable webhook platform in minutes. Looking to send webhooks? Give it a try!

We are big fans of using OpenAPI specs for describing your webhooks and APIs. In this post we will show how easy it is to add webhooks to your OpenAPI spec, even if the framework you use doesn't support it directly.

For those unfamiliar, the OpenAPI specification is a formal standard for describing HTTP APIs. Some people write these by hand, and some automatically generate these from the web framework of their choice.

Webhooks have been supported in OpenAPI since version 3.1, and through custom extensions since all the way back to 3.0.3 (e.g. x-webhooks).

How it looks like in the spec

If you write your OpenAPI spec by hand, all you need to do is to add a webhooks section to your existing spec. Webhooks sections follow a similar pattern to the paths section, so you can configure them similarly.

For example, here is a webhooks section for a pet.new event that alerts us when a new pet is added to our system.

"webhooks": {
    "pet.new": {
        "post": {
            "summary": "New Pet",
            "description": "When a new pet is created",
            "operationId": "pet.new",
            "requestBody": {
                "content": {
                    "application/json": {
                        "schema": {
                            "$ref": "#/components/schemas/PetNewEvent"
                        }
                    }
                },
                "required": true
            },
            "responses": {
                "200": {
                    "description": "Successful Response",
                    "content": {
                        "application/json": {
                            "schema": {}
                        }
                    }
                }
            }
        }
    }
},

As you can see, we set the path itself to be pet.new to indicate the type of the event, we then set a human-facing name in summary, a human facing description in description and a unique operation ID in operationID. The requestBody is used for setting the schema of the webhook body, similarly to how you would do with any other path.

Here is a full spec for reference: https://gist.github.com/tasn/edebd544817a50f85685f7414ccf15ab

Automatic generation (with full framework support)

Some web frameworks, like FastAPI, have built-in support for generating the OpenAPI webhooks section. When using one of these frameworks generating webhooks sections is fairly simple.

Here is an example from FastAPI:

import typing as t
from datetime import datetime

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


T = t.TypeVar('T')

class BaseWebhookEvent(BaseModel, t.Generic[T]):
    type: str
    timestamp: datetime
    data: T


class Pet(BaseModel):
    name: str
    age: int


class PetNewEvent(BaseWebhookEvent[Pet]):
    type: str = "pet.new"


@app.get("/pet")
async def list_pets() -> t.List[Pet]:
    return [Pet(name="Pluto", age=5), Pet(name="Spot", age=3)]


@app.webhooks.post("pet.new")
def new_pet(_: PetNewEvent):
    """
    When a new pet is created
    """

This automatically generates more or less the OpenAPI spec shared above.

Automatic generation (without framework support)

Unfortunately not all web frameworks support generating OpenAPI specs. For those we need to take a hybrid solution to OpenAPI generation where the web framework does most of the heavy lifting, and we write some code to augment the OpenAPI spec appropriately.

The augmentation code can either be a post-processing script that parses the OpenAPI spec and generates a new one instead, or just augmenting our framework of choice's OpenAPI generation method. In the example below we'll choose the latter, but both options are valid.

As mentioned above, the webhooks section is essentially identical to the paths section of the OpenAPI spec. We are going to use that to our advantage. We are going to generate webhooks paths that look exactly like normal API paths.

We will again use FastAPI, but this time assume that the @api.webhooks set of decorators don't exist. Here is the example from the previous section adjusted to show as a normal path:

@app.post("pet.new")
def new_pet(_: PetNewEvent):
    """
    When a new pet is created
    """

We will now override the FastAPI OpenAPI generation function. This may vary depending on your framework of choice, but the idea should apply:

# Move fake webhooks paths to the `webhooks` section
# This is where the magic happens.
def create_webhooks(openapi_schema: t.Dict[str, t.Any]):
    WEBHOOKS_PREFIX = "webhooks."

    webhooks = {}
    for name in list(openapi_schema['paths'].keys()):
        if name.startswith(WEBHOOKS_PREFIX):
            typp_name = name[len(WEBHOOKS_PREFIX):]  # Strip the prefix
            webhooks[type_name] = openapi_schema['paths'].pop(name)

    openapi_schema['webhooks'] = webhooks

# Custom FastAPI OpenAPI generation
def custom_openapi():
    if app.openapi_schema:
        return app.openapi_schema
    openapi_schema = get_openapi(
        title="Pet Shop",
        version="0.1.0",
        routes=app.routes,
    )
    create_webhooks(openapi_schema)
    app.openapi_schema = openapi_schema
    return app.openapi_schema

# Override FastAPI's OpenAPI generator
app.openapi = custom_openapi

In this example we prefixed the webhook paths with webhooks. in order to be able to automatically find them. Other approaches would be to use a custom decorator, a custom route, or a static list, to name a few. Though the prefix is the easiest one for the sake of this example.

This example integrates directly with FastAPI's OpenAPI generation code. Though the same method can be used as a standalone script that enriches the OpenAPI spec JSON (or YAML) directly. Such a script would look something like this:

import json


def create_webhooks(openapi_schema: t.Dict[str, t.Any]):
    WEBHOOKS_PREFIX = "webhooks."

    webhooks = {}
    for name in list(openapi_schema['paths'].keys()):
        if name.startswith(WEBHOOKS_PREFIX):
            typp_name = name[len(WEBHOOKS_PREFIX):]  # Strip the prefix
            webhooks[type_name] = openapi_schema['paths'].pop(name)

    openapi_schema['webhooks'] = webhooks


openapi = json.load(open("openapi.json))
create_webhooks(openapi)
print(json.dumps(openapi))

Closing words

We are big fans of using OpenAPI specs for describing your webhooks and APIs here at Svix. The Svix platform supports importing and automatically creating event types directly from your OpenAPI spec if it contains a valid webhooks section.

The OpenAPI webhooks definition is a fairly recent addition to the spec, so it's not yet widely supported by all web frameworks. The above, however, should make it easy for everyone to start using the webhooks section today, without waiting for widespread framework support.

The same method can also be used to enrich your API with other extensions such as tag groups and code samples.


For more content like this, make sure to follow us on Twitter, Github, RSS, or our newsletter for the latest updates for the Svix webhook service, or join the discussion on our community Slack.