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

# Better Auth Adaptor

> Add Dodo Payments to Better Auth: create customers at sign-up, open checkout sessions and the Customer Portal, record usage, and handle webhooks.

# Overview

The Better Auth adaptor, `@dodopayments/better-auth`, is a Better Auth plugin that connects your users to Dodo Payments. It provides:

* Optional customer creation or email-based customer linking on sign-up
* Checkout sessions, the preferred checkout method, with product slug mapping
* A self-service Customer Portal
* Usage ingestion and reporting endpoints for usage-based billing
* Webhook event processing with signature verification
* TypeScript types for every endpoint

<Note>
  You need a Dodo Payments account and API keys to use this integration.
</Note>

# Prerequisites

* Node.js 16 or later
* Access to your Dodo Payments dashboard
* An existing project that uses [Better Auth](https://www.npmjs.com/package/better-auth) 1.4 or a later 1.x release

# Installation

<Steps>
  <Step title="Install Dependencies">
    Run this command in your project root:

    ```bash theme={null}
    npm install @dodopayments/better-auth dodopayments better-auth zod
    ```

    <Check>
      The adaptor, the Dodo Payments SDK, Better Auth, and Zod are installed.
    </Check>
  </Step>
</Steps>

# Setup

<Steps>
  <Step title="Configure Environment Variables">
    Add these variables to your `.env` file. Create the API key under **Developer → API Keys** in the dashboard. You get the webhook secret when you add the webhook endpoint, as described under Webhooks on this page. `BETTER_AUTH_SECRET` is a random string of at least 32 characters.

    ```env expandable theme={null}
    DODO_PAYMENTS_API_KEY=your_api_key_here
    DODO_PAYMENTS_WEBHOOK_KEY=your_webhook_secret_here
    BETTER_AUTH_URL=http://localhost:3000
    BETTER_AUTH_SECRET=your_better_auth_secret_here
    ```

    <Warning>
      Never commit API keys or secrets to version control.
    </Warning>
  </Step>

  <Step title="Set Up Server-Side Integration">
    Create or update `src/lib/auth.ts`:

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

    export const dodoPayments = new DodoPayments({
      bearerToken: process.env.DODO_PAYMENTS_API_KEY!,
      environment: "test_mode", // or "live_mode" for production
    });

    export const auth = betterAuth({
      plugins: [
        dodopayments({
          client: dodoPayments,
          createCustomerOnSignUp: true,
          use: [
            checkout({
              products: [
                {
                  productId: "pdt_xxxxxxxxxxxxxxxxxxxxx",
                  slug: "premium-plan",
                },
              ],
              successUrl: "/dashboard/success",
              authenticatedUsersOnly: true,
            }),
            portal(),
            webhooks({
              webhookKey: process.env.DODO_PAYMENTS_WEBHOOK_KEY!,
              onPayload: async (payload) => {
                console.log("Received webhook:", payload.type);
              },
            }),
            usage(),
          ],
        }),
      ],
    });
    ```

    The plugin adds a `dodoCustomerId` field to the Better Auth `user` table, where it stores each user's Dodo Payments customer ID. After you add the plugin, update your database schema with the [Better Auth CLI](https://www.better-auth.com/docs/concepts/cli).

    <Tip>
      Set `environment` to `live_mode` for production.
    </Tip>
  </Step>

  <Step title="Set Up Client-Side Integration">
    Create or update `src/lib/auth-client.ts`:

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

    export const authClient = createAuthClient({
      baseURL: process.env.BETTER_AUTH_URL || "http://localhost:3000",
      plugins: [dodopaymentsClient()],
    });
    ```
  </Step>
</Steps>

# Usage Examples

<Info>
  Use `authClient.dodopayments.checkoutSession` for new integrations. The
  legacy `checkout` method is deprecated and kept only for backward
  compatibility.
</Info>

## Creating a Checkout Session (Preferred)

Create a checkout session from a configured slug or from a product cart, then redirect the customer to the returned URL:

<CodeGroup>
  ```typescript Using a slug theme={null}
  const { data: session, error } = await authClient.dodopayments.checkoutSession({
    // Option A: use a configured slug
    slug: "premium-plan",

    // Your internal ID, stored in the payment metadata as referenceId
    referenceId: "order_123",
  });

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

  ```

  ```typescript Using product cart theme={null}
  const { data: session, error } = await authClient.dodopayments.checkoutSession({
    // Option B: pass a product cart directly
    product_cart: [
      {
        product_id: "pdt_xxxxxxxxxxxxxxxxxxxxx",
        quantity: 1,
      }
    ],

    // Your internal ID, stored in the payment metadata as referenceId
    referenceId: "order_123",
  });

  if (session) {
    window.location.href = session.url;
  }
  ```
</CodeGroup>

`checkoutSession` fills in some fields for you:

* **Billing address:** Not required up front, because checkout collects it from the customer. To pre-fill it, pass `billing_address`.
* **Customer:** For a signed-in user, the plugin uses the email and name from their Better Auth session and ignores any `customer` object you pass. Without a signed-in user, it uses the `customer` object.
* **Other fields:** The argument accepts the same fields as the request body of the [Create Checkout Session](https://docs.dodopayments.com/api-reference/checkout-sessions/create) endpoint, plus `slug` and `referenceId`.

If the slug isn't configured, or you pass neither `slug` nor `product_cart`, the request fails with a 400 error.

<Note>
  The return URL comes from the `successUrl` configured in the server plugin,
  resolved against your app's URL. The plugin ignores any `return_url` in the
  client payload.
</Note>

## Legacy Checkout (Deprecated)

<Warning>
  The `authClient.dodopayments.checkout` method is deprecated. Use
  `checkoutSession` instead for new implementations.
</Warning>

The legacy method requires `billing` and `customer`, and creates a payment link through the deprecated dynamic checkout flow. Fields you set in `customer` override the email and name from the session.

```typescript expandable theme={null}
const { data: checkout, error } = await authClient.dodopayments.checkout({
  slug: "premium-plan",
  customer: {
    email: "customer@example.com",
    name: "John Doe",
  },
  billing: {
    city: "San Francisco",
    country: "US",
    state: "CA",
    street: "123 Market St",
    zipcode: "94103",
  },
  referenceId: "order_123",
});

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

## Accessing the Customer Portal

The portal endpoints require a signed-in user with a verified email address. If the user has no Dodo Payments customer yet, the plugin finds one by email or creates one. `customer.portal()` returns the portal URL:

```typescript expandable theme={null}
const { data: customerPortal, error } =
  await authClient.dodopayments.customer.portal();
if (customerPortal && customerPortal.redirect) {
  window.location.href = customerPortal.url;
}
```

## Listing Customer Data

List the signed-in customer's subscriptions and payments. `page` starts at 1, and `status` filters the results:

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

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

## Tracking Metered Usage

Enable the `usage()` plugin on the server to record usage events for usage-based billing and let customers see their usage. Both methods require a signed-in user with a verified email address.

* `authClient.dodopayments.usage.ingest` records an event for the signed-in user.
* `authClient.dodopayments.usage.meters.list` lists the signed-in customer's usage events. It accepts `page_number`, `page_size`, `event_name`, `meter_id`, `start`, and `end` query parameters.

```typescript expandable theme={null}
// Record a metered event (e.g., an API request)
const { error: ingestError } = await authClient.dodopayments.usage.ingest({
  event_id: crypto.randomUUID(),
  event_name: "api_request",
  metadata: {
    route: "/reports",
    method: "GET",
  },
  // Optional Date; defaults to now if omitted
  timestamp: new Date(),
});

if (ingestError) {
  console.error("Failed to record usage", ingestError);
}

// List recent usage for the current customer
const { data: usage, error: usageError } =
  await authClient.dodopayments.usage.meters.list({
    query: {
      page_size: 20,
      meter_id: "mtr_yourMeterId", // optional
    },
  });

if (usage?.items) {
  usage.items.forEach((event) => {
    console.log(event.event_name, event.timestamp, event.metadata);
  });
}
```

<Tip>
  Dodo Payments rejects events with timestamps more than one hour in the past
  or more than five minutes in the future.
</Tip>

If you omit `meter_id`, the list includes all of the customer's usage events. With `meter_id`, it includes only the events that match that meter.

# Webhooks

<Info>
  The webhooks plugin verifies the signature of each Dodo Payments event and
  calls your handlers. The default endpoint is
  `/api/auth/dodopayments/webhooks`.
</Info>

<Steps>
  <Step title="Generate and Set Webhook Secret">
    In the dashboard, go to **Developer → Webhooks** and add your endpoint URL, for example `https://<your-domain>/api/auth/dodopayments/webhooks`. Copy the endpoint's signing secret into your `.env` file:

    ```env theme={null}
    DODO_PAYMENTS_WEBHOOK_KEY=your_webhook_secret_here
    ```
  </Step>

  <Step title="Handle Webhook Events">
    Pass a handler for each event you want to process. `onPayload` runs for every event:

    ```typescript expandable theme={null}
    webhooks({
      webhookKey: process.env.DODO_PAYMENTS_WEBHOOK_KEY!,
      onPayload: async (payload) => {
        console.log("Received webhook:", payload.type);
      },
    });
    ```
  </Step>
</Steps>

If signature verification fails or a handler throws an error, the endpoint responds with 400. After your handlers finish, it returns `{ received: true }`.

### Supported Webhook Event Handlers

Each handler receives the verified payload for its event type:

<CodeGroup>
  ```typescript TypeScript expandable theme={null}
  onPayload?: (payload: WebhookPayload) => Promise<void>;
  onPaymentSucceeded?: (payload: WebhookPayload) => Promise<void>;
  onPaymentFailed?: (payload: WebhookPayload) => Promise<void>;
  onPaymentProcessing?: (payload: WebhookPayload) => Promise<void>;
  onPaymentCancelled?: (payload: WebhookPayload) => Promise<void>;
  onRefundSucceeded?: (payload: WebhookPayload) => Promise<void>;
  onRefundFailed?: (payload: WebhookPayload) => Promise<void>;
  onDisputeOpened?: (payload: WebhookPayload) => Promise<void>;
  onDisputeExpired?: (payload: WebhookPayload) => Promise<void>;
  onDisputeAccepted?: (payload: WebhookPayload) => Promise<void>;
  onDisputeCancelled?: (payload: WebhookPayload) => Promise<void>;
  onDisputeChallenged?: (payload: WebhookPayload) => Promise<void>;
  onDisputeWon?: (payload: WebhookPayload) => Promise<void>;
  onDisputeLost?: (payload: WebhookPayload) => Promise<void>;
  onSubscriptionActive?: (payload: WebhookPayload) => Promise<void>;
  onSubscriptionOnHold?: (payload: WebhookPayload) => Promise<void>;
  onSubscriptionRenewed?: (payload: WebhookPayload) => Promise<void>;
  onSubscriptionPlanChanged?: (payload: WebhookPayload) => Promise<void>;
  onSubscriptionCancelled?: (payload: WebhookPayload) => Promise<void>;
  onSubscriptionFailed?: (payload: WebhookPayload) => Promise<void>;
  onSubscriptionExpired?: (payload: WebhookPayload) => Promise<void>;
  onSubscriptionUpdated?: (payload: WebhookPayload) => Promise<void>;
  onSubscriptionPaused?: (payload: WebhookPayload) => Promise<void>;
  onSubscriptionUnpaused?: (payload: WebhookPayload) => Promise<void>;
  onSubscriptionUpdatePaymentMethod?: (payload: WebhookPayload) => Promise<void>;
  onLicenseKeyCreated?: (payload: WebhookPayload) => Promise<void>;
  onAbandonedCheckoutDetected?: (payload: WebhookPayload) => Promise<void>;
  onAbandonedCheckoutRecovered?: (payload: WebhookPayload) => Promise<void>;
  onDunningStarted?: (payload: WebhookPayload) => Promise<void>;
  onDunningRecovered?: (payload: WebhookPayload) => Promise<void>;
  onCreditAdded?: (payload: WebhookPayload) => Promise<void>;
  onCreditDeducted?: (payload: WebhookPayload) => Promise<void>;
  onCreditExpired?: (payload: WebhookPayload) => Promise<void>;
  onCreditRolledOver?: (payload: WebhookPayload) => Promise<void>;
  onCreditRolloverForfeited?: (payload: WebhookPayload) => Promise<void>;
  onCreditOverageCharged?: (payload: WebhookPayload) => Promise<void>;
  onCreditOverageReset?: (payload: WebhookPayload) => Promise<void>;
  onCreditManualAdjustment?: (payload: WebhookPayload) => Promise<void>;
  onCreditBalanceLow?: (payload: WebhookPayload) => Promise<void>;
  onEntitlementGrantCreated?: (payload: WebhookPayload) => Promise<void>;
  onEntitlementGrantDelivered?: (payload: WebhookPayload) => Promise<void>;
  onEntitlementGrantFailed?: (payload: WebhookPayload) => Promise<void>;
  onEntitlementGrantRevoked?: (payload: WebhookPayload) => Promise<void>;
  onPayoutCreated?: (payload: WebhookPayload) => Promise<void>;
  onPayoutOnHold?: (payload: WebhookPayload) => Promise<void>;
  onPayoutInProgress?: (payload: WebhookPayload) => Promise<void>;
  onPayoutFailed?: (payload: WebhookPayload) => Promise<void>;
  onPayoutSuccess?: (payload: WebhookPayload) => Promise<void>;
  ```
</CodeGroup>

# Configuration Reference

<AccordionGroup>
  <Accordion title="Plugin Options">
    * **client** (required): DodoPayments client instance
    * **createCustomerOnSignUp** (optional): Create a Dodo Payments customer when a user signs up, or link an existing customer with the same email. The plugin also updates the customer when the user's details change.
    * **use** (required): Array of plugins to enable (checkout, portal, usage, webhooks)
    * **getCustomerParams** (optional): Function that receives the Better Auth `User` and returns extra fields to attach to the Dodo Payments customer on creation and update (e.g. `metadata`, `phone_number`). It can be async.

    ```typescript theme={null}
    dodopayments({
      client: dodoPayments,
      createCustomerOnSignUp: true,
      use: [portal()],
      getCustomerParams: (user) => ({
        metadata: { userId: user.id },
        phone_number: user.phoneNumber ?? null,
      }),
    })
    ```
  </Accordion>

  <Accordion title="Checkout Plugin Options">
    * **products**: Array of `{ productId, slug }` objects, or an async function that returns one
    * **successUrl**: URL to redirect to after successful payment
    * **authenticatedUsersOnly**: Require user authentication (default: `false`)
  </Accordion>
</AccordionGroup>

# Troubleshooting & Tips

<AccordionGroup>
  <Accordion title="Common Issues">
    * **Invalid API key**: Check `DODO_PAYMENTS_API_KEY` in `.env`, and check that the key's mode matches `environment`.
    * **Webhook signature mismatch**: Check that the webhook secret matches the one set in the Dodo Payments dashboard.
    * **Customer not created**: Check that `createCustomerOnSignUp` is set to `true`.
    * **Portal or usage requests return 401**: The user's email address isn't verified.
  </Accordion>

  <Accordion title="Best Practices">
    * Use environment variables for all secrets and keys.
    * Test in `test_mode` before you switch to `live_mode`.
    * Log webhook events for debugging and auditing.
  </Accordion>
</AccordionGroup>

# Prompt for LLMs

Copy this prompt into your AI coding assistant to have it add the adaptor to your project. To give your agent the Dodo Payments docs and skills as well, install the [Agent Plugin](/developer-resources/build-with-ai-coding-agents).

```text expandable theme={null}
You are a skilled developer helping to integrate the @dodopayments/better-auth adapter into a typescript web application with better-auth. This adapter enables seamless payment processing through Dodo Payments with automatic customer management, checkout flows, and webhook handling.
STAGE 1: BASIC SETUP
This stage covers the foundational setup needed before implementing any plugins. Complete this stage first.
STEP 1: Installation
Install the required dependencies:
npm install @dodopayments/better-auth dodopayments better-auth zod
STEP 2: Environment Variables Setup
You will need to complete these external setup tasks. I will provide you with a TODO list for the actions you need to take outside of the code:
TODO LIST FOR USER:
1. Generate Dodo Payments API Key:
   - Go to your Dodo Payments Dashboard > Developer > API Keys
   - Create a new API key (or use existing)
   - Copy the API key value
   - Set environment variable: DODO_PAYMENTS_API_KEY=your_api_key_here
2. Generate Better Auth Secret:
   - Generate a random secret key (32+ characters)
   - Set environment variable: BETTER_AUTH_SECRET=your_better_auth_secret_here
3. Set Application URL:
   - For development: BETTER_AUTH_URL=http://localhost:3000
   - For production: BETTER_AUTH_URL=https://your-domain.com
4. Webhook Secret (only if implementing webhooks plugin):
   - This will be provided after you specify your domain name in Stage 2
   - Set environment variable: DODO_PAYMENTS_WEBHOOK_KEY=your_webhook_secret_here
Add these environment variables to your .env file:
DODO_PAYMENTS_API_KEY=your_api_key_here
DODO_PAYMENTS_WEBHOOK_KEY=your_webhook_secret_here
BETTER_AUTH_URL=http://localhost:3000
BETTER_AUTH_SECRET=your_better_auth_secret_here
STEP 3: Server Configuration
Create or update your better-auth setup file (src/lib/auth.ts):
import { betterAuth } from "better-auth";
import { dodopayments } from "@dodopayments/better-auth";
import DodoPayments from "dodopayments";
// Create DodoPayments client
export const dodoPayments = new DodoPayments({
  bearerToken: process.env.DODO_PAYMENTS_API_KEY!,
  environment: "test_mode", // Change to "live_mode" for production
});
// Configure better-auth with dodopayments adapter
export const auth = betterAuth({
  plugins: [
    dodopayments({
      client: dodoPayments,
      createCustomerOnSignUp: true, // Auto-create customers on signup
      use: [], // We'll add plugins here in Stage 2
      // Optional: attach metadata or phone_number to DodoPayments customer records
      // getCustomerParams: (user) => ({
      //   metadata: { userId: user.id },
      //   phone_number: user.phoneNumber ?? null,
      // }),
    }),
  ],
});
The plugin adds a dodoCustomerId field to the Better Auth user table. Update the database schema with the Better Auth CLI after adding the plugin.
STEP 4: Client Configuration
Create or update your auth client file (src/lib/auth-client.ts):
import { createAuthClient } from "better-auth/react";
import { dodopaymentsClient } from "@dodopayments/better-auth/client";
export const authClient = createAuthClient({
  baseURL: process.env.BETTER_AUTH_URL || "http://localhost:3000",
  plugins: [dodopaymentsClient()],
});
STAGE 2: PLUGIN IMPLEMENTATION
After completing Stage 1, you can selectively implement any of these plugins based on your needs. Each plugin is independent and can be added or removed as needed.
PLUGIN SELECTION:
Before implementing any plugins, ask the user which plugins they want to implement:
"Which plugins would you like to implement? You can choose any combination of:
1. CHECKOUT - Enables secure payment processing and checkout flows
2. PORTAL - Provides customer self-service portal for subscriptions and payments
3. USAGE - Tracks metered events and exposes usage history for billing beyond included allowances
4. WEBHOOKS - Handles real-time payment events from Dodo Payments
Please specify which plugins you want (e.g., 'checkout and usage', 'all four', 'just portal', etc.)"
If the user doesn't respond or you cannot prompt the user, implement all four plugins by default.
Based on the user's selection, implement only the requested plugins from the sections below:
CHECKOUT PLUGIN
Purpose: Enables secure payment processing using Checkout Sessions (preferred) with product slug mapping. A legacy checkout endpoint is also exposed for backward compatibility but is deprecated.
SETUP TODO LIST FOR USER:
1. Create products in Dodo Payments Dashboard:
   - Go to Dodo Payments Dashboard > Products
   - Create your products (e.g., subscription plans, one-time purchases)
   - Copy each product ID (starts with "pdt_")
   - Note down the product names for creating friendly slugs
2. Plan your checkout URLs:
   - Decide on your success URL (e.g., "/dashboard/success", "/thank-you")
   - Ensure this URL exists in your application
Configuration:
Add checkout to your imports in src/lib/auth.ts:
import { dodopayments, checkout } from "@dodopayments/better-auth";
Add checkout plugin to the use array in your dodopayments configuration:
use: [
  checkout({
    products: [
      {
        productId: "pdt_xxxxxxxxxxxxxxxxxxxxx", // Your actual product ID from Dodo Payments
        slug: "premium-plan", // Friendly slug for checkout
      },
      // Add more products as needed
    ],
    successUrl: "/dashboard/success", // Your success page URL
    authenticatedUsersOnly: true, // Require login for checkout
  }),
],
Usage Example (Preferred - Checkout Sessions):
const { data: session, error } = await authClient.dodopayments.checkoutSession({
  // Use the slug from your configuration OR provide product_cart directly
  slug: "premium-plan",
  // product_cart: [{ product_id: "pdt_xxxxxxxxxxxxxxxxxxxxx", quantity: 1 }],
  referenceId: "order_123", // Optional reference
});
if (session) {
  window.location.href = session.url;
}

Deprecated (Legacy Checkout):
const { data: checkout, error } = await authClient.dodopayments.checkout({
  slug: "premium-plan",
  customer: {
    email: "customer@example.com",
    name: "John Doe",
  },
  billing: {
    city: "San Francisco",
    country: "US",
    state: "CA",
    street: "123 Market St",
    zipcode: "94103",
  },
});
if (checkout) {
  window.location.href = checkout.url;
}
Options:
- products: Array of products or async function returning products
- successUrl: URL to redirect after successful payment
- authenticatedUsersOnly: Require user authentication (default: false)
PORTAL PLUGIN
Purpose: Provides customer self-service capabilities for managing subscriptions and viewing payment history.
Configuration:
Add portal to your imports in src/lib/auth.ts:
import { dodopayments, portal } from "@dodopayments/better-auth";
Add portal plugin to the use array in your dodopayments configuration:
use: [
  portal(),
],
Usage Examples:
// Access customer portal
const { data: customerPortal, error: portalError } = await authClient.dodopayments.customer.portal();
if (customerPortal && customerPortal.redirect) {
  window.location.href = customerPortal.url;
}
// List customer subscriptions
const { data: subscriptions, error: subscriptionsError } = await authClient.dodopayments.customer.subscriptions.list({
  query: {
    limit: 10,
    page: 1,
    status: "active",
  },
});
// List customer payments
const { data: payments, error: paymentsError } = await authClient.dodopayments.customer.payments.list({
  query: {
    limit: 10,
    page: 1,
    status: "succeeded",
  },
});
Note: All portal methods require user authentication and a verified email address.

USAGE PLUGIN (METERED BILLING)
Purpose: Records metered events (like API requests) for usage-based plans and surfaces recent usage history per customer.

SETUP TODO LIST FOR USER:
1. Create or identify usage meters in the Dodo Payments Dashboard:
   - Dashboard > Products > Meters
   - Copy the meter IDs (prefixed with mtr_) used by your usage-based plans
2. Decide which event names and metadata you will capture (e.g., api_request, route, method)
3. Ensure BetterAuth users verify their email addresses (the plugin enforces this before ingesting usage)

Configuration:
Add usage to your imports in src/lib/auth.ts:
    import { dodopayments, usage } from "@dodopayments/better-auth";

Add the plugin to the use array:
    use: [
      usage(),
    ],

Usage Examples:

    // Record a metered event for the signed-in customer
    const { error: ingestError } = await authClient.dodopayments.usage.ingest({
      event_id: crypto.randomUUID(),
      event_name: "api_request",
      metadata: {
        route: "/reports",
        method: "GET",
      },
      // Optional Date or ISO string; defaults to now
      timestamp: new Date(),
    });

    if (ingestError) {
      console.error("Failed to record usage", ingestError);
    }

    // List recent usage for the current customer
    const { data: usage, error: usageError } =
      await authClient.dodopayments.usage.meters.list({
        query: {
          page_size: 20,
          meter_id: "mtr_yourMeterId", // optional filter
        },
      });

    if (usage?.items) {
      usage.items.forEach((event) => {
        console.log(event.event_name, event.timestamp, event.metadata);
      });
    }

Notes:
  The plugin exposes authClient.dodopayments.usage.ingest and
  authClient.dodopayments.usage.meters.list. Timestamps older
  than one hour or more than five minutes in the future are rejected.

  If you do not pass meter_id when listing usage, all of the
  customer's usage events are returned. With meter_id, only the
  events that match that meter are returned.

WEBHOOKS PLUGIN
Purpose: Handles real-time payment events from Dodo Payments with secure signature verification.
BEFORE CONFIGURATION - Setup Webhook URL:
First, I need your domain name to generate the webhook URL and provide you with setup instructions.
STEP 1: Domain Name Input
What is your domain name? Please provide:
- For production: your domain name (e.g., "myapp.com", "api.mycompany.com")
- For staging: your staging domain (e.g., "staging.myapp.com")
- For development: use "localhost:3000" (or your local port)
STEP 2: After receiving your domain name, I will:
- Generate your webhook URL: https://[YOUR-DOMAIN]/api/auth/dodopayments/webhooks
- Provide you with a TODO list for webhook setup in Dodo Payments dashboard
- Give you the exact environment variable setup instructions
WEBHOOK SETUP TODO LIST (will be provided after domain input):
1. Configure webhook in Dodo Payments Dashboard:
   - Go to Dodo Payments Dashboard > Developer > Webhooks
   - Click "Add Webhook" or "Create Webhook"
   - Enter webhook URL: https://[YOUR-DOMAIN]/api/auth/dodopayments/webhooks
   - Select events you want to receive (or select all)
   - Copy the generated webhook secret
2. Set webhook secret in your environment:
   - For production: Set DODO_PAYMENTS_WEBHOOK_KEY in your hosting provider environment
   - For staging: Set DODO_PAYMENTS_WEBHOOK_KEY in your staging environment
   - For development: Add DODO_PAYMENTS_WEBHOOK_KEY=your_webhook_secret_here to your .env file
3. Deploy your application with the webhook secret configured
STEP 3: Add webhooks to your imports in src/lib/auth.ts:
import { dodopayments, webhooks } from "@dodopayments/better-auth";
STEP 4: Add webhooks plugin to the use array in your dodopayments configuration:
use: [
  webhooks({
    webhookKey: process.env.DODO_PAYMENTS_WEBHOOK_KEY!,
    // Generic handler for all webhook events
    onPayload: async (payload) => {
      console.log("Received webhook:", payload.type);
    },
    // Payment event handlers
    onPaymentSucceeded: async (payload) => {
      console.log("Payment succeeded:", payload);
    },
    onPaymentFailed: async (payload) => {
      console.log("Payment failed:", payload);
    },
    onPaymentProcessing: async (payload) => {
      console.log("Payment processing:", payload);
    },
    onPaymentCancelled: async (payload) => {
      console.log("Payment cancelled:", payload);
    },
    // Refund event handlers
    onRefundSucceeded: async (payload) => {
      console.log("Refund succeeded:", payload);
    },
    onRefundFailed: async (payload) => {
      console.log("Refund failed:", payload);
    },
    // Dispute event handlers
    onDisputeOpened: async (payload) => {
      console.log("Dispute opened:", payload);
    },
    onDisputeExpired: async (payload) => {
      console.log("Dispute expired:", payload);
    },
    onDisputeAccepted: async (payload) => {
      console.log("Dispute accepted:", payload);
    },
    onDisputeCancelled: async (payload) => {
      console.log("Dispute cancelled:", payload);
    },
    onDisputeChallenged: async (payload) => {
      console.log("Dispute challenged:", payload);
    },
    onDisputeWon: async (payload) => {
      console.log("Dispute won:", payload);
    },
    onDisputeLost: async (payload) => {
      console.log("Dispute lost:", payload);
    },
    // Subscription event handlers
    onSubscriptionActive: async (payload) => {
      console.log("Subscription active:", payload);
    },
    onSubscriptionOnHold: async (payload) => {
      console.log("Subscription on hold:", payload);
    },
    onSubscriptionRenewed: async (payload) => {
      console.log("Subscription renewed:", payload);
    },
    onSubscriptionPlanChanged: async (payload) => {
      console.log("Subscription plan changed:", payload);
    },
    onSubscriptionCancelled: async (payload) => {
      console.log("Subscription cancelled:", payload);
    },
    onSubscriptionFailed: async (payload) => {
      console.log("Subscription failed:", payload);
    },
    onSubscriptionExpired: async (payload) => {
      console.log("Subscription expired:", payload);
    },
    onSubscriptionUpdated: async (payload) => {
      console.log("Subscription updated:", payload);
    },
    // License key event handlers
    onLicenseKeyCreated: async (payload) => {
      console.log("License key created:", payload);
    },
    // Abandoned checkout event handlers
    onAbandonedCheckoutDetected: async (payload) => {
      console.log("Abandoned checkout detected:", payload);
    },
    onAbandonedCheckoutRecovered: async (payload) => {
      console.log("Abandoned checkout recovered:", payload);
    },
    // Dunning event handlers
    onDunningStarted: async (payload) => {
      console.log("Dunning started:", payload);
    },
    onDunningRecovered: async (payload) => {
      console.log("Dunning recovered:", payload);
    },
    // Credit event handlers
    onCreditAdded: async (payload) => {
      console.log("Credit added:", payload);
    },
    onCreditDeducted: async (payload) => {
      console.log("Credit deducted:", payload);
    },
    onCreditExpired: async (payload) => {
      console.log("Credit expired:", payload);
    },
    onCreditRolledOver: async (payload) => {
      console.log("Credit rolled over:", payload);
    },
    onCreditRolloverForfeited: async (payload) => {
      console.log("Credit rollover forfeited:", payload);
    },
    onCreditOverageCharged: async (payload) => {
      console.log("Credit overage charged:", payload);
    },
    onCreditManualAdjustment: async (payload) => {
      console.log("Credit manual adjustment:", payload);
    },
    onCreditBalanceLow: async (payload) => {
      console.log("Credit balance low:", payload);
    },
  }),
],
Supported Webhook Event Handlers:
- onPayload: Generic handler for all webhook events
- onPaymentSucceeded: Payment completed successfully
- onPaymentFailed: Payment failed
- onPaymentProcessing: Payment is being processed
- onPaymentCancelled: Payment was cancelled
- onRefundSucceeded: Refund completed successfully
- onRefundFailed: Refund failed
- onDisputeOpened: Dispute was opened
- onDisputeExpired: Dispute expired
- onDisputeAccepted: Dispute was accepted
- onDisputeCancelled: Dispute was cancelled
- onDisputeChallenged: Dispute was challenged
- onDisputeWon: Dispute was won
- onDisputeLost: Dispute was lost
- onSubscriptionActive: Subscription became active
- onSubscriptionOnHold: Subscription was put on hold
- onSubscriptionRenewed: Subscription was renewed
- onSubscriptionPlanChanged: Subscription plan was changed
- onSubscriptionCancelled: Subscription was cancelled
- onSubscriptionFailed: Subscription failed
- onSubscriptionExpired: Subscription expired
- onSubscriptionUpdated: Subscription was updated
- onSubscriptionPaused: Subscription was paused
- onSubscriptionUnpaused: Subscription was unpaused
- onSubscriptionUpdatePaymentMethod: Subscription payment method was updated
- onLicenseKeyCreated: License key was created
- onAbandonedCheckoutDetected: Abandoned checkout was detected
- onAbandonedCheckoutRecovered: Abandoned checkout was recovered
- onDunningStarted: Dunning process started
- onDunningRecovered: Dunning process recovered
- onCreditAdded: Credits were added
- onCreditDeducted: Credits were deducted
- onCreditExpired: Credits expired
- onCreditRolledOver: Credits were rolled over
- onCreditRolloverForfeited: Credit rollover was forfeited
- onCreditOverageCharged: Credit overage was charged
- onCreditOverageReset: Credit overage was reset
- onCreditManualAdjustment: Manual credit adjustment was made
- onCreditBalanceLow: Credit balance is low
- onEntitlementGrantCreated: Entitlement grant was created
- onEntitlementGrantDelivered: Entitlement grant was delivered
- onEntitlementGrantFailed: Entitlement grant failed
- onEntitlementGrantRevoked: Entitlement grant was revoked
- onPayoutCreated: Payout was created
- onPayoutOnHold: Payout was put on hold
- onPayoutInProgress: Payout is in progress
- onPayoutFailed: Payout failed
- onPayoutSuccess: Payout succeeded
COMBINING SELECTED PLUGINS:
After implementing the user's selected plugins, update your src/lib/auth.ts file to include all chosen plugins in the imports and use array:

Example for all four plugins:
    import {
      dodopayments,
      checkout,
      portal,
      usage,
      webhooks,
    } from "@dodopayments/better-auth";

    use: [
      checkout({
        // checkout configuration
      }),
      portal(),
      usage(),
      webhooks({
        // webhook configuration
      }),
    ];

Example for checkout + portal + usage:
    import { dodopayments, checkout, portal, usage } from "@dodopayments/better-auth";

    use: [
      checkout({
        // checkout configuration
      }),
      portal(),
      usage(),
    ];

Example for just webhooks:
    import { dodopayments, webhooks } from "@dodopayments/better-auth";

    use: [
      webhooks({
        // webhook configuration
      }),
    ];
IMPORTANT NOTES:
1. Complete Stage 1 before implementing any plugins
2. Ask the user which plugins they want to implement, or implement all four if no response
3. Only implement the plugins the user specifically requested
4. ALWAYS provide TODO lists for external actions the user needs to complete:
   - API key generation and environment variable setup
   - Product creation in Dodo Payments dashboard (for checkout plugin)
   - Usage meter configuration and API event definition (for usage plugin)
   - Webhook setup in Dodo Payments dashboard (for webhooks plugin)
   - Domain name collection for webhook URL generation
5. For webhook plugin: Ask for the user's domain name and generate the exact webhook URL: https://[domain]/api/auth/dodopayments/webhooks
6. The usage plugin requires the BetterAuth session user to exist and have a verified email before ingesting or listing usage
7. All client methods return { data, error } objects for proper error handling
8. Use test_mode for development and live_mode for production
9. The webhook endpoint is automatically created and secured with signature verification (if webhooks plugin is selected)
10. Customer portal and subscription listing require user authentication and a verified email (if portal plugin is selected)
11. Handle errors appropriately and test webhook functionality in development before going live
12. Present all external setup tasks as clear TODO lists with specific environment variable names
13. Use getCustomerParams to attach metadata or phone_number to DodoPayments customer records — the function receives the BetterAuth User object and runs on every customer creation and update

```


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