Installation

1

Install the package

Run the following command in your project root:
npm install @dodopayments/convex
2

Add Component to Convex Config

Add the Dodo Payments component to your Convex configuration:
// convex/convex.config.ts
import { defineApp } from "convex/server";
import dodopayments from "@dodopayments/convex/convex.config";

const app = defineApp();
app.use(dodopayments);
export default app;
After editing convex.config.ts, run npx convex dev once to generate the necessary types.
3

Set up environment variables

Set up environment variables in your Convex dashboard (SettingsEnvironment Variables). You can access the dashboard by running:
npx convex dashboard
Add the following environment variables:
  • DODO_PAYMENTS_API_KEY - Your Dodo Payments API key
  • DODO_PAYMENTS_ENVIRONMENT - Set to test_mode or live_mode
  • DODO_PAYMENTS_WEBHOOK_SECRET - Your webhook secret (required for webhook handling)
Always use Convex environment variables for sensitive information. Never commit secrets to version control.

Component Setup Examples

// convex/dodo.ts
import { DodoPayments } from "@dodopayments/convex";
import { components } from "./_generated/api";

export const dodo = new DodoPayments(components.dodopayments, {
// This function maps your Convex user to a Dodo Payments customer
// Customize it based on your authentication provider and user database
identify: async (ctx) => {
  const identity = await ctx.auth.getUserIdentity();
  if (!identity) {
    return null; // User is not logged in
  }
  
  // Lookup user from your database
  const user = await ctx.db.query("users")
     .withIndex("by_auth_id", (q) => q.eq("authId", identity.subject))
     .first();
   if (!user) {
     return null; // User not found in database
   }
   
   return {
     dodoCustomerId: user.dodoCustomerId, // Use stored dodoCustomerId
     customerData: {
       name: user.name,
       email: user.email,
     },
   };
},
apiKey: process.env.DODO_PAYMENTS_API_KEY!,
environment: process.env.DODO_PAYMENTS_ENVIRONMENT as "test_mode" | "live_mode",
});

// Export the API methods for use in your app
export const { checkout, customerPortal } = dodo.api();
Use this function to integrate Dodo Payments checkout into your Convex app. Uses session-based checkout with full feature support.
// convex/payments.ts
import { action } from "./_generated/server";
import { v } from "convex/values";
import { checkout } from "./dodo";

export const createCheckout = action({
  args: { 
    product_cart: v.array(v.object({
      product_id: v.string(),
      quantity: v.number(),
    })),
    returnUrl: v.optional(v.string()),
  },
  handler: async (ctx, args) => {
    try {
      const session = await checkout(ctx, {
        payload: {
          product_cart: args.product_cart,
          return_url: args.returnUrl,
          billing_currency: "USD",
          feature_flags: {
            allow_discount_code: true,
          },
        },
      });
      if (!session?.checkout_url) {
        throw new Error("Checkout session did not return a checkout_url");
      }
      return session;
    } catch (error) {
      console.error("Failed to create checkout session", error);
      throw new Error("Unable to create checkout session. Please try again.");
    }
  },
});

Checkout Function

The Dodo Payments Convex component uses session-based checkout, providing a secure, customizable checkout experience with pre-configured product carts and customer details. This is the recommended approach for all payment flows.

Usage

const result = await checkout(ctx, {
  payload: {
    product_cart: [{ product_id: "prod_123", quantity: 1 }],
    customer: { email: "user@example.com" },
    return_url: "https://example.com/success"
  }
});
Refer Checkout Sessions for more details and a complete list of supported fields.

Response Format

The checkout function returns a JSON response with the checkout URL:
{
  "checkout_url": "https://checkout.dodopayments.com/session/..."
}

Customer Portal Function

The Customer Portal Function enables you to seamlessly integrate the Dodo Payments customer portal into your Convex application.

Usage

const result = await customerPortal(ctx, {
  send_email: false
});

Parameters

send_email
boolean
If set to true, sends an email to the customer with the portal link.
The customer is automatically identified using the identify function configured in your DodoPayments setup. This function should return the customer’s dodoCustomerId and optional customerData.

Webhook Handler

  • Method: Only POST requests are supported. Other methods return 405.
  • Signature Verification: Verifies the webhook signature using 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

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>;
onLicenseKeyCreated?: (payload: WebhookPayload) => Promise<void>;

Frontend Usage

Use the checkout function from your React components with Convex hooks.
import { useAction } from "convex/react";
import { api } from "../convex/_generated/api";

export function CheckoutButton() {
  const createCheckout = useAction(api.payments.createCheckout);

  const handleCheckout = async () => {
    try {
      const { checkout_url } = await createCheckout({
        product_cart: [{ product_id: "prod_123", quantity: 1 }],
        returnUrl: "https://example.com/success"
      });
      if (!checkout_url) {
        throw new Error("Missing checkout_url in response");
      }
      window.location.href = checkout_url;
    } catch (error) {
      console.error("Failed to create checkout", error);
      throw new Error("Unable to create checkout. Please try again.");
    }
  };

  return <button onClick={handleCheckout}>Buy Now</button>;
}
import { useAction } from "convex/react";
import { api } from "../convex/_generated/api";

export function CustomerPortalButton() {
  const getPortal = useAction(api.payments.getCustomerPortal);

  const handlePortal = async () => {
    try {
      const { portal_url } = await getPortal({ send_email: false });
      if (!portal_url) {
        throw new Error("Missing portal_url in response");
      }
      window.location.href = portal_url;
    } catch (error) {
      console.error("Unable to open customer portal", error);
      alert("We couldn't open the customer portal. Please try again.");
    }
  };

  return <button onClick={handlePortal}>Manage Subscription</button>;
}

Prompt for LLM

You are an expert Convex developer assistant. Your task is to guide a user through integrating the @dodopayments/convex component into their existing Convex application.

The @dodopayments/convex adapter provides a Convex component for Dodo Payments' Checkout, Customer Portal, and Webhook functionalities, built using the official Convex component architecture pattern.

First, install the necessary package:

npm install @dodopayments/convex

Here's how you should structure your response:

1. Ask the user which functionalities they want to integrate.

"Which parts of the @dodopayments/convex component would you like to integrate into your project? You can choose one or more of the following:

- Checkout Function (for handling product checkouts)
- Customer Portal Function (for managing customer subscriptions/details)
- Webhook Handler (for receiving Dodo Payments webhook events)
- All (integrate all three)"

2. Based on the user's selection, provide detailed integration steps for each chosen functionality.

If Checkout Function is selected:

Purpose: This function handles session-based checkout flows and returns checkout URLs for programmatic handling.

Integration Steps:

Step 1: Add the component to your Convex configuration.

// convex/convex.config.ts
import { defineApp } from "convex/server";
import dodopayments from "@dodopayments/convex/convex.config";

const app = defineApp();
app.use(dodopayments);
export default app;

Step 2: Guide the user to set up environment variables in the Convex dashboard. Instruct them to open the dashboard by running:

npx convex dashboard

Then add the required environment variables (e.g., DODO_PAYMENTS_API_KEY, DODO_PAYMENTS_ENVIRONMENT, DODO_PAYMENTS_WEBHOOK_SECRET) in **Settings → Environment Variables**. Do not use .env files for backend functions.

Step 3: Create your payment functions file.

// convex/dodo.ts
import { DodoPayments } from "@dodopayments/convex";
import { components } from "./_generated/api";

export const dodo = new DodoPayments(components.dodopayments, {
  // This function maps your Convex user to a Dodo Payments customer
  // Customize it based on your authentication provider and user database
  identify: async (ctx) => {
    const identity = await ctx.auth.getUserIdentity();
    if (!identity) {
      return null; // User is not logged in
    }
    
    // Lookup user from your database
    const user = await ctx.db.query("users")
       .withIndex("by_auth_id", (q) => q.eq("authId", identity.subject))
       .first();
     if (!user) {
       return null; // User not found in database
     }
     
     return {
       dodoCustomerId: user.dodoCustomerId, // Use stored dodoCustomerId
       customerData: {
         name: user.name,
         email: user.email,
       },
     };
  },
  apiKey: process.env.DODO_PAYMENTS_API_KEY!,
  environment: process.env.DODO_PAYMENTS_ENVIRONMENT as "test_mode" | "live_mode",
});

// Export the API methods for use in your app
export const { checkout, customerPortal } = dodo.api();

Step 4: Create actions that use the checkout function.

// convex/payments.ts
import { action } from "./_generated/server";
import { v } from "convex/values";
import { checkout } from "./dodo";

// Checkout session with full feature support
export const createCheckout = action({
  args: { 
    product_cart: v.array(v.object({
      product_id: v.string(),
      quantity: v.number(),
    })),
    returnUrl: v.optional(v.string()),
  },
  handler: async (ctx, args) => {
    return await checkout(ctx, {
      payload: {
        product_cart: args.product_cart,
        return_url: args.returnUrl,
        billing_currency: "USD",
        feature_flags: {
          allow_discount_code: true,
        },
      },
    });
  },
});

Step 5: Use in your frontend.

// Your frontend component
import { useAction } from "convex/react";
import { api } from "../convex/_generated/api";

export function CheckoutButton() {
  const createCheckout = useAction(api.payments.createCheckout);

  const handleCheckout = async () => {
    const { checkout_url } = await createCheckout({
      product_cart: [{ product_id: "prod_123", quantity: 1 }],
    });
    window.location.href = checkout_url;
  };

  return <button onClick={handleCheckout}>Buy Now</button>;
}

Configuration Details:

- `checkout()`: Checkout session with full feature support using session checkout.
- Returns: `{"checkout_url": "https://checkout.dodopayments.com/..."}`

For complete API documentation, refer to:
- Checkout Sessions: https://docs.dodopayments.com/developer-resources/checkout-session
- One-time Payments: https://docs.dodopayments.com/api-reference/payments/post-payments
- Subscriptions: https://docs.dodopayments.com/api-reference/subscriptions/post-subscriptions

If Customer Portal Function is selected:

Purpose: This function allows customers to manage their subscriptions and payment methods. The customer is automatically identified via the `identify` function.

Integration Steps:

Follow Steps 1-3 from the Checkout Function section, then:

Step 4: Create a customer portal action.

// convex/payments.ts (add to existing file)
import { action } from "./_generated/server";
import { v } from "convex/values";
import { customerPortal } from "./dodo";

export const getCustomerPortal = action({
  args: {
    send_email: v.optional(v.boolean()),
  },
  handler: async (ctx, args) => {
    try {
      const portal = await customerPortal(ctx, args);
      if (!portal?.portal_url) {
        throw new Error("Customer portal did not return a portal_url");
      }
      return portal;
    } catch (error) {
      console.error("Failed to generate customer portal link", error);
      throw new Error("Unable to generate customer portal link. Please retry.");
    }
  },
});

Step 5: Use in your frontend.

// Your frontend component
import { useAction } from "convex/react";
import { api } from "../convex/_generated/api";

export function CustomerPortalButton() {
  const getPortal = useAction(api.payments.getCustomerPortal);

  const handlePortal = async () => {
    const { portal_url } = await getPortal({ send_email: false });
    window.location.href = portal_url;
  };

  return <button onClick={handlePortal}>Manage Subscription</button>;
}

Configuration Details:
- Requires authenticated user (via `identify` function).
- Customer identification is handled automatically by the `identify` function.
- `send_email`: Optional boolean to send portal link via email.

If Webhook Handler is selected:

Purpose: This handler processes incoming webhook events from Dodo Payments, allowing your application to react to events like successful payments or subscription changes.

Integration Steps:

Step 1: Add the webhook secret to your environment variables in the Convex dashboard. You can open the dashboard by running:

Guide the user to open the Convex dashboard by running:

npx convex dashboard

In the dashboard, go to **Settings → Environment Variables** and add:

- `DODO_PAYMENTS_WEBHOOK_SECRET=whsec_...`

Do not use .env files for backend functions; always set secrets in the Convex dashboard.

Step 2: Create a file `convex/http.ts`:

// convex/http.ts
import { httpRouter } from "convex/server";
import { createDodoWebhookHandler } from "@dodopayments/convex";

const http = httpRouter();

http.route({
  path: "/dodopayments-webhook",
  method: "POST",
  handler: createDodoWebhookHandler({
    onPaymentSucceeded: async (payload) => {
      console.log("Payment succeeded:", payload.data.payment_id);
      // Add your logic here to handle the successful payment
    },
    onSubscriptionActive: async (payload) => {
      console.log("Subscription activated:", payload.data.subscription_id);
      // Add your logic here
    },
    // Add other event handlers as needed
  }),
});

export default http;

Now, you can set the webhook endpoint URL in your Dodo Payments dashboard to `https://<your-convex-deployment-url>/dodopayments-webhook`.

Environment Variable Setup:

Set up the following environment variables in your Convex dashboard if you haven't already (Settings → Environment Variables):

- `DODO_PAYMENTS_API_KEY` - Your Dodo Payments API key
- `DODO_PAYMENTS_ENVIRONMENT` - Set to `test_mode` or `live_mode`
- `DODO_PAYMENTS_WEBHOOK_SECRET` - Your webhook secret (required for webhook handling)

Usage in your component configuration:

apiKey: process.env.DODO_PAYMENTS_API_KEY
environment: process.env.DODO_PAYMENTS_ENVIRONMENT as "test_mode" | "live_mode"

Important: Never commit sensitive environment variables directly into your code. Always use Convex environment variables for all sensitive information.

If the user needs assistance setting up environment variables or deployment, ask them about their specific setup and provide guidance accordingly.

Run `npx convex dev` after setting up the component to generate the necessary types.