# Stripe

Turn Stripe payments, trials and cancellations into contact events and attributes, and credit each payment to the email that earned it.

## What it does

Connect your project's own Stripe account and what your customers do there reaches their contact:

- **Events.** Every payment, failed charge, trial and cancellation becomes an event on the customer's contact, so an automation can start on it.
- **Attributes.** Each customer's plan, MRR and lifetime value are kept on their contact, for segments and for personalizing email.
- **Revenue attribution.** Every payment is credited to the email that earned it, so campaign and automation reports show what each one made.

Customers are matched to contacts by email address, ignoring case. Several Stripe customers at one address — a second checkout often makes a second customer — are one contact: their subscriptions and lifetime value are combined. When a customer's address changes in Stripe, later events go to the new one. A project connects one Stripe account; connecting again replaces the previous connection and removes its webhook endpoint. Only admins and owners can connect or disconnect it.

## Connect Stripe

1. **Open Integrations** — In the dashboard, go to **Integrations** and choose **Connect Stripe**.
2. **Create a restricted key** — **Open Stripe** takes you to Stripe's form for a new restricted key, with the permissions below already ticked. Create the key there. To try it on test data first, switch Stripe to test mode before you create it.
3. **Paste the key** — Paste it into the dialog and choose **Connect**. We check the key, create the webhook, and start importing your customers.

To connect, re-sync or disconnect from code instead, use the [Stripe API](/docs/stripe-api).

The key asks for these permissions:

- `rak_customer_read`, `rak_subscription_read`, `rak_invoice_read`, `rak_charge_read`, `rak_product_read` — read access to the data the events and attributes are built from, and to the invoice a refunded charge paid.
- `rak_webhook_write` — the one write permission. It lets us create a single webhook endpoint on your account that sends events back to us.
- `rak_connected_account_read` — used only to show your account's name on the Integrations page.

A restricted key starts `rk_live_` or `rk_test_`. A full live secret key (`sk_live_…`) is refused — it can move money on your account — and so is a publishable key (`pk_…`). Test-mode keys work too: test payments arrive as events and update contacts, but are never credited to emails, so trying it out can't put test revenue in your reports.

The [CLI](/cli) and the [MCP server](/mcp) take the same keys, so an agent can try the integration on test data with a `sk_test_…` key.

Before saving, we check the key can read customers, subscriptions, invoices, charges and products. If a permission is missing you see Stripe's own error, which names the permission.

### The webhook

There is no webhook to set up by hand. We create the endpoint on your Stripe account ourselves, pinned to API version `2026-08-26.dahlia` so a change to your account's default version can't change what we receive, and subscribe it to:

- `invoice.paid`, `invoice.payment_failed`
- `checkout.session.completed`, `checkout.session.async_payment_succeeded`
- `customer.subscription.created`, `customer.subscription.updated`, `customer.subscription.deleted`, `customer.subscription.trial_will_end`
- `charge.refunded`
- `customer.updated` — used only when an address changes
- `invoice.voided` — used only to end a voided invoice's payment link

Every delivery is checked against its signature before we act on it. Stripe delivers at least once, so the same event can arrive twice; a duplicate is ignored rather than recorded again.

> **Where the key is kept** — The key and the webhook's signing secret stay on our servers and are never sent back to your browser. You can delete the key in Stripe at any time — see [Disconnect](#disconnect).

## Existing customers

Connecting starts an import, and **Sync customers now** runs it again. It reads every subscription and successful charge on the account, and gives each Stripe customer who has an email address the [contact attributes](#contact-attributes) below.

It records no events and credits no revenue for past payments. A payment from last spring isn't something a flow should react to today, and crediting it would put months of revenue into today's report. Events start with the first one Stripe sends after you connect.

It also clears the `stripe_*` attributes from contacts who aren't customers of the connected account — after you connect a different account, say — so segments on them stop matching.

## Customers who aren't contacts yet

**Add Stripe customers who aren't contacts yet** is on by default, and you can turn it off on the Integrations page. While it's on, a Stripe customer with no matching contact becomes a subscribed contact:

- Customers found by the import are added in bulk, like a CSV import: no notification for each one, and no contact-created automations.
- A new customer arriving by webhook is added like a contact created through the API, so contact-created automations start for them.
- A refund never adds a contact. It's still taken back from the email its payment was credited to, even after that contact is deleted.

Either way they count toward your plan's contact limit. With the setting off, only existing contacts are updated. Addresses on reserved documentation domains, such as `example.com`, are always skipped.

## Events

Each of these is recorded on the customer's contact. The key is what a flow trigger or a **Wait for event** step names. Once Stripe is connected they are all in the flow trigger's event picker, even before the first one has happened.

- `stripe_payment_succeeded` — **Stripe payment succeeded** — An invoice is paid for more than zero, or a one-time Checkout or Payment Link payment completes. The amount is the event's value, with its currency. Properties include `invoice_id`, `subscription_id`, `billing_reason`, `hosted_invoice_url` and `plan`.
- `stripe_payment_failed` — **Stripe payment failed** — An invoice payment fails. It carries no value, so it never counts as revenue; the amount is in the `amount_due` property. Properties also include `attempt_count`, `next_payment_attempt` and `hosted_invoice_url`.
- `stripe_subscription_started` — **Stripe subscription started** — A subscription starts without a trial, or an incomplete one becomes active.
- `stripe_trial_started` — **Stripe trial started** — A subscription starts in a trial. Carries `trial_end`.
- `stripe_trial_ending` — **Stripe trial ending** — Stripe's advance warning, normally about three days before the trial ends.
- `stripe_trial_converted` — **Stripe trial converted** — A trial becomes an active, paid subscription.
- `stripe_subscription_canceled` — **Stripe subscription canceled** — A cancellation is scheduled, for the end of the period or a set date. The customer keeps access until then. Carries `cancel_at`, `reason`, `feedback` and `comment`.
- `stripe_subscription_ended` — **Stripe subscription ended** — The subscription ends — Stripe reports it as deleted.
- `stripe_plan_upgraded` — **Stripe plan upgraded** — The subscription's monthly value goes up. Carries `previous_mrr` and `mrr`.
- `stripe_plan_downgraded` — **Stripe plan downgraded** — The subscription's monthly value goes down. Carries `previous_mrr` and `mrr`.
- `stripe_refund` — **Stripe refund** — A charge is refunded. The value is the amount newly refunded, as a negative number, so a revenue segment nets it out.

Subscription events also carry `subscription_id`, `status`, `mrr`, `currency`, `billing_interval` and `plan`.

`stripe_subscription_canceled` fires while the customer still has access, which is when an email can still change their mind. `stripe_subscription_ended` fires when the access stops.

## Contact attributes

Kept current on every matching contact. Build segments on them, and use them in email as `{{ contact.stripe_plan }}` and so on.

- `stripe_customer_id` — *string* — The Stripe customer id, `cus_…`.
- `stripe_status` — *string* — The best standing across all their subscriptions: `active`, `past_due`, `trialing`, `unpaid`, `paused`, `incomplete` or `canceled`. Absent for someone who has never subscribed.
- `stripe_plan` — *string* — The product name, or names when they pay for more than one.
- `stripe_billing_interval` — *string* — `month`, `year`, and so on.
- `stripe_mrr` — *number* — Monthly recurring revenue in major units — `29.0`, not cents. Other intervals are converted to monthly, so a yearly price is divided by 12. Only active, trialing and past-due subscriptions count, and only those in `stripe_currency`; metered and tiered prices count as 0.
- `stripe_ltv` — *number* — Lifetime value: successful charges minus refunds, in major units, in `stripe_currency`. Charges in other currencies aren't added in.
- `stripe_currency` — *string* — The currency those amounts are in.
- `stripe_trial_end` — *string* — When the trial ends, as an ISO 8601 timestamp. Present only while they are trialing.
- `stripe_unpaid_invoice_url` — *string* — Set when a payment fails, to the invoice's Stripe-hosted page, where they can pay it or update their card. Removed when that invoice is paid or voided.

Some segments worth having: `stripe_mrr` at least 99 for your largest customers, `stripe_status` is `past_due` for people whose card is failing, and `stripe_status` is `canceled` for people who have left. Numeric comparisons need the attribute to be a number, and `stripe_mrr` and `stripe_ltv` are.

> **Why the invoice link is on the contact** — A flow's emails render from the contact, not from the event that started the flow. So the link a failed-payment email needs lives on the contact, as `{{ contact.stripe_unpaid_invoice_url }}`.

## Example: failed payments

An automation that chases a failed payment and stops once it's paid:

1. **Trigger** — Start a flow on the event `stripe_payment_failed`.
2. **Send the link** — Email them the invoice page, `{{ contact.stripe_unpaid_invoice_url }}`, where they can pay or update their card.
3. **Wait for payment** — Add a **Wait for event** step on `stripe_payment_succeeded`, with a timeout of a few days.
4. **Remind** — On the timeout path, send a reminder with the same link. If they pay first, the reminder never goes.

The same flow from code, with the [Flows API](/docs/flows-api):

```bash
curl -X POST https://emailbump.com/api/v1/flows \
  -H "Authorization: Bearer ebk_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Failed payment",
    "status": "draft",
    "trigger": { "type": "event", "event_key": "stripe_payment_failed" },
    "steps": [
      { "type": "send", "subject": "Your payment did not go through",
        "html": "<p>We could not take your last payment. <a href=\"{{ contact.stripe_unpaid_invoice_url }}\">Pay the invoice or update your card</a>.</p>" },
      { "type": "wait_event", "event": "stripe_payment_succeeded", "timeout_days": 3,
        "timeout": [
          { "type": "send", "subject": "Your invoice is still unpaid",
            "html": "<p>A reminder: <a href=\"{{ contact.stripe_unpaid_invoice_url }}\">pay the invoice or update your card</a>.</p>" }
        ] }
    ]
  }'
```

It is created as a draft, so nothing sends until you activate it. Stripe retries a failed payment, and each failed retry is another `stripe_payment_failed`. The default re-enrollment rule, `not_while_active`, keeps someone from entering the flow a second time while they are still in it.

## Revenue attribution

This applies to Stripe payments and to any event you send to the [Events API](/docs/events-api) with a `value`. With Stripe connected, don't also send the same purchases to the Events API — or send them with `"attribute": false` — or each one is credited twice.

When a contact's event carries a value above zero, the credit goes to the most recent **click** on a campaign or automation email inside your attribution window. If nothing was clicked and the project counts opens, it goes to the most recent human **open**. A click always beats an open, however recent the open. Automated opens — Apple Mail Privacy Protection, image proxies, security scanners — never count, and neither does a click on an unsubscribe link.

Only campaign and automation emails can earn credit. Transactional mail, such as a receipt or a password reset, never does, and neither do test sends.

The window is 5 days unless you change it. Set anything from 1 to 30 days under **Settings → Revenue attribution**, along with **Credit opens too**, which is on by default. A change applies to purchases from then on; revenue already credited stays where it is. Credit is decided when the purchase is recorded, so a click that reaches us after that — email providers report clicks within seconds — doesn't move it.

The [Revenue API](/docs/revenue-api) reads the credited totals from code and changes these settings.

### Stripe payments

- A subscription's routine renewal is recorded — it is income, and it raises lifetime value — but it isn't credited to an email. No email earned this month's renewal.
- A customer's first payment is credited even when Stripe bills it as a renewal, as it does when a trial converts.
- Upgrades, proration invoices and one-time purchases are credited.
- A refund takes its amount back from the credit its own payment earned — matched through the invoice or Checkout payment it refunds, even when Stripe sends the refund first — never more than that payment earned. The debit is dated with the purchase, so every report that counted the purchase nets it out, and the purchase still counts as a conversion. Refunding a payment no email earned, like a renewal, takes nothing back.
- A payment your own code makes with a PaymentIntent, outside invoices and Checkout, isn't recorded as it happens. It reaches lifetime value at the next import. Send it through the [events API](/docs/events) if an email should get credit for it.
- Failed payments are never revenue.
- Test-mode payments are never credited.

Amounts in different currencies are never added together. Reports lead with the currency most purchases were in and list the others separately.

### Where it shows

- **Analytics** — a Revenue tile, with revenue per delivered email, and a **Top earning emails** card listing campaigns and automations.
- Each campaign's **Metrics** tab.
- An automation's metrics, per send step on the canvas and in the side panel.
- Each contact's activity timeline — for example, "Credited to Spring sale (clicked before buying)".

## Disconnect

**Integrations → Stripe → Disconnect** removes our webhook endpoint from your Stripe account. If the key was already revoked we can't reach Stripe to do that, so delete the endpoint yourself under **Developers → Webhooks** in Stripe; the dashboard tells you when that's needed.

Contacts keep their `stripe_*` attributes and past events. Automations waiting on Stripe events stop receiving them.

## Troubleshooting

The Integrations page shows when the last event arrived and, if one couldn't be processed, the error. Stripe retries a failed delivery for up to three days.

If the key was rolled or deleted in Stripe, events can't be processed. Disconnect, then connect again with a new key.

If a payment landed on the wrong contact, or on none, compare the email address on the Stripe customer with the contact's. That address is the only thing they are matched on.
