M2 adds a production-shaped, still-kernel-sized webhook seam for Dominatus.Actuators.Payments.Stripe.
On 2026-06-24, the implementation was checked against official Stripe docs:
- Stripe recommends securing webhook endpoints by verifying the event with the exact raw request body string, the
Stripe-Signatureheader, and the endpoint secret. - Endpoint secrets shown by Stripe/Stripe CLI use the
whsec_prefix. - The raw request body must be the UTF-8 body Stripe sent, without whitespace changes, JSON reserialization, key reordering, or framework body-parser mutation.
- Stripe documents HTTPS for public webhook endpoints and recommends quickly returning
2xxbefore long business work. - Stripe event types include
checkout.session.completed,checkout.session.expired,payment_intent.succeeded,payment_intent.payment_failed,payment_intent.canceled,payment_intent.amount_capturable_updated,charge.refunded,refund.created, andrefund.updated. - Stripe.net exposes official webhook signature helpers; M2 uses
EventUtility.ValidateSignaturebefore parsing and mapping.
StripeWebhookVerifier.VerifyAndNormalizeAsync(rawBody, stripeSignatureHeader, options, cancellationToken) returns a safe StripeWebhookProcessingResult instead of requiring endpoint code to handle provider exceptions directly.
Required inputs:
rawBody: exact raw Stripe request body string.stripeSignatureHeader: the fullStripe-Signatureheader value from the request.StripeWebhookOptions.EndpointSecret: the webhook endpoint secret. It is required and must not be logged.
Validation and failure behavior:
- missing endpoint secret, missing signature, or missing raw body returns
Verified = falsewith a sanitizedSafeError; - signature failures return
Verified = falseand do not include endpoint secrets, API keys, client secrets, raw payloads, or full signature values; - JSON parsing happens only after signature validation;
- unknown verified event types are not signature failures.
Raw provider event JSON is excluded by default. If IncludeRawProviderEventJson is explicitly true, RawProviderEventJson is capped by MaxRawEventJsonChars. Prefer leaving it off in production logs/storage.
The base payments package now exposes provider-neutral event ingestion primitives:
PaymentEventEnvelopePaymentNormalizedEventKindIPaymentEventDedupStoreInMemoryPaymentEventDedupStorePaymentWebhookIngestor
The envelope says: “Stripe says this verified payment event happened.” It intentionally does not say what a product, order, user, tenant, entitlement, or merchant should do.
| Stripe event type | Normalized kind | Important fields |
|---|---|---|
checkout.session.completed |
CheckoutSessionCompleted |
checkout session id, payment intent id when present, amount total/currency, session payment status, metadata |
checkout.session.expired |
CheckoutSessionExpired |
checkout session id, payment intent id when present, canceled payment status |
payment_intent.succeeded |
PaymentSucceeded |
payment intent id, amount/currency, succeeded status, metadata |
payment_intent.payment_failed |
PaymentFailed |
payment intent id, amount/currency, failed status, metadata |
payment_intent.canceled |
PaymentCanceled |
payment intent id, amount/currency, canceled status, metadata |
payment_intent.amount_capturable_updated |
PaymentAuthorized |
payment intent id, capturable amount if present, authorized status |
refund.created |
RefundCreated |
refund id, payment intent id when present, amount/currency, refund status |
refund.updated |
RefundSucceeded, RefundFailed, or RefundCreated |
conservative status-based refund mapping |
charge.refunded |
PaymentRefunded |
payment intent id when present, refunded amount/currency |
Unsupported verified events return an envelope with Kind = Unknown and IsSupportedEvent = false. They should be acknowledged safely by endpoint code and ignored or routed by the application layer as appropriate.
Refund mapping is conservative. M2 does not claim partial-vs-full refunds unless the event object safely carries enough information for the mapped shape.
Stripe can deliver duplicate webhook events. M2 provides an optional in-memory dedup store for deterministic tests and simple hosts:
- the dedup key is
providerId + providerEventId; - the first accepted event returns
Accepted; - duplicate deliveries return
Duplicate; PaymentWebhookIngestordoes not publish or invoke business reactions.
Production applications should replace InMemoryPaymentEventDedupStore with durable app-owned persistence if duplicate resistance must survive process restarts.
Current Dominatus.Core event infrastructure is AiEventBus, exposed as a per-agent bus on AiAgent.Events and routed by AiWorld.Mail to agent event buses. Its comments describe append-only per-type buckets, typed waiters, single-threaded publish/consume assumptions, and at-most-one-active-waiter-per-event-type assumptions. Replay support can inject external replay events into an agent, but the event bus itself is not a durable cross-domain payment event ledger.
Answers for M2:
- Dominatus has an event bus, but it is per-agent workflow/simulation infrastructure, not a general cross-application payment ingestion bus.
- Events publish immediately into the target agent bus; there is no global staged payment boundary.
- Existing persistence can replay selected runtime/replay events, but payment webhook ingestion is not automatically durable/auditable through Core persistence.
AiEventBusis best understood as internal agent/runtime coordination. External provider events may be bridged into it by an application, but the Stripe adapter should not choose an agent/world target.- A payment webhook can be deterministic once raw inputs are supplied, but injecting it directly into an
AiWorldfrom the provider package would couple external HTTP timing to simulation behavior and require app-owned routing decisions. - M2 returns normalized ingestion results and dedup primitives. Application layers decide whether and when to bridge accepted events into mailboxes, blackboards,
AiEventBus, durable storage, or business workflows. - Boundary: Stripe package verifies/maps; base payment package provides neutral event/dedup primitives; application owns endpoint hosting and explicit injection into any world/event bus.
Therefore M2 does not add automatic EventBus publication or a VerifyAndPublish API. This avoids bending Core event semantics around payments and keeps Leviathan/application decisions out of Dominatus.Pay.
This is intentionally a sketch, not a hosted endpoint in Dominatus.Pay:
app.MapPost("/stripe/webhook", async (HttpRequest request, StripeWebhookVerifier verifier, IPaymentEventDedupStore dedup) =>
{
using var reader = new StreamReader(request.Body);
var rawBody = await reader.ReadToEndAsync();
var sig = request.Headers["Stripe-Signature"].ToString();
var result = await verifier.VerifyAndNormalizeAsync(rawBody, sig, new StripeWebhookOptions
{
EndpointSecret = configuration["Stripe:WebhookSecret"]!
});
if (!result.Verified) return Results.BadRequest(result.SafeError);
if (result.NormalizedEvent is null || !result.IsSupportedEvent) return Results.Ok();
var ingestion = await new PaymentWebhookIngestor().IngestAsync(result.NormalizedEvent, dedup);
if (ingestion.Duplicate) return Results.Ok();
// Application/Leviathan layer decides: enqueue fulfillment, store sale, publish to AiWorld, etc.
return Results.Ok();
});- Never log endpoint secrets, signature headers, API keys, client secrets, or raw payment credentials.
- Do not parse, mutate, or reserialize the request body before verification.
- Public live webhook endpoints should use HTTPS.
- Duplicate events are expected; use dedup before app reactions.
- Unknown verified events should not crash business flow.
- Dominatus.Pay must not grant access, fulfill orders, notify users, record sales, decide tenant/merchant routing, or apply platform policy.