better-auth-integration

Guide only for applications using @dodopayments/better-auth, covering authenticated customer sync, checkout, portal access, usage ingestion, and verified webhook callbacks.

dodopayments/skills49 installsMITSynced Aug 26

Works with

Claude CodeCursorCodex CLIGitHub CopilotGemini CLI
---
name: better-auth-integration
description: Guide only for applications using @dodopayments/better-auth, covering authenticated customer sync, checkout, portal access, usage ingestion, and verified webhook callbacks.
license: MIT
---

# Better Auth Integration

Use `@dodopayments/better-auth` to synchronize Better Auth users with Dodo Payments and expose authenticated checkout, customer portal, subscription, payment, usage, and webhook endpoints.

## When to use this skill

- Create a Dodo customer automatically when a Better Auth user signs up.
- Start checkout for a signed-in user without exposing a Dodo API key.
- Let users open their customer portal or list their subscriptions and payments.
- Submit metered usage for the signed-in customer.
- Receive signature-verified Dodo webhooks through Better Auth.

## Install

```bash
npm install @dodopayments/better-auth dodopayments better-auth
```

The package exports the lowercase server functions `dodopayments`, `checkout`, `portal`, `usage`, and `webhooks`. Its client entry point exports `dodopaymentsClient`.

## Server setup

Every endpoint is registered by a feature in `use`. Include every feature that the application calls; omitting one makes its route return 404.

```typescript
import { betterAuth } from "better-auth";
import DodoPayments from "dodopayments";
import { checkout, dodopayments, portal, usage, webhooks } from "@dodopayments/better-auth";

const dodoPayments = new DodoPayments({
  bearerToken: process.env.DODO_PAYMENTS_API_KEY,
  environment: "test_mode",
});

export const auth = betterAuth({
  database: {
    // your database config
  },
  plugins: [
    dodopayments({
      client: dodoPayments,
      createCustomerOnSignUp: true,
      // `user` here is Better Auth's base user: id, name, email, emailVerified,
      // image, createdAt, updatedAt. Anything else (phone number, company, plan)
      // must first be declared via `user.additionalFields` in your Better Auth
      // config, otherwise it does not exist on the type and will be undefined.
      getCustomerParams: (user) => ({
        metadata: { better_auth_user_id: user.id },
      }),
      use: [
        checkout({
          products: [
            { productId: "pdt_premium", slug: "premium-plan" },
          ],
          successUrl: "https://app.example.com/billing/success",
          authenticatedUsersOnly: true,
        }),
        portal(),
        usage(),
        webhooks({
          webhookKey: process.env.DODO_PAYMENTS_WEBHOOK_KEY,
          onPayload: async (payload) => {
            console.log("Verified Dodo webhook:", payload.type);
          },
        }),
      ],
    }),
  ],
});
```

`products` maps an application slug to a Dodo product ID. `authenticatedUsersOnly: true` requires a Better Auth session for checkout. `getCustomerParams` adds application metadata when customer synchronization runs.

## Browser client setup

```typescript
import { createAuthClient } from "better-auth/react";
import { dodopaymentsClient } from "@dodopayments/better-auth/client";

export const authClient = createAuthClient({
  plugins: [dodopaymentsClient()],
});
```

These are browser methods. They send the Better Auth session cookie automatically; do not pass `request.headers` to them.

## Create a checkout session

```typescript
import { authClient } from "@/lib/auth-client";

async function startCheckout() {
  const { data: session, error } = await authClient.dodopayments.checkoutSession({
    slug: "premium-plan",
    referenceId: "order_123",
  });

  if (error) {
    console.error("Checkout error:", error);
    return;
  }

  if (session) {
    window.location.href = session.url;
  }
}
```

`slug` must match a product configured in `checkout({ products: [...] })`. `referenceId` is optional reconciliation metadata.

## Open the customer portal

```typescript
const { data: portalSession, error } =
  await authClient.dodopayments.customer.portal();

if (error) {
  console.error("Portal error:", error);
} else if (portalSession?.redirect) {
  window.location.href = portalSession.url;
}
```

The portal endpoint requires a signed-in, verified user and resolves that user's synchronized Dodo customer.

## List subscriptions and payments

```typescript
const { data: subscriptions, error: subscriptionsError } =
  await authClient.dodopayments.customer.subscriptions.list({
    query: { page: 1, limit: 10, status: "active" },
  });

const { data: payments, error: paymentsError } =
  await authClient.dodopayments.customer.payments.list({
    query: { page: 1, limit: 10, status: "succeeded" },
  });

if (subscriptionsError || paymentsError) {
  console.error("Could not load billing history");
} else {
  console.log("Subscriptions:", subscriptions?.items);
  console.log("Payments:", payments?.items);
}
```

Both list responses use an `items` envelope. Subscription filters accept `page`, `limit`, and `status`; payment filters use the same pagination fields and payment statuses.

## Ingest metered usage

Enable `usage()` on the server, then submit an event for the signed-in customer:

```typescript
const { data, error } = await authClient.dodopayments.usage.ingest({
  event_id: "evt_usage_123",
  event_name: "api_calls",
  metadata: { requests: 100 },
  timestamp: new Date(),
});

if (error) {
  console.error("Usage ingestion failed:", error);
} else {
  console.log("Ingested events:", data?.ingested_count);
}
```

The plugin derives `customer_id` from the authenticated session. `event_id` is the event's idempotency key; reuse it when retrying the same event.

## Webhook endpoint

`webhooks({ webhookKey, onPayload })` verifies and handles the request itself. With Better Auth's default `/api/auth` base path, configure this URL in the Dodo dashboard:

```text
https://app.example.com/api/auth/dodopayments/webhooks
```

Do not create a second route and do not call `auth.api.dodopayments.verifyWebhook()`; that method does not exist. The plugin reads the raw request, verifies the Standard Webhooks signature with `webhookKey`, and invokes `onPayload` plus any configured event-specific callback.

## Registered endpoints

- `POST /dodopayments/checkout`
- `POST /dodopayments/checkout-session`
- `GET /dodopayments/customer/portal`
- `GET /dodopayments/customer/subscriptions/list`
- `GET /dodopayments/customer/payments/list`
- `POST /dodopayments/usage/ingest`
- `GET /dodopayments/usage/meters/list`
- `POST /dodopayments/webhooks`

Better Auth prefixes these plugin paths with its configured API base path.

## Common mistakes

**Enabling only `portal()`.** Checkout, usage, and webhook routes do not exist unless `checkout()`, `usage()`, and `webhooks()` are also present in `use`.

**Mixing browser and server APIs.** Call `authClient.dodopayments...` in browser code without a `headers` option. Use Better Auth's server API separately when handling server requests.

**Creating duplicate customers.** When `createCustomerOnSignUp` is enabled, do not also call `client.customers.create()` for the same user.

**Granting access from browser state.** Treat verified webhook events or a server-side Dodo lookup as the source of truth for entitlements.

**Adding a custom webhook verifier.** The plugin owns `/dodopayments/webhooks`, signature verification, and callback dispatch.

## Resources

- [Better Auth Adapter](https://docs.dodopayments.com/developer-resources/better-auth-adaptor)
- [Dodo Payments Better Auth package](https://www.npmjs.com/package/@dodopayments/better-auth)
- [Dodo Payments SDK](https://github.com/dodopayments/dodopayments-typescript)

More API Design skills

← All API Design skills

Check your AI visibility

One URL in, a 0–100 score and the exact fixes out.

RUN THE CHECK

Browse all the tools

15 tools across six categories
13 of them never send your data anywhere

Free · No signup · No trial clock

SEE THE DIRECTORY