> ## Documentation Index
> Fetch the complete documentation index at: https://docs.shipthing.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Stripe

> Learn how to set up Stripe with ShipThing.

ShipThing uses [Stripe](https://stripe.com/) by default for payments and billing. Implementing Stripe in your project is straightforward.

## Get the api key

After you have created your account for Stripe, you will need to get the API key. You can do this by going to the [API page](https://dashboard.stripe.com/apikeys) in the dashboard. Here you will find the `Secret key` and the `Publishable key`. You will need the `Secret key` for the integration to work.

### Add environment variables

To use the Stripe integration, you need to define the following environment variables to your `.env.local` as well as your production environment:

```env .env.local theme={"system"}
STRIPE_SECRET_KEY=
STRIPE_WEBHOOK_SECRET=
```

## In-App Purchases

You can use Stripe anywhere in your app by importing the `stripe` object like so:

```ts page.tsx {1,5} theme={"system"}
import { stripe } from '@repo/payments';

// ...

await stripe.prices.list();
```

### Creating a Checkout Session

We need to create a checkout session to charge your users , and we have implemented this whole process for you. It consists of two parts:

* Client

It sends a request to create a session to the `/api/stripe/checkout` endpoint.

```tsx apps/web/[locale]/components/payment/price-form.tsx theme={"system"}
      const response = await fetch('/api/stripe/checkout', {
        method: 'POST',
        body: JSON.stringify({
          priceId: item.priceId,
          interval: item.interval,
        }),
      });
```

* Server

```ts apps/web/api/stripe/checkout/route.ts theme={"system"}
  const session = await stripe.checkout.sessions.create({
    line_items: [{ price: priceId, quantity: 1 }],
    mode: interval === 'one-time' ? 'payment' : 'subscription',
    success_url: `${env.NEXT_PUBLIC_APP_URL}/login`,
    cancel_url: `${env.NEXT_PUBLIC_WEB_URL}/#pricing`,
  });
```

<Tip>
  This is out of the box, and if you want to customize, you can modify both files.
</Tip>

<Tip>
  Stripe allows using the product's Buy Link without creating a checkout session through the API. But create the checkout session with API is recommended for better tracking and analytics.
</Tip>

## Webhooks

### Create a webhook

To configure a new webhook, go to the [Webhooks page](https://dashboard.stripe.com/webhooks) in the Stripe dashboard. Click the Add endpoint button and select at least the following events:

For subscriptions:

* `customer.subscription.created`
* `customer.subscription.updated`
* `customer.subscription.deleted`

For one-off payments:

* `checkout.session.completed`

### Webhook Handler

Stripe webhooks are handled in the `POST /webhooks/stripe` route in the `apps/api` app. This route constructs the event and then switches on the event type to determine how to process the event.

<Note>
  We have implemented the basic structure of the `Webhook` handler for you. You can modify it as needed.
</Note>

### Local Development

To test webhooks locally, we've configured the [Stripe CLI](https://stripe.com/docs/stripe-cli) in `apps/api` app to forward webhooks to your local server. This will start automatically when you run `pnpm dev`.

## Anti-Fraud

As your app grows, you will inevitably encounter credit card fraud. [Stripe Radar](https://stripe.com/radar) is enabled by default if you integrate payments using their SDK as described above. This provides a set of tools to help you detect and prevent fraud.

Stripe Radar supports more [advanced anti-fraud features](https://docs.stripe.com/disputes/prevention/advanced-fraud-detection) if you embed the Stripe JS script in every page load. This is not enabled by default in ShipThing, but you can add it as follows:

<Steps>
  <Step title="Edit the layout">
    Edit `apps/app/app/layout.tsx` and add `<Script src="https://js.stripe.com/v3/" />` after the opening `<html>` tag and before the opening `<body>` tag. You will also need to add `import Script from 'next/script'`

    ```tsx {5, 13} theme={"system"}
    import '@repo/design-system/styles/globals.css';
    import { DesignSystemProvider } from '@repo/design-system';
    import { fonts } from '@repo/design-system/lib/fonts';
    import type { ReactNode } from 'react';
    import Script from 'next/script';

    type RootLayoutProperties = {
      readonly children: ReactNode;
    };

    const RootLayout = ({ children }: RootLayoutProperties) => (
      <html lang="en" className={fonts} suppressHydrationWarning>
        <Script src="https://js.stripe.com/v3/" />
        <body>
          <DesignSystemProvider>{children}</DesignSystemProvider>
        </body>
      </html>
    );

    export default RootLayout;
    ```
  </Step>

  <Step title="Add script to the website">
    Add the same script to the website in `apps/web/app/layout.tsx`.
  </Step>

  <Step title="Prevent common fraud patterns with Arcjet">
    Prevent common fraud patterns by using [Arcjet](/packages/security/application) [IP address analysis](https://docs.arcjet.com/reference/nextjs#ip-analysis) to [block requests from VPNs and proxies](https://docs.arcjet.com/blueprints/vpn-proxy-detection). These are commonly used by fraudsters to hide their location, but have legitimate uses as well so are not blocked by default. You could simply block these users, or you could adjust the checkout process to require approval before processing their payment.
  </Step>
</Steps>

For example, in `apps/app/app/(authenticated)/layout.tsx` you could add this after the call to `aj.protect()`:

```ts theme={"system"}
import { redirect } from 'next/navigation';
// ...

if (
  decision.ip.isHosting() ||
  decision.ip.isVpn() ||
  decision.ip.isProxy() ||
  decision.ip.isRelay()
) {
  // The IP is from a hosting provider, VPN, or proxy. We can check the name
  // of the service and customize the response
  if (decision.ip.hasService()) {
    if (decision.ip.service !== 'Apple Private Relay') {
      // We trust Apple Private Relay because it requires an active iCloud
      // subscription, so deny all other VPNs
      redirect('/vpn-blocked');
    }
  } else {
    // The service name is not available, but we still think it's a VPN
    redirect('/vpn-blocked');
  }
}
```

In this case we are providing a friendly redirect to a page that explains why the user is being blocked (which you would need to create). You could also return a more generic error message. [See the Arcjet documentation](https://docs.arcjet.com/blueprints/payment-form#additional-checks) for more advanced examples.
