Summary
Adopters writing their first DecisioningPlatform tend to run with the defaults: auto_emit_completion_webhooks=True (the framework default), no webhook_sender=, no webhook_supervisor=. That combination silently no-ops every completion webhook — buyers who registered push_notification_config.url get no notifications, and the seller has no signal that anything is wrong.
Compounding this: examples/hello_seller.py opts out via auto_emit_completion_webhooks=False rather than demonstrating the wired path, because there's no universal "default sender" — the existing WebhookSender requires a signing key, which a hello-world doesn't have. So new adopters see the canonical example with the buyer-facing webhook feature off and have to figure out from scratch how to turn it on correctly.
Background
Originally filed as item #19 in salesagent's SDK_FEEDBACK round 2 ("ship a DefaultWebhookSender derived from the supervisor"). After working through the existing surface, that ask isn't quite the right shape:
WebhookSender requires a private key (RFC 9421 JWK signing) or a bearer/HMAC secret. There's no universal default that's safe in production.
- A
DefaultWebhookSender that signs nothing would be a footgun.
- The
WebhookSender ↔ WebhookDeliverySupervisor relationship is well-designed once you know the pattern; the friction is for adopters who don't know it yet.
So the actionable shape is two smaller changes that together make the silent-no-op impossible.
Proposed changes
1. Boot-time fail-fast (or loud warning) on the silent-no-op combo
In adcp.decisioning.serve.serve() (or adcp.server.serve.serve() — wherever the auto-emit gate lives), when:
auto_emit_completion_webhooks=True (default), AND
webhook_sender is None, AND
webhook_supervisor is None
…raise AdcpError at boot with a structured message:
auto_emit_completion_webhooks=True but no webhook_sender or webhook_supervisor is wired. Buyers who register push_notification_config.url will not receive notifications. Either wire a sender (see WebhookSender.from_jwk / from_bearer_token / from_standard_webhooks_secret), or set auto_emit_completion_webhooks=False if you intentionally emit webhooks manually inside your handlers.
A loud WARNING log is acceptable for the soft-mode rollout; raise is the right end state. Mirrors the existing ADCP_DECISIONING_STRICT_VALIDATE_PLATFORM ratchet pattern.
2. Brighter hello_seller.py documentation of the opt-out
Today (examples/hello_seller.py):
```python
serve(HelloSeller(), name="hello-seller", auto_emit_completion_webhooks=False)
```
…with a brief docstring note. Adopters reading the canonical example may copy this verbatim into production, where auto_emit_completion_webhooks=False is rarely the right answer (most sellers want completion webhooks).
Suggestion: expand the inline comment to spell out the why of the opt-out and link to a docs/handler-authoring.md#webhooks section that walks through the three sender constructors with a worked example for each. Something like:
```python
Production sellers WANT webhooks (auto_emit_completion_webhooks=True
is the default). hello-seller opts out only because it has no
signing key — webhooks need either:
- WebhookSender.from_jwk(private_key=..., key_id=..., alg="ES256")
(RFC 9421, recommended)
- WebhookSender.from_bearer_token(token=...) (simplest; agreed shared secret)
- WebhookSender.from_standard_webhooks_secret(secret=...)
(compatible with standardwebhooks.com receivers)
See docs/handler-authoring.md#webhooks for the wiring recipe.
serve(HelloSeller(), name="hello-seller", auto_emit_completion_webhooks=False)
```
3. (Optional) Adopter-friendly variant of hello_seller.py
A second hello-world example (examples/hello_seller_with_webhooks.py) that wires WebhookSender.from_bearer_token against a fixture token + an InMemoryWebhookDeliverySupervisor. ~30 LOC, gives copy-paste-ready code for the dominant case.
Memory profile note
The existing WebhookSender reuses one httpx.AsyncClient per instance — good. But the framework doesn't appear to have a clear contract for per-tenant senders (different signing key per tenant). Multi-tenant adopters may construct one sender per tenant, and if those aren't cached the httpx.AsyncClient instances accumulate.
If multi-tenant signing-key isolation is an expected use case, a MultiTenantWebhookSender or WebhookSenderFactory would help. Currently flagged in salesagent's slow-leak-investigation lens; not yet confirmed as a leak source there but worth a docs answer either way.
Context
Working through the salesagent greenfield rebuild on adcp 4.3 main. SDK_FEEDBACK round-2 doc (the source of items #16-#24 from salesagent's perspective) is at `core/SDK_FEEDBACK.md` in the salesagent repo if useful for triage; this issue captures the deferred #19 specifically.
Related shipped: #544 CallableSubdomainTenantRouter (round-2 #20), #545 BearerTokenAuthMiddleware header customization (round-2 #18). Both PR'd in the past hour.
🤖 Filed via Claude Code
Summary
Adopters writing their first
DecisioningPlatformtend to run with the defaults:auto_emit_completion_webhooks=True(the framework default), nowebhook_sender=, nowebhook_supervisor=. That combination silently no-ops every completion webhook — buyers who registeredpush_notification_config.urlget no notifications, and the seller has no signal that anything is wrong.Compounding this:
examples/hello_seller.pyopts out viaauto_emit_completion_webhooks=Falserather than demonstrating the wired path, because there's no universal "default sender" — the existingWebhookSenderrequires a signing key, which a hello-world doesn't have. So new adopters see the canonical example with the buyer-facing webhook feature off and have to figure out from scratch how to turn it on correctly.Background
Originally filed as item #19 in salesagent's SDK_FEEDBACK round 2 ("ship a
DefaultWebhookSenderderived from the supervisor"). After working through the existing surface, that ask isn't quite the right shape:WebhookSenderrequires a private key (RFC 9421 JWK signing) or a bearer/HMAC secret. There's no universal default that's safe in production.DefaultWebhookSenderthat signs nothing would be a footgun.WebhookSender↔WebhookDeliverySupervisorrelationship is well-designed once you know the pattern; the friction is for adopters who don't know it yet.So the actionable shape is two smaller changes that together make the silent-no-op impossible.
Proposed changes
1. Boot-time fail-fast (or loud warning) on the silent-no-op combo
In
adcp.decisioning.serve.serve()(oradcp.server.serve.serve()— wherever the auto-emit gate lives), when:auto_emit_completion_webhooks=True(default), ANDwebhook_sender is None, ANDwebhook_supervisor is None…raise
AdcpErrorat boot with a structured message:A loud
WARNINGlog is acceptable for the soft-mode rollout;raiseis the right end state. Mirrors the existingADCP_DECISIONING_STRICT_VALIDATE_PLATFORMratchet pattern.2. Brighter
hello_seller.pydocumentation of the opt-outToday (
examples/hello_seller.py):```python
serve(HelloSeller(), name="hello-seller", auto_emit_completion_webhooks=False)
```
…with a brief docstring note. Adopters reading the canonical example may copy this verbatim into production, where
auto_emit_completion_webhooks=Falseis rarely the right answer (most sellers want completion webhooks).Suggestion: expand the inline comment to spell out the why of the opt-out and link to a
docs/handler-authoring.md#webhookssection that walks through the three sender constructors with a worked example for each. Something like:```python
Production sellers WANT webhooks (auto_emit_completion_webhooks=True
is the default). hello-seller opts out only because it has no
signing key — webhooks need either:
- WebhookSender.from_jwk(private_key=..., key_id=..., alg="ES256")
(RFC 9421, recommended)
- WebhookSender.from_bearer_token(token=...) (simplest; agreed shared secret)
- WebhookSender.from_standard_webhooks_secret(secret=...)
(compatible with standardwebhooks.com receivers)
See docs/handler-authoring.md#webhooks for the wiring recipe.
serve(HelloSeller(), name="hello-seller", auto_emit_completion_webhooks=False)
```
3. (Optional) Adopter-friendly variant of
hello_seller.pyA second hello-world example (
examples/hello_seller_with_webhooks.py) that wiresWebhookSender.from_bearer_tokenagainst a fixture token + anInMemoryWebhookDeliverySupervisor. ~30 LOC, gives copy-paste-ready code for the dominant case.Memory profile note
The existing
WebhookSenderreuses onehttpx.AsyncClientper instance — good. But the framework doesn't appear to have a clear contract for per-tenant senders (different signing key per tenant). Multi-tenant adopters may construct one sender per tenant, and if those aren't cached thehttpx.AsyncClientinstances accumulate.If multi-tenant signing-key isolation is an expected use case, a
MultiTenantWebhookSenderorWebhookSenderFactorywould help. Currently flagged in salesagent's slow-leak-investigation lens; not yet confirmed as a leak source there but worth a docs answer either way.Context
Working through the salesagent greenfield rebuild on adcp 4.3 main. SDK_FEEDBACK round-2 doc (the source of items #16-#24 from salesagent's perspective) is at `core/SDK_FEEDBACK.md` in the salesagent repo if useful for triage; this issue captures the deferred #19 specifically.
Related shipped: #544
CallableSubdomainTenantRouter(round-2 #20), #545BearerTokenAuthMiddlewareheader customization (round-2 #18). Both PR'd in the past hour.🤖 Filed via Claude Code