Skip to content

Webhook

Overview

The Webhook domain in comby is designed to manage webhooks, enabling systems to notify external services of events or changes in real time. Webhooks are a critical part of integration workflows, providing a mechanism for external systems to react to specific events by consuming data through HTTP endpoints. The Webhook aggregate models the configuration and state of a webhook, ensuring consistency and traceability through event sourcing.

Concept

What is a Webhook?

A Webhook is an HTTP callback that automatically sends event data to an external URL when a specific domain event occurs. Webhooks enable real-time integrations with external systems, third-party services, and custom applications without polling.

Key Characteristics:

  • Event-Driven: Triggered automatically when domain events occur
  • Dual-Scope: Can be tenant-scoped or workspace-scoped
  • Event Filtering: Subscribe to specific domain events (e.g., Customer.CustomerCreatedEvent)
  • Active/Inactive: Can be enabled or disabled without deletion
  • HTTP POST: Sends event data as JSON via HTTP POST
  • Test Capability: Manual testing endpoint to verify webhook configuration
Event Occurs → Webhook Middleware → HTTP POST → External URL

Architecture

Hierarchy

System
└── Tenant
    ├── Webhooks (Tenant-scoped)
    │   ├── DomainEvtIdentifier (event filter)
    │   ├── WebhookUrl (target URL)
    │   └── Active (on/off)
    └── Workspaces
        └── Webhooks (Workspace-scoped)
            ├── DomainEvtIdentifier
            ├── WebhookUrl
            └── Active

Webhook is connected to:

  • Tenant: Each webhook belongs to a tenant
  • Workspace: Webhooks can optionally be scoped to a workspace
  • All Domains: Webhooks can listen to events from any domain
  • Events: Webhooks are triggered by domain events

Domain Model

go
type Webhook struct {
    AggregateUuid string

    // References
    WorkspaceUuid string  // Optional - for workspace-scoped webhooks

    // Value Objects
    Active              bool
    DomainEvtIdentifier string  // e.g., "Customer.CustomerCreatedEvent"
    WebhookUrl          string  // Target URL for HTTP POST
}

Webhook Scopes

Tenant-Scoped Webhooks

Webhooks created at the tenant level receive events for all resources within the tenant:

Tenant Webhook:
├── Listens to: All events in tenant matching DomainEvtIdentifier
├── Receives: Events from all workspaces and tenant-level resources
└── Use Case: System-wide integrations, monitoring, logging

Workspace-Scoped Webhooks

Webhooks created within a workspace only receive events from that workspace:

Workspace Webhook:
├── Listens to: Events in workspace matching DomainEvtIdentifier
├── Receives: Only events from specific workspace
└── Use Case: Project-specific integrations, team notifications

Event Filtering

Webhooks filter events using the DomainEvtIdentifier format:

{Domain}.{EventName}

Examples:

  • Customer.CustomerCreatedEvent - Customer creation events
  • Invoice.InvoiceCreatedEvent - Invoice creation events
  • Identity.IdentityAddedGroupEvent - Group assignment events
  • Workspace.WorkspaceCreatedEvent - Workspace creation events

Matching Logic:

  • Webhook only triggers if event's domain identifier matches exactly
  • Webhook only triggers if event's tenant matches webhook's tenant
  • For workspace-scoped webhooks, event must be from that workspace

HTTP POST Format

When a webhook is triggered, it sends an HTTP POST request:

Request:

http
POST {WebhookUrl}
Content-Type: application/json
Accept: application/json

{
  "eventUuid": "event-uuid",
  "aggregateUuid": "aggregate-uuid",
  "tenantUuid": "tenant-uuid",
  "workspaceUuid": "workspace-uuid",
  "domain": "Customer",
  "domainEvtName": "CustomerCreatedEvent",
  "domainEvt": {
    // Event-specific data
    "name": "Acme Corp",
    "email": "contact@acme.com"
  },
  "createdAt": 1638360000000000000,
  "version": 1
}

Structure

The Webhook aggregate extends the BaseAggregate, leveraging comby's core capabilities to manage state, track changes, and persist events. The aggregate defines fields that represent the attributes of a webhook:

  • References: Reserved for potential relationships with other entities (not explicitly included in the default implementation).

  • Value Objects:

    • Active: A boolean indicating whether the webhook is active and capable of sending notifications.
    • EventDataIdentifier: The identifier of the event data that the webhook is configured to process.
    • WebhookUrl: The HTTP endpoint to which the webhook sends its notifications.

This structure allows the Webhook aggregate to model the configuration and status of webhooks effectively, ensuring compatibility with a wide range of integration scenarios.

Use Cases

1. External System Integration

E-Commerce Platform Integration:
├── Webhook: Customer.CustomerCreatedEvent
│   └── URL: https://crm.example.com/api/customers
├── Webhook: Invoice.InvoiceCreatedEvent
│   └── URL: https://accounting.example.com/api/invoices
└── Webhook: Invoice.InvoiceCreatedEvent
    └── URL: https://email-service.example.com/api/send

2. Team Notifications

Slack/Discord Notifications:
├── Workspace A Webhook: Workspace.WorkspaceInvitationCreatedEvent
│   └── URL: https://hooks.slack.com/services/workspace-a
├── Workspace B Webhook: Workspace.WorkspaceInvitationCreatedEvent
│   └── URL: https://hooks.slack.com/services/workspace-b
└── Tenant Webhook: Identity.IdentityCreatedEvent
    └── URL: https://hooks.slack.com/services/admin-channel

3. Audit & Compliance Logging

Audit System:
├── Webhook: *.* (All events)
│   └── URL: https://audit-system.example.com/api/events
├── Webhook: Identity.IdentityAddedGroupEvent
│   └── URL: https://compliance.example.com/api/permission-changes
└── Webhook: Tenant.TenantRemovedEvent
    └── URL: https://compliance.example.com/api/data-deletion

4. Real-Time Analytics

Analytics Platform:
├── Webhook: Customer.CustomerCreatedEvent
│   └── URL: https://analytics.example.com/api/track/customer-created
├── Webhook: Invoice.InvoiceCreatedEvent
│   └── URL: https://analytics.example.com/api/track/invoice-created
└── Webhook: Asset.AssetUploadedEvent
    └── URL: https://analytics.example.com/api/track/file-upload

5. Multi-Environment Sync

Environment Synchronization:
├── Production Webhook: Customer.CustomerCreatedEvent
│   └── URL: https://staging.example.com/api/sync/customers
└── Production Webhook: Tenant.TenantCreatedEvent
    └── URL: https://test.example.com/api/sync/tenants

Features

  • Event-driven HTTP notifications
  • Tenant-scoped and workspace-scoped webhooks
  • Event filtering by domain identifier
  • Enable/disable without deletion
  • Manual webhook testing
  • Custom attributes for metadata
  • Automatic retry on failure (via middleware)
  • JSON payload format
  • Real-time event delivery

Best Practices

Webhook URLs

  • Use HTTPS for security
  • Implement authentication on receiving endpoint
  • Use descriptive, versioned URLs (e.g., /api/v1/webhooks/customer-created)
  • Set up dedicated endpoints for different event types

Event Filtering

  • Create specific webhooks for each event type (avoid wildcards)
  • Use descriptive attributes to document webhook purpose
  • Keep workspace webhooks focused on workspace-specific events
  • Use tenant webhooks for system-wide monitoring

Error Handling

  • Implement idempotency on receiving endpoint (events may be sent multiple times)
  • Return 2xx status codes quickly to avoid timeouts
  • Log all webhook deliveries for debugging
  • Implement retry logic on receiving side

Security

  • Validate webhook signatures (if implemented)
  • Use HTTPS endpoints only
  • Implement rate limiting on receiving endpoint
  • Store webhook URLs securely

Performance

  • Process webhook payloads asynchronously
  • Respond quickly to webhook requests (< 5 seconds)
  • Disable webhooks that consistently fail
  • Use attributes to track webhook health metrics

Testing

  • Use test endpoint before activating webhooks
  • Monitor webhook delivery logs
  • Test error scenarios (timeouts, 5xx responses)
  • Validate payload format matches expectations

Webhook Middleware

The webhook domain includes middleware that intercepts all commands:

How it Works:

  1. Command executes and generates events
  2. Webhook middleware intercepts events
  3. For each event, middleware checks all active webhooks
  4. If webhook's DomainEvtIdentifier matches event, HTTP POST is sent
  5. Tenant UUID and workspace UUID are validated

Example Flow:

Command: CustomerCommandCreate

Events: [CustomerCreatedEvent]

Webhook Middleware

Active Webhooks: [Webhook1: Customer.CustomerCreatedEvent]

Match Found → HTTP POST to Webhook1.WebhookUrl

Troubleshooting

Webhook not triggering

Check:

  1. Is the webhook active? (Active = true)
  2. Does DomainEvtIdentifier match exactly?
  3. Does event's tenant match webhook's tenant?
  4. For workspace webhooks, does event's workspace match?

Debug:

go
// Check webhook configuration
webhook, _ := GetWebhookReadmodel().GetModel("webhook-uuid")
fmt.Printf("Active: %v\n", webhook.Active)
fmt.Printf("DomainEvtIdentifier: %s\n", webhook.DomainEvtIdentifier)
fmt.Printf("WebhookUrl: %s\n", webhook.WebhookUrl)

// Check if events are being generated
// Look for middleware logs

Events not reaching endpoint

Possible causes:

  1. Endpoint is unreachable (DNS, network)
  2. Endpoint returns non-2xx status code
  3. Request times out
  4. Firewall blocking requests

Solution:

  • Use webhook test endpoint to verify connectivity
  • Check endpoint logs for incoming requests
  • Verify URL is correct and publicly accessible
  • Test with tools like webhook.site or requestbin.com

Duplicate webhook deliveries

Cause: Event sourcing and middleware architecture may send same event multiple times.

Solution:

  • Implement idempotency on receiving endpoint
  • Use event UUID to deduplicate
  • Store processed event UUIDs to prevent reprocessing

Wrong events being sent

Cause: DomainEvtIdentifier doesn't match expected pattern.

Solution:

  • Verify exact event name from domain (e.g., Customer.CustomerCreatedEvent)
  • Check capitalization and spelling
  • List available domain events using Runtime domain queries

Performance issues

Cause: Webhook endpoint is slow or blocking.

Solution:

  • Respond with 200 OK immediately, process asynchronously
  • Implement timeout on receiving side (< 5 seconds)
  • Use message queue on receiving side for processing
  • Consider disabling slow webhooks

Security Considerations

Endpoint Security

  • Only use HTTPS endpoints for webhooks
  • Implement authentication on receiving endpoint
  • Validate webhook signatures (if implemented)
  • Rate limit incoming webhook requests

Data Privacy

  • Webhooks contain full event data, including sensitive information
  • Only send webhooks to trusted endpoints
  • Consider filtering sensitive fields before sending
  • Implement data retention policies on receiving side

Access Control

  • Limit who can create/modify webhooks (use group permissions)
  • Audit webhook creation and modifications
  • Review webhook URLs regularly
  • Delete unused webhooks promptly

Network Security

  • Whitelist outbound webhook IPs if possible
  • Monitor webhook delivery failures
  • Implement circuit breakers for failing webhooks
  • Log all webhook attempts for audit

Integration Examples

Slack Integration

go
// Create webhook for Slack notifications
cmd, _ := comby.NewCommand("Webhook", &command.WebhookCommandAdd{
    WebhookUuid:         uuid.New().String(),
    DomainEvtIdentifier: "Customer.CustomerCreatedEvent",
    WebhookUrl:          "https://hooks.slack.com/services/YOUR/WEBHOOK/URL",
    Attributes:          "integration:slack,channel:#sales",
})
facade.DispatchCommand(ctx, cmd)

Receiving Endpoint (Slack Webhook): Slack expects a specific format. You may need an adapter service:

json
{
  "text": "New customer created: {{event.domainEvt.name}}"
}

Custom Integration

go
// Create webhook for custom application
cmd, _ := comby.NewCommand("Webhook", &command.WebhookCommandAdd{
    WebhookUuid:         uuid.New().String(),
    DomainEvtIdentifier: "Invoice.InvoiceCreatedEvent",
    WebhookUrl:          "https://myapp.example.com/api/webhooks/invoice",
    Attributes:          "integration:custom-app,environment:production",
})
facade.DispatchCommand(ctx, cmd)

Receiving Endpoint:

go
func WebhookHandler(w http.ResponseWriter, r *http.Request) {
    // Parse webhook payload
    var event comby.Event
    json.NewDecoder(r.Body).Decode(&event)

    // Verify tenant matches expected tenant
    if event.TenantUuid != expectedTenant {
        w.WriteHeader(http.StatusForbidden)
        return
    }

    // Process event asynchronously
    go processEvent(event)

    // Respond immediately
    w.WriteHeader(http.StatusOK)
    w.Write([]byte(`{"status":"received"}`))
}

Commands

WebhookCommandAdd

Domain Command Struct:

go
type WebhookCommandAdd struct {
	WebhookUuid         string `json:"webhookUuid"`
	DomainEvtIdentifier string `json:"domainEvtIdentifier"`
	WebhookUrl          string `json:"webhookUrl"`
	WorkspaceUuid       string `json:"workspaceUuid,omitempty"` // Optional workspace UUID for workspace-scoped webhooks
	Attributes          string `json:"attributes,omitempty"`
}

Domain Command Handling Method:

go
func (cs *commandHandler) WebhookCommandAdd(ctx context.Context, cmd comby.Command, domainCmd *WebhookCommandAdd) ([]comby.Event, error)

WebhookCommandRemove

Domain Command Struct:

go
type WebhookCommandRemove struct {
	WebhookUuid string `json:"webhookUuid"`
}

Domain Command Handling Method:

go
func (cs *commandHandler) WebhookCommandRemove(ctx context.Context, cmd comby.Command, domainCmd *WebhookCommandRemove) ([]comby.Event, error)

WebhookCommandRemoveAttribute

Domain Command Struct:

go
type WebhookCommandRemoveAttribute struct {
	WebhookUuid string `json:"webhookUuid"`
	Key         string `json:"key"`
}

Domain Command Handling Method:

go
func (cs *commandHandler) WebhookCommandRemoveAttribute(ctx context.Context, cmd comby.Command, domainCmd *WebhookCommandRemoveAttribute) ([]comby.Event, error)

WebhookCommandSetAttribute

Domain Command Struct:

go
type WebhookCommandSetAttribute struct {
	WebhookUuid string `json:"webhookUuid"`
	Key         string `json:"key"`
	Value       any    `json:"value"`
}

Domain Command Handling Method:

go
func (cs *commandHandler) WebhookCommandSetAttribute(ctx context.Context, cmd comby.Command, domainCmd *WebhookCommandSetAttribute) ([]comby.Event, error)

WebhookCommandUpdate

Domain Command Struct:

go
type WebhookCommandUpdate struct {
	WebhookUuid         string   `json:"webhookUuid"`
	Active              bool     `json:"active,omitempty"`
	DomainEvtIdentifier string   `json:"domainEvtIdentifier,omitempty"`
	WebhookUrl          string   `json:"webhookUrl,omitempty"`
	Attributes          string   `json:"attributes,omitempty"`
	PatchedFields       []string `json:"patchedFields"`
}

Domain Command Handling Method:

go
func (cs *commandHandler) WebhookCommandUpdate(ctx context.Context, cmd comby.Command, domainCmd *WebhookCommandUpdate) ([]comby.Event, error)

Queries

Domain Query Structs:

Domain Query Responses:

WebhookQueryList

Domain Query Struct:

go
type WebhookQueryList struct {
	TenantUuid     string `json:"tenantUuid"`
	Page           int64  `json:"page,omitempty"`
	PageSize       int64  `json:"pageSize,omitempty"`
	OrderBy        string `json:"orderBy,omitempty"`
	Attributes     string `json:"attributes,omitempty"`
	IncludeHistory bool   `json:"includeHistory,omitempty"`
}

Domain Query Handling Method:

go
func (qs *queryService) WebhookQueryList(ctx context.Context, qry comby.Query, domainQry *WebhookQueryList) (*WebhookQueryListResponse, error)

WebhookQueryModel

Domain Query Struct:

go
type WebhookQueryModel struct {
	WebhookUuid    string `json:"webhookUuid"`
	IncludeHistory bool   `json:"includeHistory,omitempty"`
}

Domain Query Handling Method:

go
func (qs *queryService) WebhookQueryModel(ctx context.Context, qry comby.Query, domainQry *WebhookQueryModel) (*WebhookQueryItemResponse, error)

WebhookQueryListResponse

go
type WebhookQueryListResponse struct {
	Items    []*readmodel.WebhookModel `json:"items,omitempty"`
	Total    int64                     `json:"total,omitempty"`
	Page     int64                     `json:"page,omitempty"`
	PageSize int64                     `json:"pageSize,omitempty"`
}

WebhookQueryItemResponse

go
type WebhookQueryItemResponse struct {
	Item *readmodel.WebhookModel `json:"item,omitempty"`
}

Events

WebhookAddedEvent

Domain Event Struct:

go
type WebhookAddedEvent struct {
	DomainEvtIdentifier string `json:"domainEvtIdentifier"`
	WebhookUrl          string `json:"webhookUrl"`
	WorkspaceUuid       string `json:"workspaceUuid,omitempty"` // Optional workspace UUID for workspace-scoped webhooks
	Attributes          string `json:"attributes,omitempty"`
}

Domain Event Handling Method:

go
func (agg *Webhook) WebhookAddedEvent(ctx context.Context, evt comby.Event, domainEvt *WebhookAddedEvent) (error)

WebhookRemovedEvent

Domain Event Struct:

go
type WebhookRemovedEvent struct {
	Reason string `json:"reason,omitempty"`
}

Domain Event Handling Method:

go
func (agg *Webhook) WebhookRemovedEvent(ctx context.Context, evt comby.Event, domainEvt *WebhookRemovedEvent) (error)

WebhookAttributeRemovedEvent

Domain Event Struct:

go
type WebhookAttributeRemovedEvent struct {
	Key string `json:"key"`
}

Domain Event Handling Method:

go
func (agg *Webhook) WebhookAttributeRemovedEvent(ctx context.Context, evt comby.Event, domainEvt *WebhookAttributeRemovedEvent) (error)

WebhookAttributeSetEvent

Domain Event Struct:

go
type WebhookAttributeSetEvent struct {
	Key   string `json:"key"`
	Value any    `json:"value"`
}

Domain Event Handling Method:

go
func (agg *Webhook) WebhookAttributeSetEvent(ctx context.Context, evt comby.Event, domainEvt *WebhookAttributeSetEvent) (error)

WebhookUpdatedEvent

Domain Event Struct:

go
type WebhookUpdatedEvent struct {
	Active              bool   `json:"active"`
	DomainEvtIdentifier string `json:"domainEvtIdentifier"`
	WebhookUrl          string `json:"webhookUrl"`
	Attributes          string `json:"attributes,omitempty"`
}

Domain Event Handling Method:

go
func (agg *Webhook) WebhookUpdatedEvent(ctx context.Context, evt comby.Event, domainEvt *WebhookUpdatedEvent) (error)

Aggregate

Aggregate Struct:

go
type Webhook struct {
	*comby.BaseAggregate
	// WorkspaceUuid - when set, this webhook is for a specific workspace
	WorkspaceUuid string
	// Value Objects
	Active              bool
	DomainEvtIdentifier string
	WebhookUrl          string
}

Methods

Add

go
func (agg *Webhook) Add(opts ) (error)

Remove

go
func (agg *Webhook) Remove(opts ) (error)

RemoveAttribute

go
func (agg *Webhook) RemoveAttribute(opts ) (error)

SetAttribute

go
func (agg *Webhook) SetAttribute(opts ) (error)

Update

go
func (agg *Webhook) Update(opts ) (error)

Event Handlers

WebhookReadmodel

Domain EventMethod
workspaceAggregate.WorkspaceCreatedEventWorkspaceCreatedEvent
workspaceAggregate.WorkspaceRemovedEventWorkspaceRemovedEvent
workspaceAggregate.WorkspaceUpdatedEventWorkspaceUpdatedEvent
aggregate.WebhookAddedEventWebhookAddedEvent
aggregate.WebhookUpdatedEventWebhookUpdatedEvent
aggregate.WebhookRemovedEventWebhookRemovedEvent
aggregate.WebhookAttributeSetEventWebhookAttributeSetEvent
aggregate.WebhookAttributeRemovedEventWebhookAttributeRemovedEvent
tenantAggregate.TenantCreatedEventTenantCreatedEvent
tenantAggregate.TenantRemovedEventTenantRemovedEvent
tenantAggregate.TenantUpdatedEventTenantUpdatedEvent