Claude built your app in two hours, and you want to charge for it. Before you flip Stripe to live mode, there are 20 things to ask the agent for: verify webhook signatures, handle failed payments, refunds, upgrades and downgrades, idempotency keys, server-side feature gates, subscription status sync, test clocks, a billing portal, receipts, payment logs, expired cards, tax, trial rules, duplicate charges, currencies, a cancel flow, dunning emails, webhook alerts, and a full test as a customer.
Each item is a prompt, and each prompt adds billing code your app owns from then on. The agent writes webhook handlers for failed payments and plan changes, a status table that mirrors the processor's, and dashboard settings the code has to agree with. Every one of them has to stay correct after you've forgotten how it works.
On Flint, most of the list is default behavior. Flint is a commerce API that runs the subscription itself: it charges each renewal as an order, retries failed cards, emails the customer, and hosts the account page where subscribers update a card or cancel. Of the 20 checks, eight are built in, six are one API call or setting, and five stay with your app, as they would on any platform. The last one, multiple currencies, doesn't apply: Flint charges in US dollars. Create a free sandbox account and your agent can run the whole list against test cards today.
All 20 checks at a glance#
| Check | On Flint | What that means |
|---|---|---|
| Verify webhook signatures | Yours on any platform | One library call with your whsec_ secret |
| Handle failed payments | Built in | Subscription moves to past_due; Flint retries and emails the customer |
| Handle refunds | One API call or setting | POST /v1/refunds; tax reversal and refund receipt included |
| Handle upgrades and downgrades | One API call or setting | POST /v1/subscriptions/{id}/change-plan computes the credit |
| Add idempotency keys | Yours on any platform | Send Idempotency-Key on your own POSTs |
| Gate features server-side | Yours on any platform | Your server checks the subscription's status |
| Sync subscription status to your DB | One API call or setting | Store the subscription_id; read status from Flint |
| Test with test clocks | One API call or setting | A daily plan in the sandbox renews every day |
| Add a billing portal | Built in | Hosted buyer account for every merchant |
| Email receipts | Built in | Every renewal is an order with a receipt |
| Log every payment event | Built in | Orders, order activities, and a queryable event log |
| Handle expired cards | Built in | Network card updates, then the failed-payment path |
| Add tax collection | One API call or setting | Turn on automatic tax and add your registrations |
| Set trial rules | One API call or setting | trial_period_days on the plan |
| Prevent duplicate charges | Built in | Flint refuses a second payment while one is in flight |
| Handle multiple currencies | Doesn't apply | Charges in USD; international cards accepted |
| Build a cancel flow that works | Built in | Cancel, pause, and reasons in the buyer account |
| Retry dunning emails | Built in | A payment-failure email, then four retries over 16 days |
| Alert you on failed webhooks | Yours on any platform | Point an alert at the failed-deliveries query |
| Test the whole flow as a customer | Yours on any platform | Sign up, fail a renewal, and cancel with test cards |
Webhooks and duplicate charges#
Verify webhook signatures#
Correct looks like this: your handler checks the signature on the raw request body before it reads anything else, and rejects the request when the check fails. On Stripe that is the Stripe-Signature header and your endpoint's whsec_ secret; a framework that parses the body first breaks the check.
On Flint: deliveries follow the Standard Webhooks spec, with webhook-id, webhook-timestamp, and webhook-signature headers. Verify them with the standard-webhooks library and your endpoint's whsec_ secret. This one stays yours: every platform signs, and only your server can check. Webhooks has the handler.
Add idempotency keys#
Correct looks like this: every request that creates or changes something carries a key your code generates, so a request that timed out can be retried without making a second subscription or a second refund. Stripe accepts an Idempotency-Key header on every POST, and the client generates the key.
On Flint: every POST accepts an Idempotency-Key header, and POST /v1/subscriptions requires one. Sending it on your own requests is yours. Hosted checkout and Flint's own renewal charges are Flint's responsibility, so the requests your app makes are the only ones to key. Idempotency covers retries and key reuse.
Prevent duplicate charges#
Correct looks like this: a double click, a retried request, or two browser tabs never charge the same customer twice for the same thing, and your webhook handler grants access once even when an event arrives twice. Stripe documents that the same event can arrive more than once and in no guaranteed order.
On Flint: a payment request that would overlap a payment already in flight returns 409 PAYMENT_IN_PROGRESS and charges nothing. Paying an order with an open checkout session requires expected_outstanding_money, and a balance that changed since the buyer loaded the checkout page returns 409 ORDER_CHANGED_REFRESH_REQUIRED without charging. Renewals are charged by Flint on its schedule, not by your code. On the webhook side, dedupe on webhook_event_id, which is identical on every retry and resend of an event.
Log every payment event#
Correct looks like this: when a customer asks "why was I charged twice?", you can see every charge, failure, retry, and refund for them, in order, without grepping server logs.
On Flint: every renewal is an ordinary order with origin: subscription, subscription_id, and subscription_cycle, so the order list is the payment record. GET /v1/orders/{order_id}/activities returns a readable history of one order: created, payment failed, refund settled. GET /v1/webhook-events lists the events Flint sent you and how each delivery went. Your app keeps its own logs only for what it does itself.
Alert you on failed webhooks#
Correct looks like this: when your handler is down, someone finds out before a customer does, and the missed events get processed once it's fixed.
On Flint: a failed delivery retries up to 9 times, the last attempt 71 to 86 hours after the first. After that the delivery is failed, stays queryable, and can be resent for 14 days. GET /v1/webhook-events?delivery_status=failed lists them, and Flint's production checklist says to point an alert at it. The alert itself is yours: a scheduled job that calls the query and pages you when the list isn't empty.
The subscription lifecycle#
Sync subscription status to your DB#
Correct looks like this: your database never says a user is paid when the processor says they aren't. The usual build is a subscriptions table updated from webhooks, which drifts when an event is missed or arrives out of order.
On Flint: Flint holds the subscription's state, and your app stores the subscription_id against the user. Treat each webhook as a signal: fetch GET /v1/subscriptions/{subscription_id} and act on its current status. The minimal handler needs three transitions: provision on subscription.created when the subscription is trialing or on subscription.activated, prompt for a new card on subscription.past_due, and revoke on subscription.canceled. The envelope carries event_type, webhook_event_id, and the subscription in data.object. Step 6: grant access from webhooks has the full event list.
Gate features server-side#
Correct looks like this: the server decides what a user can do on every request that matters. A flag in the browser, or a field the client sends, can be edited by anyone.
On Flint: this one stays yours. Your server reads the subscription's status: trialing and active mean provision, past_due means the card failed and Flint is retrying, and canceled means revoke. What a past_due user can still do is your product decision.
Handle failed payments#
Correct looks like this: a declined renewal doesn't silently end the subscription or keep access open forever. The customer hears about it, the charge is retried, and access follows the result. On Stripe, Smart Retries exists and is configured in the Dashboard, and your app listens to invoice.payment_failed to update its own state.
On Flint: a failed renewal moves the subscription to past_due, sends subscription.past_due, and Flint emails the customer a link to update their card. Any successful retry restores active. When retries run out, subscription.dunning_exhausted arrives with your configured end action: cancel, pause, or notify_only. You can also start a retry yourself with POST /v1/subscriptions/{id}/payment-retries once the customer saves a new card.
Retry dunning emails#
Correct looks like this: retries are spaced so a card with a temporary problem has time to recover, and the customer gets a way to fix it before the last attempt.
On Flint: by default Flint retries four times over 16 days, on days 1, 4, 9, and 16 after the decline, and the retry window is a merchant billing setting. Dunning emails are one of six email families Flint sends by default; you can switch any family to merchant_sends and send your own. When a renewal payment fails has the sequence.
Handle expired cards#
Correct looks like this: a card that expired or was reissued doesn't end a subscription the customer meant to keep.
On Flint: when a bank reissues a card, the card network can update the saved card, and Flint applies the new brand, last four digits, and expiry without changing the payment_method_id. A card that still can't be charged fails its renewal and follows the failed-payment path above, and the dunning email sends the customer to update the card in their account. Saved cards lists the card statuses.
Handle upgrades and downgrades#
Correct looks like this: moving from $29 to $49 mid-month charges the difference for the time left, moving down doesn't refund money by accident, and the customer sees the amount before confirming. Stripe prorates by default and offers a preview invoice; your app decides the proration_behavior on each change.
On Flint: POST /v1/subscriptions/{id}/change-plan takes the new subscription_plan_id and billing_starts. now charges the new plan today minus a credit for unused time: on a $29 plan with 19 of 31 days left, the credit is $17.77 and the new $49 period costs $31.23 before tax. next_renewal charges nothing now and is the usual choice for downgrades. A plan_change preview returns the charge before you commit. Plan changes take your API key, so the upgrade button lives in your app, and Flint doesn't email the buyer about a higher price: send that yourself on subscription.plan_changed.
Set trial rules#
Correct looks like this: you decide whether a trial collects a card, what happens when it ends, and what a cancel during the trial means, and the code does exactly that.
On Flint: set trial_period_days on the plan, up to 365. The card is collected and validated up front with no charge, the subscription is trialing with a trial_end_at, and it converts on its own at trial end. subscription.trial_ending arrives before the end. A customer who cancels during the trial is scheduled to cancel at trial_end_at and is never charged. Free trials covers the conversion events.
What the customer sees#
Add a billing portal#
Correct looks like this: a subscriber can update a card, see what they paid, and cancel without emailing you. Stripe has a hosted customer portal, configured in the Dashboard, and your app learns about changes made there through webhooks.
On Flint: every merchant has a hosted buyer account at account.withflintpay.com, brandable, and able to run on your own domain with the custom domain add-on. Subscribers update their card or address, pause and resume, reverse a scheduled cancellation, and cancel. Every account link in Flint's receipts and dunning emails points at it. To build your own pages instead, the same actions run headless through /v1/me/subscriptions. Customer accounts has the settings.
Build a cancel flow that works#
Correct looks like this: cancel is easy to find, ends access when the customer was told it would, and records why they left.
On Flint: the buyer account's settings decide whether a cancellation ends at the close of the period or the buyer can end it now, which reasons the buyer picks from, how long a pause can be, and whether a pause is offered before the cancel. Buyers can always cancel: no setting removes the button. Your app gets subscription.cancellation_scheduled with the reason, then subscription.canceled when access should end.
Email receipts#
Correct looks like this: every charge, renewal, and refund produces an email that matches what was charged.
On Flint: each renewal is an order with its own line items, tax, and receipt. Flint sends receipts, subscription lifecycle emails (renewals, pauses, cancellations, trial endings), and dunning notices by default, and refund receipts always. Customer email delivery shows how to take over one family at a time.
Money: refunds, tax, and currency#
Handle refunds#
Correct looks like this: a refund names what it returns, reverses the tax that was collected on it, and leaves a record that matches the bank.
On Flint: refund the renewal's order through POST /v1/refunds, in full, by amount, or by line item with tax_refund_mode: "automatic" to reverse that line's tax. Flint emails the refund receipt. Flint never refunds unused time on its own, so a prorated refund after a cancel is a refund of that period's order. Your refund policy stays yours.
Add tax collection#
Correct looks like this: tax is calculated from the customer's address and what you sell, collected on every renewal, and recorded per order so you can file from it.
On Flint: automatic tax calculates and collects US sales tax at checkout from the buyer's address and your tax categories, and renewal orders carry their own tax. You add the states where you're registered. Registering and filing stay with you, as on any platform that isn't the merchant of record. Sales tax covers registrations.
Handle multiple currencies#
Correct looks like this: every amount is stored with its currency, and nothing adds two currencies together.
On Flint: Flint charges in USD and accepts international cards, with a +1.50% international card fee. Amounts are integer minor units with an explicit currency, such as {"amount": 2900, "currency": "USD"}.
Testing#
Test with test clocks#
Correct looks like this: you've watched a trial convert, a renewal succeed, and a renewal fail before a real customer does. Stripe's test clocks move a sandbox subscription forward in time so you can see a renewal without waiting a month.
On Flint: create a daily plan in the sandbox, so renewals arrive within a day. Subscribe with 4242 4242 4242 4242 and every cycle succeeds. To rehearse a failed renewal, save 4000 0000 0000 0341, a card that saves but never charges, and point the subscription at it with POST /v1/subscriptions/{id}/payment-method; the next daily renewal fails and sends subscription.past_due. flint listen --forward-to http://localhost:8080/webhooks/flint forwards the signed events to your laptop without a tunnel.
Test the whole flow as a customer#
Correct looks like this: before launch, someone signs up from your real pricing page, uses the product, fails a renewal, fixes the card, and cancels, and every step looks right to the customer and to your database.
On Flint: this one stays yours, and the sandbox makes it cheap: test keys are free, and the daily plan and 0341 card give you a failed renewal in a day. When you go live, re-create your webhook endpoint and plans in live mode, since test IDs don't exist there, and confirm with one real payment. Your code doesn't change. Going live has the five steps.
What stays yours on any platform#
These don't move to a processor or a commerce API:
- Feature gating. Your server decides what a user can reach, on every request.
- Signature checks. Only your server can verify that a webhook came from your provider.
- Testing as a customer. Someone has to walk the flow before a real buyer does.
- Sales tax registration and filing. Flint calculates and collects; registering in each state and filing returns are the seller's.
- Disputes. Flint records each one with its deadline and sends
dispute.needs_response; answering it, in the dashboard or your own interface, is yours. Card dispute fees pass through at cost. - Your refund policy. Flint executes refunds; what you promise customers is yours to write.
What Flint removes is the code around those decisions: card data that never reaches your server, money math such as plan-change credits and tax reversal, the second record of what was sold, and the subscription state you would otherwise sync. What you're responsible for when you sell online covers the obligations in more depth.
The Flint path#
Connect Flint's docs server to your agent so it reads the current API instead of guessing:
claude mcp add --transport http flint-docs https://developers.withflintpay.com/mcp
Then give it the checklist as one prompt:
Connect the Flint docs MCP server, then add a Pro
plan to my app: $29 a month with a 14-day trial.
Create the plan with the sandbox key in my
environment. Send signed-in users to a hosted
checkout session for it, passing their Flint
customer ID. Grant and revoke access from webhooks:
verify the signature, dedupe on webhook_event_id,
fetch the subscription, and act on its status.
Then create a daily test plan, subscribe with the
4242 card, switch it to the 0341 card, and show me
the subscription in past_due after the next
renewal fails.
The plan it creates looks like this:
curl -X POST https://api.withflintpay.com/v1/subscription-plans \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Idempotency-Key: plan-pro-monthly-v1" \
-d '{
"name": "Pro Monthly",
"billing_interval": "monthly",
"billing_interval_count": 1,
"currency": "USD",
"trial_period_days": 14,
"line_items": [
{
"name": "Pro Plan",
"unit_price_money": {"amount": 2900, "currency": "USD"},
"quantity": 1
}
]
}'
And the signup page for one of your users:
curl -X POST https://api.withflintpay.com/v1/checkout-sessions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Idempotency-Key: checkout-pro-signup-001" \
-d '{
"type": "subscription_plan",
"subscription_plan_id": "plan_1kmn0aExample",
"customer_id": "cus_1kmn0aExample",
"redirects": {
"success_url": "https://example.com/welcome"
}
}'
Flint creates the subscription, saves the card for renewals, and sends subscription.created. From there the renewals, retries, dunning emails, receipts, and the cancel page run without more code in your app. Subscription billing is the full guide, and taking payments in a vibe-coded app covers one-time sales and payment links. Sign up for a free sandbox to run it.
FAQ#
Do I need test clocks to test subscriptions?#
Not on Flint. Use a daily plan in the sandbox so renewals arrive within a day, and the 4000 0000 0000 0341 card to make a renewal fail. On Stripe, test clocks are the tool for moving a sandbox subscription forward in time.
Does Flint handle dunning for failed subscription payments?#
Yes. A failed renewal moves the subscription to past_due, Flint emails the customer a link to update their card, and it retries on days 1, 4, 9, and 16 after the decline by default. When retries run out, your configured end action applies: cancel, pause, or notify only.
Can users cancel a subscription on their own?#
Yes. Every Flint merchant has a hosted buyer account where subscribers cancel, pause, resume, and update their card. Your settings decide whether a cancellation waits for the end of the period, which reasons to ask for, and whether to offer a pause first. Buyers can't change to a different plan there; that runs through your API key.
Do I still need to verify webhook signatures on Flint?#
Yes. Flint signs every delivery under the Standard Webhooks spec, and your handler verifies it with the standard-webhooks library and your whsec_ secret before acting on it. Then dedupe on webhook_event_id and fetch the current subscription.
How much does Flint cost for a SaaS subscription?#
There's no monthly fee. Card and wallet payments are 3.79% + 35¢, with Stripe processing included, and payments that start or renew a subscription add 0.40%. Automatic tax adds 0.50% on payments that carry tax, international cards add 1.50%, and card disputes pass through at cost. Sandbox and test keys are free. Pricing has the rate card.
Do I need my own Stripe account to use Flint?#
No. Flint processes cards on Stripe, and you don't need a Stripe account of your own.
Sources#
Stripe claims come from its documentation, checked on 2026-10-26.
- Receive Stripe events in your webhook endpoint: the
Stripe-Signatureheader andwhsec_secret, raw-body verification, duplicate deliveries, no guaranteed event order. - Idempotent requests: all POST requests accept idempotency keys, which the client generates.
- Automate payment retries: Smart Retries, configured in the Dashboard;
invoice.payment_failedfor failure and retry updates. - Prorations:
create_prorationsis the defaultproration_behavior; preview invoices. - Provide a customer portal to your customers: the hosted portal, configured in the Dashboard, with cancellation reasons collected through webhooks.
- Test your Billing integration: test clocks simulate Billing objects over time in a sandbox.
