Skip to content

Webhooks Module

SDK reference for building custom webhook adapters

The webhooks module provides base classes and a decorator for building custom webhook adapters in your workspace. Adapters handle subscription management, validation, and event normalization for services that need bespoke logic beyond the platform’s generic webhook receiver.

from bifrost.webhooks import (
WebhookAdapter, adapter,
WebhookRequest, SubscribeResult, RenewResult,
ValidationResponse, Deliver, Rejected,
)

Adapters are auto-discovered from workspace/adapters/*.py.

Class / FunctionPurpose
@adapter(name, integration)Decorator that registers a WebhookAdapter subclass
WebhookAdapter.subscribe()Create a subscription with an external service
WebhookAdapter.unsubscribe()Delete a subscription (best effort, no-op default)
WebhookAdapter.renew()Renew before expiration (returns None by default)
WebhookAdapter.handle_request()Validate and normalize an incoming webhook request
WebhookAdapter.verify_hmac_sha256()Helper: HMAC-SHA256 signature verification
WebhookAdapter.generate_secret()Helper: random hex secret
WebhookAdapter.expiration_datetime()Helper: ISO8601 expiration string
WebhookAdapter.parse_datetime()Helper: parse ISO8601 datetime

Decorator that registers a WebhookAdapter subclass for discovery.

@adapter(name: str | None = None, integration: str | None = None)
ParameterTypeDescription
namestr | NoneUnique adapter name. Defaults to the class name converted to snake_case (with _adapter suffix stripped).
integrationstr | NoneRequired integration name (e.g. "Microsoft"). The platform passes the resolved integration into subscribe/renew/unsubscribe.
from bifrost.webhooks import WebhookAdapter, adapter
@adapter(name="my_service", integration="MyService")
class MyServiceAdapter(WebhookAdapter):
display_name = "My Service"
description = "Webhooks from MyService API"

Abstract base class for adapters. Subclass and implement subscribe and handle_request; override the others as needed.

AttributeTypeDescription
namestrSet by @adapter decorator
display_namestrHuman-readable name for the UI
descriptionstrOne-line description
requires_integrationstr | NoneIntegration name to bind to (mirrors @adapter(integration=))
config_schemadictJSON Schema for adapter config
renewal_intervaltimedelta | NoneHow often to call renew() (None = no renewal)

Create a subscription with the external service. Called when a new event source is created.

async def subscribe(
self,
callback_url: str,
config: dict[str, Any],
integration: Any | None,
) -> SubscribeResult
ParameterTypeDescription
callback_urlstrFull URL the external service should POST to
configdictAdapter configuration the user provided
integrationAny | NoneOAuth integration instance (when requires_integration is set)

Returns a SubscribeResult with external_id, state, and expires_at.

Delete the subscription on the external service. Default: no-op. Best effort — does not raise on failure.

async def unsubscribe(
self,
external_id: str | None,
state: dict[str, Any],
integration: Any | None,
) -> None

Renew the subscription before expiration. Called periodically when renewal_interval is set. Return None to signal that renewal failed and the subscription should be recreated.

async def renew(
self,
external_id: str | None,
state: dict[str, Any],
integration: Any | None,
) -> RenewResult | None

Validate and parse an incoming webhook request. Must return one of ValidationResponse, Deliver, or Rejected.

async def handle_request(
self,
request: WebhookRequest,
config: dict[str, Any],
state: dict[str, Any],
) -> HandleResult
Return typeWhen to use
ValidationResponseThe service is doing a handshake/challenge (e.g. Microsoft Graph validation)
DeliverThe request is a real event — platform will store it and fan out to subscriptions
RejectedSignature mismatch, replay, or otherwise invalid; platform returns the given status code
@staticmethod
def generate_secret(length: int = 32) -> str

Random hex secret of length characters.

@staticmethod
def verify_hmac_sha256(
payload: bytes,
secret: str,
signature: str,
prefix: str = "sha256=",
) -> bool

Constant-time HMAC-SHA256 verification. Common pattern for GitHub, Stripe, etc.

@staticmethod
def expiration_datetime(days: int = 0, hours: int = 0, minutes: int = 0) -> str

ISO8601 string for “now + delta”, useful for subscription expirations.

@staticmethod
def parse_datetime(dt_string: str) -> datetime | None

Parse an ISO8601 string. Returns None on failure.

Incoming HTTP request data passed to handle_request.

FieldTypeDescription
methodstrHTTP method
headersdict[str, str]Lowercase header keys
query_paramsdict[str, str]Query string parameters
bodybytesRaw request body
source_ipstr | NoneClient IP
json_body (property)dict | NoneParsed JSON body, or None if invalid
text_body (property)strUTF-8 decoded body
FieldTypeDescription
external_idstr | NoneSubscription ID returned by the external service
statedict | NoneAdapter-managed state to persist (secrets, tokens)
expires_atdatetime | NoneWhen this subscription expires (drives renew())
FieldTypeDescription
expires_atdatetime | NoneNew expiration
statedict | NoneUpdated state (merged with existing)
ValidationResponse(status_code: int, body: str, content_type: str = "text/plain", headers: dict | None = None)
Deliver(data: dict, event_type: str | None = None, raw_headers: dict | None = None)
Rejected(message: str, status_code: int = 400)
from bifrost.webhooks import (
WebhookAdapter, adapter, Deliver, Rejected, ValidationResponse,
)
@adapter(name="github")
class GitHubAdapter(WebhookAdapter):
display_name = "GitHub Webhooks"
description = "Receive events from GitHub repositories"
config_schema = {
"type": "object",
"properties": {"secret": {"type": "string"}},
"required": ["secret"],
}
async def subscribe(self, callback_url, config, integration):
# GitHub webhooks are configured manually in the repo settings.
# We just store the secret in state for verification.
return SubscribeResult(state={"secret": config["secret"]})
async def handle_request(self, request, config, state):
signature = request.headers.get("x-hub-signature-256", "")
if not self.verify_hmac_sha256(request.body, state["secret"], signature):
return Rejected("Invalid signature", status_code=401)
payload = request.json_body or {}
return Deliver(
data=payload,
event_type=request.headers.get("x-github-event"),
)