> ## 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.

# TanStack Adaptor

> Add Dodo Payments checkout, Customer Portal, and webhook server routes to a TanStack Start project with the @dodopayments/tanstack package.

The `@dodopayments/tanstack` package gives your TanStack Start project three request handlers. `Checkout` returns checkout URLs, `CustomerPortal` sends a customer to the Customer Portal, and `Webhooks` verifies webhook events and routes them to your code. Each handler takes a standard `Request` and returns a `Response`, so you call it from a server route handler.

<CardGroup cols={2}>
  <Card title="Checkout Handler" icon="cart-shopping" href="#checkout-route-handler">
    Create checkout URLs with static, dynamic, and checkout session flows.
  </Card>

  <Card title="Customer Portal" icon="user" href="#customer-portal-route-handler">
    Let customers manage their subscriptions and details.
  </Card>

  <Card title="Webhooks" icon="bell" href="#webhook-route-handler">
    Receive and process Dodo Payments webhook events.
  </Card>
</CardGroup>

## Installation

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

    ```bash theme={null}
    npm install @dodopayments/tanstack
    ```

    The package also needs `zod` 3.25 or later, which it lists as a peer dependency.
  </Step>

  <Step title="Set Up Environment Variables">
    Create a `.env` file in your project root. Create the API key under **Developer → API Keys**. Add your webhook endpoint under **Developer → Webhooks**, and copy its **Signing secret** into `DODO_PAYMENTS_WEBHOOK_KEY`:

    ```env expandable 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
    # test_mode or live_mode
    DODO_PAYMENTS_ENVIRONMENT=test_mode
    ```

    TanStack Start loads `.env` files, and server routes read the values from `process.env`. `DODO_PAYMENTS_RETURN_URL` is where customers land after checkout. If you don't pass an environment, the handlers use `live_mode`. A test mode API key works only with `test_mode`.

    <Warning>
      Never commit your `.env` file or secrets to version control.
    </Warning>
  </Step>
</Steps>

## Route Handler Examples

<Info>
  The examples are TanStack Start server routes in `src/routes/api/`. Each one defines its handlers under `server.handlers` in `createFileRoute`. Older TanStack Start releases, such as 1.129, define server routes with `createServerFileRoute` from `@tanstack/react-start/server` and a `.methods()` call instead. The Dodo Payments handlers work the same way with both APIs: pass them the `request`.
</Info>

<Tabs>
  <Tab title="Checkout Handler">
    <Info>
      Use this handler to add Dodo Payments checkout to your app. The `GET` handler serves static checkout. The `POST` handler serves checkout sessions, or dynamic checkout when you set `type: "dynamic"`. The dynamic checkout example assumes you set `type: "dynamic"`.
    </Info>

    <CodeGroup>
      ```typescript TanStack Route Handler expandable theme={null}
      // src/routes/api/checkout.ts
      import { Checkout } from "@dodopayments/tanstack";
      import { createFileRoute } from "@tanstack/react-router";

      export const Route = createFileRoute("/api/checkout")({
        server: {
          handlers: {
            GET: async ({ request }) => {
              return Checkout({
                bearerToken: process.env.DODO_PAYMENTS_API_KEY,
                returnUrl: process.env.DODO_PAYMENTS_RETURN_URL,
                environment: process.env.DODO_PAYMENTS_ENVIRONMENT as "test_mode" | "live_mode",
                type: "static", // optional, defaults to 'static'
              })(request);
            },
            POST: async ({ request }) => {
              return Checkout({
                bearerToken: process.env.DODO_PAYMENTS_API_KEY,
                returnUrl: process.env.DODO_PAYMENTS_RETURN_URL,
                environment: process.env.DODO_PAYMENTS_ENVIRONMENT as "test_mode" | "live_mode",
                type: "session", // or "dynamic" for dynamic checkout
              })(request);
            },
          },
        },
      });
      ```
    </CodeGroup>

    <CodeGroup>
      ```bash Static Checkout cURL Example theme={null}
      curl --request GET \
        --url 'https://example.com/api/checkout?productId=pdt_fqJhl7pxKWiLhwQR042rh'
      ```
    </CodeGroup>

    <CodeGroup>
      ```bash Dynamic Checkout cURL Example expandable theme={null}
      curl --request POST \
        --url https://example.com/api/checkout \
        --header 'Content-Type: application/json' \
        --data '{
        "billing": {
          "city": "Austin",
          "country": "US",
          "state": "TX",
          "street": "123 Main St",
          "zipcode": "78701"
        },
        "customer": {
          "email": "customer@example.com",
          "name": "John Doe"
        },
        "metadata": {},
        "payment_link": true,
        "product_id": "pdt_QMDuvLkbVzCRWRQjLNcs",
        "quantity": 1,
        "billing_currency": "USD",
        "discount_codes": ["IKHZ23M9GQ"],
        "return_url": "https://example.com",
        "trial_period_days": 10
      }'
      ```
    </CodeGroup>

    <CodeGroup>
      ```bash Checkout Session cURL Example expandable theme={null}
      curl --request POST \
        --url https://example.com/api/checkout \
        --header 'Content-Type: application/json' \
        --data '{
        "product_cart": [
          {
            "product_id": "pdt_QMDuvLkbVzCRWRQjLNcs",
            "quantity": 1
          }
        ],
        "customer": {
          "email": "customer@example.com",
          "name": "John Doe"
        },
        "return_url": "https://example.com/success"
      }'
      ```
    </CodeGroup>
  </Tab>

  <Tab title="Customer Portal Handler">
    <Info>
      Use this handler to send customers to the Dodo Payments Customer Portal, where they manage their subscriptions and details.
    </Info>

    <CodeGroup>
      ```typescript TanStack Route Handler expandable theme={null}
      // src/routes/api/customer-portal.ts
      import { CustomerPortal } from "@dodopayments/tanstack";
      import { createFileRoute } from "@tanstack/react-router";

      export const Route = createFileRoute("/api/customer-portal")({
        server: {
          handlers: {
            GET: async ({ request }) => {
              return CustomerPortal({
                bearerToken: process.env.DODO_PAYMENTS_API_KEY,
                environment: process.env.DODO_PAYMENTS_ENVIRONMENT as "test_mode" | "live_mode",
              })(request);
            },
          },
        },
      });
      ```
    </CodeGroup>

    <CodeGroup>
      ```bash Customer Portal cURL Example theme={null}
      curl --request GET \
        --url 'https://example.com/api/customer-portal?customer_id=cus_9VuW4K7O3GHwasENg31m&send_email=true'
      ```
    </CodeGroup>
  </Tab>

  <Tab title="Webhook Handler">
    <Info>
      Use this handler to receive Dodo Payments webhook events in your TanStack Start app and verify their signatures.
    </Info>

    <CodeGroup>
      ```typescript TanStack Route Handler expandable theme={null}
      // src/routes/api/webhook.ts
      import { Webhooks } from "@dodopayments/tanstack";
      import { createFileRoute } from "@tanstack/react-router";

      export const Route = createFileRoute("/api/webhook")({
        server: {
          handlers: {
            POST: async ({ request }) => {
              return Webhooks({
                webhookKey: process.env.DODO_PAYMENTS_WEBHOOK_KEY!,
                onPayload: async (payload) => {
                  console.log(payload);
                },
              })(request);
            },
          },
        },
      });
      ```
    </CodeGroup>
  </Tab>
</Tabs>

## Checkout Route Handler

The checkout handler supports all three ways to take payments with Dodo Payments:

* **Static Payment Links:** Shareable URLs that collect payments without code.
* **Dynamic Payment Links:** Payment links you generate with custom details. They use deprecated endpoints.
* **Checkout Sessions:** Hosted checkout with a product cart, customer details, and customization options. This is the recommended flow.

`Checkout` takes these options:

| Option | Type | Description |
| - | - | - |
| `bearerToken` | `string` | Your Dodo Payments API key. |
| `environment` | `string` | `test_mode` or `live_mode`. Defaults to `live_mode`. |
| `returnUrl` | `string` | Optional. The URL that checkout redirects the customer to after payment. A `return_url` in a POST body takes precedence. |
| `type` | `string` | The checkout flow: `static`, `dynamic`, or `session`. Defaults to `static`. |

The handler serves static checkout for `GET` requests. For `POST` requests, it creates a dynamic payment link when `type` is `dynamic`, and a checkout session otherwise.

<AccordionGroup>
  <Accordion title="Static Checkout (GET)">
    ### Supported Query Parameters

    <ParamField query="productId" type="string" required>
      Product identifier, for example `?productId=pdt_nZuwz45WAs64n3l07zpQR`.
    </ParamField>

    <ParamField query="quantity" type="integer" default="1">
      Quantity of the product.
    </ParamField>

    <ParamField query="fullName" type="string">
      Customer's full name. Ignored if `firstName` or `lastName` is provided.
    </ParamField>

    <ParamField query="firstName" type="string">
      Customer's first name.
    </ParamField>

    <ParamField query="lastName" type="string">
      Customer's last name.
    </ParamField>

    <ParamField query="email" type="string">
      Customer's email address.
    </ParamField>

    <ParamField query="country" type="string">
      Customer's country, as an ISO 3166-1 alpha-2 code.
    </ParamField>

    <ParamField query="addressLine" type="string">
      Customer's street address.
    </ParamField>

    <ParamField query="city" type="string">
      Customer's city.
    </ParamField>

    <ParamField query="state" type="string">
      Customer's state or province.
    </ParamField>

    <ParamField query="zipCode" type="string">
      Customer's ZIP or postal code.
    </ParamField>

    <ParamField query="disableFullName" type="boolean">
      Set to `true` to disable the full name field.
    </ParamField>

    <ParamField query="disableFirstName" type="boolean">
      Set to `true` to disable the first name field.
    </ParamField>

    <ParamField query="disableLastName" type="boolean">
      Set to `true` to disable the last name field.
    </ParamField>

    <ParamField query="disableEmail" type="boolean">
      Set to `true` to disable the email field.
    </ParamField>

    <ParamField query="disableCountry" type="boolean">
      Set to `true` to disable the country field.
    </ParamField>

    <ParamField query="disableAddressLine" type="boolean">
      Set to `true` to disable the address line field.
    </ParamField>

    <ParamField query="disableCity" type="boolean">
      Set to `true` to disable the city field.
    </ParamField>

    <ParamField query="disableState" type="boolean">
      Set to `true` to disable the state field.
    </ParamField>

    <ParamField query="disableZipCode" type="boolean">
      Set to `true` to disable the ZIP code field.
    </ParamField>

    <ParamField query="paymentCurrency" type="string">
      Payment currency, for example `USD`.
    </ParamField>

    <ParamField query="showCurrencySelector" type="boolean" default="true">
      Show or hide the currency selector.
    </ParamField>

    <ParamField query="paymentAmount" type="number">
      Fixes the amount charged, in major currency units, for example `12.5` for \$12.50. Works with Pay What You Want products only, and is ignored if it's below the product's minimum price.
    </ParamField>

    <ParamField query="showDiscounts" type="boolean" default="true">
      Show or hide the discounts section.
    </ParamField>

    <ParamField query="metadata_*" type="string">
      Any query parameter that starts with `metadata_` is passed to checkout as metadata, for example `metadata_orderId=123`.
    </ParamField>

    A disable flag takes effect only when the matching field has a value, for example `email` with `disableEmail=true`. The handler adds `returnUrl` from its config to the link as `redirect_url`.

    <Warning>
      If `productId` is missing, the handler returns a 400 response. Invalid query parameters, or a product that doesn't exist in your account, also return 400.
    </Warning>

    ### Response Format

    Static checkout returns a JSON response with the checkout URL. In test mode, the URL uses `test.checkout.dodopayments.com`:

    ```json theme={null}
    {
      "checkout_url": "https://test.checkout.dodopayments.com/buy/pdt_fqJhl7pxKWiLhwQR042rh?quantity=1&redirect_url=https%3A%2F%2Fyourdomain.com%2Fcheckout%2Fsuccess"
    }
    ```
  </Accordion>

  <Accordion title="Dynamic Checkout (POST)">
    * Send the parameters as a JSON body in a POST request.
    * Supports both one-time and recurring payments. The handler retrieves the product, then creates a subscription if the product is recurring and a one-time payment otherwise.
    * The body needs `billing` (with `street`, `city`, `state`, `country`, and `zipcode`) and `customer`, plus `product_id` or `product_cart`. Subscriptions need `product_id`.
    * For every supported body field, see:
      * [Request body for a One Time Payment Product](https://docs.dodopayments.com/api-reference/payments/post-payments)
      * [Request body for a Subscription Product](https://docs.dodopayments.com/api-reference/subscriptions/post-subscriptions)

    <Warning>
      Dynamic checkout proxies the deprecated `POST /payments` and `POST /subscriptions` endpoints. It keeps working for existing integrations, but new integrations should use checkout sessions.
    </Warning>

    ### Response Format

    Dynamic checkout returns a JSON response with the payment link as the checkout URL:

    ```json theme={null}
    {
      "checkout_url": "https://test.checkout.dodopayments.com/cbq"
    }
    ```
  </Accordion>

  <Accordion title="Checkout Sessions (POST)">
    Checkout sessions create a hosted checkout for one-time purchases and subscriptions, with full control over customization. `product_cart` is the only required field, and it needs at least one product. If the body has no `return_url`, the handler uses `returnUrl` from its config.

    Each `checkout_url` works once and expires after 24 hours, or after 15 minutes when you pass `confirm: true`. A session created with `payment_method_id` returns no `checkout_url`, so the handler responds with 400.

    For more details and every supported field, see the [Checkout Sessions Integration Guide](https://docs.dodopayments.com/developer-resources/checkout-session).

    ### Response Format

    Checkout sessions return a JSON response with the checkout URL:

    ```json theme={null}
    {
      "checkout_url": "https://test.checkout.dodopayments.com/session/cks_Gi6KGJ2zFJo9rq9Ukifwa"
    }
    ```
  </Accordion>
</AccordionGroup>

## Customer Portal Route Handler

The Customer Portal route handler creates a Customer Portal session for the customer you pass and redirects the browser to it. `CustomerPortal` takes the same `bearerToken` and `environment` options as `Checkout`.

<Warning>
  The handler doesn't check who is calling it. Anyone who requests it with a customer ID gets that customer's portal. Protect the route with your own authentication, and pass only the signed-in user's customer ID.
</Warning>

### Query Parameters

<ParamField query="customer_id" type="string" required>
  The customer ID for the portal session, for example `?customer_id=cus_123`.
</ParamField>

<ParamField query="send_email" type="boolean">
  If set to `true`, Dodo Payments also emails the portal link to the customer.
</ParamField>

The handler returns 400 if `customer_id` is missing, and 500 if the portal session can't be created.

## Webhook Route Handler

The webhook route handler verifies each request with your webhook secret, passed as `webhookKey`, before it runs your code:

* **Method:** Only POST requests are supported. Other methods return 405.
* **Signature Verification:** Verifies the `webhook-id`, `webhook-timestamp`, and `webhook-signature` headers with `webhookKey`, following the Standard Webhooks specification. Returns 401 if verification fails.
* **Payload Validation:** Validates the payload with Zod. Returns 400 for an invalid payload.
* **Error Handling:**
  * 401: Invalid signature
  * 400: Invalid payload
  * 500: Internal error during verification
* **Event Routing:** Calls `onPayload` for every event, then the handler for the event's type, and returns 200.

The adaptor doesn't catch errors thrown in your handlers. They propagate to TanStack Start, and the request fails.

### Supported Webhook Event Handlers

Every handler is optional and async, and 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>

For what each event means, see the [Webhook Event Guide](/developer-resources/webhooks/intents/webhook-events-guide).

## Prompt for LLM

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 an expert TanStack developer assistant. Your task is to guide a user through integrating the @dodopayments/tanstack adapter into their existing TanStack Start project.

The @dodopayments/tanstack adapter provides request handlers for Dodo Payments' Checkout, Customer Portal, and Webhook functionalities. Each handler takes a Request and returns a Response, so call it from a TanStack Start server route.

First, install the necessary packages. Use the package manager appropriate for your project (npm, yarn, or bun) based on the presence of lock files (e.g., package-lock.json for npm, yarn.lock for yarn, bun.lockb for bun):

npm install @dodopayments/tanstack

The examples below define server routes with createFileRoute and server.handlers. If the project uses an older TanStack Start release that exports createServerFileRoute from @tanstack/react-start/server instead, use createServerFileRoute("/api/checkout").methods({ GET, POST }) with the same handler bodies.

Here's how you should structure your response:

    Ask the user which functionalities they want to integrate.

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

    Checkout Route Handler (for handling product checkouts)

    Customer Portal Route Handler (for managing customer subscriptions/details)

    Webhook Route Handler (for receiving Dodo Payments webhook events)

    All (integrate all three)"

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

If Checkout Route Handler is selected:

Purpose: This handler manages different types of checkout flows. All checkout types (static, dynamic, and sessions) return JSON responses with checkout URLs for programmatic handling.
File Creation: Create a new file at src/routes/api/checkout.ts in your TanStack Start project.

Code Snippet:

// src/routes/api/checkout.ts
import { Checkout } from "@dodopayments/tanstack";
import { createFileRoute } from "@tanstack/react-router";

export const Route = createFileRoute("/api/checkout")({
  server: {
    handlers: {
      GET: async ({ request }) => {
        return Checkout({
          bearerToken: process.env.DODO_PAYMENTS_API_KEY,
          returnUrl: process.env.DODO_PAYMENTS_RETURN_URL,
          environment: process.env.DODO_PAYMENTS_ENVIRONMENT as "test_mode" | "live_mode",
          type: "static", // optional, defaults to 'static'
        })(request);
      },
      POST: async ({ request }) => {
        return Checkout({
          bearerToken: process.env.DODO_PAYMENTS_API_KEY,
          returnUrl: process.env.DODO_PAYMENTS_RETURN_URL,
          environment: process.env.DODO_PAYMENTS_ENVIRONMENT as "test_mode" | "live_mode",
          type: "session", // or "dynamic" for dynamic link
        })(request);
      },
    },
  },
});


Configuration & Usage:

    bearerToken: Your Dodo Payments API key. It's recommended to set this via the DODO_PAYMENTS_API_KEY environment variable.

    returnUrl: (Optional) The URL to redirect the user to after a successful checkout. A return_url in the POST body takes precedence.

    environment: (Optional) Set to "test_mode" for testing, or omit/set to "live_mode" for production.

    type: (Optional) Set to "static" for GET/static checkout, "dynamic" for POST/dynamic checkout, or "session" for POST/checkout sessions.

Static Checkout (GET) Query Parameters:

    productId (required): Product identifier (e.g., ?productId=pdt_nZuwz45WAs64n3l07zpQR)

    quantity (optional): Quantity of the product

    Customer Fields (optional): fullName, firstName, lastName, email, country, addressLine, city, state, zipCode

    Disable Flags (optional, set to true to disable): disableFullName, disableFirstName, disableLastName, disableEmail, disableCountry, disableAddressLine, disableCity, disableState, disableZipCode

    Advanced Controls (optional): paymentCurrency, showCurrencySelector, paymentAmount, showDiscounts

    Metadata (optional): Any query parameter starting with metadata_ (e.g., ?metadata_userId=abc123)

    Returns: {"checkout_url": "https://test.checkout.dodopayments.com/buy/pdt_nZuwz45WAs64n3l07zpQR?quantity=1"}

Dynamic Checkout (POST) - Returns JSON with checkout_url: Parameters are sent as a JSON body. It requires billing and customer, plus product_id or product_cart. Supports both one-time and recurring payments. Returns: {"checkout_url": "https://test.checkout.dodopayments.com/cbq"}. For a complete list of supported POST body fields, refer to:

    Docs - One Time Payment Product: https://docs.dodopayments.com/api-reference/payments/post-payments

    Docs - Subscription Product: https://docs.dodopayments.com/api-reference/subscriptions/post-subscriptions


Checkout Sessions (POST) - (Recommended) A more customizable checkout experience. Returns JSON with checkout_url: Parameters are sent as a JSON body, and product_cart is required. Supports both one-time and recurring payments. Returns: {"checkout_url": "https://test.checkout.dodopayments.com/session/cks_Gi6KGJ2zFJo9rq9Ukifwa"}. For a complete list of supported fields, refer to:

  Checkout Sessions Integration Guide: https://docs.dodopayments.com/developer-resources/checkout-session

Error Handling: If productId is missing or other query parameters are invalid, the handler will return a 400 response.

If Customer Portal Route Handler is selected:

Purpose: This handler redirects a customer to their Dodo Payments customer portal. It does not authenticate the caller, so protect the route and pass only the signed-in user's customer ID.
File Creation: Create a new file at src/routes/api/customer-portal.ts in your TanStack Start project.

Code Snippet:

// src/routes/api/customer-portal.ts
import { CustomerPortal } from "@dodopayments/tanstack";
import { createFileRoute } from "@tanstack/react-router";

export const Route = createFileRoute("/api/customer-portal")({
  server: {
    handlers: {
      GET: async ({ request }) => {
        return CustomerPortal({
          bearerToken: process.env.DODO_PAYMENTS_API_KEY,
          environment: process.env.DODO_PAYMENTS_ENVIRONMENT as "test_mode" | "live_mode",
        })(request);
      },
    },
  },
});

Query Parameters:

    customer_id (required): The customer ID for the portal session (e.g., ?customer_id=cus_123)

    send_email (optional, boolean): If set to true, sends an email to the customer with the portal link.

    Returns 400 if customer_id is missing.

If Webhook Route Handler is selected:

Purpose: This handler processes incoming webhook events from Dodo Payments, allowing your application to react to events like successful payments, refunds, or subscription changes.
File Creation: Create a new file at src/routes/api/webhook.ts in your TanStack Start project.

Code Snippet:

// src/routes/api/webhook.ts
import { Webhooks } from "@dodopayments/tanstack";
import { createFileRoute } from "@tanstack/react-router";

export const Route = createFileRoute("/api/webhook")({
  server: {
    handlers: {
      POST: async ({ request }) => {
        return Webhooks({
          webhookKey: process.env.DODO_PAYMENTS_WEBHOOK_KEY!,
          onPayload: async (payload) => {
            console.log(payload);
          },
        })(request);
      },
    },
  },
});

Handler Details:

    Method: Only POST requests are supported. Other methods return 405.

    Signature Verification: The handler verifies the webhook signature using the webhookKey and returns 401 if verification fails.

    Payload Validation: The payload is validated with Zod. Returns 400 for invalid payloads.

Error Handling:

    401: Invalid signature

    400: Invalid payload

    500: Internal error during verification

Event Routing: Calls onPayload for every event, then the event handler that matches 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>

    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>

    Environment Variable Setup:

To ensure the adapter functions correctly, you will need to manually set up the following environment variables in your TanStack project's deployment environment (e.g., Vercel, Netlify, AWS, etc.):

    DODO_PAYMENTS_API_KEY: Your Dodo Payments API Key (required for Checkout and Customer Portal).

    DODO_PAYMENTS_RETURN_URL: (Optional) The URL to redirect to after a successful checkout (for Checkout handler).

    DODO_PAYMENTS_WEBHOOK_KEY: Your Dodo Payments Webhook Secret (required for Webhook handler).

    DODO_PAYMENTS_ENVIRONMENT: test_mode or live_mode. The handlers default to live_mode.

Example .env file:

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

Usage in your code:

bearerToken: process.env.DODO_PAYMENTS_API_KEY
webhookKey: process.env.DODO_PAYMENTS_WEBHOOK_KEY!

Important: Never commit sensitive environment variables directly into your version control. Use environment variables for all sensitive information.

If the user needs assistance setting up environment variables for their specific deployment environment, ask them what platform they are using (e.g., Vercel, Netlify, AWS, etc.), and provide guidance. You can also add comments to their PR or chat depending on the context

```


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