# Stripe API

Connect a project's Stripe account from code, check on it, re-sync its customers, and disconnect it.

**Project key.** Acts in the project the key belongs to. An all-access key works too — name the project with an `X-Project-Id` header. See [Authentication](https://emailbump.com/docs/api-keys).

## Endpoints

- `GET /v1/integrations/stripe`
- `POST /v1/integrations/stripe`
- `PATCH /v1/integrations/stripe`
- `DELETE /v1/integrations/stripe`
- `POST /v1/integrations/stripe/sync`

This is the connection on the dashboard's **Integrations** page, and the two stay in step. What it records — the events, the contact attributes, and how payments are credited to emails — is in the [Stripe guide](/docs/stripe). Every endpoint answers with the same object: the connection, or `null`, and what connecting provides.

## Read the connection

`GET /v1/integrations/stripe` answers whether or not Stripe is connected.

```bash
curl https://emailbump.com/api/v1/integrations/stripe \
  -H "Authorization: Bearer ebk_your_key"
```

```json
{
  "connected": true,
  "connection": {
    "id": "3f1c9a7e-2b4d-4e8f-9a61-0c5d7b2e8f14",
    "team_id": "111f9258-6b77-4edb-a9f3-14bc66118b14",
    "account_id": "acct_1Example",
    "account_name": "Acme",
    "livemode": true,
    "api_key_hint": "rk_live_…wxyz",
    "webhook_endpoint_id": "we_1Example",
    "create_contacts": true,
    "last_event_at": "2026-09-28T14:02:11+00:00",
    "last_error": null,
    "sync_status": "done",
    "sync_started_at": "2026-09-27T10:15:02+00:00",
    "sync_finished_at": "2026-09-27T10:15:40+00:00",
    "sync_error": null,
    "sync_stats": {
      "customers": 1840,
      "matched_contacts": 1210,
      "created_contacts": 630,
      "subscriptions": 912,
      "charges": 5230
    },
    "connected_by": null,
    "created_at": "2026-09-27T10:15:01+00:00",
    "updated_at": "2026-09-28T14:02:11+00:00"
  },
  "key_url": "https://dashboard.stripe.com/apikeys/create?name=Email%20Bump&permissions%5B%5D=rak_customer_read&permissions%5B%5D=rak_subscription_read&…",
  "permissions": [
    "rak_customer_read",
    "rak_subscription_read",
    "rak_invoice_read",
    "rak_charge_read",
    "rak_product_read",
    "rak_webhook_write",
    "rak_connected_account_read"
  ],
  "events": [
    { "name": "Stripe payment succeeded", "key": "stripe_payment_succeeded" },
    { "name": "Stripe payment failed", "key": "stripe_payment_failed" },
    { "name": "Stripe subscription started", "key": "stripe_subscription_started" },
    { "name": "Stripe trial started", "key": "stripe_trial_started" },
    { "name": "Stripe trial ending", "key": "stripe_trial_ending" },
    { "name": "Stripe trial converted", "key": "stripe_trial_converted" },
    { "name": "Stripe subscription canceled", "key": "stripe_subscription_canceled" },
    { "name": "Stripe subscription ended", "key": "stripe_subscription_ended" },
    { "name": "Stripe plan upgraded", "key": "stripe_plan_upgraded" },
    { "name": "Stripe plan downgraded", "key": "stripe_plan_downgraded" },
    { "name": "Stripe refund", "key": "stripe_refund" }
  ],
  "attributes": [
    "stripe_customer_id",
    "stripe_status",
    "stripe_plan",
    "stripe_billing_interval",
    "stripe_mrr",
    "stripe_ltv",
    "stripe_currency",
    "stripe_trial_end",
    "stripe_unpaid_invoice_url"
  ]
}
```

With nothing connected, `connected` is `false` and `connection` is `null`; the rest is the same.

**Response fields**

- `connected` — *boolean* — Whether the project has a Stripe account connected.
- `connection` — *object | null* — The connection, described below.
- `key_url` — *string* — Stripe's form for a new restricted key, with the permissions already ticked. Open it signed in to the Stripe account you want to connect, in test mode for a test key.
- `permissions` — *string[]* — The permissions that key asks for. What each is for is in the [Stripe guide](/docs/stripe#connect).
- `events` — *array* — The contact events Stripe records, each with its `name` and `key`. The key is what a flow trigger or a `wait_event` step names.
- `attributes` — *string[]* — The contact attributes Stripe keeps current.

## The connection

**connection fields**

- `id` — *uuid* — This connection. Connecting again makes a new one.
- `team_id` — *uuid* — The project.
- `account_id` — *string | null* — The Stripe account, `acct_…`. `null` when the key can't read the account.
- `account_name` — *string | null* — The account's name in Stripe, when the key can read it.
- `livemode` — *boolean* — `false` for a test-mode key.
- `api_key_hint` — *string* — The key's prefix and last four characters, `rk_live_…wxyz`, to tell keys apart in Stripe. The key itself and the webhook's signing secret are never returned.
- `webhook_endpoint_id` — *string* — The endpoint we created on the Stripe account, `we_…`, listed in Stripe under **Developers → Webhooks**.
- `create_contacts` — *boolean* — Whether a Stripe customer who isn't a contact becomes one.
- `last_event_at` — *string | null* — When Stripe last sent an event, ISO 8601.
- `last_error` — *string | null* — The last event we couldn't process.
- `sync_status` — *string* — The customer import: `idle`, `running`, `done` or `failed`.
- `sync_started_at`, `sync_finished_at` — *string | null* — When the last import started and finished.
- `sync_error` — *string | null* — Why the last import failed.
- `sync_stats` — *object | null* — What the last completed import found: `customers`, `matched_contacts`, `created_contacts`, `subscriptions` and `charges`.
- `connected_by` — *uuid | null* — The dashboard user who connected it; `null` when an API key did.
- `created_at`, `updated_at` — *string* — ISO 8601.

## Connect Stripe

`POST /v1/integrations/stripe` checks the key can read customers, subscriptions, invoices, charges and products, creates a webhook endpoint on the Stripe account — pinned to API version `2026-08-26.dahlia` — and starts importing its customers in the background.

**Body parameters**

- `api_key` — *string*, required — A restricted key, `rk_live_…` or `rk_test_…`, made from `key_url`. A live secret key (`sk_live_…`) is refused: it can move money on the account.
- `create_contacts` — *boolean*, optional — Whether a Stripe customer who isn't a contact becomes one. Defaults to `true`. See [Customers who aren't contacts yet](/docs/stripe#new-contacts).

Any other field is a `400`.

```bash
curl -X POST https://emailbump.com/api/v1/integrations/stripe \
  -H "Authorization: Bearer ebk_your_key" \
  -H "Content-Type: application/json" \
  -d '{ "api_key": "rk_live_your_restricted_key", "create_contacts": true }'
```

It answers with the object above, `sync_status` `running`. Read it again until the import is `done` or `failed`. The import sets the `stripe_*` attributes on each customer's contact; it records no events and credits no revenue for past payments. Events start with the first one Stripe sends after you connect.

A project has one Stripe connection. Connecting again replaces it, even with a different Stripe account, and removes the old webhook endpoint.

A refused key is a `400`:

- A live secret key (`sk_live_…`), a publishable key (`pk_…`), or anything that isn't a Stripe key.
- A key Stripe doesn't accept — mistyped, rolled or deleted.
- A key missing a permission. The error is `Stripe refused the key:` followed by Stripe's own message, which names the permission.

```json
{ "error": "Stripe didn't accept that key. Check you copied all of it, and that it hasn't been rolled or deleted." }
```

## Change settings

`PATCH /v1/integrations/stripe` turns adding Stripe customers as contacts on or off. With it off, only existing contacts are updated. It is a `404` when Stripe isn't connected.

**Body parameters**

- `create_contacts` — *boolean*, required — Whether a Stripe customer who isn't a contact becomes one.

```bash
curl -X PATCH https://emailbump.com/api/v1/integrations/stripe \
  -H "Authorization: Bearer ebk_your_key" \
  -H "Content-Type: application/json" \
  -d '{ "create_contacts": false }'
```

It answers with the connection object.

## Re-sync customers

`POST /v1/integrations/stripe/sync` runs the import again, like **Sync customers now** on the Integrations page: it re-reads every subscription and successful charge and refreshes each customer's contact attributes. It takes no body and answers with the connection object, `sync_status` `running`. Like the first import, it records no events and credits no revenue for past payments.

```bash
curl -X POST https://emailbump.com/api/v1/integrations/stripe/sync \
  -H "Authorization: Bearer ebk_your_key"
```

One import runs at a time; asking while one is running is a `409`:

```json
{ "error": "A sync is already running" }
```

An import that has said `running` for more than an hour is taken to have died, and can be started again. It is a `404` when Stripe isn't connected.

## Disconnect

`DELETE /v1/integrations/stripe` removes our webhook endpoint from the Stripe account and forgets the key. It is a `404` when Stripe isn't connected.

```bash
curl -X DELETE https://emailbump.com/api/v1/integrations/stripe \
  -H "Authorization: Bearer ebk_your_key"
```

```json
{
  "connected": false,
  "connection": null,
  "endpoint_removed": true,
  "key_url": "https://dashboard.stripe.com/apikeys/create?name=Email%20Bump&…",
  "permissions": [ … ],
  "events": [ … ],
  "attributes": [ … ]
}
```

`endpoint_removed` is `false` when we couldn't reach Stripe to remove the endpoint — usually because the key was already revoked. Delete it yourself in Stripe under **Developers → Webhooks**; it is the `webhook_endpoint_id` the connection had.

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