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

# Kotlin

> 使用 Dodo Payments Kotlin SDK 从 Kotlin 创建结账会话、产品、订阅和用量事件，并利用可空类型和协程。

Kotlin SDK 为 Kotlin 应用提供了对 Dodo Payments REST API 的类型化访问。它在各处都使用 Kotlin 类型：对于可能缺失的字段使用可空值，使用 `Sequence` 遍历结果，并使用 `suspend` 函数执行异步调用。

## 安装

### Gradle (Kotlin DSL)

在你的 `build.gradle.kts` 中添加依赖：

```kotlin build.gradle.kts theme={null}
implementation("com.dodopayments.api:dodo-payments-kotlin:1.118.0")
```

### Maven

在你的 `pom.xml` 中添加依赖：

```xml pom.xml theme={null}
<dependency>
  <groupId>com.dodopayments.api</groupId>
  <artifactId>dodo-payments-kotlin</artifactId>
  <version>1.118.0</version>
</dependency>
```

> [!TIP]
>
> SDK 版本会增加对 API 变更的支持。要查找最新版本，请查看 [Maven Central](https://central.sonatype.com/artifact/com.dodopayments.api/dodo-payments-kotlin)。

> [!NOTE]
>
> SDK 要求 Java 8 或更高版本。它可以在 JVM 和 Android 上运行，并附带 ProGuard 和 R8 keep 规则。

## 快速开始

创建客户端，然后创建结账会话：

```kotlin theme={null}
import com.dodopayments.api.client.DodoPaymentsClient
import com.dodopayments.api.client.okhttp.DodoPaymentsOkHttpClient
import com.dodopayments.api.models.checkoutsessions.CheckoutSessionRequest
import com.dodopayments.api.models.checkoutsessions.CheckoutSessionResponse
import com.dodopayments.api.models.checkoutsessions.ProductItemReq

// Configure using environment variables (DODO_PAYMENTS_API_KEY, DODO_PAYMENTS_WEBHOOK_KEY, DODO_PAYMENTS_BASE_URL)
// Or system properties (dodopayments.apiKey, dodopayments.webhookKey, dodopayments.baseUrl)
val client: DodoPaymentsClient = DodoPaymentsOkHttpClient.fromEnv()

val params: CheckoutSessionRequest = CheckoutSessionRequest.builder()
    .addProductCart(ProductItemReq.builder()
        .productId("pdt_123")
        .quantity(1)
        .build())
    .build()
    
val checkoutSessionResponse: CheckoutSessionResponse = client.checkoutSessions().create(params)
println(checkoutSessionResponse.sessionId())
```

`fromEnv()` 默认连接到 live mode，除非 `DODO_PAYMENTS_BASE_URL` 或 `dodopayments.baseUrl` 另有说明。要使用 test mode，请参阅 [Test Mode](#test-mode)。test mode API key 只能在 test mode 中使用。

> [!WARNING]
>
> 请将 API key 保存在环境变量或 secrets manager 中。切勿将其提交到版本控制系统。

## 核心功能

> **Coroutines**
>
> 异步客户端的方法是 `suspend` 函数，应从协程中调用。

> **Null Safety**
>
> 可能缺失的字段是可空类型，而不是 `Optional`。

> **Sequences**
>
> 在同步客户端上，`autoPager()` 返回一个 `Sequence`，你可以遍历它来获取更多页面。在异步客户端上，它返回一个 `Flow`。

> **Immutable Models**
>
> 模型类是不可变的，`toBuilder()` 会返回一个用于创建修改后副本的构建器。

## 配置

### 从环境变量配置

`fromEnv()` 从环境变量或系统属性读取设置。系统属性优先：

```kotlin theme={null}
val client: DodoPaymentsClient = DodoPaymentsOkHttpClient.fromEnv()
```

API key 来自 `DODO_PAYMENTS_API_KEY` 或 `dodopayments.apiKey`。Webhook signing secret 来自 `DODO_PAYMENTS_WEBHOOK_KEY` 或 `dodopayments.webhookKey`，base URL 来自 `DODO_PAYMENTS_BASE_URL` 或 `dodopayments.baseUrl`。请创建一个客户端并重复使用，因为每个客户端都有自己的连接池和线程池。

要验证 webhook，请将原始请求正文和请求头传递给 `client.webhooks().unwrap(UnwrapWebhookParams.builder().body(rawBody).headers(headers).build())`，其中 `headers` 是一个 `com.dodopayments.api.core.http.Headers`。它会使用你的 webhook key 检查签名，并返回解析后的事件；验证失败时会抛出 `DodoPaymentsWebhookException`。如果不提供请求头，`unwrap` 不会验证签名。`client.webhooks().unsafeUnwrap(rawBody)` 会在不验证签名的情况下解析正文，因此只能用于测试。请参阅 [Webhooks](/developer-resources/webhooks)。

### 手动配置

在构建器上设置每个选项：

```kotlin theme={null}
import java.time.Duration

val client = DodoPaymentsOkHttpClient.builder()
    .bearerToken("your_api_key_here")
    .baseUrl("https://live.dodopayments.com")
    .maxRetries(3)
    .timeout(Duration.ofSeconds(30))
    .build()
```

### Test Mode

要使用 test mode（`https://test.dodopayments.com`），请在构建器上调用 `testMode()`：

```kotlin theme={null}
val testClient = DodoPaymentsOkHttpClient.builder()
    .fromEnv()
    .testMode()
    .build()
```

### 超时和重试

默认情况下，客户端会重试两次，并在 1 分钟后超时。对于连接错误以及状态为 408、409、429 或 500 及以上的响应，客户端会使用指数退避进行重试。在客户端上设置默认值，或将 `RequestOptions` 传递给单次调用：

```kotlin theme={null}
import com.dodopayments.api.core.RequestOptions
import java.time.Duration

// Global configuration
val client = DodoPaymentsOkHttpClient.builder()
    .fromEnv()
    .timeout(Duration.ofSeconds(45))
    .maxRetries(3)
    .build()

// Per-request timeout override
val product = client.products().retrieve(
    "pdt_123",
    RequestOptions.builder()
        .timeout(Duration.ofSeconds(10))
        .build()
)
```

## 常见操作

本节中的示例使用 [Quick Start](#quick-start) 中的 `client`。

### 创建结账会话

创建结账会话，然后将客户重定向到返回的结账 URL：

```kotlin theme={null}
val params = CheckoutSessionRequest.builder()
    .addProductCart(ProductItemReq.builder()
        .productId("pdt_123")
        .quantity(1)
        .build())
    .returnUrl("https://yourdomain.com/return")
    .build()

val session = client.checkoutSessions().create(params)
println("Checkout URL: ${session.checkoutUrl()}")
```

`checkoutUrl()` 返回一个可空的 `String?`。每个结账 URL 只能使用一次，并会在 24 小时后过期。有关每个会话选项，请参阅 [Checkout Sessions](/developer-resources/checkout-session)。

### 创建产品

创建一个价格为 \$29.99 的月度订阅产品：

```kotlin theme={null}
import com.dodopayments.api.models.products.Price
import com.dodopayments.api.models.products.Product
import com.dodopayments.api.models.products.ProductCreateParams
import com.dodopayments.api.models.misc.Currency
import com.dodopayments.api.models.misc.TaxCategory
import com.dodopayments.api.models.subscriptions.TimeInterval

val createParams = ProductCreateParams.builder()
    .name("Premium Subscription")
    .description("Monthly subscription with all features")
    .price(
        Price.RecurringPrice.builder()
            .currency(Currency.USD)
            .price(2999) // $29.99 in cents
            .discountBps(0)
            .purchasingPowerParity(false)
            .paymentFrequencyCount(1)
            .paymentFrequencyInterval(TimeInterval.MONTH)
            .subscriptionPeriodCount(1)
            .subscriptionPeriodInterval(TimeInterval.MONTH)
            .build()
    )
    .taxCategory(TaxCategory.DIGITAL_PRODUCTS)
    .build()

val product: Product = client.products().create(createParams)
println("Created product ID: ${product.productId()}")
```

`price` 以货币的最小单位表示。`discountBps` 以基点设置折扣，并替代已弃用的 `discount` 字段。

### 激活 License Key

为设备或安装激活 License Key。如果该密钥已达到激活次数限制，API 会返回 `422`，SDK 会抛出 `UnprocessableEntityException`。未激活的密钥会返回 `403`（`PermissionDeniedException`），未知密钥会返回 `404`（`NotFoundException`）：

```kotlin theme={null}
import com.dodopayments.api.errors.UnprocessableEntityException
import com.dodopayments.api.models.licenses.LicenseActivateParams
import com.dodopayments.api.models.licenses.LicenseActivateResponse

val activateParams = LicenseActivateParams.builder()
    .licenseKey("XXXX-XXXX-XXXX-XXXX")
    .name("user-laptop-01")
    .build()

try {
    val activationResult: LicenseActivateResponse = client.licenses()
        .activate(activateParams)

    println("License activated successfully")
    println("Instance ID: ${activationResult.id()}")
    println("License key ID: ${activationResult.licenseKeyId()}")
} catch (e: UnprocessableEntityException) {
    println("License activation failed: ${e.message}")
}
```

### 处理订阅

创建订阅；如果是按需订阅，则对其收费。

> [!WARNING]
>
> `POST /subscriptions`（SDK 的 `subscriptions().create()` 方法）已**弃用**。它仍可用于现有集成，但新集成应通过 [Checkout Session](/developer-resources/checkout-session) 创建订阅。

```kotlin theme={null}
import com.dodopayments.api.models.payments.AttachExistingCustomer
import com.dodopayments.api.models.payments.BillingAddress
import com.dodopayments.api.models.misc.CountryCode
import com.dodopayments.api.models.subscriptions.SubscriptionChargeParams
import com.dodopayments.api.models.subscriptions.SubscriptionCreateParams

// Create a subscription
val subscriptionParams = SubscriptionCreateParams.builder()
    .billing(BillingAddress.builder()
        .city("San Francisco")
        .country(CountryCode.US)
        .state("CA")
        .street("1 Market St")
        .zipcode("94105")
        .build())
    .customer(AttachExistingCustomer.builder()
        .customerId("cus_123")
        .build())
    .productId("pdt_456")
    .quantity(1)
    .build()

val subscription = client.subscriptions().create(subscriptionParams)
println("Subscription ID: ${subscription.subscriptionId()}")

// Charge an on-demand subscription
// productPrice is in the lowest currency denomination (e.g., 2500 = $25.00 USD)
val chargeParams = SubscriptionChargeParams.builder()
    .subscriptionId(subscription.subscriptionId())
    .productPrice(2500)
    .build()

val chargeResponse = client.subscriptions().charge(chargeParams)
println("Payment ID: ${chargeResponse.paymentId()}")
```

> [!NOTE]
>
> `billing` 只需要 `country`，即两个字母的 ISO 国家代码。使用 `AttachExistingCustomer` 关联现有客户，或使用 `NewCustomer` 创建客户。`charge` 用于[按需订阅](/developer-resources/ondemand-subscriptions)，`productPrice` 以货币的最小单位表示。

## 基于用量的计费

### 记录用量事件

向客户发送用量事件。跟踪该事件的 `eventName` 会对其进行聚合：

```kotlin theme={null}
import com.dodopayments.api.models.usageevents.EventInput
import com.dodopayments.api.models.usageevents.UsageEventIngestParams

val usageParams = UsageEventIngestParams.builder()
    .addEvent(EventInput.builder()
        .customerId("cus_456")
        .eventId("event_123")
        .eventName("api_call")
        .build())
    .build()

client.usageEvents().ingest(usageParams)
println("Usage event recorded")
```

`eventId` 是幂等键，因此请为每个事件提供唯一值。每个请求最多接受 1,000 个事件。

## 异步操作

### 异步客户端

异步客户端与同步客户端具有相同的方法，但其中大多数是 `suspend` 函数。请从协程中调用它们：

```kotlin theme={null}
import com.dodopayments.api.client.DodoPaymentsClientAsync
import com.dodopayments.api.client.okhttp.DodoPaymentsOkHttpClientAsync
import kotlinx.coroutines.runBlocking

val asyncClient: DodoPaymentsClientAsync = DodoPaymentsOkHttpClientAsync.fromEnv()

runBlocking {
    val customer = asyncClient.customers().retrieve("cus_123")
    println("Customer email: ${customer.email()}")
}
```

你也可以在同步客户端上调用 `client.async()`，以获取其异步版本。

## 错误处理

对于错误状态，SDK 会抛出 `DodoPaymentsServiceException` 的子类，该类包含 `statusCode()`、`headers()` 和 `body()`。其子类包括 `BadRequestException`（400）、`UnauthorizedException`（401）、`PermissionDeniedException`（403）、`NotFoundException`（404）、`UnprocessableEntityException`（422）、`RateLimitException`（429）、`InternalServerException`（5xx），以及用于其他状态（例如 409）的 `UnexpectedStatusCodeException`：

```kotlin theme={null}
import com.dodopayments.api.errors.*

try {
    // Note: POST /payments is deprecated — prefer checkout sessions for new
    // integrations. Shown here only to illustrate error handling.
    // paymentParams is a PaymentCreateParams with billing, customer, and productCart.
    val payment = client.payments().create(paymentParams)
    println("Success: ${payment.paymentId()}")
} catch (e: UnauthorizedException) {
    println("Authentication failed: ${e.message}")
} catch (e: BadRequestException) {
    println("Invalid request: ${e.message}")
    println("Response body: ${e.body()}")
} catch (e: RateLimitException) {
    println("Rate limit exceeded, retry after: ${e.headers().values("retry-after")}")
} catch (e: DodoPaymentsServiceException) {
    println("API error: ${e.statusCode()} - ${e.message}")
}
```

网络故障会抛出 `DodoPaymentsIoException`，SDK 无法解析的响应会抛出 `DodoPaymentsInvalidDataException`。所有 SDK 异常都继承自 `DodoPaymentsException`。

### 函数式错误处理

使用 `Result` 进行函数式错误处理：

```kotlin theme={null}
import com.dodopayments.api.client.DodoPaymentsClient
import com.dodopayments.api.models.payments.PaymentCreateResponse

fun safeCreatePayment(client: DodoPaymentsClient): Result<PaymentCreateResponse> = runCatching {
    // Note: POST /payments is deprecated — prefer checkout sessions for new integrations
    // paymentParams is a PaymentCreateParams with billing, customer, and productCart.
    client.payments().create(paymentParams)
}

// Usage
safeCreatePayment(client)
    .onSuccess { payment -> println("Created: ${payment.paymentId()}") }
    .onFailure { error -> println("Error: ${error.message}") }
```

> [!TIP]
>
> `runCatching` 会捕获所有异常，包括 SDK 异常，并将其作为失败的 `Result` 返回。

## Android 集成

Kotlin SDK 是一个服务器端 SDK。它使用你的 secret API key 进行身份验证，任何拥有你的 APK 的人都可以提取其中编译的密钥，因此绝不要在 Android 应用中使用它。

要在 Android 应用中收款：

1. 在你的服务器上，使用此 SDK 创建 checkout session（参见 [Ktor 集成](#ktor-integration)），并返回其 `checkout_url`。
2. 在应用中，从你的服务器获取该 `checkout_url`，并使用不包含 API key 的 [Android SDK](/developer-resources/sdks/android) 打开它。

```kotlin theme={null}
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch

class PaymentViewModel(private val api: YourBackendApi) : ViewModel() {
    fun createCheckout(productId: String) {
        viewModelScope.launch {
            // Your backend creates the session with the Kotlin SDK and returns checkout_url.
            // YourBackendApi and openCheckout are your app's own code; openCheckout
            // passes the URL to DodoCheckout from the Android SDK.
            val checkoutUrl = api.createCheckoutSession(productId).checkoutUrl
            openCheckout(checkoutUrl)
        }
    }
}
```

## 响应验证

默认情况下，SDK 仅会在你读取类型异常的属性时抛出 `DodoPaymentsInvalidDataException`。若要预先检查整个响应，请为请求启用验证，或在响应上调用 `validate()`：

```kotlin theme={null}
import com.dodopayments.api.core.RequestOptions

// Per-request validation
val product = client.products().retrieve(
    "pdt_123",
    RequestOptions.builder()
        .responseValidation(true)
        .build()
)

// Or validate explicitly
val validatedProduct = product.validate()
```

## 高级功能

### 代理配置

要通过代理发送请求，请将 `java.net.Proxy` 传递给 builder：

```kotlin theme={null}
import java.net.InetSocketAddress
import java.net.Proxy

val client = DodoPaymentsOkHttpClient.builder()
    .fromEnv()
    .proxy(
        Proxy(
            Proxy.Type.HTTP,
            InetSocketAddress("proxy.example.com", 8080)
        )
    )
    .build()
```

### 临时配置

`withOptions` 会返回一个设置经过修改的客户端，该客户端与原始客户端共享连接池和线程池。原始客户端不会发生变化：

```kotlin theme={null}
val customClient = client.withOptions {
    it.baseUrl("https://example.com")
    it.maxRetries(5)
}
```

## Ktor 集成

创建一次客户端，然后从路由中调用它：

```kotlin theme={null}
import com.dodopayments.api.client.okhttp.DodoPaymentsOkHttpClient
import com.dodopayments.api.errors.DodoPaymentsServiceException
import com.dodopayments.api.models.checkoutsessions.CheckoutSessionRequest
import com.dodopayments.api.models.checkoutsessions.ProductItemReq
import io.ktor.http.HttpStatusCode
import io.ktor.server.application.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*

fun Application.configureRouting() {
    val client = DodoPaymentsOkHttpClient.builder()
        .bearerToken(environment.config.property("dodo.apiKey").getString())
        .build()
    
    routing {
        post("/create-checkout") {
            try {
                // CheckoutRequest is your own @Serializable request class with a productId field
                val request = call.receive<CheckoutRequest>()
                val params = CheckoutSessionRequest.builder()
                    .addProductCart(ProductItemReq.builder().productId(request.productId).quantity(1).build())
                    .build()
                val session = client.checkoutSessions().create(params)
                call.respond(mapOf("checkout_url" to session.checkoutUrl()))
            } catch (e: DodoPaymentsServiceException) {
                call.respond(HttpStatusCode.BadRequest, mapOf("error" to e.message))
            }
        }
    }
}
```

## 资源

> **[GitHub Repository](https://github.com/dodopayments/dodopayments-kotlin)**
>
> 源代码、发行版本和完整的方法列表。

> **[API Reference](/api-reference/introduction)**
>
> 每个 endpoint、参数和响应。

> **[Discord Community](https://discord.gg/bYqAp4ayYh)**
>
> 提出问题并与其他开发者交流。

> **[Report Issues](https://github.com/dodopayments/dodopayments-kotlin/issues)**
>
> 报告 bug 或请求新功能。

## 支持

如需 Kotlin SDK 方面的帮助：

* **Discord**：加入[社区服务器](https://discord.gg/bYqAp4ayYh)获取实时帮助。
* **Email**：联系 [support@dodopayments.com](mailto:support@dodopayments.com)。
* **GitHub**：在[代码仓库](https://github.com/dodopayments/dodopayments-kotlin)中提交 issue。

## 贡献

如需贡献代码，请阅读[贡献指南](https://github.com/dodopayments/dodopayments-kotlin/blob/main/CONTRIBUTING.md)。


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