---
title: "Stripe"
description: "Use Stripe for MainSDK payments and usage metering - test and live keys, main.yaml, webhook signing secrets, local testing and swapping."
section: "integrations"
canonical: "https://mainsdk.dev/docs/integrations/stripe"
updated: "2026-10-28"
---

# Stripe

> Use Stripe for MainSDK payments and usage metering - test and live keys, main.yaml, webhook signing secrets, local testing and swapping.

Canonical: https://mainsdk.dev/docs/integrations/stripe · Updated: 2026-10-28

Stripe is the default provider behind [`pay`](https://mainsdk.dev/docs/interfaces/pay) and one of the two drivers behind [`meter`](https://mainsdk.dev/docs/interfaces/meter). Your app starts a hosted checkout, a subscription or a refund through `pay`, and Stripe holds the cards, customers and subscriptions. Pick Stripe when you sell to customers outside India, or as the default next to Razorpay for India. Pick its meters when your pricing charges by usage and you already bill through Stripe.

## Interfaces it serves

### Payments (`pay`)

- Status: Building for 0.3 · ships as a stable API
- Driver: `stripe` (Stripe, default)
- Docs: https://mainsdk.dev/docs/interfaces/pay.md

```yaml
payments:
  driver: stripe
```

Swap to another provider: `razorpay`, `paddle`, `local`.

### Usage metering (`meter`)

- Status: Building for 0.3 · ships as a beta API
- Driver: `stripe-meters` (Stripe Billing meters (export through pay))
- Docs: https://mainsdk.dev/docs/interfaces/meter.md

```yaml
meter:
  driver: stripe-meters
```

Swap to another provider: `db`, `redis`, `local`, `lago`.

## Before you start

You need a Stripe account. Stripe gives every account a test mode and a live mode, each with its own keys, objects and webhook endpoints.

1. **Get your API keys.** In the Stripe Dashboard, open the developers area and copy the secret key for test mode. You get the live secret key once your account is activated for payments. Stripe also lets you create restricted keys with narrower permissions; use one if your team's policy calls for it.
2. **Create a webhook endpoint.** In the webhooks section of the developers area, add an endpoint that points at the URL where your app receives `pay` events, and subscribe it to the payment, refund and subscription events your app handles. How `pay.on()` handlers are mounted on a route is not defined yet [TBD]. Stripe shows a signing secret for each endpoint; copy it.
3. **Repeat for live mode.** Test and live endpoints are separate, and each has its own signing secret.
4. **For metering:** set up the meters and usage-based prices you bill on in Stripe Billing.

Store the keys in your secrets manager, never in code or a committed env file.

## Configure

Payments route by market under `payments:`; Stripe's settings live under `pay.stripe`.

```yaml title="main.yaml"
payments: { default: stripe }
pay:
  stripe:
    key: secret:STRIPE_SECRET_KEY          # suggested name; the secret API key
    webhook: secret:STRIPE_WEBHOOK_SECRET  # suggested name; the endpoint's signing secret
```

Use test-mode values in development and stage, and live values in production, by pointing each environment's secrets at the right keys. A literal key in `main.yaml` fails `mainsdk check` with `MAIN-CONFIG-0004`.

For usage metering:

```yaml title="main.yaml"
meter:
  driver: stripe-meters
```

The `meter` interface has no spec yet, so how the driver gets its key and how metrics map to Stripe meters are [TBD].

## Use it

Start a hosted checkout. Card details stay on Stripe's page or hosted fields.

```ts title="app/checkout/actions.ts"
"use server";
import { auth, pay } from "mainsdk";
import { redirect } from "next/navigation";

export async function startCheckout(req: Request, orderId: string) {
  await auth.require(req);
  const session = await pay.checkout({
    order: orderId,
    amount: 4900,              // integer minor units
    currency: "USD",           // ISO 4217
    successUrl: "/orders/done",
    cancelUrl: "/cart",
  });
  if ("url" in session) redirect(session.url);
  return session;              // { clientSecret } for hosted fields
}
```

React to verified Stripe events with `pay.on()`:

```ts title="app/api/pay/events.ts"
import { pay } from "mainsdk";

pay.on("payment.succeeded", async (event) => {
  // runs only after the Stripe signature and timestamp are checked, once per event
});
```

Record usage for Stripe meters:

```ts title="lib/usage.ts"
import { meter } from "mainsdk";

meter.record("api_calls", 1, { feature: "export" });
```

## Local development and testing

- **No Stripe account:** `mainsdk dev` with no provider key runs the local test card flow, which completes a checkout and fires `payment.succeeded`.
- **Stripe test mode:** use your test-mode secret key and signing secret to try the real hosted checkout with Stripe's test cards. To receive webhooks on your machine, the Stripe CLI can forward test events to a local URL and prints a signing secret for that session.
- **Tests:** `createTestApp()` from `@mainsdk/testing` runs `pay` on its in-memory fake, and `chaos.on("pay", { outage: true })` simulates a Stripe outage.

```ts title="app/checkout/actions.test.ts"
import { it } from "node:test";
import { createTestApp, expect } from "@mainsdk/testing";

it("charges once when checkout is retried", async () => {
  const app = createTestApp({ main: "./main.yaml" });
  // ... run your checkout action twice
  expect(app.pay).toHaveCharged({ amount: 4900, currency: "USD", times: 1 });
});
```

## What MainSDK adds on top

- Checkout only on Stripe's hosted page or hosted fields; card numbers never touch your server, and card handling in your code fails `MAIN-PAY-0001`.
- Every Stripe webhook verified (signature and a 5-minute timestamp window) before your handler runs; unverified calls get 400 and are logged, and duplicates run your handler once.
- Idempotency keys on checkout, subscribe and refund, passed to Stripe's own idempotency, so a retry never charges or refunds twice.
- Amounts in integer minor units with an ISO 4217 currency.

See [Payments](https://mainsdk.dev/docs/interfaces/pay) for the full contract.

## Limits and notes

- Stripe's availability depends on the country of your business and of your customers; check that Stripe supports both.
- Use Razorpay for UPI and India-first checkout, routed by market so one deployment serves both.
- `pay.raw()` returns the Stripe client for features `pay` does not wrap, such as Stripe-only billing settings. `mainsdk check` lists every use.
- Store purchases (Play Billing, StoreKit) are not part of `pay@1`.

## Swap to or from it

Route a market to another provider, or change the default, in one line:

```yaml title="main.yaml"
payments: { in: razorpay, default: stripe }   # was { default: stripe }
```

> **Warning:** Payments are stateful. Customers, saved payment methods and subscriptions live in Stripe, so moving existing ones to another provider is a migration that MainSDK runs as a guided job with a rollback, not a silent switch. Routing new checkouts to a different provider needs no migration.

## FAQ

**Do I need a Stripe account to start building?** No. `mainsdk dev` runs a local test card flow with no key, and tests use the in-memory fake. You need an account when you want the real hosted checkout.

**Can I use Stripe and Razorpay in the same app?** Yes. Route by market with `payments: { in: razorpay, default: stripe }`. Each request uses the provider for the user's market.

**Can I build my own card form on Stripe Elements?** Only with Stripe's hosted fields, which keep card data on Stripe. A card-number input that posts to your own route fails `MAIN-PAY-0001`.

## Next

- [Payments](https://mainsdk.dev/docs/interfaces/pay) and [Usage metering](https://mainsdk.dev/docs/interfaces/meter)
- [Razorpay](https://mainsdk.dev/docs/integrations/razorpay), [Paddle](https://mainsdk.dev/docs/integrations/paddle) and [Lago](https://mainsdk.dev/docs/integrations/lago)
- [Swap a driver](https://mainsdk.dev/docs/guides/swap-a-driver)
