# Stripe

- Tool: Bubble
- Difficulty: Intermediate
- Time required: 2–4 hours
- Last updated: July 2026

## TL;DR

Connect Bubble to Stripe using two layers: the Stripe.js plugin (or Zeroqode's Stripe Payments plugin) handles client-side card tokenization with your publishable key, while the API Connector group with your secret key as a Private header processes charges, subscriptions, and refunds entirely server-side. You never need a separate backend — Bubble's API Connector infrastructure handles all secret key operations.

## The Two-Layer Stripe Architecture in Bubble

The most common mistake Bubble builders make with Stripe is trying to handle everything through a single plugin, or conversely trying to set up a complex backend when Bubble's built-in API Connector is more than sufficient. Understanding the two-layer model removes all the confusion.

Layer 1 — The Plugin (Client-Side, Browser-Safe): Stripe's card UI must run in the browser for security and compliance (PCI-DSS). The plugin embeds Stripe's own hosted card input fields on your Bubble page. Your publishable key (pk_*) is used here — this key is designed to be visible in browser code. The plugin's job is one thing: tokenize the card details and return a PaymentMethod ID (pm_*) that represents the card without exposing the actual card number to your servers.

Layer 2 — The API Connector (Server-Side, Secret Key): Once you have the pm_* from the plugin, your Bubble workflow calls the Stripe API server-side to create a PaymentIntent with that pm_*. This call includes your secret key (sk_*) as a Private header in the API Connector. Bubble proxies this call through its own infrastructure, so the browser never sees the secret key. The PaymentIntent represents the actual charge authorization — when you set confirm=true, Stripe immediately charges the card.

This two-layer model applies to every Stripe operation in Bubble: one-time charges, subscriptions, refunds, and customer management. The plugin handles the user-facing UI; the API Connector handles the business logic. You do not need a Lambda function, a Vercel serverless function, or any external backend — Bubble's server-side proxy is sufficient for all Stripe operations.

For subscriptions specifically, the flow adds one step: you first create a Stripe Customer (POST /v1/customers) to store the user's payment method, then attach a Price ID (from Stripe Dashboard) to a subscription call. Bubble stores the subscription_id for future cancellations or upgrades.

If you are building a marketplace where sellers receive money, you need Stripe Connect instead — that is a separate page covering Express accounts, fee splits, and seller onboarding.

## Before you start

- A Stripe account — sign up at stripe.com. Start in test mode (no business verification needed) for development
- Your Stripe publishable key (pk_test_* for testing) and secret key (sk_test_* for testing) — found in Stripe Dashboard → Developers → API keys
- A Bubble app (any plan) — API Connector is available on all plans, including Free
- For receiving Stripe webhooks via Backend Workflows: a paid Bubble plan (Starter $32/month or above)
- For subscriptions: Price IDs created in Stripe Dashboard → Products → Add product → Add price (both one-time and recurring prices get a price_* ID)

## Step-by-step guide

### 1. Install the Stripe Plugin and Add Your Publishable Key

The first step is getting the client-side card collection component into your Bubble app. This uses a plugin — either the free official one or the popular Zeroqode option — and requires only your publishable key.

In your Bubble editor, click Plugins in the left sidebar, then 'Add plugins'. Search for 'Stripe'. You will see two main options:

1. Stripe.js (by Bubble, free): The official Bubble plugin. It installs Stripe's JavaScript library and exposes a 'Card Element' that you place on your payment page. It has basic functionality for collecting and tokenizing cards.

2. Stripe Payments (by Zeroqode, $39 one-time): The most widely used paid Stripe plugin in the Bubble ecosystem. It adds visual elements for the Payment Sheet, subscription management helpers, and webhook event type detection. Worth the cost for production apps.

For this guide, we use Stripe.js (free). Click Install.

After installation, click on the 'Stripe.js' plugin in your Plugins list. You will see a configuration field for your publishable key. Paste your pk_test_* key here. Do NOT paste your secret key in this field — only the publishable key belongs in the plugin.

Now add the Stripe Card Element to your payment page: in the page editor, click the + button to add elements. Under 'Plugins', find the 'Stripe.js - Card Element'. Drag it onto your page. This is a Stripe-hosted card input field — Stripe handles the card number formatting, validation, and security. You cannot style it beyond the basic options in the element's properties.

Test that the card element appears on your published page (or in Bubble's preview mode) before proceeding.

```
// Plugin configuration:
// Plugin: Stripe.js (by Bubble)
// Field: Publishable Key
// Value: pk_test_YOUR_PUBLISHABLE_KEY  ← paste here

// Page element to add:
// Type: Stripe.js - Card Element
// Place on your checkout/payment page

// Test card numbers (Stripe sandbox):
// 4242 4242 4242 4242  → Success
// 4000 0027 6000 3184  → 3D Secure required
// 4000 0000 0000 9995  → Card declined
```

**Expected result:** The Stripe.js plugin is installed with your publishable key. A Card Element appears on your payment page, ready to collect credit card details from users.

### 2. Configure the API Connector with Your Secret Key

Now set up the server-side component: an API Connector group that uses your Stripe secret key as a Private header. This group will contain all calls that handle money — creating charges, managing customers, and issuing refunds.

In Plugins, find 'API Connector' (if not already installed, search for it and install the free Bubble plugin). Click 'Add another API' and name the group 'Stripe'.

In the shared headers section, add one header:
- Key: Authorization
- Value: Bearer sk_test_YOUR_SECRET_KEY (paste your actual secret key)
- Check the 'Private' checkbox — this is CRITICAL. The Private checkbox encrypts this value on Bubble's servers and ensures it is never sent to the browser.

With this group configured, you can now add the individual API calls that your Bubble workflows will use.

Add Call 1 — Create PaymentIntent:
- Name: Create PaymentIntent
- Method: POST
- URL: https://api.stripe.com/v1/payment_intents
- Use as: Action
- Body parameters: amount (integer, cents), currency (text), payment_method (text), confirm (text, 'true')

Add Call 2 — Create Customer:
- Name: Create Customer
- Method: POST
- URL: https://api.stripe.com/v1/customers
- Use as: Action
- Body parameters: email (text), payment_method (text), name (text)

Add Call 3 — Create Subscription:
- Name: Create Subscription
- Method: POST
- URL: https://api.stripe.com/v1/subscriptions
- Use as: Action
- Body parameters: customer (text, cus_* ID), items[0][price] (text, price_* ID)

Add Call 4 — Create Refund:
- Name: Create Refund
- Method: POST
- URL: https://api.stripe.com/v1/refunds
- Use as: Action
- Body parameters: payment_intent (text, pi_* ID), amount (optional, integer cents for partial refunds)

```
{
  "api_group": "Stripe",
  "shared_headers": [
    {
      "key": "Authorization",
      "value": "Bearer sk_test_YOUR_SECRET_KEY",
      "private": true
    }
  ],
  "calls": [
    {
      "name": "Create PaymentIntent",
      "method": "POST",
      "url": "https://api.stripe.com/v1/payment_intents",
      "body": {
        "amount": "<integer_cents>",
        "currency": "usd",
        "payment_method": "<pm_id>",
        "confirm": "true"
      },
      "use_as": "Action"
    },
    {
      "name": "Create Customer",
      "method": "POST",
      "url": "https://api.stripe.com/v1/customers",
      "body": {
        "email": "<user_email>",
        "payment_method": "<pm_id>",
        "name": "<user_name>"
      },
      "use_as": "Action"
    },
    {
      "name": "Create Subscription",
      "method": "POST",
      "url": "https://api.stripe.com/v1/subscriptions",
      "body": {
        "customer": "<cus_id>",
        "items[0][price]": "<price_id>"
      },
      "use_as": "Action"
    },
    {
      "name": "Create Refund",
      "method": "POST",
      "url": "https://api.stripe.com/v1/refunds",
      "body": {
        "payment_intent": "<pi_id>"
      },
      "use_as": "Action"
    }
  ]
}
```

**Expected result:** The 'Stripe' API Connector group is configured with a Private Authorization header. All four API calls are created and initialized. Bubble has detected response fields including 'id' and 'status' for the PaymentIntent call.

### 3. Build the One-Time Payment Checkout Workflow

This is the core checkout flow: the user fills in the card element, clicks Pay, and Bubble tokenizes the card then charges it server-side. The entire flow runs in one Bubble workflow.

Create a 'Pay' button on your checkout page. Add a click workflow to this button.

Action 1 — Tokenize the card: Add the Stripe.js plugin action 'Stripe.js - Create PaymentMethod'. This action reads the card details from the Card Element and returns a pm_* PaymentMethod ID. It does NOT charge the card — it only tokenizes it safely.

Action 2 — Calculate the amount in cents: Bubble Number fields are decimals. Before charging, convert your price to cents: use a 'Calculate' step with expression 'Price × 100', then round to avoid floating-point values. For a $29.99 price field, this yields 2999.

Action 3 — Create the PaymentIntent: Call 'Stripe - Create PaymentIntent' from the API Connector. Parameters:
- amount: the cents value from step 2 (must be an integer — use 'rounded to 0 decimal places')
- currency: usd (or your preferred currency code)
- payment_method: Result of step 1's id (the pm_* token)
- confirm: true (charges immediately)

Action 4 — Check the result and update your database: Add a condition on Action 4 using 'Only when: Result of step 3's status = succeeded'. In the success branch, create or update the relevant database record (e.g., set Order's payment_status to Paid, store the pi_* payment_intent_id for records).

Action 5 — Handle 3D Secure: For EU payments, Stripe may return status = 'requires_action' instead of 'succeeded'. This means the customer's bank requires additional authentication. Install the Toolbox plugin (free) and use a 'Run JavaScript' action to call Stripe.js's handleNextAction() with the client_secret from the PaymentIntent response. After the user completes 3DS, the PaymentIntent status updates to 'succeeded'.

```
// Checkout Button Workflow:

// Step 1: Stripe.js - Create PaymentMethod
// (reads from Card Element on page)
// Returns: pm_1ABCPaymentMethodToken

// Step 2: Only when Step 1's id is not empty
// Call API: Stripe - Create PaymentIntent
{
  "amount": 2999,
  "currency": "usd",
  "payment_method": "pm_1ABCPaymentMethodToken",
  "confirm": "true"
}
// Returns:
{
  "id": "pi_1JKLMNCharge",
  "amount": 2999,
  "currency": "usd",
  "status": "succeeded",
  "payment_method": "pm_1ABCPaymentMethodToken"
}

// Step 3: Only when Step 2's status = succeeded
// Make changes to Order:
//   payment_status = Paid
//   stripe_payment_intent_id = pi_1JKLMNCharge

// Step 4 (3DS handling): Only when Step 2's status = requires_action
// Run JavaScript (Toolbox plugin):
// stripe.handleNextAction({ clientSecret: '[Result of step 2 client_secret]' })
```

**Expected result:** Clicking Pay collects the card via the plugin, sends a server-side PaymentIntent to Stripe, and on success marks the order as paid in your Bubble database. A test payment with card 4242 4242 4242 4242, any future expiry, and any CVC shows status 'succeeded'.

### 4. Set Up Subscription Billing

Stripe subscriptions require a slightly different flow from one-time charges: you first create a Stripe Customer record (to associate a payment method with a customer across multiple billing cycles), then create a Subscription referencing a Price ID from your Stripe Dashboard.

Before building the Bubble workflow, create your subscription price in Stripe Dashboard: go to Products → Add product → give it a name (e.g., 'Pro Plan') → Add price → choose Recurring → set the billing interval and amount → save. Copy the resulting price_* ID — you will use it in Bubble.

Add two fields to your User data type in Bubble's Data tab:
- stripe_customer_id (text) — stores the cus_* Customer ID from Stripe
- stripe_subscription_id (text) — stores the sub_* Subscription ID
- subscription_status (text) — 'active', 'canceled', 'past_due'

Now build the subscription checkout workflow:

Action 1 — Tokenize the card: Same as one-time payments — call Stripe.js 'Create PaymentMethod' to get pm_*.

Action 2 — Create a Stripe Customer: Call 'Stripe - Create Customer' with email = Current User's email, name = Current User's name, payment_method = pm_* from step 1. Stripe creates a cus_* Customer record that stores the payment method. The response returns 'id' (the cus_* Customer ID).

Action 3 — Save Customer ID: Make changes to Current User → stripe_customer_id = Result of step 2's id.

Action 4 — Create Subscription: Call 'Stripe - Create Subscription' with customer = Current User's stripe_customer_id, items[0][price] = your price_* ID. Stripe automatically charges the customer now (and on every billing cycle).

Action 5 — Save Subscription ID: Make changes to Current User → stripe_subscription_id = Result of step 4's id, subscription_status = active.

For cancellation: create a separate API Connector call (DELETE /v1/subscriptions/{id} or POST /v1/subscriptions/{id} with cancel_at_period_end=true) and a cancellation workflow that sets subscription_status to 'canceled'.

```
// Subscription Checkout Workflow:

// Step 1: Stripe.js - Create PaymentMethod → returns pm_xyz

// Step 2: Call API - Stripe Create Customer
{
  "email": "user@email.com",
  "name": "Jane Doe",
  "payment_method": "pm_xyz"
}
// Returns: { "id": "cus_ABCCustomer", ... }

// Step 3: Make changes to Current User
//   stripe_customer_id = cus_ABCCustomer

// Step 4: Call API - Stripe Create Subscription
{
  "customer": "cus_ABCCustomer",
  "items[0][price]": "price_1ABCMonthlyPlan"
}
// Returns:
{
  "id": "sub_XYZSubscription",
  "status": "active",
  "current_period_end": 1735689600
}

// Step 5: Make changes to Current User
//   stripe_subscription_id = sub_XYZSubscription
//   subscription_status = active

// Cancellation call (add to API Connector):
// POST https://api.stripe.com/v1/subscriptions/[sub_id]
// Body: cancel_at_period_end=true
```

**Expected result:** The user completes checkout, a Stripe Customer is created, a Subscription is activated, and both the cus_* and sub_* IDs are saved in your Bubble User record. The user now has access to subscription features, controlled by the subscription_status field.

### 5. Receive Stripe Webhooks via Backend Workflow

Stripe sends asynchronous events for subscription renewals, payment failures, and cancellations. Your Bubble app needs to handle these to keep database records in sync — without webhooks, your app would never know when a subscription expires or a payment fails.

This step requires a paid Bubble plan (Starter $32/month or above) to enable Backend Workflows.

Enable Backend Workflows: go to Settings → API tab → check 'This app exposes a Workflow API'. Save.

Create a Backend Workflow: click 'Backend Workflows' in the left sidebar → 'New API Workflow' → name it 'stripe_events'. Set to run without authentication (Stripe's webhook requests have no Bubble session cookie).

Click 'Detect request data'. Bubble generates an endpoint URL like https://yourapp.bubbleapps.io/api/1.1/wf/stripe_events. Copy this URL.

In Stripe Dashboard: Developers → Webhooks → Add endpoint. Paste your Bubble URL. Select these events: invoice.paid, customer.subscription.deleted, customer.subscription.updated, payment_intent.payment_failed.

Back in Stripe Dashboard, use 'Send test webhook' to send a sample invoice.paid event. In Bubble, click 'Stop detecting' — Bubble detects the incoming JSON structure: type (string), data.object.customer (cus_* ID), data.object.subscription (sub_* ID), data.object.status (string).

Build the workflow logic with conditions:
- When type = 'invoice.paid' → search for User where stripe_customer_id = body customer → set subscription_status = active
- When type = 'customer.subscription.deleted' → search for User where stripe_subscription_id = body subscription → set subscription_status = canceled
- When type = 'payment_intent.payment_failed' → search for User and send email notification

For webhook security: Stripe sends a Stripe-Signature header for verification. Bubble cannot natively verify HMAC signatures. Add a secret query parameter to your webhook URL (e.g., ?secret=randomstring) and add a condition in your Backend Workflow: only proceed when the URL parameter 'secret' equals your expected value.

```
// Bubble Backend Workflow endpoint:
// https://yourapp.bubbleapps.io/api/1.1/wf/stripe_events

// Sample Stripe webhook payloads:

// invoice.paid:
{
  "type": "invoice.paid",
  "data": {
    "object": {
      "customer": "cus_ABCCustomer",
      "subscription": "sub_XYZSubscription",
      "amount_paid": 2900,
      "status": "paid"
    }
  }
}

// customer.subscription.deleted:
{
  "type": "customer.subscription.deleted",
  "data": {
    "object": {
      "id": "sub_XYZSubscription",
      "customer": "cus_ABCCustomer",
      "status": "canceled"
    }
  }
}

// Stripe Dashboard → Developers → Webhooks:
// Endpoint URL: https://yourapp.bubbleapps.io/api/1.1/wf/stripe_events
// Events: invoice.paid, customer.subscription.deleted,
//         customer.subscription.updated, payment_intent.payment_failed
```

**Expected result:** Your Bubble Backend Workflow receives Stripe webhook events. When a subscription renews, the database updates automatically. When a subscription is cancelled via Stripe Dashboard (or by billing failure), the user's subscription_status in Bubble changes to 'canceled' without any manual intervention.

## Best practices

- Always use the Private checkbox on the Authorization header in the API Connector — this is the single most important security step that keeps your sk_* secret key off the browser.
- Store the Stripe payment_intent_id (pi_*) and customer_id (cus_*) in your Bubble database for every transaction — these are your keys to issuing refunds, looking up charge history, and correlating Stripe events with your app's data.
- Convert prices to cents immediately before calling the Stripe API: multiply by 100 and round to zero decimal places. Never pass decimal values like 29.99 to Stripe's amount field.
- Set up Privacy Rules on any data type that stores Stripe IDs or payment data — restrict visibility so users can only see their own payment records, not other users' stripe_customer_id or subscription_id values.
- Use Stripe's test mode and test card numbers during development. Switch to live mode only when you are ready to accept real payments and have tested the full checkout, subscription, and refund flows.
- Handle the 'requires_action' PaymentIntent status for EU cards — 3D Secure is mandatory for many European payment methods. Ignoring this status means EU customers will have their payments silently fail.
- Register Stripe webhooks to keep your database in sync automatically — manually polling for subscription status is unreliable and wastes Workload Units. The webhook for customer.subscription.deleted is especially important for access control.
- For Stripe's rate limits, avoid calling the API in tight loops (e.g., iterating through a list of 1000 users and creating a charge for each in rapid succession). Use Bubble's scheduled workflows with built-in delays for bulk operations.

## Use cases

### One-Time Product Purchase

A Bubble e-commerce or course platform charges customers for individual products. The Stripe.js plugin collects the card, and an API Connector workflow creates a PaymentIntent for the exact price. On success, the Order record in Bubble is marked as paid and the product is unlocked.

Prompt example:

```
Build a Bubble checkout workflow: when the user clicks 'Buy Now', call the Stripe plugin's 'Create PaymentMethod' action to get a pm_* token, then call the API Connector to POST /v1/payment_intents with amount=2999, currency=usd, payment_method=pm_*, confirm=true. If the response status is succeeded, update the Order's payment_status to Paid.
```

### SaaS Subscription Billing

A Bubble SaaS app charges users monthly or annually. The flow creates a Stripe Customer, attaches the payment method, then creates a Subscription with a Price ID from the Stripe Dashboard. Bubble's Backend Workflow receives invoice.paid and customer.subscription.deleted webhooks to keep access permissions in sync.

Prompt example:

```
Set up a Bubble workflow that creates a Stripe Customer (POST /v1/customers with email and payment_method), then creates a Subscription (POST /v1/subscriptions with customer ID and items[0][price]=price_abc123), stores the subscription_id in Bubble, and sends the user a welcome email.
```

### Refund Management Dashboard

An admin panel in Bubble lets the team issue partial or full refunds for orders. The workflow looks up the PaymentIntent ID stored in the Order record, calls POST /v1/refunds via the API Connector, and updates the order status to Refunded. Stripe confirms the refund in the webhook.

Prompt example:

```
Create a Bubble admin workflow for a 'Refund Order' button: find the Order's stripe_payment_intent_id, call the API Connector POST /v1/refunds with payment_intent=the_pi_id (and optionally amount for partial refunds), then set the Order's status to Refunded and send a confirmation email.
```

## Troubleshooting

### Stripe PaymentIntent fails with 'Your card was declined' or 'Invalid integer' error

Cause: Either the card number is a real card in test mode (test mode only accepts test card numbers), or the amount value passed to Stripe is a decimal (e.g., 29.99) instead of an integer in cents (2999).

Solution: Use only Stripe's official test card numbers in test mode — 4242 4242 4242 4242 is the universal success card. For the amount error, verify that your Bubble workflow converts the price to cents (multiply by 100) and rounds to zero decimal places before passing to the API Connector. The expression should be: 'Price field * 100 rounded to 0 decimal places'.

```
// Correct amount calculation in Bubble:
// Expression: Price * 100
// Rounding: rounded to 0 decimal places
// Result for $29.99: 2999 (integer, correct)
// Result without rounding: 2999.0 (Stripe rejects as non-integer)
```

### The Stripe secret key appears in the browser's network tab or console

Cause: The secret key was placed in a plugin field or an API Connector header WITHOUT the Private checkbox checked. Any non-Private header value in Bubble's API Connector is included in browser-side requests.

Solution: Go to Plugins → API Connector → Stripe group → Authorization header. Make sure the 'Private' checkbox is checked. If you also accidentally added sk_* to a plugin field (like the Stripe.js plugin's key field), remove it immediately — the plugin only accepts publishable keys. Change your Stripe secret key in the Stripe Dashboard after any exposure.

### 'There was an issue setting up your call' when initializing the PaymentIntent API Connector call

Cause: The Initialize call sent insufficient or invalid test parameters to Stripe, causing Stripe to return an error rather than a successful response. Bubble cannot detect response fields from an error response.

Solution: For initializing the Create PaymentIntent call, use these exact test values: amount=100, currency=usd, payment_method=pm_card_visa, confirm=true. Stripe's pm_card_visa is a test-only payment method token that works for initialization without a real card session. Click Initialize and Bubble will detect the id, status, amount, and other fields from a successful response.

```
// Test values for PaymentIntent Initialize call:
// amount: 100
// currency: usd
// payment_method: pm_card_visa
// confirm: true
// Expected response status: succeeded
```

### EU payments fail or get stuck — PaymentIntent status is 'requires_action' instead of 'succeeded'

Cause: The card requires 3D Secure (SCA) authentication — common for European cards. When confirm=true, Stripe immediately charges, but if the card requires 3DS, it returns requires_action status instead of succeeded.

Solution: Install the Toolbox plugin in Bubble. After the PaymentIntent call, add a condition: Only when Result's status = requires_action → Run JavaScript: stripe.handleNextAction({clientSecret: '[Result's client_secret]'}). This opens Stripe's 3DS authentication popup. After the user authenticates, check the PaymentIntent status again. Alternatively, use Stripe Checkout (hosted page) which handles 3DS automatically.

### Backend Workflow is not receiving Stripe webhooks

Cause: Either Backend Workflows are not enabled in Settings → API, or the webhook URL registered in Stripe Dashboard is incorrect, or the Bubble app is on the Free plan which does not support Backend Workflows.

Solution: Check Settings → API → 'This app exposes a Workflow API' is checked and saved. Verify the exact endpoint URL in Stripe Dashboard matches your Bubble Backend Workflow name. The URL format is https://yourapp.bubbleapps.io/api/1.1/wf/{workflow_name} — case-sensitive. If on the Free plan, upgrade to Starter to enable Backend Workflows.

## Frequently asked questions

### Do I need the paid Zeroqode Stripe plugin, or is the free Stripe.js plugin enough?

The free Stripe.js plugin is sufficient for one-time payments and basic card collection. The Zeroqode Stripe Payments plugin ($39 one-time) adds pre-built subscription management UI elements, better Payment Sheet styling, and webhook event type helpers that can save hours of development time. For a production SaaS or e-commerce app, the Zeroqode plugin is often worth the cost.

### Can I use Stripe without the plugin by putting everything in the API Connector?

Not for card collection. Stripe's card input fields must run in the browser as part of their PCI compliance requirements — you cannot collect raw card numbers through Bubble's server-side API Connector. The plugin (or any Stripe.js-based element) is required for the client-side tokenization step. Once you have a pm_* PaymentMethod token, all subsequent operations can run through the API Connector.

### How do I issue a refund in Bubble?

Add a Create Refund call to your Stripe API Connector group: POST https://api.stripe.com/v1/refunds with the payment_intent ID from your Bubble database. For a full refund, pass only the payment_intent parameter. For a partial refund, also pass the amount in cents. Create a workflow (typically on an admin page) that retrieves the payment_intent_id from the Order record and calls this API Connector action.

### How do I know which Bubble plan I need for Stripe?

Basic one-time payments work on all Bubble plans, including Free. To receive Stripe webhook events (for subscription lifecycle, payment confirmations), you need Backend Workflows, which require a paid plan (Starter $32/month or above). If your app has recurring billing, webhooks are essential — budget for a paid Bubble plan from the start.

### Is it safe to run Stripe payments through Bubble's servers?

Yes. Bubble's API Connector with Private headers is PCI-compliant for transmitting payment data. Your secret key is encrypted and stored server-side. The card numbers never pass through Bubble's servers — only the pm_* token (which is useless without your secret key) does. Stripe's own security infrastructure handles the actual card data. Bubble has been used to build payment-processing apps by thousands of businesses.

---

Source: https://www.rapidevelopers.com/bubble-integrations/stripe
© RapidDev — https://www.rapidevelopers.com/bubble-integrations/stripe
