Failed Payments and Stripe's Emails
The Stripe switches that retry and email for you, what BuildBase does when a payment fails, and the notice before a saved card expires.
Most of revenue recovery happens inside your own Stripe account, and most of it is off until you turn it on. A founder who never flips the switches gets one charge attempt and silence. This page lists what to turn on in Stripe, what BuildBase does on top of it, and the card-expiring notice BuildBase sends before a renewal fails.
Turn these on in Stripe
These are settings in your Stripe dashboard. Stripe has no API that reports them, so BuildBase cannot read or change them, and the setup check on the payment credentials page lists them without saying whether they are on.
- Smart Retries. Stripe retries a failed renewal on its own schedule instead of once. See Stripe's Smart Retries for its recommended setting.
- Customer emails for a failed payment, a card about to expire, a trial ending and an upcoming renewal. See Stripe's customer emails. They are sent from your Stripe account, to the email on the Stripe customer, and link to Stripe's pages.
- Receipts and invoices. Stripe can also email a receipt for a successful payment and a finalized invoice. Check both, so you know whether your customers get Stripe's receipt as well as anything you send.
- What happens after the last retry. Stripe's default is to cancel the subscription. That reaches BuildBase as a canceled subscription with the reason
payment_failed, and the workspace moves to your Free plan when one is published, after the grace days you set. Leave the subscription past due or unpaid instead, and BuildBase's own ladder below runs to suspension.
What BuildBase does when a payment fails
- Dunning state from Stripe's attempt count. The first failed attempt marks the subscription
notified, the secondwarning, the thirdfinal, and the fourthsuspended, which revokes the workspace's access. A paid invoice ends it and restores access. - Notices to the workspace owner.
payment.failedsends an email once a sender is connected and the Payment Failed notification event is on (and email notifications are on for the organization), and a push once browsers are subscribed and push is on. A new organization has no sender: see connect a sender and notification events.payment.action_required, for a payment that needs 3-D Secure, sends one with the link to Stripe's invoice page. - Billing notices and unsubscribes. The failed-payment email, the plan-ended-unpaid email and the notices of a higher price or lost features coming are billing email, so by default someone who unsubscribed from billing email does not get them, though the change still applies. Turn on Send them even to people who unsubscribed from billing email under Settings > General, card Billing notices (
billingNoticesMandatory) and they go with no unsubscribe link, also to someone who unsubscribed from all email. They never go to an address that bounced or marked mail as spam: Email Logs shows such a notice as skipped, with the reason. - The subscription screen shows a banner while a workspace is in dunning, with the way to update the card.
- Workflows trigger on
payment.failedandsubscription.suspended. The Payment Failed Recovery template emails the owner at once and sends a push two days later if the workspace is still in dunning. - Webhooks carry all of it to your backend. See Webhooks.
- Entitlement checks (what your app asks before it shows a paid feature) read the workspace as
lapsedonce its payment has failed past every retry. A locked screen can then say the payment failed, instead of offering a plan. See Entitlement checks. - Your team can see the subscription's state, re-sync it from Stripe or comp it (give it for free) from the workspace's Actions menu. See Managing subscriptions.
Who gets the payment mail
Payment emails always go to the workspace owner. Add billing contacts when someone else should get them too, such as a finance person who isn't the owner. Billing contacts get the emails for:
- a failed payment, and one that recovered
- a payment waiting for the card holder to confirm it with their bank (3-D Secure)
- receipts and refunds
- a card about to expire
- a suspension
- a failed automatic top-up
Set the list from your app's billing settings with this route. There's no SDK hook for it yet.
curl -X PUT "https://api.console.buildbase.app/api/v1/public/workspaces/$WORKSPACE_ID/subscription/billing-contacts" \
-H "x-session-id: $SESSION_ID" \
-H "Content-Type: application/json" \
-d '{"userIds": ["66f0c1d2e3a4b5c6d7e8f902"]}'- Each
PUTreplaces the whole list, up to themaxthe route answers. - Every contact must be a member who can view the workspace's billing. If one isn't, the whole list is refused with
400 BILLING_CONTACT_REFUSED, naming thatuserId. - Setting the list needs permission to manage the workspace's billing. Reading it, with
GETon the same route, needs permission to view it. - A contact who later loses billing access stops getting the emails. Nobody has to edit the list.
Before a card expires
A renewal on an expired card fails, and everything above starts. BuildBase checks first. On the first of every month it reads the card each live subscription is charged on, and when that card expires within 30 days it sends payment.card_expiring, once for that expiry:
- an email and a push to the workspace owner, naming the card, when it expires and when the next charge is;
- the
payment.card_expiringwebhook, withbrand,last4,expMonth,expYear,expiresAtandnextChargeAt; - a Slack message, and the Card Expiring workflow trigger, so you can build your own reminders on it.
A card saved or changed between checks is checked straight away, when your Stripe webhook endpoint sends payment_method.attached, payment_method.automatically_updated and customer.updated. They are on the list of events the payment credentials page shows, and Fix the endpoint for me adds them to an endpoint you made by hand.
A subscription that is set to cancel before the card expires is skipped, and so is one that is not charged to a card. A workspace in dunning is notified: a new card is usually the fix.
The email links to your app when your auth clients' redirect URLs name one address; it never contains a billing portal link, which Stripe mints per visit and expires soon after. To send someone straight to the card form from your app, open the portal with its card-update flow. The flow option arrives in SDK 1.0.0; on 0.0.90 and earlier, openBillingPortal takes the return URL only and opens the portal's home page:
const { openBillingPortal } = useBillingPortal(workspaceId);
openBillingPortal(window.location.href, { flow: 'payment_method_update' });Note
Stripe's own expiring-card email and this notice do the same job. With both on, a customer gets two. Keep one: turn Stripe's off in its customer emails settings, or turn this one's email or push off under Notifications in the console, where it is listed as Card Expiring Soon.
Stripe's card updater replaces many expiring cards on its own, so a notice can arrive for a card that is already fixed. The email says so. A replaced card is picked up from payment_method.automatically_updated, and its new expiry is announced only if it, too, comes within 30 days.
When Stripe events fail
Everything on this page depends on Stripe's webhooks: the messages Stripe sends BuildBase when a payment or a subscription changes. If one is lost or fails, a subscription could end up wrong. Two background jobs catch that:
- An hourly sweep re-runs every stored Stripe event that failed or never ran. It tries each one a few times, then gives up on it and says so. A Resend from the Stripe dashboard also retries a failed event in place.
- A daily check reads each live subscription back from Stripe and puts right any difference it can, through the same handler the webhook would have run. A difference only a person can settle is reported, not changed. If it finds a cancellation, it does exactly what the lost
customer.subscription.deletedwould have done: it closes the subscription, revokes recurring add-ons, and moves the workspace to your Free plan if you've published one.
When either job needs a person to step in, BuildBase tells your organization once per run, in Slack and on your webhooks:
| Event | When |
|---|---|
payment.stripe_events_failed | The hourly sweep re-ran stored events, or gave up on some. Resend those from the Stripe dashboard |
subscription.stripe_out_of_sync | The daily check found subscriptions Stripe doesn't have, a difference from Stripe only a person can settle, or Stripe refused your secret key |
Tick Stripe events failing and Subscriptions out of sync with Stripe under Settings > Slack notifications in the console to get them in a channel. See Slack and the payloads in Webhooks.
Most of this never happens when your Stripe setup is right. Check Stripe setup under Billing > Credentials finds:
- a key from the other mode (a test key where a live one belongs, or the reverse)
- an endpoint that is missing events
- a second endpoint pointing at the same URL
- a signing secret from another endpoint
- events Stripe sent that never arrived
Fix the endpoint for me adds the missing events. The Payment webhooks screen lists each Stripe event that arrived, with its customer and amount.
Next steps
- Managing subscriptions - What your team can do on one subscription.
- Entitlement checks - Lock what a lapsed workspace may not use.
- Webhooks - Every payment and subscription event.