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

# Switch to Paddle Billing

> How to change the default payment processor to Paddle Billing.

export const Authors = ({data}) => {
  const baseUrl = typeof window !== 'undefined' && window.location.origin.includes('localhost') ? '' : 'https://raw.githubusercontent.com/haydenbleasel/JTBDOS/refs/heads/main/docs';
  return <div style={{
    marginBottom: '3rem',
    display: 'flex',
    flexDirection: 'column',
    gap: '0.5rem'
  }}>
      <span style={{
    color: 'rgb(107, 114, 128)',
    fontSize: '0.875rem'
  }}>Co-authored by</span>
      <div style={{
    display: 'flex',
    flexWrap: 'wrap',
    alignItems: 'center',
    gap: '0.5rem'
  }}>
        {data.map(author => <div key={author.name} style={{
    padding: '0.75rem',
    paddingRight: '1rem',
    display: 'inline-flex',
    alignItems: 'center',
    gap: '0.75rem',
    fontWeight: 'normal',
    position: 'relative',
    ringWidth: '2px',
    ringColor: 'transparent',
    borderRadius: '0.75rem',
    backgroundColor: 'white',
    border: '1px solid rgba(0,0,0,0.1)',
    overflow: 'hidden'
  }}>
            <div style={{
    position: 'relative'
  }}>
              <div style={{
    overflow: 'hidden',
    border: '1px solid #e5e7eb',
    borderRadius: '9999px',
    width: '2rem',
    height: '2rem'
  }}>
                <img style={{
    margin: 0,
    width: '100%',
    height: '100%',
    objectFit: 'cover'
  }} src={`${baseUrl}/images/authors/${author.company.id}/${author.user.id}.jpg`} alt="" width={32} height={32} />
              </div>
              <div style={{
    position: 'absolute',
    border: '1px solid white',
    overflow: 'hidden',
    borderRadius: '9999px',
    objectFit: 'cover',
    width: '1rem',
    height: '1rem',
    right: '-0.25rem',
    bottom: '-0.25rem'
  }}>
                <img style={{
    margin: 0,
    width: '100%',
    height: '100%',
    objectFit: 'cover'
  }} src={`${baseUrl}/images/authors/${author.company.id}/logo.jpg`} alt="" width={16} height={16} />
              </div>
            </div>
            <div style={{
    display: 'flex',
    flexDirection: 'column'
  }}>
              <span style={{
    fontWeight: 600,
    lineHeight: 1.25,
    fontSize: '13px',
    letterSpacing: '-0.01em'
  }}>
                {author.user.name}
              </span>
              <span style={{
    color: 'rgb(107, 114, 128)',
    lineHeight: 1.25,
    fontSize: '11px'
  }}>
                {author.company.name}
              </span>
            </div>
          </div>)}
      </div>
    </div>;
};

<Authors
  data={[{
user: {
name: 'Hayden Bleasel',
id: 'haydenbleasel',
},
company: {
name: 'JTBDOS',
id: 'JTBDOS',
},
}, {
user: {
name: 'Dan Billson',
id: 'danbillson',
},
company: {
name: 'Paddle',
id: 'paddle',
},
}, {
user: {
name: 'Michael McGovern',
id: 'heymcgovern',
},
company: {
name: 'Paddle',
id: 'paddle',
}}]}
/>

[Paddle Billing](https://www.paddle.com/) is a merchant of record for selling digital products and subscriptions. It takes care of payments, global tax compliance, fraud prevention, localization, and subscriptions. Here's how to switch the default payment processor from Stripe to Paddle Billing.

<Note>
  This guide is for Paddle Billing, which is the latest version of Paddle. It doesn't include Paddle Classic.
</Note>

## 1. Swap out the required dependencies

First, uninstall the existing dependencies from the Payments package...

```sh Terminal
pnpm remove stripe --filter @repo/payments
```

... and install the new dependencies...

```sh Terminal
pnpm add @paddle/paddle-node-sdk --filter @repo/payments
```

## 2. Update the Payment keys

Update the required Payment keys in the `packages/payments/keys.ts` file:

```ts packages/payments/keys.ts
import { createEnv } from '@t3-oss/env-nextjs';
import { Environment } from '@paddle/paddle-node-sdk'
import { z } from 'zod';

export const keys = () =>
  createEnv({
    server: {
      PADDLE_SECRET_KEY: z.string().min(1),
      PADDLE_WEBHOOK_SECRET: z.string().min(1).optional(),
      PADDLE_ENV: z.enum([Environment.sandbox, Environment.production]).optional(),
    },
    client: {
      NEXT_PUBLIC_PADDLE_CLIENT_TOKEN: z
        .union([
          z.string().min(1).startsWith('live_'),
          z.string().min(1).startsWith('test_'),
        ]),
      NEXT_PUBLIC_PADDLE_ENV: z.enum([Environment.sandbox, Environment.production]).optional(),
    },
    runtimeEnv: {
      PADDLE_SECRET_KEY: process.env.PADDLE_SECRET_KEY,
      PADDLE_WEBHOOK_SECRET: process.env.PADDLE_WEBHOOK_SECRET,
      PADDLE_ENV: process.env.PADDLE_ENV,
      NEXT_PUBLIC_PADDLE_ENV: process.env.NEXT_PUBLIC_PADDLE_ENV,
      NEXT_PUBLIC_PADDLE_CLIENT_TOKEN: process.env.NEXT_PUBLIC_PADDLE_CLIENT_TOKEN,
    },
  });

```

## 3. Update the environment variables

Next, update the environment variables across the project, replacing the existing Stripe keys with the new Paddle keys:

```js apps/app/.env
# Server
PADDLE_SECRET_KEY=""
PADDLE_WEBHOOK_SECRET=""
PADDLE_ENV="sandbox"

# Client
NEXT_PUBLIC_PADDLE_ENV="sandbox"
NEXT_PUBLIC_PADDLE_CLIENT_TOKEN="test_"
```

## 4. Update the payments client

Initialize the payments client in the `packages/payments/index.ts` file with the new API key.

```ts packages/payments/index.ts
import 'server-only';
import { type Environment, Paddle } from '@paddle/paddle-node-sdk';
import { keys } from './keys';

const { PADDLE_SECRET_KEY, PADDLE_ENV } = keys();

export const paddle = new Paddle(PADDLE_SECRET_KEY, {
  environment: PADDLE_ENV,
});

export * from '@paddle/paddle-node-sdk';
```

## 5. Update the payments webhook handler

Remove the Stripe webhook handler from the API package...

```sh Terminal
rm apps/api/app/webhooks/stripe/route.ts
```

... and create a new webhook handler for Paddle:

```ts apps/api/app/webhooks/paddle/route.ts
import { env } from '@/env';
import { NextResponse } from 'next/server';
import { headers } from 'next/headers';
import { paddle } from '@repo/payments';

export const POST = async (request: Request) => {
  try {
    const body = await request.text();
    const headerPayload = await headers();
    const signature = headerPayload.get('paddle-signature');

    if (!signature) {
      throw new Error('missing paddle-signature header');
    }

    const event = await paddle.webhooks.unmarshal(
      body,
      env.PADDLE_WEBHOOK_SECRET,
      signature
    );

    switch (event.eventType) {}

    return NextResponse.json({ result: event, ok: true });
  }
};
```

There's quite a lot you can do with Paddle, so check out the following resources for more information:

<Card title="Webhooks Overview" icon="webhook" href="https://developer.paddle.com/webhooks/respond-to-webhooks" horizontal>
  Learn how to handle webhooks from Paddle Billing
</Card>

<Card title="Signature Verification" icon="badge-check" href="https://developer.paddle.com/webhooks/signature-verification" horizontal>
  Learn how to verify webhooks from Paddle Billing
</Card>

<Card title="Simulate Webhooks" icon="vial-circle-check" href="https://developer.paddle.com/webhooks/test-webhooks" horizontal>
  Learn how to send test webhooks from Paddle Billing
</Card>

## 6. Create a Checkout hook

Create a new file for `checkout` and install `paddle-js`:

```sh Terminal
pnpm add @paddle/paddle-js --filter @repo/payments
```

Then, create a new hook to initialize Paddle in the `packages/payments/checkout.tsx` file:

```tsx packages/payments/checkout.tsx
'use client';

import {
  type Environments,
  type Paddle,
  initializePaddle,
} from '@paddle/paddle-js';
import { useEffect, useState } from 'react';
import { keys } from './keys';

const { NEXT_PUBLIC_PADDLE_CLIENT_TOKEN, NEXT_PUBLIC_PADDLE_ENV } = keys();

export function usePaddle() {
  const [paddle, setPaddle] = useState<Paddle>();

  useEffect(() => {
    initializePaddle({
      environment: NEXT_PUBLIC_PADDLE_ENV,
      token: NEXT_PUBLIC_PADDLE_CLIENT_TOKEN,
      checkout: {
        settings: {
          variant: 'one-page',
        },
      },
    }).then((paddleInstance: Paddle | undefined) => {
      if (paddleInstance) {
        setPaddle(paddleInstance);
      }
    });
  }, []);

  return paddle;
}
```

## 7. Use the Checkout hook

Finally, open a checkout on your pricing page:

```tsx apps/web/app/pricing/page.tsx
'use client';

import { usePaddle } from '@repo/payments/checkout';

const Pricing = () => {
  const paddle = usePaddle();

  function openCheckout(priceId: string) {
    paddle?.Checkout.open({
      items: [
        {
          priceId,
          quantity: 1,
        },
      ],
    });
  }

  return (
    <Button
      className="mt-8 gap-4"
      onClick={() => openCheckout('pri_01jkzb4x1hc91s8w38cr3m86yy')}
    >
      Subscribe now <MoveRight className="h-4 w-4" />
    </Button>
  );
};

export default Pricing;
```
