Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,45 @@ reader_checkout = client.readers.create_checkout(
print(f"Reader checkout created: {reader_checkout}")
```

### Handling Events

Create a handler with your event signing secret and register typed callbacks:

```python
import os

from sumup import Sumup
from sumup.events import EventNotification, ReaderCreatedEvent

client = Sumup(api_key=os.environ["SUMUP_API_KEY"])


def fallback(event: EventNotification) -> None:
print(f"Received {event.type}")


events = client.events_handler(os.environ["SUMUP_EVENT_SECRET"], fallback)


@events.on_reader_created
def reader_created(event: ReaderCreatedEvent) -> None:
reader = event.fetch_object()
print(f"Reader paired: {reader.id}")


# In your HTTP route, pass the unchanged body and the complete
# X-SumUp-Webhook-Signature header value:
events.handle(raw_body, signature)
```

You can also register an existing function with `events.on_reader_created(callback)`.

Send a 2xx response after handling succeeds. Reject `EventSignatureError` and `EventPayloadError` with 400; return 500 for `EventCallbackError` so processing can be retried. Make callbacks idempotent and configure body limits in your server.

Use `AsyncSumup` with async callbacks, `await events.handle(...)`, and `await event.fetch_object_async()` for async servers. For parsing without callbacks, use `client.parse_event_notification(body, signature, secret)`.

See the runnable [Flask](examples/events-flask/) and [FastAPI](examples/events-fastapi/) examples for complete HTTP routes and error handling.

## Version Support Policy

`sumup-py` maintains compatibility with Python versions that have not passed end-of-life. As of June 8, 2026, that means Python 3.10 through 3.14. See [Status of Python versions](https://devguide.python.org/versions/).
Expand Down
9 changes: 9 additions & 0 deletions codegen/pkg/builder/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ type Builder struct {

pathsByTag map[string]*v3.Paths

events []EventDefinition

templates *template.Template

start time.Time
Expand Down Expand Up @@ -82,6 +84,9 @@ func (b *Builder) Load(spec *v3.Document) error {

b.collectPaths()
b.collectSchemas()
if err := b.collectEvents(); err != nil {
return err
}

return nil
}
Expand All @@ -101,6 +106,10 @@ func (b *Builder) Build() error {
return err
}

if err := b.writeEventsFile(path.Join(b.cfg.Out, "events.py")); err != nil {
return err
}

for tagName, paths := range b.pathsByTag {
if err := b.generateResource(tagName, paths); err != nil {
return err
Expand Down
114 changes: 114 additions & 0 deletions codegen/pkg/builder/events.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package builder

import (
"bytes"
"fmt"
"slices"
"strings"

"github.com/iancoleman/strcase"

"github.com/sumup/sumup-py/codegen/pkg/extension"
)

type eventObjectExtension struct {
Reference string `yaml:"$ref"`
}

func (b *Builder) collectEvents() error {
if b.spec == nil || b.spec.Webhooks == nil {
return nil
}

events := make([]EventDefinition, 0, b.spec.Webhooks.Len())
for eventType, pathItem := range b.spec.Webhooks.FromOldest() {
if pathItem == nil || pathItem.Post == nil {
continue
}

operation := pathItem.Post
name := strings.TrimSuffix(operation.OperationId, "Webhook")
if name == "" {
return fmt.Errorf("webhook %q is missing an operationId", eventType)
}

if operation.Extensions == nil {
return fmt.Errorf("webhook %q is missing x-object", eventType)
}
object, ok := extension.Get[eventObjectExtension](operation.Extensions, "x-object")
if !ok || object.Reference == "" {
return fmt.Errorf("webhook %q is missing x-object", eventType)
}

const schemaPrefix = "#/components/schemas/"
objectSchema, ok := strings.CutPrefix(object.Reference, schemaPrefix)
if !ok || objectSchema == "" {
return fmt.Errorf(
"webhook %q has unsupported x-object reference %q",
eventType,
object.Reference,
)
}

objectKind, ok := extension.Get[string](operation.Extensions, "x-object-type")
if !ok || strings.TrimSpace(objectKind) == "" {
return fmt.Errorf("event %q is missing x-object-type", eventType)
}

description := strings.TrimSpace(operation.Description)
if description == "" {
description = strings.TrimSpace(operation.Summary)
}

events = append(events, EventDefinition{
RegistrationMethod: "on_" + strcase.ToSnake(name),
ObjectKind: objectKind,
ClassName: strcase.ToCamel(name) + "Event",
EventType: eventType,
ObjectType: strcase.ToCamel(objectSchema),
Description: description,
})
}

slices.SortFunc(events, func(a, b EventDefinition) int {
return strings.Compare(a.ClassName, b.ClassName)
})
b.events = events
return nil
}

type eventsTemplateData struct {
Events []EventDefinition
ObjectTypes []string
}

func (b *Builder) writeEventsFile(filename string) error {
objectTypes := make([]string, 0, len(b.events))
for _, event := range b.events {
if !slices.Contains(objectTypes, event.ObjectType) {
objectTypes = append(objectTypes, event.ObjectType)
}
}
slices.Sort(objectTypes)

buf := bytes.NewBuffer(nil)
if err := b.templates.ExecuteTemplate(buf, "events.py.tmpl", eventsTemplateData{
Events: b.events,
ObjectTypes: objectTypes,
}); err != nil {
return fmt.Errorf("generate events: %w", err)
}

file, err := openGeneratedFile(filename)
if err != nil {
return err
}
defer func() {
_ = file.Close()
}()

if _, err := file.Write(buf.Bytes()); err != nil {
return fmt.Errorf("write events: %w", err)
}
return nil
}
76 changes: 76 additions & 0 deletions codegen/pkg/builder/events_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package builder

import (
"reflect"
"testing"

"github.com/pb33f/libopenapi"
)

func TestCollectEvents(t *testing.T) {
document, err := libopenapi.NewDocument([]byte(`{
"openapi": "3.1.0",
"info": {"title": "Events", "version": "1.0.0"},
"paths": {},
"components": {
"schemas": {
"Member": {"type": "object"},
"Reader": {"type": "object"}
}
},
"webhooks": {
"readers.created": {
"post": {
"operationId": "ReaderCreatedWebhook",
"description": "Sent when a reader is paired.",
"responses": {"2XX": {"description": "Acknowledged"}},
"x-object": {"$ref": "#/components/schemas/Reader"},
"x-object-type": "reader"
}
},
"members.updated": {
"post": {
"operationId": "MemberUpdatedWebhook",
"description": "Sent when a member changes.",
"responses": {"2XX": {"description": "Acknowledged"}},
"x-object": {"$ref": "#/components/schemas/Member"},
"x-object-type": "member"
}
}
}
}`))
if err != nil {
t.Fatalf("load document: %v", err)
}
model, err := document.BuildV3Model()
if err != nil {
t.Fatalf("build model: %v", err)
}

builder := New(Config{})
if err := builder.Load(&model.Model); err != nil {
t.Fatalf("load builder: %v", err)
}

want := []EventDefinition{
{
RegistrationMethod: "on_member_updated",
ClassName: "MemberUpdatedEvent",
ObjectKind: "member",
EventType: "members.updated",
ObjectType: "Member",
Description: "Sent when a member changes.",
},
{
RegistrationMethod: "on_reader_created",
ClassName: "ReaderCreatedEvent",
ObjectKind: "reader",
EventType: "readers.created",
ObjectType: "Reader",
Description: "Sent when a reader is paired.",
},
}
if !reflect.DeepEqual(builder.events, want) {
t.Fatalf("events mismatch:\n got: %#v\nwant: %#v", builder.events, want)
}
}
11 changes: 11 additions & 0 deletions codegen/pkg/builder/intermediate_representation.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,17 @@ type OneOfDeclaration struct {
GenerateInput bool
}

// EventDefinition describes a typed event notification generated from an
// OpenAPI webhook operation.
type EventDefinition struct {
RegistrationMethod string
ObjectKind string
ClassName string
EventType string
ObjectType string
Description string
}

// Property holds the information for Property of a type.
type Property struct {
// Name of the field
Expand Down
Loading
Loading