# 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](https://docs.stripe.com/billing/revenue-recovery/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](https://docs.stripe.com/billing/revenue-recovery/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 second `warning`, the third `final`, and the fourth `suspended`, which revokes the workspace's access. A paid invoice ends it and restores access.
- **Notices to the workspace owner.** `payment.failed` sends 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](/email/connect-a-sender) and [notification events](/notifications/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.failed` and `subscription.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](/webhooks/overview).
- **Entitlement checks** (what your app asks before it shows a paid feature) read the workspace as `lapsed` once its payment has failed past every retry. A locked screen can then say the payment failed, instead of offering a plan. See [Entitlement checks](/billing/entitlements#fails-open).
- **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](/billing/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.

```bash
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 `PUT` replaces the whole list, up to the `max` the 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 that `userId`.
- Setting the list needs permission to manage the workspace's billing. Reading it, with `GET` on 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_expiring` webhook, with `brand`, `last4`, `expMonth`, `expYear`, `expiresAt` and `nextChargeAt`;
- 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:

```tsx
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.deleted` would 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](/notifications/slack) and the payloads in [Webhooks](/webhooks/overview).

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](/billing/managing-subscriptions) - What your team can do on one subscription.
- [Entitlement checks](/billing/entitlements) - Lock what a lapsed workspace may not use.
- [Webhooks](/webhooks/overview) - Every payment and subscription event.
