Skip to content

docs(payments): document payment fulfilment and the checkout webhook events - #295

Open
zeryabkhan91 wants to merge 5 commits into
masterfrom
docs/PS-2138-payments-fulfilment
Open

zeryabkhan91 wants to merge 5 commits into
masterfrom
docs/PS-2138-payments-fulfilment

Conversation

@zeryabkhan91

Copy link
Copy Markdown
Contributor

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:

const RECORDABLE_EVENTS = ['checkout.session.completed', 'checkout.session.async_payment_succeeded'];

Both are now listed, with a note on why async_payment_succeeded matters (delayed payment methods settle later).

paymentFulfilment. New section covering what it does, that it belongs on the master app, the mandatory expectedCurrency / minimumAmountTotal guards, and that client_reference_id is 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 + flPurchaseToken for 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:

Enabling paymentFulfilment on an existing app turns this check on for the first time. If your app has an ownershipTokenColumn but its screens do not yet send flPurchaseToken, and its access rules do not grant the buyer an update, every checkout will start failing. Ship the token first, then enable fulfilment.

paymentIncomplete vs paymentPending. 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 as paymentIncomplete, 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.js for the event list, the fulfilment settings and the merge behaviour; routes/v1/apps-billing.js for the ownership check and token handling; public/assets/fliplet-payments/1.0/payments.js for what the promise resolves and rejects with.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LyqiysfcPv4FXp1ZMSUoe5

…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
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 2, 2026

Copy link
Copy Markdown

Deploying fliplet-cli with  Cloudflare Pages  Cloudflare Pages

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

View logs

zeryabkhan91 and others added 4 commits September 2, 2026 12:54
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 Arpanexe left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-fulfilment does not follow the generic-repository feature/{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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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" } }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
{ "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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants