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

# Fastify Adaptor

> Add Dodo Payments checkout, Customer Portal, and webhook route handlers to a Fastify app with the @dodopayments/fastify adaptor.

The `@dodopayments/fastify` adaptor gives your Fastify app three route handlers: `Checkout` returns checkout URLs, `CustomerPortal` sends a customer to the Customer Portal, and `Webhooks` verifies webhook requests and calls your event handlers.

<CardGroup cols={2}>
  <Card title="Checkout Handler" icon="cart-shopping" href="#checkout-route-handler">
    Create payment links and checkout sessions from your Fastify app.
  </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">
    Verify and process Dodo Payments webhook events.
  </Card>
</CardGroup>

## Installation

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

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

    The package requires Fastify 5.4.0 or later.
  </Step>

  <Step title="Set Up Environment Variables">
    Create a `.env` file in your project root:

    ```env expandable theme={null}
    DODO_PAYMENTS_API_KEY=your-api-key
    DODO_PAYMENTS_WEBHOOK_KEY=your-webhook-secret
    # test_mode or live_mode
    DODO_PAYMENTS_ENVIRONMENT=test_mode
    DODO_PAYMENTS_RETURN_URL=https://yourapp.com/success
    ```

    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`. While you build, use a test mode API key with `DODO_PAYMENTS_ENVIRONMENT=test_mode`, because a test mode key works only against test mode. `DODO_PAYMENTS_RETURN_URL` is optional.

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

## Route Handler Examples

<Info>
  The examples register routes on a Fastify instance created with `Fastify()`. The webhook route needs the raw request body, so its example adds a string body parser inside a plugin that contains only the webhook route.
</Info>

<Tabs>
  <Tab title="Checkout Handler">
    <Info>
      Use this handler to integrate Dodo Payments checkout into your Fastify app. Supports static (GET), dynamic (POST), and session (POST) payment flows. `Checkout()` returns a `getHandler` for the static flow and a `postHandler` for the dynamic and session flows. Register each POST flow on its own path.
    </Info>

    <CodeGroup>
      ```typescript Fastify Route Handler expandable theme={null}
      // route.ts
      import { Checkout } from '@dodopayments/fastify';
      import Fastify from 'fastify'

      const fastify = Fastify({})
      const checkoutGet = Checkout({
        bearerToken: process.env.DODO_PAYMENTS_API_KEY,
        environment: process.env.DODO_PAYMENTS_ENVIRONMENT,
        returnUrl: process.env.DODO_PAYMENTS_RETURN_URL,
        type: 'static'
      });

      const checkoutPost = Checkout({
        bearerToken: process.env.DODO_PAYMENTS_API_KEY,
        environment: process.env.DODO_PAYMENTS_ENVIRONMENT,
        returnUrl: process.env.DODO_PAYMENTS_RETURN_URL,
        type: 'dynamic'
      });

      const checkoutSession = Checkout({
        bearerToken: process.env.DODO_PAYMENTS_API_KEY,
        environment: process.env.DODO_PAYMENTS_ENVIRONMENT,
        returnUrl: process.env.DODO_PAYMENTS_RETURN_URL,
        type: 'session'
      });

      fastify.get('/api/checkout', checkoutGet.getHandler);
      fastify.post('/api/checkout', checkoutPost.postHandler);
      fastify.post('/api/checkout-session', checkoutSession.postHandler);
      ```
    </CodeGroup>

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

    <CodeGroup>
      ```bash Dynamic Checkout cURL expandable theme={null}
      curl --request POST \
        --url https://example.com/api/checkout \
        --header 'Content-Type: application/json' \
        --data '{
        "billing": {
          "city": "San Francisco",
          "country": "US",
          "state": "CA",
          "street": "123 Main St",
          "zipcode": "94102"
        },
        "customer": {
          "email": "test@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 expandable theme={null}
      curl --request POST \
        --url https://example.com/api/checkout-session \
        --header 'Content-Type: application/json' \
        --data '{
        "product_cart": [
          {
            "product_id": "pdt_QMDuvLkbVzCRWRQjLNcs",
            "quantity": 1
          }
        ],
        "customer": {
          "email": "test@example.com",
          "name": "John Doe"
        },
        "return_url": "https://example.com/success"
      }'
      ```
    </CodeGroup>
  </Tab>

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

    <CodeGroup>
      ```typescript Fastify Route Handler expandable theme={null}
      // route.ts
      import { CustomerPortal } from "@dodopayments/fastify";
      import Fastify from 'fastify'

      const fastify = Fastify({})
      const customerPortalHandler = CustomerPortal({
        bearerToken: process.env.DODO_PAYMENTS_API_KEY,
        environment: process.env.DODO_PAYMENTS_ENVIRONMENT
      });
      fastify.get('/api/customer-portal', customerPortalHandler);
      ```
    </CodeGroup>

    <CodeGroup>
      ```bash Customer Portal cURL 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, verify, and process Dodo Payments webhook events in your Fastify app.
    </Info>

    <CodeGroup>
      ```typescript Fastify Route Handler expandable theme={null}
      // route.ts
      import Fastify from 'fastify'
      import { Webhooks } from '@dodopayments/fastify'

      const fastify = Fastify({})

      // Keep the raw-body parser inside this plugin, so other routes still receive parsed JSON
      fastify.register(async (webhookRoutes) => {
        webhookRoutes.addContentTypeParser('application/json', { parseAs: 'string' }, function (req, body, done) {
          done(null, body)
        })

        webhookRoutes.post('/api/webhooks', Webhooks({
          webhookKey: process.env.DODO_PAYMENTS_WEBHOOK_KEY,
          onPayload: async (payload) => {
            // Handle Payload Here
            console.log(payload)
          }
        }))
      })
      ```
    </CodeGroup>
  </Tab>
</Tabs>

## Checkout Route Handler

<Info>
  The adaptor supports all three Dodo Payments checkout flows. Set `type` in the handler config to choose the flow a route serves. Every flow responds with JSON that contains a `checkout_url` for the customer to open.
</Info>

* **Static Payment Links:** `type: "static"`, GET. Builds a payment link for one product from query parameters, after checking that the product exists.
* **Dynamic Payment Links:** `type: "dynamic"`, POST. Creates a one-time payment or a subscription with a payment link, depending on whether the product is recurring.
* **Checkout Sessions:** `type: "session"`, POST. Creates a checkout session from a product cart and customer details. Use this flow for new integrations.

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

`Checkout` returns an object with two handlers. Register `getHandler` for GET when `type` is `static`, and `postHandler` for POST when `type` is `dynamic` or `session`.

<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 postal or ZIP 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">
      The 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 it's `true` and the matching field has a value, for example `email` with `disableEmail`. The handler passes these parameters to a [static payment link](/developer-resources/integration-guide#static-payment-links).

    <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 result in a 400 response.
    </Warning>

    ### Response Format

    Static checkout returns a JSON response with the checkout URL:

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

  <Accordion title="Dynamic Checkout (POST)">
    * Send 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` (with an optional `quantity`) or `product_cart`. Subscriptions need `product_id`.
    * The handler also forwards `metadata`, `allowed_payment_method_types`, `billing_currency`, `discount_codes` (or the deprecated `discount_code`), `return_url`, `show_saved_payment_methods`, and `tax_id`. For subscriptions, it forwards `addons`, `on_demand`, and `trial_period_days` too. It ignores other fields.
    * For field details, refer to:
      * [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 calls the deprecated `POST /payments` and `POST /subscriptions` endpoints. Use Checkout Sessions for new integrations.
    </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)">
    Send a checkout session payload as the JSON body. The handler creates a checkout session, which handles the complete payment flow for one-time purchases and subscriptions, and returns its `checkout_url`. `product_cart` is required and must contain at least one product.

    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.

    Refer to [Checkout Sessions Integration Guide](https://docs.dodopayments.com/developer-resources/checkout-session) for more details and a complete list of supported fields.

    ### 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 in `customer_id` and redirects the request to the portal link. `CustomerPortal` takes the `bearerToken` and `environment` options, the same as `Checkout`. If Dodo Payments can't create the session, the handler returns 500.

### 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`, sends an email to the customer with the portal link.
</ParamField>

<Warning>
  Returns 400 if `customer_id` is missing. The handler doesn't authenticate the request and opens the portal for any `customer_id` it receives, so put the route behind your own authentication and pass only the signed-in user's customer ID.
</Warning>

## Webhook Route Handler

The webhook handler verifies each request with your webhook secret, passed as `webhookKey`, then calls your event handlers.

<Warning>
  The webhook handler needs the raw request body as a string, so add a content type parser for `application/json` with `parseAs: 'string'`. Fastify applies a parser to every route in the scope where you add it. Add it inside a plugin that registers only the webhook route, as the example does. On the root instance, it would also pass a string to the POST checkout handlers, which then return 400.
</Warning>

* **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](https://standardwebhooks.com/) specification. 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 `onPayload` for every event, then the handler for the event's type, and returns 200 when they finish. The handler doesn't catch errors that your event handlers throw.

### Supported Webhook Event Handlers

Every handler is optional and async. For the payload of each event, see the [Webhook Event Guide](/developer-resources/webhooks/intents/webhook-events-guide).

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

***

## Prompt for LLM

```text theme={null}
You are an expert Fastify developer assistant. Your task is to guide a user through integrating the @dodopayments/fastify adapter into their existing Fastify project.

The @dodopayments/fastify adapter provides route handlers for Dodo Payments' Checkout, Customer Portal, and Webhook functionalities, designed to plug directly into a Fastify app.

First, install the necessary package. Use the package manager appropriate for the user's project (npm, yarn, or bun):

npm install @dodopayments/fastify

The package requires Fastify 5.4.0 or later.

---

Here's how you should structure your response:

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

"Which parts of the @dodopayments/fastify 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)"

---

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

**Integration**:
Create routes in your Fastify app for static (GET), dynamic (POST), and checkout sessions (POST). Checkout() returns getHandler for the static flow and postHandler for the dynamic and session flows. Give each POST flow its own path.


import { Checkout } from '@dodopayments/fastify';
import Fastify from 'fastify'

const fastify = Fastify({})
const checkoutGet = Checkout({
    bearerToken: process.env.DODO_PAYMENTS_API_KEY,
    environment: process.env.DODO_PAYMENTS_ENVIRONMENT,
    returnUrl: process.env.DODO_PAYMENTS_RETURN_URL,
    type: 'static'
});

const checkoutPost = Checkout({
    bearerToken: process.env.DODO_PAYMENTS_API_KEY,
    environment: process.env.DODO_PAYMENTS_ENVIRONMENT,
    returnUrl: process.env.DODO_PAYMENTS_RETURN_URL,
    type: 'dynamic'
});

const checkoutSession = Checkout({
    bearerToken: process.env.DODO_PAYMENTS_API_KEY,
    environment: process.env.DODO_PAYMENTS_ENVIRONMENT,
    returnUrl: process.env.DODO_PAYMENTS_RETURN_URL,
    type: 'session'
});

fastify.get('/api/checkout', checkoutGet.getHandler);
fastify.post('/api/checkout', checkoutPost.postHandler);
fastify.post('/api/checkout-session', checkoutSession.postHandler);


Config Options:

    bearerToken: Your Dodo Payments API key (recommended to be stored in DODO_PAYMENTS_API_KEY env variable).

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

    environment: "test_mode" or "live_mode" (defaults to "live_mode")

    type: "static" (GET), "dynamic" (POST), or "session" (POST)

GET (static checkout) expects query parameters:

    productId (required)

    quantity, customer fields (fullName, email, etc.), and metadata (metadata_*) are optional.

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

POST (dynamic checkout) expects a JSON body with payment details (one-time or subscription). It requires billing and customer, plus product_id or product_cart. Returns: {"checkout_url": "https://test.checkout.dodopayments.com/cbq"}. Reference the docs for the full POST schema:

    One-time payments: https://docs.dodopayments.com/api-reference/payments/post-payments

    Subscriptions: https://docs.dodopayments.com/api-reference/subscriptions/post-subscriptions

POST (checkout sessions) - (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

If Customer Portal Route Handler is selected:

Purpose: This route allows customers to manage their subscriptions via the Dodo Payments portal. It redirects the request to the portal link.

Integration:

import { CustomerPortal } from "@dodopayments/fastify";
import Fastify from 'fastify'

const fastify = Fastify({})
const customerPortalHandler = CustomerPortal({
    bearerToken: process.env.DODO_PAYMENTS_API_KEY,
    environment: process.env.DODO_PAYMENTS_ENVIRONMENT
});
fastify.get('/api/customer-portal', customerPortalHandler);

Query Parameters:

    customer_id (required): e.g., ?customer_id=cus_123

    send_email (optional): if true, customer is emailed the portal link

Returns 400 if customer_id is missing. The handler does not authenticate the request, so protect the route with the app's own authentication and pass only the signed-in user's customer ID.

If Webhook Route Handler is selected:

Purpose: Processes incoming webhook events from Dodo Payments to trigger events in your app.

Integration:

The handler needs the raw request body as a string. Add the string content type parser inside a plugin that contains only the webhook route, so the POST checkout handlers still receive parsed JSON.

import Fastify from 'fastify'
import { Webhooks } from '@dodopayments/fastify'

const fastify = Fastify({})

fastify.register(async (webhookRoutes) => {
  webhookRoutes.addContentTypeParser('application/json', { parseAs: 'string' }, function (req, body, done) {
    done(null, body)
  })

  webhookRoutes.post('/api/webhooks', Webhooks({
    webhookKey: process.env.DODO_PAYMENTS_WEBHOOK_KEY,
    onPayload: async (payload) => {
      // Handle Payload Here
      console.log(payload)
    }
  }))
})

Features:

    Only POST method is allowed — others return 405

    Signature verification is performed using webhookKey. Returns 401 if invalid.

    Zod-based payload validation. Returns 400 if invalid schema.

    All handlers are async functions. onPayload runs for every event, before the event-specific handler.

Supported Webhook Event Handlers:

You may pass in any of the following handlers:

    onPayload

    onPaymentSucceeded

    onPaymentFailed

    onPaymentProcessing

    onPaymentCancelled

    onRefundSucceeded

    onRefundFailed

    onDisputeOpened, onDisputeExpired, onDisputeAccepted, onDisputeCancelled, onDisputeChallenged, onDisputeWon, onDisputeLost

    onSubscriptionActive, onSubscriptionOnHold, onSubscriptionRenewed, onSubscriptionPlanChanged, onSubscriptionCancelled, onSubscriptionFailed, onSubscriptionExpired, onSubscriptionUpdated, onSubscriptionPaused, onSubscriptionUnpaused, onSubscriptionUpdatePaymentMethod

    onLicenseKeyCreated

    onAbandonedCheckoutDetected, onAbandonedCheckoutRecovered

    onDunningStarted, onDunningRecovered

    onCreditAdded, onCreditDeducted, onCreditExpired, onCreditRolledOver, onCreditRolloverForfeited, onCreditOverageCharged, onCreditOverageReset, onCreditManualAdjustment, onCreditBalanceLow

    onEntitlementGrantCreated, onEntitlementGrantDelivered, onEntitlementGrantFailed, onEntitlementGrantRevoked

    onPayoutCreated, onPayoutOnHold, onPayoutInProgress, onPayoutFailed, onPayoutSuccess

Environment Variable Setup:

Make sure to define these environment variables in your project:

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://yourapp.com/success

Use these inside your code as:

process.env.DODO_PAYMENTS_API_KEY
process.env.DODO_PAYMENTS_WEBHOOK_KEY

Security Note: Do NOT commit secrets to version control. Use .env files locally and secrets managers in deployment environments (e.g., AWS, Vercel, Heroku, etc.).
```


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