> ## 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 适配器

> 将 Dodo Payments checkout、Customer Portal 和 webhook 处理程序添加到使用 @dodopayments/bun 包、通过 Bun.serve() 构建的 Bun 服务器中。

`@dodopayments/bun` 包为你的 Bun 服务器提供三个请求处理程序。`Checkout` 返回 checkout URL，`CustomerPortal` 将客户发送到 Customer Portal，而 `Webhooks` 验证 webhook 事件并将其路由到你的代码。每个处理程序都接收标准的 `Request` 并返回 `Response`，因此你可以从 `Bun.serve()` 的 `fetch` 处理程序中调用它。

> **[Checkout Handler](#checkout-route-handler)**
>
> 使用静态、动态和 checkout session 流程创建 checkout URL。

> **[Customer Portal](#customer-portal-route-handler)**
>
> 让客户管理其订阅和详细信息。

> **[Webhooks](#webhook-route-handler)**
>
> 接收并处理 Dodo Payments webhook 事件。

## 安装

**1. Install the Package**

在项目根目录运行此命令：

```bash theme={null}
bun add @dodopayments/bun
```

该包还需要 `zod` 3.25 或更高版本，并将其列为 peer dependency。

**2. Set Up Environment Variables**

在项目根目录创建一个 `.env` 文件。在 **Developer → API Keys** 下创建 API key。在 **Developer → Webhooks** 下添加 webhook endpoint，并将其 **Signing secret** 复制到 `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 会自动读取 `.env` 文件，因此示例会从 `process.env` 读取这些值。`DODO_PAYMENTS_RETURN_URL` 是 checkout 后客户返回的地址。如果不传入 environment，处理程序会使用 `live_mode`。test mode API key 只能与 `test_mode` 配合使用。

> [!WARNING]
>
> 切勿将 `.env` 文件或 secrets 提交到版本控制系统。

## 路由处理程序示例

> [!NOTE]
>
> 所有示例都使用 Bun 的原生服务器 `Bun.serve()`，并在其 `fetch` 处理程序中按路径和方法路由请求。

**Checkout Handler**

> [!NOTE]
>
> 使用此处理程序将 Dodo Payments checkout 添加到你的 Bun 服务器。静态处理程序处理 `GET` 请求。session 和 dynamic 处理程序处理 `POST` 请求。动态 checkout 示例假设服务器会为 `POST` 请求返回 `dynamicCheckoutHandler(request)`。

```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 });
  },
});
```

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

```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
}'
```

```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"
}'
```

**Customer Portal Handler**

> [!NOTE]
>
> 使用此处理程序将客户发送到 Dodo Payments Customer Portal，以便他们管理订阅和详细信息。

```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 });
  },
});
```

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

**Webhook Handler**

> [!NOTE]
>
> 使用此处理程序在 Bun 服务器中接收 Dodo Payments webhook 事件并验证其签名。

```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 });
  },
});
```

## Checkout 路由处理程序

checkout 处理程序支持使用 Dodo Payments 收款的三种方式：

* **静态 Payment Links：** 可分享的 URL，无需代码即可收款。
* **动态 Payment Links：** 使用自定义详细信息生成的 payment links。它们使用已弃用的 endpoints。
* **Checkout Sessions：** 为产品购物车、客户详细信息和自定义选项提供托管 checkout。这是推荐的流程。

`Checkout` 接收以下选项：

| 选项 | 类型 | 描述 |
| - | - | - |
| `bearerToken` | `string` | 你的 Dodo Payments API key。 |
| `environment` | `string` | `test_mode` 或 `live_mode`。默认为 `live_mode`。 |
| `returnUrl` | `string` | 可选。checkout 在付款后将客户重定向到的 URL。POST body 中的 `return_url` 优先级更高。 |
| `type` | `string` | checkout 流程：`static`、`dynamic` 或 `session`。默认为 `static`。 |

处理程序会为 `GET` 请求提供静态 checkout。对于 `POST` 请求，当 `type` 为 `dynamic` 时，它会创建 dynamic payment link，否则会创建 checkout session。

> **Static Checkout (GET)**
>
> ### 支持的 Query Parameters
>
> **productId** `string` *required*
>
> 产品标识符，例如 `?productId=pdt_xxx`。
>
> **quantity** `integer` (default: 1)
>
> 产品数量。
>
> **fullName** `string`
>
> 客户的全名。如果提供了 `firstName` 或 `lastName`，则忽略此项。
>
> **firstName** `string`
>
> 客户的名字。
>
> **lastName** `string`
>
> 客户的姓氏。
>
> **email** `string`
>
> 客户的电子邮件地址。
>
> **country** `string`
>
> 客户所在国家，以 ISO 3166-1 alpha-2 代码表示。
>
> **addressLine** `string`
>
> 客户的街道地址。
>
> **city** `string`
>
> 客户所在城市。
>
> **state** `string`
>
> 客户所在州或省。
>
> **zipCode** `string`
>
> 客户的 ZIP 或邮政编码。
>
> **disableFullName** `boolean`
>
> 设置为 `true` 以禁用全名字段。
>
> **disableFirstName** `boolean`
>
> 设置为 `true` 以禁用名字字段。
>
> **disableLastName** `boolean`
>
> 设置为 `true` 以禁用姓氏字段。
>
> **disableEmail** `boolean`
>
> 设置为 `true` 以禁用电子邮件字段。
>
> **disableCountry** `boolean`
>
> 设置为 `true` 以禁用国家字段。
>
> **disableAddressLine** `boolean`
>
> 设置为 `true` 以禁用地址行字段。
>
> **disableCity** `boolean`
>
> 设置为 `true` 以禁用城市字段。
>
> **disableState** `boolean`
>
> 设置为 `true` 以禁用州字段。
>
> **disableZipCode** `boolean`
>
> 设置为 `true` 以禁用 ZIP code 字段。
>
> **paymentCurrency** `string`
>
> 付款货币，例如 `USD`。
>
> **showCurrencySelector** `boolean` (default: true)
>
> 显示或隐藏货币选择器。
>
> **paymentAmount** `number`
>
> 固定收取的金额，以主要货币单位表示，例如 `12.5` 表示 \$12.50。仅适用于 Pay What You Want 产品；如果低于产品最低价格，则会被忽略。
>
> **showDiscounts** `boolean` (default: true)
>
> 显示或隐藏 discounts 部分。
>
> **metadata\_**\* `string`
>
> 任何以 `metadata_` 开头的 query parameter 都会作为 metadata 传递给 checkout，例如 `metadata_orderId=123`。
>
> 只有当匹配字段具有值时，disable flag 才会生效，例如 `email` 与 `disableEmail=true` 一起使用。处理程序会将其配置中的 `returnUrl` 作为 `redirect_url` 添加到链接中。
>
> > [!WARNING]
> >
> > 如果缺少 `productId`，处理程序会返回 400 响应。无效的 query parameters 或账户中不存在的产品同样会返回 400。
>
> ### 响应格式
>
> 静态 checkout 返回包含 checkout URL 的 JSON 响应。在 test mode 中，URL 使用 `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"
> }
> ```

> **Dynamic Checkout (POST)**
>
> * 在 POST request 中将参数作为 JSON body 发送。
> * 同时支持一次性付款和 recurring payments。处理程序会获取产品；如果产品是 recurring，则创建 subscription，否则创建一次性付款。
> * body 需要 `billing`（包含 `street`、`city`、`state`、`country` 和 `zipcode`）以及 `customer`，另外还需要 `product_id` 或 `product_cart`。Subscriptions 需要 `product_id`。
> * 有关所有支持的 body fields，请参阅：
>   * [One Time Payment Product 的 Request body](https://docs.dodopayments.com/api-reference/payments/post-payments)
>
>   * [Subscription Product 的 Request body](https://docs.dodopayments.com/api-reference/subscriptions/post-subscriptions)
>
> > [!WARNING]
> >
> > Dynamic checkout 代理已弃用的 `POST /payments` 和 `POST /subscriptions` endpoints。它会继续为现有 integrations 工作，但新 integrations 应使用 checkout sessions。
>
> ### 响应格式
>
> Dynamic checkout 返回包含 payment link（作为 checkout URL）的 JSON 响应：
>
> ```json theme={null}
> {
>   "checkout_url": "https://test.checkout.dodopayments.com/cbq"
> }
> ```

> **Checkout Sessions (POST)**
>
> Checkout sessions 为一次性购买和 subscriptions 创建 hosted checkout，并提供完整的自定义控制。`product_cart` 是唯一必填字段，并且至少需要一个产品。如果 body 不包含 `return_url`，处理程序会使用其配置中的 `returnUrl`。
>
> 每个 `checkout_url` 只能使用一次，并会在 24 小时后过期；传入 `confirm: true` 时，则在 15 分钟后过期。使用 `payment_method_id` 创建的 session 不会返回 `checkout_url`，因此处理程序会返回 400。
>
> 有关更多详细信息和所有支持的字段，请参阅 [Checkout Sessions Integration Guide](https://docs.dodopayments.com/developer-resources/checkout-session)。
>
> ### 响应格式
>
> Checkout sessions 返回包含 checkout URL 的 JSON 响应：
>
> ```json theme={null}
> {
>   "checkout_url": "https://test.checkout.dodopayments.com/session/cks_Gi6KGJ2zFJo9rq9Ukifwa"
> }
> ```

## Customer Portal 路由处理程序

Customer Portal 路由处理程序会为你传入的客户创建 Customer Portal session，并将浏览器重定向到该 session。`CustomerPortal` 与 `Checkout` 接收相同的 `bearerToken` 和 `environment` 选项。

> [!WARNING]
>
> 处理程序不会检查调用者的身份。任何使用 customer ID 请求它的人都能访问该客户的 portal。请使用自己的 authentication 保护此 route，并且只传入已登录用户的 customer ID。

### Query Parameters

**customer\_id** `string` *required*

portal session 的 customer ID，例如 `?customer_id=cus_123`。

**send\_email** `boolean`

如果设置为 `true`，Dodo Payments 还会将 portal link 通过电子邮件发送给客户。

如果缺少 `customer_id`，处理程序会返回 400；如果无法创建 portal session，则返回 500。

## Webhook 路由处理程序

webhook 路由处理程序会在运行你的代码之前，使用作为 `webhookKey` 传入的 webhook secret 验证每个请求：

* **Method：** 仅支持 POST requests。其他 methods 返回 405。
* **Signature Verification：** 使用 `webhookKey` 验证 `webhook-id`、`webhook-timestamp` 和 `webhook-signature` headers，并遵循 Standard Webhooks specification。验证失败时返回 401。
* **Payload Validation：** 将 body 解析为 JSON，并使用 Zod 验证。JSON 无效或 payload 无效时返回 400。
* **Error Handling：**
  * 401：无效签名
  * 400：无效 payload
  * 500：验证期间发生内部错误
* **Event Routing：** 对每个 event 调用 `onPayload`，然后调用该 event 类型对应的处理程序，并返回 200。

该 adaptor 不会捕获你的 handlers 抛出的 errors。它们会传播到 `Bun.serve()`，请求也会失败。

### 支持的 Webhook Event Handlers

每个 handler 都是可选的 async handler，并会接收其 event type 对应的已验证 payload：

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

有关每个 event 的含义，请参阅 [Webhook Event Guide](/developer-resources/webhooks/intents/webhook-events-guide)。

## LLM 提示词

将此提示词复制到你的 AI coding assistant 中，让它将 adaptor 添加到你的项目中。若还要向 agent 提供 Dodo Payments 文档和 skills，请安装 [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.