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

# Bun Adaptor

> Add Dodo Payments checkout, Customer Portal, and webhook handlers to a Bun server built with Bun.serve() using the @dodopayments/bun package.

The `@dodopayments/bun` package gives your Bun server 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 the `fetch` handler of `Bun.serve()`.

<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}
    bun add @dodopayments/bun
    ```

    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
    # test_mode or live_mode
    DODO_PAYMENTS_ENVIRONMENT=test_mode
    DODO_PAYMENTS_RETURN_URL=https://yourdomain.com/checkout/success
    ```

    Bun reads `.env` files automatically, so the examples read these 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>
  All examples use Bun's native server, `Bun.serve()`, and route requests by path and method in its `fetch` handler.
</Info>

<Tabs>
  <Tab title="Checkout Handler">
    <Info>
      Use this handler to add Dodo Payments checkout to your Bun server. The static handler serves `GET` requests. The session and dynamic handlers serve `POST` requests. The dynamic checkout example assumes the server returns `dynamicCheckoutHandler(request)` for `POST` requests.
    </Info>

    <CodeGroup>
      ```typescript Bun Server Handler expandable theme={null}
      import { Checkout } from '@dodopayments/bun';

      const staticCheckoutHandler = Checkout({
          bearerToken: process.env.DODO_PAYMENTS_API_KEY,
          returnUrl: process.env.DODO_PAYMENTS_RETURN_URL,
          environment: process.env.DODO_PAYMENTS_ENVIRONMENT,
          type: "static"
      });

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

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

      Bun.serve({
        port: 3000,
        fetch(request) {
          const url = new URL(request.url);
          
          if (url.pathname === "/api/checkout") {
            if (request.method === "GET") {
              return staticCheckoutHandler(request);
            }
            if (request.method === "POST") {
              return sessionCheckoutHandler(request);
              // or return dynamicCheckoutHandler(request);
            }
          }
          
          return new Response("Not Found", { status: 404 });
        },
      });
      ```
    </CodeGroup>

    <CodeGroup>
      ```bash Static Checkout cURL Example theme={null}
      curl --request GET \
        --url 'https://example.com/api/checkout?productId=pdt_xxx'
      ```
    </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_xxx",
        "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_xxx",
            "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 Bun Server Handler expandable theme={null}
      import { CustomerPortal } from "@dodopayments/bun";

      const customerPortalHandler = CustomerPortal({
        bearerToken: process.env.DODO_PAYMENTS_API_KEY,
        environment: process.env.DODO_PAYMENTS_ENVIRONMENT,
      });

      Bun.serve({
        port: 3000,
        fetch(request) {
          const url = new URL(request.url);
          
          if (url.pathname === "/api/customer-portal" && request.method === "GET") {
            return customerPortalHandler(request);
          }
          
          return new Response("Not Found", { status: 404 });
        },
      });
      ```
    </CodeGroup>

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

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

    <CodeGroup>
      ```typescript Bun Server Handler expandable theme={null}
      import { Webhooks } from "@dodopayments/bun";

      const webhookHandler = Webhooks({
        webhookKey: process.env.DODO_PAYMENTS_WEBHOOK_KEY,
        onPayload: async (payload) => {
          // handle the payload
        },
        // ... other event handlers for granular control
      });

      Bun.serve({
        port: 3000,
        fetch(request) {
          const url = new URL(request.url);
          
          if (url.pathname === "/api/webhook/dodo-payments" && request.method === "POST") {
            return webhookHandler(request);
          }
          
          return new Response("Not Found", { status: 404 });
        },
      });
      ```
    </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_xxx`.
    </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_xxx?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:** Parses the body as JSON and validates it with Zod. Returns 400 for invalid JSON or 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 `Bun.serve()`, 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 Bun developer assistant. Your task is to guide a user through integrating the @dodopayments/bun adapter into their existing Bun project.

The @dodopayments/bun adapter provides handlers for Dodo Payments' Checkout, Customer Portal, and Webhook functionalities. Each handler takes a Web Standard Request and returns a Response, so it works with Bun's native server, Bun.serve().

First, install the necessary package:

bun add @dodopayments/bun

Here's how you should structure your response:

    Ask the user which functionalities they want to integrate.

"Which parts of the @dodopayments/bun adapter would you like to integrate into your project? You can choose one or more of the following:
1. Checkout (static, dynamic, or session-based)
2. Customer Portal
3. Webhooks"

    Based on the user's selection, provide step-by-step integration instructions.

For each selected functionality, show:

    The environment variables required
    The exact code to add to their server file
    Where to place the code in their Bun.serve() configuration

Provide complete, working examples that the user can copy and paste directly into their project.

    Environment Variables Setup

Always include instructions for setting up the .env file. Bun reads .env files automatically:

DODO_PAYMENTS_API_KEY=your-api-key
DODO_PAYMENTS_WEBHOOK_KEY=your-webhook-secret
DODO_PAYMENTS_ENVIRONMENT=test_mode
DODO_PAYMENTS_RETURN_URL=your-return-url

DODO_PAYMENTS_ENVIRONMENT is test_mode or live_mode. The handlers default to live_mode. Remind the user to never commit their .env file to version control.

    For Checkout Integration

If the user selects Checkout, ask which type they need:

    Static checkout (GET requests with query parameters)
    Dynamic checkout (POST requests with JSON body)
    Checkout sessions (POST requests with product cart, recommended for new integrations)

Provide the appropriate handler code for their selection. Every checkout type returns JSON with a checkout_url.

    For Customer Portal Integration

Provide a complete example showing how to integrate the customer portal handler into their Bun server. The handler does not authenticate the caller, so protect the route and pass only the signed-in user's customer ID.

    For Webhook Integration

Provide a complete example showing how to integrate the webhook handler with available event handlers for granular control.

    Full Example

If the user wants to integrate all three functionalities, provide a complete Bun server example that combines all handlers.

```typescript
import { Checkout, CustomerPortal, Webhooks } from "@dodopayments/bun";

const staticCheckoutHandler = Checkout({
  bearerToken: process.env.DODO_PAYMENTS_API_KEY,
  returnUrl: process.env.DODO_PAYMENTS_RETURN_URL,
  environment: process.env.DODO_PAYMENTS_ENVIRONMENT,
  type: "static",
});

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

const customerPortalHandler = CustomerPortal({
  bearerToken: process.env.DODO_PAYMENTS_API_KEY,
  environment: process.env.DODO_PAYMENTS_ENVIRONMENT,
});

const webhookHandler = Webhooks({
  webhookKey: process.env.DODO_PAYMENTS_WEBHOOK_KEY,
  onPaymentSucceeded: async (payload) => {
    console.log("Payment succeeded:", payload);
    // Your business logic here
  },
  onSubscriptionActive: async (payload) => {
    console.log("Subscription activated:", payload);
    // Your business logic here
  },
});

Bun.serve({
  port: 3000,
  fetch(request) {
    const url = new URL(request.url);
    
    // Checkout routes
    if (url.pathname === "/api/checkout") {
      if (request.method === "GET") {
        return staticCheckoutHandler(request);
      }
      if (request.method === "POST") {
        return sessionCheckoutHandler(request);
      }
    }
    
    // Customer portal route
    if (url.pathname === "/api/customer-portal" && request.method === "GET") {
      return customerPortalHandler(request);
    }
    
    // Webhook route
    if (url.pathname === "/api/webhook/dodo-payments" && request.method === "POST") {
      return webhookHandler(request);
    }
    
    return new Response("Not Found", { status: 404 });
  },
});

console.log("Server running on http://localhost:3000");

Additional Guidance:
• Explain that the handlers plug directly into the fetch handler of Bun.serve()
• Highlight the use of Web Standard Request and Response objects
• Mention that the handlers return HTTP status codes for errors: 400 for invalid input, 401 for an invalid webhook signature, 405 for a webhook request that is not POST, and 500 for internal errors
• Provide links to the Dodo Payments documentation for more details
````


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