> ## Documentation Index
> Fetch the complete documentation index at: https://vlyai-1c28d863.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Stripe

> Add Stripe payments to your vly app — one-time charges, subscriptions, marketplaces, billing portals. Test mode by default, production with one switch.

export const Prerequisites = ({items}) => <Note>
    <strong>Before you start.</strong>
    <ul>
      {(items || ["A vly.ai account (free tier is enough)", "A modern browser (Chrome, Firefox, Safari, Edge — all current versions)", "About 10 minutes"]).map((item, i) => <li key={i}>{item}</li>)}
    </ul>
  </Note>;

[Stripe](https://stripe.com) is the standard for online payments. vly's Stripe integration covers the full surface: one-time charges, subscriptions, marketplaces (Connect), billing portals, and webhook handling. You can add Stripe to any project in three lines of prompt.

## Prerequisites

<Prerequisites
  items={[
"A vly project (free tier works for development)",
"A Stripe account (free; sign up at stripe.com)",
"Stripe test mode keys for development; switch to live keys when ready"
]}
/>

## Setup

<Steps>
  <Step title="Get your Stripe API keys">
    In your [Stripe dashboard](https://dashboard.stripe.com/test/apikeys), copy:

    * **Publishable key** (starts with `pk_test_...` or `pk_live_...`)
    * **Secret key** (starts with `sk_test_...` or `sk_live_...`)

    Use **test mode** keys (`pk_test_...`, `sk_test_...`) for development.
  </Step>

  <Step title="Connect Stripe in vly">
    Two paths:

    <Tabs>
      <Tab title="By prompt (recommended)">
        ```text theme={null}
        Add Stripe payments. Use the test mode keys for now.
        ```

        vly opens an integration setup dialog, asks for both keys, and stores them as encrypted environment variables.
      </Tab>

      <Tab title="From the integrations dashboard">
        1. Open your project.
        2. **Settings → Integrations → Stripe**.
        3. Paste your publishable and secret keys.
        4. Click **Connect**.
      </Tab>
    </Tabs>
  </Step>

  <Step title="Define your products and prices in Stripe">
    In the Stripe dashboard → Products, create the products you want to sell. For subscriptions, configure recurring prices.

    Note the **Price IDs** (starts with `price_...`). You'll reference these in vly.
  </Step>

  <Step title="Ask vly to wire up checkout">
    ```text theme={null}
    Add a Pro subscription tier ($10/month) using Stripe.
    The price ID is price_1AbC...

    On the pricing page, add an "Upgrade to Pro" button.
    Clicking it starts a Stripe Checkout session and redirects.

    On successful checkout, mark the user's plan as 'pro'
    in the database.
    ```

    vly creates the checkout session action, the success/cancel routes, the webhook handler, and the user-facing button.
  </Step>

  <Step title="Set up the webhook endpoint">
    vly tells you the webhook URL after the previous step:

    ```
    https://your-app.vly.app/api/stripe-webhook
    ```

    1. In Stripe → Developers → Webhooks, click **Add endpoint**.
    2. Paste the URL above.
    3. Select events: `checkout.session.completed`, `customer.subscription.updated`, `customer.subscription.deleted`, `invoice.payment_failed`.
    4. Copy the **signing secret** (starts with `whsec_...`).
    5. Paste it back into vly's integration settings as `STRIPE_WEBHOOK_SECRET`.

    Test the webhook by clicking **Send test webhook** in the Stripe dashboard.
  </Step>

  <Step title="Test the flow end-to-end">
    1. Open your preview deploy.
    2. Sign up as a test user.
    3. Click **Upgrade to Pro**.
    4. Use Stripe's test card: `4242 4242 4242 4242`, any future expiry, any CVC.
    5. After successful checkout, refresh — your user should now show the Pro plan.

    <Tip>
      Stripe has a [full list of test cards](https://stripe.com/docs/testing#cards) for testing failures, 3D Secure, and other edge cases.
    </Tip>
  </Step>
</Steps>

## Configuration reference

<ParamField path="STRIPE_PUBLISHABLE_KEY" type="string" required>
  Your Stripe publishable key. Safe to expose to the client. Starts with `pk_test_` or `pk_live_`.
</ParamField>

<ParamField path="STRIPE_SECRET_KEY" type="string" required>
  Your Stripe secret key. Server-only. Starts with `sk_test_` or `sk_live_`.
</ParamField>

<ParamField path="STRIPE_WEBHOOK_SECRET" type="string" required>
  Your Stripe webhook signing secret. Server-only. Starts with `whsec_`. Required to verify webhook authenticity.
</ParamField>

<ParamField path="STRIPE_*_PRICE_ID" type="string">
  The Stripe price IDs you'll reference. Add one per product / tier — e.g., `STRIPE_PRO_PRICE_ID`, `STRIPE_BUSINESS_PRICE_ID`.
</ParamField>

## Code patterns

The patterns vly generates. You can hand-edit any of them in [Code mode](/building/code-mode).

### Creating a checkout session

```typescript convex/stripe.ts theme={null}
import { action } from "./_generated/server";
import { v } from "convex/values";
import Stripe from "stripe";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

export const createCheckoutSession = action({
  args: { priceId: v.string(), userId: v.id("users") },
  handler: async (ctx, { priceId, userId }) => {
    const user = await ctx.runQuery(api.users.get, { id: userId });
    if (!user) throw new Error("User not found");

    const session = await stripe.checkout.sessions.create({
      mode: "subscription",
      line_items: [{ price: priceId, quantity: 1 }],
      customer_email: user.email,
      client_reference_id: userId,
      success_url: `${process.env.APP_URL}/billing/success?session_id={CHECKOUT_SESSION_ID}`,
      cancel_url:  `${process.env.APP_URL}/billing/cancelled`,
    });

    return session.url;
  },
});
```

### Webhook handler

```typescript convex/http.ts theme={null}
import { httpRouter } from "convex/server";
import { httpAction } from "./_generated/server";
import Stripe from "stripe";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
const http   = httpRouter();

http.route({
  path: "/api/stripe-webhook",
  method: "POST",
  handler: httpAction(async (ctx, req) => {
    const sig    = req.headers.get("stripe-signature")!;
    const body   = await req.text();
    let   event: Stripe.Event;

    try {
      event = stripe.webhooks.constructEvent(
        body, sig, process.env.STRIPE_WEBHOOK_SECRET!,
      );
    } catch {
      return new Response("Bad signature", { status: 400 });
    }

    if (event.type === "checkout.session.completed") {
      const session = event.data.object as Stripe.Checkout.Session;
      await ctx.runMutation(api.users.setPlan, {
        userId: session.client_reference_id as Id<"users">,
        plan: "pro",
      });
    }
    // ... handle other events

    return new Response(null, { status: 200 });
  }),
});

export default http;
```

### Customer billing portal

```typescript convex/stripe.ts theme={null}
export const createPortalSession = action({
  args: { userId: v.id("users") },
  handler: async (ctx, { userId }) => {
    const user = await ctx.runQuery(api.users.get, { id: userId });
    if (!user?.stripeCustomerId) throw new Error("No subscription");

    const session = await stripe.billingPortal.sessions.create({
      customer:    user.stripeCustomerId,
      return_url:  `${process.env.APP_URL}/settings/billing`,
    });

    return session.url;
  },
});
```

The portal handles upgrade, downgrade, payment method updates, invoices — all built by Stripe, no UI work on your side.

## Going live

When you're ready for real payments:

<Steps>
  <Step title="Activate your Stripe account">
    In the Stripe dashboard, complete account verification (business details, bank account, identity).
  </Step>

  <Step title="Get live mode keys">
    Switch the Stripe dashboard to **live mode** (top-right toggle). Copy `pk_live_...` and `sk_live_...`.
  </Step>

  <Step title="Update vly's secrets for production">
    In vly, **Settings → Environments → Production → Edit secrets**. Replace the test keys with the live keys.
  </Step>

  <Step title="Register a production webhook">
    Add a webhook endpoint in Stripe (live mode), pointing to your production URL: `https://yourapp.com/api/stripe-webhook`. Copy the new signing secret into production secrets.
  </Step>

  <Step title="Switch test → live in your code">
    No code change needed if you've used `process.env.STRIPE_*` everywhere — the secrets just point to live keys now.
  </Step>

  <Step title="Test with a real card">
    Make a small charge on a real card (you can refund yourself). Confirms end-to-end live flow.
  </Step>
</Steps>

## Common features

<AccordionGroup>
  <Accordion title="Subscriptions" icon="repeat">
    Recurring billing, trial periods, prorating on upgrade/downgrade. Use the [billing portal](#customer-billing-portal) for self-service.
  </Accordion>

  <Accordion title="Marketplaces (Connect)" icon="users">
    Multi-party payments — buyers pay sellers, you take a cut. Use Stripe Connect with Express or Standard accounts. See the [marketplace recipe](/recipes/marketplace-with-stripe).
  </Accordion>

  <Accordion title="Invoicing" icon="receipt">
    Bill customers manually or send recurring invoices. Useful for B2B contracts.
  </Accordion>

  <Accordion title="Tax" icon="percent">
    Stripe Tax automatically calculates and remits sales tax / VAT. Enable in Stripe dashboard; vly's checkout supports it out of the box.
  </Accordion>

  <Accordion title="Refunds" icon="undo">
    `stripe.refunds.create({ charge: chargeId })`. Add an admin-only "Refund" action in your support tool.
  </Accordion>

  <Accordion title="Disputes / chargebacks" icon="alert-triangle">
    Stripe sends webhook events when disputes are filed. Vly can wire these into a Slack notification or admin dashboard.
  </Accordion>
</AccordionGroup>

## Troubleshooting

<AccordionGroup>
  <Accordion title="Webhook says 'No signatures found matching the expected signature'" icon="alert-triangle">
    The `STRIPE_WEBHOOK_SECRET` doesn't match the webhook endpoint. Each endpoint has its own secret — re-copy from the Stripe dashboard for the correct endpoint.
  </Accordion>

  <Accordion title="Checkout shows 'No such price'" icon="alert-triangle">
    The price ID exists in test mode but you're using live keys (or vice versa). Recreate the price in the right mode, or switch keys to match.
  </Accordion>

  <Accordion title="Customer's subscription doesn't update after billing portal change" icon="alert-triangle">
    The portal change triggers a `customer.subscription.updated` webhook. Make sure your webhook handler is processing this event and updating your local database.
  </Accordion>

  <Accordion title="Test mode webhooks not arriving" icon="alert-triangle">
    Confirm the webhook URL is reachable (not behind auth). For local development, use the [Stripe CLI](https://stripe.com/docs/stripe-cli) to forward events to localhost.
  </Accordion>
</AccordionGroup>

## Related

<CardGroup cols={3}>
  <Card title="Marketplace recipe" icon="utensils-crossed" href="/recipes/marketplace-with-stripe">
    Complete walkthrough — Stripe Connect, escrow, payouts.
  </Card>

  <Card title="Subscription billing recipe" icon="repeat" href="/recipes/subscription-billing">
    Multi-tier, trials, billing portal.
  </Card>

  <Card title="Webhooks pattern" icon="webhook" href="/recipes/webhooks">
    The general pattern for any webhook-based integration.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.