.env file in your project root:
```env expandable theme={null}
DODO_PAYMENTS_API_KEY=your-api-key
DODO_PAYMENTS_WEBHOOK_KEY=your-webhook-secret
DODO_PAYMENTS_ENVIRONMENT="test_mode" or "live_mode"
DODO_PAYMENTS_RETURN_URL=https://yourdomain.com/checkout/success
```
.env file or secrets to version control.
?productId=pdt\_nZuwz45WAs64n3l07zpQR).
USD).
1000 for \$10.00).
metadata\_ will be passed as metadata.
productId is missing, the handler returns a 400 response. Invalid query parameters also result in a 400 response.
?customer\_id=cus\_123).
true, sends an email to the customer with the portal link.
customer\_id is missing.
webhookKey. Returns 401 if verification fails.
* **Payload Validation:** Validated with Zod. Returns 400 for invalid payloads.
* **Error Handling:**
* 401: Invalid signature
* 400: Invalid payload
* 500: Internal error during verification
* **Event Routing:** Calls the appropriate event handler based on the payload type.
### Supported Webhook Event Handlers
.env file:
```env expandable theme={null}
DODO_PAYMENTS_API_KEY=your_api_key_here
DODO_PAYMENTS_WEBHOOK_SECRET=your_webhook_secret_here
BETTER_AUTH_URL=http://localhost:3000
BETTER_AUTH_SECRET=your_better_auth_secret_here
```
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, endpoints, client } = 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_SECRET!,
onPayload: async (payload) => {
console.log("Received webhook:", payload.event_type);
},
}),
usage(),
],
}),
],
});
```
environment to live\_mode for production.
src/lib/auth-client.ts:
```typescript expandable theme={null}
import { createAuthClient } from "better-auth/react";
import { dodopaymentsClient } from "@dodopayments/better-auth";
export const authClient = createAuthClient({
baseURL: process.env.BETTER_AUTH_URL || "http://localhost:3000",
plugins: [dodopaymentsClient()],
});
```
authClient.dodopayments.checkoutSession for new
integrations. The legacy checkout method is deprecated and kept
only for backward compatibility.
checkout method, checkoutSession
does not require billing information upfront as it will be taken from the user
at the checkout page. You can still override it by passing the
billing key in the argument.
checkout method,
checkoutSession takes customer's email and name from their
better-auth session, however, you can override it by passing the
customer object with email and name.
successUrl configured in the
server plugin. You do not need to include return\_url in the
client payload.
authClient.dodopayments.checkout method is deprecated. Use
checkoutSession instead for new implementations.
usage() plugin on the server to capture metered events and let customers inspect their usage-based billing.
* authClient.dodopayments.usage.ingest ingests events for the signed-in, email-verified user.
* authClient.dodopayments.usage.meters.list lists recent usage for the meters tied to that customer’s subscriptions.
```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);
});
}
```
meter\_id when listing usage meters, all meters
tied to the customer’s subscriptions are returned.
/api/auth/dodopayments/webhooks.
https\://\/api/auth/dodopayments/webhooks ) in the Dodo Payments Dashboard and set it in your .env file:
```env theme={null}
DODO_PAYMENTS_WEBHOOK_SECRET=your_webhook_secret_here
```
DODO\_PAYMENTS\_API\_KEY in .env. -
Webhook signature mismatch: Ensure the webhook secret matches the one
set in the Dodo Payments Dashboard. - Customer not created: Confirm
createCustomerOnSignUp is set to true.
test\_mode before switching to live\_mode. - Log
webhook events for debugging and auditing.
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);
});
}
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.
meter_id when listing usage meters, all meters tied to
the customer’s active subscriptions are returned.
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 (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
```
# Bun Adaptor
Source: https://docs.dodopayments.com/developer-resources/bun-adaptor
Learn how to integrate Dodo Payments with your Bun server project using our Bun Adaptor. Covers checkout, customer portal, webhooks, and secure environment setup.
.env file in your project root:
```env expandable theme={null}
DODO_PAYMENTS_API_KEY=your-api-key
DODO_PAYMENTS_WEBHOOK_KEY=your-webhook-secret
DODO_PAYMENTS_ENVIRONMENT="test_mode" or "live_mode"
DODO_PAYMENTS_RETURN_URL=your-return-url
```
.env file or secrets to version control.
Bun.serve().
?productId=pdt\_xxx).
USD).
1000 for \$10.00).
metadata\_ will be passed as metadata.
productId is missing, the handler returns a 400 response. Invalid query parameters also result in a 400 response.
?customer\_id=cus\_123).
true, sends an email to the customer with the portal link.
customer\_id is missing.
webhookKey. Returns 401 if verification fails.
* **Payload Validation:** Validated with Zod. Returns 400 for invalid payloads.
* **Error Handling:**
* 401: Invalid signature
* 400: Invalid payload
* 500: Internal error during verification
* **Event Routing:** Calls the appropriate event handler based on the payload type.
### Supported Webhook Event Handlers
createPayment mutation to record successful payments or a createSubscription mutation to manage subscription state.
true, sends an email to the customer with the portal link.
DODO\_PAYMENTS\_WEBHOOK\_SECRET. Returns 401 if verification fails.
* **Payload Validation:** Validated with Zod. Returns 400 for invalid payloads.
* **Error Handling:**
* 401: Invalid signature
* 400: Invalid payload
* 500: Internal error during verification
* **Event Routing:** Calls the appropriate event handler based on the payload type.
### Supported Webhook Event Handlers
.env file in your project root:
```env expandable theme={null}
DODO_PAYMENTS_API_KEY=your-api-key
DODO_PAYMENTS_WEBHOOK_KEY=your-webhook-secret
DODO_PAYMENTS_ENVIRONMENT="test_mode" or "live_mode"
DODO_PAYMENTS_RETURN_URL=your-return-url
```
.env file or secrets to version control.
?productId=pdt\_nZuwz45WAs64n3l07zpQR).
USD).
1000 for \$10.00).
metadata\_ will be passed as metadata.
productId is missing, the handler returns a 400 response. Invalid query parameters also result in a 400 response.
?customer\_id=cus\_123).
true, sends an email to the customer with the portal link.
customer\_id is missing.
webhookKey. Returns 401 if verification fails.
* **Payload Validation:** Validated with Zod. Returns 400 for invalid payloads.
* **Error Handling:**
* 401: Invalid signature
* 400: Invalid payload
* 500: Internal error during verification
* **Event Routing:** Calls the appropriate event handler based on the payload type.
### Supported Webhook Event Handlers
.env file in your project root:
```env expandable theme={null}
DODO_PAYMENTS_API_KEY=your-api-key
DODO_PAYMENTS_RETURN_URL=https://yourapp.com/success
DODO_PAYMENTS_WEBHOOK_KEY=your-webhook-secret
DODO_PAYMENTS_ENVIRONMENT="test_mode" or "live_mode""
```
.env file or secrets to version control.
?productId=pdt\_nZuwz45WAs64n3l07zpQR).
USD).
1000 for \$10.00).
metadata\_ will be passed as metadata.
productId is missing, the handler returns a 400 response. Invalid query parameters also result in a 400 response.
?customer\_id=cus\_123).
true, sends an email to the customer with the portal link.
customer\_id is missing.
webhookKey. Returns 401 if verification fails.
* **Payload Validation:** Validated with Zod. Returns 400 for invalid payloads.
* **Error Handling:**
* 401: Invalid signature
* 400: Invalid payload
* 500: Internal error during verification
* **Event Routing:** Calls the appropriate event handler based on the payload type.
### Supported Webhook Event Handlers
.env file in your project root:
```env expandable theme={null}
DODO_PAYMENTS_API_KEY=your-api-key
DODO_PAYMENTS_RETURN_URL=https://yourapp.com/success
DODO_PAYMENTS_WEBHOOK_KEY=your-webhook-secret
DODO_PAYMENTS_ENVIRONMENT="test_mode" or "live_mode""
```
.env file or secrets to version control.
?productId=pdt\_nZuwz45WAs64n3l07zpQR).
USD).
1000 for \$10.00).
metadata\_ will be passed as metadata.
productId is missing, the handler returns a 400 response. Invalid query parameters also result in a 400 response.
?customer\_id=cus\_123).
true, sends an email to the customer with the portal link.
customer\_id is missing.
webhookKey. Returns 401 if verification fails.
* **Payload Validation:** Validated with Zod. Returns 400 for invalid payloads.
* **Error Handling:**
* 401: Invalid signature
* 400: Invalid payload
* 500: Internal error during verification
* **Event Routing:** Calls the appropriate event handler based on the payload type.
### Supported Webhook Event Handlers
.env file in your project root:
```env theme={null}
DODO_PAYMENTS_API_KEY=your-api-key
DODO_PAYMENTS_WEBHOOK_KEY=your-webhook-secret
DODO_PAYMENTS_RETURN_URL=https://yourdomain.com/checkout/success
DODO_PAYMENTS_ENVIRONMENT="test_mode"or"live_mode"
```
.env file or secrets to version control.
?productId=pdt\_nZuwz45WAs64n3l07zpQR).
USD).
1000 for \$10.00).
metadata\_ will be passed as metadata.
productId is missing, the handler returns a 400 response. Invalid query parameters also result in a 400 response.
?customer\_id=cus\_123).
true, sends an email to the customer with the portal link.
customer\_id is missing.
webhookKey. Returns 401 if verification fails.
* **Payload Validation:** Validated with Zod. Returns 400 for invalid payloads.
* **Error Handling:**
* 401: Invalid signature
* 400: Invalid payload
* 500: Internal error during verification
* **Event Routing:** Calls the appropriate event handler based on the payload type.
### Supported Webhook Event Handlers
@dodopayments/nuxt to your modules array and configure it:
```typescript nuxt.config.ts expandable theme={null}
export default defineNuxtConfig({
modules: ["@dodopayments/nuxt"],
devtools: { enabled: true },
compatibilityDate: "2025-02-25",
runtimeConfig: {
private: {
bearerToken: process.env.NUXT_PRIVATE_BEARER_TOKEN,
webhookKey: process.env.NUXT_PRIVATE_WEBHOOK_KEY,
environment: process.env.NUXT_PRIVATE_ENVIRONMENT,
returnUrl: process.env.NUXT_PRIVATE_RETURNURL
},
}
});
```
.env file or secrets to version control.
server/routes/api/ directory.
productId is missing or invalid, the handler returns a 400 response.
customer\_id as a query parameter.
customer\_id (required): The customer ID for the portal session (e.g., ?customer\_id=cus\_123)
* send\_email (optional, boolean): If true, sends an email to the customer with the portal link.
customer\_id is missing.
?productId=pdt\_nZuwz45WAs64n3l07zpQR).
USD).
1000 for \$10.00).
metadata\_ will be passed as metadata.
productId is missing, the handler returns a 400 response. Invalid query parameters also result in a 400 response.
?customer\_id=cus\_123).
true, sends an email to the customer with the portal link.
customer\_id is missing.
webhookKey. Returns 401 if verification fails.
* **Payload Validation:** Validated with Zod. Returns 400 for invalid payloads.
* **Error Handling:**
* 401: Invalid signature
* 400: Invalid payload
* 500: Internal error during verification
* **Event Routing:** Calls the appropriate event handler based on the payload type.
### Supported Webhook Event Handlers
2500.
product\_price. If false, fees are added on top.
.env file in your project root:
```env expandable theme={null}
DODO_PAYMENTS_API_KEY=your-api-key
DODO_PAYMENTS_WEBHOOK_SECRET=your-webhook-secret
DODO_PAYMENTS_RETURN_URL=https://yourdomain.com/checkout/success
DODO_PAYMENTS_ENVIRONMENT="test_mode" or "live_mode"
```
.env file or secrets to version control.
?productId=pdt\_nZuwz45WAs64n3l07zpQR).
USD).
1000 for \$10.00).
metadata\_ will be passed as metadata.
productId is missing, the handler returns a 400 response. Invalid query parameters also result in a 400 response.
?customer\_id=cus\_123).
true, sends an email to the customer with the portal link.
customer\_id is missing.
webhookKey. Returns 401 if verification fails.
* **Payload Validation:** Validated with Zod. Returns 400 for invalid payloads.
* **Error Handling:**
* 401: Invalid signature
* 400: Invalid payload
* 500: Internal error during verification
* **Event Routing:** Calls the appropriate event handler based on the payload type.
### Supported Webhook Event Handlers
.env file in your project root:
```env expandable theme={null}
DODO_PAYMENTS_API_KEY=your-api-key
DODO_PAYMENTS_WEBHOOK_KEY=your-webhook-secret
DODO_PAYMENTS_ENVIRONMENT="test_mode" or "live_mode"
DODO_PAYMENTS_RETURN_URL=https://yourdomain.com/checkout/success
```
.env file or secrets to version control.
?productId=pdt\_nZuwz45WAs64n3l07zpQR).
USD).
1000 for \$10.00).
metadata\_ will be passed as metadata.
productId is missing, the handler returns a 400 response. Invalid query parameters also result in a 400 response.
?customer\_id=cus\_123).
true, sends an email to the customer with the portal link.
customer\_id is missing.
webhookKey. Returns 401 if verification fails.
* **Payload Validation:** Validated with Zod. Returns 400 for invalid payloads.
* **Error Handling:**
* 401: Invalid signature
* 400: Invalid payload
* 500: Internal error during verification
* **Event Routing:** Calls the appropriate event handler based on the payload type.
### Supported Webhook Event Handlers
.env file in your project root:
```env expandable theme={null}
DODO_PAYMENTS_API_KEY=your-api-key
DODO_PAYMENTS_WEBHOOK_KEY=your-webhook-secret
DODO_PAYMENTS_RETURN_URL=your-return-url
DODO_PAYMENTS_ENVIRONMENT="test_mode" or "live_mode"
```
.env file or secrets to version control.
?productId=pdt\_nZuwz45WAs64n3l07zpQR).
USD).
1000 for \$10.00).
metadata\_ will be passed as metadata.
productId is missing, the handler returns a 400 response. Invalid query parameters also result in a 400 response.
?customer\_id=cus\_123).
true, sends an email to the customer with the portal link.
customer\_id is missing.
webhookKey. Returns 401 if verification fails.
* **Payload Validation:** Validated with Zod. Returns 400 for invalid payloads.
* **Error Handling:**
* 401: Invalid signature
* 400: Invalid payload
* 500: Internal error during verification
* **Event Routing:** Calls the appropriate event handler based on the payload type.
### Supported Webhook Event Handlers
Powered by AI-SDK & Dodo Payments
confirm=true, sessions are valid for 15 minutes and all required fields must be provided.
[https://example.com/?payment\_id=pay\_ts2ySpzg07phGeBZqePbH\&status=succeeded](https://example.com/?payment_id=pay_ts2ySpzg07phGeBZqePbH\&status=succeeded)
disable... flag to true:
showDiscounts=false will disable and hide the discounts section in the checkout form. Use this if you want to prevent customers from entering coupon or promo codes during checkout.
metadata\_orderId=123).?session=sess\_1a2b3c4d). The stored information persists through page refreshes and is accessible throughout the checkout process.
Thank you for your purchase.
Please try again or contact support.
Generate checkout links with custom seat quantities:
invoice\_id and payment\_id are returned only when an immediate charge and/or invoice is created during the plan change. Always rely on webhook events (e.g., payment.succeeded, subscription.plan\_changed) to confirm outcomes.
difference\_immediately are subscription-scoped and distinct from Customer Credits. They automatically apply to future renewals of the same subscription and are not transferable between subscriptions.
Hi ${p.customer.name},
Your payment of $${(p.total_amount / 100).toFixed(2)} has been processed successfully.
Thank you for your business!
`, text: `Payment Successful! Your payment of $${(p.total_amount / 100).toFixed(2)} has been processed. Payment ID: ${p.payment_id}` }; } return webhook; } ``` ### Subscription Welcome Email ```javascript subscription_welcome.js icon="js" expandable theme={null} function handler(webhook) { if (webhook.eventType === "subscription.active") { const s = webhook.payload.data; webhook.url = "https://api.resend.com/emails"; webhook.payload = { from: "welcome@yourdomain.com", to: [s.customer.email], subject: "Welcome to Your Subscription!", html: `Hi ${s.customer.name},
Your subscription has been activated successfully.
You can manage your subscription anytime from your account dashboard.
`, text: `Welcome! Your subscription is now active. Amount: $${(s.recurring_pre_tax_amount / 100).toFixed(2)}/${s.payment_frequency_interval}` }; } return webhook; } ``` ### Payment Failure Notification ```javascript payment_failure.js icon="js" expandable theme={null} function handler(webhook) { if (webhook.eventType === "payment.failed") { const p = webhook.payload.data; webhook.url = "https://api.resend.com/emails"; webhook.payload = { from: "support@yourdomain.com", to: [p.customer.email], subject: "Payment Failed - Action Required", html: `Hi ${p.customer.name},
We were unable to process your payment of $${(p.total_amount / 100).toFixed(2)}.
Please update your payment method or contact support for assistance.
Update Payment Method `, text: `Payment Failed: We couldn't process your $${(p.total_amount / 100).toFixed(2)} payment. Please update your payment method.` }; } return webhook; } ``` ## Tips * Use verified sender domains for better deliverability * Include both HTML and text versions of emails * Personalize content with customer data * Use clear, action-oriented subject lines * Include unsubscribe links for compliance * Test email templates before going live ## Troubleshooting