Payments

This node anchors the Store's payment webhook inbox.

🚨 The payment provider is an OPTIONAL MODULE, and the Store binds an abstraction. Signature verification, event parsing and checkout-session creation live in MeshWeaver.Payments.Stripe, shipped by the Stripe Payments package; what stays on this side is what the Store DOES with a verified delivery. The seam is the platform's IPaymentProvider β€” see The Payment Provider Contract. Its wiring β€” the three configuration keys and this endpoint β€” is unchanged and is written up in the module's guide; the reasoning is in the module design.

🚨 A portal that mounts NO payments module is a portal that does not sell β€” not a broken one. Every surface below answers that as a state: a checkout refuses with a sentence in the viewer's language, the payment-path audit reports out of scope rather than healthy or broken, and a delivery that arrives is LEFT in the inbox (nothing can authenticate it, which is the same recoverable arm as a missing signing secret β€” never a rejection, which would mean a check failed). Before Plugins#1328 the Store's node types named the module directly, and a deployment without it could not compile them at all.

When a delivery cannot be verified

A delivery whose signature does not verify but which names an order is kept at Store/Payments/_Rejected/{id}, with the same id, the delivery preserved verbatim, and rejectedAt / rejectionReason / receivedSignature stamped onto it. It is not deleted, because deleting it left a buyer's paid order sitting at PendingPayment with an empty inbox β€” a state indistinguishable from "Stripe never called", which needs the opposite response (#1069).

So a non-empty _Rejected means: someone may have paid and not been served. The usual cause is the wrong Commerce:Stripe:WebhookSecret β€” a rotation, a second endpoint, a copy-paste slip. Fix the secret, then replay the delivery from the Stripe dashboard.

Two deliberate limits:

_Rejected is covered by the same Public/Anonymous deny as the rest of Payments (CatalogGate.SensitiveSegments): readable by a Store admin, never public.

What "fulfilled" means

Fulfilment is entitle β†’ stamp β†’ install, in that order and in one chain (OrderFulfilment), shared by both writers β€” the verified webhook (PaymentInboxWatcher.Fulfill) and the amount-zero branch of OrderControlPlane. The buyer gets the entitlement and the package's material copied into their own space (DependencyInstall.InstallAfterAcquisition β€” dependencies leaves-first, then the package into {buyer}/{packageId}), so a card payment ends the way a coupon redemption always did.

When Stripe never calls at all β€” the alarm, and how to read it

Everything above assumes a delivery arrives. The failure mode that took days to identify (#1109) is the one where none ever does: a Checkout session completes at Stripe, no webhook is sent, and on this side the order stays at PendingPayment with both _Inbox and _Rejected empty. Nothing errors, nothing logs, and every surface on the mesh reports health. The only way anyone found out was by buying something and finding nothing.

Two configurations produce exactly that picture, and neither is visible from the mesh:

  1. No webhook endpoint registered at Stripe for this instance's URL β€” including one registered in the other mode. Test and live endpoints are configured separately, so a live-mode endpoint never fires for a sk_test_… checkout, and vice versa.
  2. The portal does not allowlist the target β€” WebhookInbox:Targets is fail-closed, so POST /api/hooks/Store/Payments answers 404 and stores nothing, however correct the Stripe side is.

Store/Maintenance β†’ PaymentPathAudit measures both, as cases in that type's Tests area β€” the same operator health board ModulePinAudit runs on. The same secret key that creates Checkout sessions can read the account's webhook endpoints (GET /v1/webhook_endpoints), and β€” the property that makes the check answer the real question β€” a key lists only the endpoints of its own mode. So a sk_test_… key asking "is there an endpoint for my URL" is asking "is there a TEST-mode endpoint for my URL", which is precisely the unknown #1109 could not close from this side.

Three states, never two

Verdict What it means Rendered
Delivering measured: an enabled endpoint for {Commerce:BaseUrl}/api/hooks/Store/Payments, subscribed to all four events pass
NotDelivering measured: absent / registered in the other mode / disabled / missing an event fail, naming the mode, the URL, what the account does list, and the remedy
Undetermined cannot tell β€” Stripe unreachable, a 401, a truncated listing, or no Commerce:BaseUrl to compare against fail

Undetermined failing is the point. An unreachable Stripe, a 401 and an account with genuinely no endpoint all answer "nothing found"; only keeping the third state separate tells them apart, and a health check that cannot is the defect, not the fix. (Same rule as ModulePinAudit: a source that cannot be READ is not a clean bill of health.)

A fourth value, NotSelling, is out of scope rather than health: a portal with no Commerce:Stripe:SecretKey creates no Checkout sessions, so it has no payment path that can be broken. It says so in those words and never reads as "payments work" β€” and it is why the audit reaches nothing at all (no HTTP call, no mesh read) on a CI mesh.

It also sees the deliveries that DID arrive

On a portal that sells, the same run reports:

Ages, never wall clocks: a duration is the same number in every time zone, so the report cannot show container UTC to a viewer in ZΓΌrich.

The demanded set and the documented set are the SAME set

The four events in the first bullet of this page are not prose beside the code β€” they are the set the audit measures an endpoint against, and a test holds the two together (StripePaymentProviderTest.TheDemandedEventsAreTheOnesTheRunbookTellsAnOperatorToConfigure): this document is embedded in the module's test assembly, the events are read out of that one bullet β€” the line beginning - **Stripe endpoint**:, the line that instructs the configuration β€” and the result must equal what the provider demands. Reword that bullet away and the guard reds for a missing anchor rather than passing on a line it could not find.

That guard exists because they drifted. The demanded set carried only the two checkout events from the self-serve plan checkout landing until Plugins#1530 β€” through two rewrites of the array, with every test green, because the only coverage asserted the array against a literal copy of ITSELF. The consequence was not academic:

Adding an event to this page without adding it to the provider (or the reverse) now reds. Whatever the demand becomes, the operator is told the same thing.

What was actually measured β€” the #1109 record

The definition of done for #1109 is one test-card purchase reaching Fulfilled with installedAt set on memex.meshweaver.cloud. It is not met, and nothing below claims it is. What is settled is why, and the answer is not in this repository.

The denominator. Every purchase attempt anyone has recorded against this instance, and how far it got:

when order got to fulfilled
2026-08-03 sglauser/_Orders/order-e9d4535a… (cs_test_a1YeWyz…) PendingPayment no
2026-09-01 sglauser-private/_Orders/order-14887283… (cs_test_a1FNcrqr…, 900 CHF) PendingPayment no
2026-09-02 sglauser-private/_Orders/order-bdcf93f6…, paid at the provider after the #1069 deploy PendingPayment no

Store/Payments/_Inbox and _Rejected were read directly (not through the search index) on every one of those days and on 2026-09-08: empty every time.

🚨 An empty container is not by itself proof that nothing arrived, and this record must not be read as if it were. Both containers are drained by design β€” a processed event is deleted, and before #1069 a REJECTED one was deleted too β€” so "empty" is exactly as consistent with a delivery that came and went as with one that never came. That indistinguishability is #1069. What the observations do establish, each from something other than an absence:

established by
No delivery has ever been processed successfully a processed checkout.session.completed stamps its order Fulfilled and writes an entitlement. No order has ever left PendingPayment, and no entitlement exists.
No delivery has arrived since 2026-09-02 from the #1069 deploy onward an unverifiable delivery naming an order is RETAINED at _Rejected/{id}. A purchase completed at the provider after that deploy produced nothing there.
None can arrive today the 2026-09-08 audit measured no test-mode endpoint for this instance's hook URL at all.

What is not established, and cannot be from this side: whether some delivery arrived before the 2026-09-02 deploy and was silently dropped by the deleting watcher. The endpoint listing is a reading taken on 2026-09-08 β€” it says what the account holds now, not what it held in August. Nothing turns on the difference (the remedy is the same either way), but the claim "nothing has ever been delivered" is a stronger one than the evidence carries, and this page does not make it.

🚨 The order counts are what the mesh could be made to SHOW, not a census. Orders live in each buyer's own partition, so an identity without access to that partition reads zero β€” and reads it identically whether there are no orders or merely none it may see. Anyone re-establishing this number must say whose identity took it.

The exact step that does not complete. Not "the webhook" β€” the subscription:

PaymentInboxWatcher.AddPaymentInboxWatcher
  β†’ mesh.Query<MeshNode>("path:Store/Payments/_Inbox scope:children")
      .Where(change => change.ChangeType is Initial or Added or Reset)   ← never emits an Added

Nothing arrives, so ProcessEvent never runs, so Fulfill never runs, so OrderFulfilment.Complete's stream.Update(… Status = Fulfilled …) is never subscribed and the order stays where the buyer left it. Every step downstream of that emission is exercised and green β€” the fulfilment chain is covered live by "Paid (live): a card payment INSTALLS, not just entitles" β€” so nothing between the delivery and the entitlement is what is broken.

Everything upstream of it has been excluded, in order β€” each by a positive observation rather than by an absence. The signature computation is pinned to a vector computed outside this codebase (#1154). The target IS allowlisted (POST {} to the hook answers 200; a real node that is not allowlisted answers 404). A signing secret IS configured β€” the probe delivery that 200 stored was discarded, and Decide reaches Discard only when a secret is present. A wrong secret is out, because since #1069 that RETAINS a node under _Rejected/{id} and a purchase completed after that deploy produced none. That leaves one arm, and on 2026-09-08 the audit measured it:

The payment provider lists NO webhook endpoint at all for this instance's hook URL in TEST mode β€” the mode this portal's own credential charges in, and the only mode whose endpoints can fire for its checkout sessions.

So the remaining step is an account action, and it is the whole of what is left. In the provider's dashboard, in test mode: add an endpoint for {portal}/api/hooks/Store/Payments subscribed to all four events named at the top of this page; put its signing secret in Commerce:Stripe:WebhookSecret; restart the deployment (the vault is read only at pod start); then replay the already-paid session β€” fulfilment runs off the session metadata and is idempotent, so an existing order completes without a second payment. Nothing in this repository can do that step, and nothing in it should try: creating an endpoint would be a write to a live payment account, and the audit deliberately never writes.

What it is careful not to do

Reconnecting…
The server was updated. Reloading the page to pick up the latest version.