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.
- Stripe endpoint:
{portal}/api/hooks/Store/Paymentsβ configure it in the Stripe dashboard for the eventscheckout.session.completed,checkout.session.expired,invoice.paidandcustomer.subscription.deleted, and allowlist the target in the portal configuration:WebhookInbox:Targets:0 = Store/Payments. - π¨ The last two are what keeps a SUBSCRIPTION alive.
invoice.paidis the monthly renewal, and an endpoint not subscribed to it grants each plan exactly one month while Stripe keeps charging the card;customer.subscription.deletedis what actually ends a plan, so without it a cancelled subscriber keeps their access forever. Both were added with the self-serve plan checkout β see Subscriptions. - Every delivery is stored verbatim under
Store/Payments/_Inbox/{id}; the Store hub's watcher verifies theStripe-SignatureHMAC againstCommerce:Stripe:WebhookSecret, acts on it, and removes the processed event. What it acts on is a checkout naming an ORDER (a package purchase, or a plan subscription) or a subscription-lifecycle event naming a viewer and a plan. Deliveries it cannot verify are retained β see below. - No payment ever fulfills without a verified signature β the inbox itself accepts anything the portal allowlists, by design.
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:
- Junk is still discarded. The hook endpoint is public, so retention is earned by naming an order β or, for the recurring lane, a viewer and a plan; an unverifiable body without either is dropped as before. Otherwise anyone could fill the container and bury the delivery that matters. A dropped RENEWAL is as consequential as a dropped first payment (the subscriber lapses a month later with nothing anywhere to say why), which is why it earns the same retention.
- A retained delivery is evidence, never a fact. Nothing reads it back to stamp an order or grant anything β it is unauthenticated, and acting on it would let a forged POST write to any order path. It is there for a human to compare against Stripe.
_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.
- The install runs as System: a webhook has no user session, and the copies land in a home that
is not the caller's β the same shape
StandardPacksuses for onboarding installs. - The order records the second half:
installedAtwhen the material landed,error(with the status stillFulfilled) when it did not. AFulfilledorder with noinstalledAtis a buyer who paid and has nothing β a webhook redelivery finishes such a delivery instead of returning early, and the cover's install step is the buyer-side retry. - A setup failure never unwinds the payment: the money landed and the entitlement stands.
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:
- 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. - The portal does not allowlist the target β
WebhookInbox:Targetsis fail-closed, soPOST /api/hooks/Store/Paymentsanswers 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:
- anything held at
_Rejectedβ always a finding: Stripe called, the signature did not verify, someone may have paid and not been served; _Inboxentries older than 15 minutes β nothing is draining them (noCommerce:Stripe:WebhookSecret, or the watcher is not armed). A fresher entry is a delivery in flight, and alerting on it would train an operator to ignore the line.
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:
- an endpoint subscribed to the checkout pair and nothing else satisfied the board, so the payment path read healthy while every renewal and every cancellation was dropped at the door;
- and the audit's remedy β the sentence an operator reads while standing in the dashboard, acting on the finding below β instructed exactly that configuration.
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
- It never writes β
GET /v1/webhook_endpointsand two node reads. Nothing is created at Stripe or on the mesh. - It never restates a credential. Only the key's prefix is read (to name the mode), and any key-shaped word in an error is redacted before it reaches a rendered page β Stripe's own invalid-key message echoes a masked key.
- A truncated listing is not an absence. Stripe pages at 100;
has_morewith no match reportsUndetermined, because "I did not see it on the page I read" is not "it does not exist".