docs(payments): document payment fulfilment and the checkout webhook events - #295
zeryabkhan91 wants to merge 5 commits into
Conversation
…events The page predates server-side payment fulfilment, so it documented neither the feature nor the two things an app must do to use it safely. - Adds checkout.session.completed and checkout.session.async_payment_succeeded to the webhook events. The page previously listed only the three customer.subscription events, so an app configured by following it could never have a one-off checkout recorded. - Documents paymentFulfilment: where it goes, the mandatory currency and amount guards, and that client_reference_id names the row. - Documents the ownership check on the checkout route, both routes through it, and ownershipTokenColumn / flPurchaseToken for apps whose buyers have no account. Includes the upgrade warning: enabling fulfilment on an existing app turns the check on for the first time, so the token has to ship first. - Separates paymentIncomplete from paymentPending, and says why an app must not present the second as a failed payment. - Corrects the checkout example, which documented a response.transactionDetails that the API does not return; it resolves with the checkout session. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LyqiysfcPv4FXp1ZMSUoe5
Deploying fliplet-cli with
|
| Latest commit: |
a3138ef
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://37066780.fliplet-cli.pages.dev |
| Branch Preview URL: | https://docs-ps-2138-payments-fulfil.fliplet-cli.pages.dev |
The section gave the shape of paymentFulfilment but never how to set it. Adds Fliplet.App.Settings.set() and the RESTful equivalent, plus the three things that decide whether the saved value is the one that takes effect: it must be the master app; Settings.set() from Studio preview or Viewer resolves without saving anything; and a published app's own copy takes precedence, so it needs republishing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LyqiysfcPv4FXp1ZMSUoe5
Runs bin/build-agent-indexes.mjs so llms-full.txt and llms-v3-libraries.json carry the payments changes, matching how other docs commits update them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LyqiysfcPv4FXp1ZMSUoe5
Three behaviours found while configuring this on a live app, none of them previously written down and each one silent when it bites. - The target data source must belong to the app doing the checkout, and statusColumn must be a real column on it. Both refusals are invisible to the app: nothing is recorded and no error surfaces. Calls out app copies specifically, since a copy inherits paymentFulfilment still pointing at the original app's data source. - A hook declaring triggers never runs from this path, which identifies itself as 'webhook'. Component-created hooks are commonly scoped to a widget id, which a webhook cannot match. - Enabling runUpdateHooks alongside a confirmation already sent by the app's own screens produces two messages, because neither sender can see the other. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LyqiysfcPv4FXp1ZMSUoe5
Leaves runUpdateHooks described in the settings shape, without the separate guidance section. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LyqiysfcPv4FXp1ZMSUoe5
Arpanexe
left a comment
There was a problem hiding this comment.
Code review (via /fliplet-pr-review)
Review Basis
Review basis: subject=a3138eff87bf30e9cb8ae7365c10f46118b5e5a3 standards=fliplet-engineering@0.15.5 revision=unknown source=fliplet-pr-review digest=sha256:6be97d58979ac8e8
Summary
This adds the missing public documentation for webhook-driven payment fulfilment, checkout ownership checks, payment outcomes, and regenerates the agent indexes. The structure is substantially clearer, but several copyable instructions currently give unsafe or inaccurate guarantees.
Regression Risk
High: developers and AI agents following this page could silently disable fulfilment, accept underpayment for higher-priced orders, or trigger duplicate customer workflows.
Context from Prior Reviews
There were no existing reviews or inline threads on this PR. Related implementation history matters here: Fliplet/fliplet-api#8543 requires auditing update hooks before enabling them; #8565 says only Fliplet-minted checkout sessions can fulfil; and still-open #8572 fixes the blank optional-column behavior that this PR already describes as working. No relevant recent merge touched the source payments page. The local incident-query helper is installed, but its corpus is unavailable.
Acceptance Criteria
PS-2138 has no enumerated acceptance criteria. Its stated outcome is that a completed payment produces a persisted registration, ticket, and email. This documentation is only a partial follow-up: it does not configure the affected app, wire its confirmation hook, or recover the 13 affected registrations, and the instructions cannot safely support that outcome until the inline issues are corrected.
Suggestions
- Describe the token threshold as at least 16 UTF-8 bytes; the implementation checks
Buffer.length, not JavaScript character count. - The base is correct, but
docs/PS-2138-payments-fulfilmentdoes not follow the generic-repositoryfeature/{TICKET}-{slug}branch convention.
CI Status
Green — CircleCI build and Validate docs succeeded.
Verdict
REQUEST_CHANGES — 3 Critical issues and 3 Warnings are attached inline. The Critical items can cause silent payment failures or underpayment exposure.
| } | ||
| ``` | ||
|
|
||
| `expectedCurrency` and `minimumAmountTotal` (in the currency's smallest unit) are |
There was a problem hiding this comment.
Critical — this promises per-order price protection that the implementation does not provide.
Observed defect: a cheaper checkout can fulfil a more expensive order even though this paragraph says it cannot.
Mechanism: minimumAmountTotal is one app-wide floor. The webhook only checks amount_total >= minimumAmountTotal; it never compares the charge with the particular row or product. Because line_items are caller-supplied, any legitimate cheaper checkout above the floor can satisfy the guard.
Required outcome: describe this as a global lower bound, not per-order price binding. Multi-tier apps need a server-side price-to-row binding or must not rely on this guard to protect higher-priced rows.
| ``` | ||
|
|
||
| ```json | ||
| { "paymentFulfilment": { "dataSourceId": 123456, "statusColumn": "Payment Status", "paidValue": "Paid" } } |
There was a problem hiding this comment.
Critical — the copyable REST payload disables fulfilment.
Both omitted fields are mandatory: current fliplet-api refuses every webhook target without expectedCurrency and minimumAmountTotal. The settings route also replaces the complete paymentFulfilment value rather than recursively merging its fields, so copying this over an existing configuration can delete valid guards.
Include the complete minimum valid object, and clarify below that only top-level app settings are merged.
| { "paymentFulfilment": { "dataSourceId": 123456, "statusColumn": "Payment Status", "paidValue": "Paid" } } | |
| { "paymentFulfilment": { "dataSourceId": 123456, "statusColumn": "Payment Status", "paidValue": "Paid", "expectedCurrency": "eur", "minimumAmountTotal": 100 } } |
| "statusColumn": "Payment Status", | ||
| "paidValue": "Paid", | ||
|
|
||
| // Optional columns, filled only where the row leaves them blank |
There was a problem hiding this comment.
Critical — this documents behavior that is not yet present on fliplet-api@master.
Form Builder pre-creates configured columns as "". The current SQL merge resolves by key presence and lets the stored empty key override the optional Stripe value, so fields such as Stripe Session ID remain blank. Apps that reconcile by session ID then cannot see the recovered payment.
This is the exact defect in still-open Fliplet/fliplet-api#8572. Make that fix an explicit merge/deployment dependency, or document the current limitation until it ships.
| customerColumn: 'Stripe Customer ID', | ||
| expectedCurrency: 'eur', | ||
| minimumAmountTotal: 100, | ||
| runUpdateHooks: true |
There was a problem hiding this comment.
Warning — the copyable example opts into duplicate side effects without the required hook audit.
The webhook write and the browser's normal write can both run update hooks. An unconditioned hook therefore sends two confirmation emails or calls a workflow twice. Fliplet/fliplet-api#8543 and its release instructions explicitly require checking this before enabling hooks.
Omit/default this to false in the example and explain that hooks must be conditioned/idempotent, with one deliberate owner for confirmation behavior, before opting in.
| buyer is charged, their order stays pending, and nothing you can write in the page | ||
| fixes it — the code that would react is in the page that has gone away. | ||
|
|
||
| Fliplet can record these payments for you from the Stripe webhook instead. Configure |
There was a problem hiding this comment.
Warning — the absolute promise omits the server-issued binding requirement.
The webhook accepts only sessions carrying the signed metadata.flFulfilment binding minted by Fliplet's POST /checkout path. Sessions created through Payment Links, Stripe Dashboard, or another integration are ignored even when they have client_reference_id.
State explicitly that this feature supports sessions created through Fliplet.Payments.Checkout.create(), not arbitrary Stripe checkout sessions.
|
|
||
| The two `checkout.session` events are what let Fliplet record a completed payment on | ||
| its own, without depending on the buyer's browser coming back to your app. Enable both: | ||
| `completed` covers the ordinary card path, and `async_payment_succeeded` is the |
There was a problem hiding this comment.
Warning — this introduces delayed-payment support without its failure-path limitation.
The API records checkout.session.async_payment_succeeded but does not handle checkout.session.async_payment_failed. A browser flow that already marked the row Paid is therefore not reverted when a delayed method later fails; this is a known gap in the implementation PR.
Add the limitation before readers interpret this as complete support for enabling SEPA, Klarna, or other delayed-notification methods.
Updates
docs/API/fliplet-payments.md, which predates server-side payment fulfilment.Why
The page documents neither the feature nor the two things an app has to do to use it safely — and one existing instruction is actively wrong for anyone accepting one-off payments.
What changed
Webhook events. The page listed only the three
customer.subscription.*events. An app set up by following it would never have a one-off checkout recorded, because the API only acts on:Both are now listed, with a note on why
async_payment_succeededmatters (delayed payment methods settle later).paymentFulfilment. New section covering what it does, that it belongs on the master app, the mandatoryexpectedCurrency/minimumAmountTotalguards, and thatclient_reference_idis what names the row.The ownership check. New section on why the checkout route verifies the caller is entitled to the row it names, and the two ways that check passes — the data source's access rules, or
ownershipTokenColumn+flPurchaseTokenfor apps whose buyers have no account. Includes the 16-character minimum, and that the token is stripped before the payload reaches Stripe.It also carries the upgrade warning, which is the part most likely to catch people out:
paymentIncompletevspaymentPending. These mean different things — a verdict versus the absence of one — and telling a buyer their card was not charged while the charge is in flight is how a second charge happens. Also notes that a blocked pop-up surfaces aspaymentIncomplete, which is a common source of confused reports.Correction. The checkout example documented
response.transactionDetails. The API returns no such field —Checkout.create()resolves with the checkout session (id,currency,customer,customer_details,customer_email).Verification
Every statement was checked against
Fliplet/fliplet-api@master:libs/app-payments.jsfor the event list, the fulfilment settings and the merge behaviour;routes/v1/apps-billing.jsfor the ownership check and token handling;public/assets/fliplet-payments/1.0/payments.jsfor what the promise resolves and rejects with.🤖 Generated with Claude Code
https://claude.ai/code/session_01LyqiysfcPv4FXp1ZMSUoe5