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

> Use the Dodo Payments Kotlin SDK to create checkout sessions, products, subscriptions, and usage events from Kotlin, with nullable types and coroutines.

The Kotlin SDK gives Kotlin applications typed access to the Dodo Payments REST API. It uses Kotlin types throughout: nullable values for fields that can be missing, `Sequence` for iterating results, and `suspend` functions for asynchronous calls.

## Installation

### Gradle (Kotlin DSL)

Add the dependency to your `build.gradle.kts`:

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

### Maven

Add the dependency to your `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 releases add support for API changes. To find the most recent version, check [Maven Central](https://central.sonatype.com/artifact/com.dodopayments.api/dodo-payments-kotlin).
</Tip>

<Info>
  The SDK requires Java 8 or later. It runs on the JVM and on Android, and it ships ProGuard and R8 keep rules.
</Info>

## Quick Start

Create a client, then create a checkout session:

```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()` connects to live mode unless `DODO_PAYMENTS_BASE_URL` or `dodopayments.baseUrl` says otherwise. To use test mode, see [Test Mode](#test-mode). A test mode API key works only in test mode.

<Warning>
  Keep API keys in environment variables or a secrets manager. Never commit them to version control.
</Warning>

## Core Features

<CardGroup cols={2}>
  <Card title="Coroutines" icon="bolt">
    The async client's methods are `suspend` functions that you call from a coroutine.
  </Card>

  <Card title="Null Safety" icon="shield-check">
    Fields that can be missing are nullable types, not `Optional`.
  </Card>

  <Card title="Sequences" icon="code">
    On the synchronous client, `autoPager()` returns a `Sequence` that fetches more pages as you iterate. On the async client, it returns a `Flow`.
  </Card>

  <Card title="Immutable Models" icon="layer-group">
    Model classes are immutable, and `toBuilder()` returns a builder for a modified copy.
  </Card>
</CardGroup>

## Configuration

### From Environment Variables

`fromEnv()` reads your settings from environment variables or system properties. System properties take precedence:

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

The API key comes from `DODO_PAYMENTS_API_KEY` or `dodopayments.apiKey`. The webhook signing secret comes from `DODO_PAYMENTS_WEBHOOK_KEY` or `dodopayments.webhookKey`, and the base URL from `DODO_PAYMENTS_BASE_URL` or `dodopayments.baseUrl`. Create one client and reuse it, because each client has its own connection pool and thread pools.

To verify a webhook, pass the raw request body and headers to `client.webhooks().unwrap(UnwrapWebhookParams.builder().body(rawBody).headers(headers).build())`, where `headers` is a `com.dodopayments.api.core.http.Headers`. It checks the signature with your webhook key and returns the parsed event, or throws `DodoPaymentsWebhookException`. Without headers, `unwrap` doesn't verify the signature. `client.webhooks().unsafeUnwrap(rawBody)` parses the body without verifying it, so use it only for testing. See [Webhooks](/developer-resources/webhooks).

### Manual Configuration

Set each option on the builder:

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

To use test mode (`https://test.dodopayments.com`), call `testMode()` on the builder:

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

### Timeouts and Retries

By default, the client retries twice and times out after 1 minute. It retries connection errors and responses with status 408, 409, 429, or 500 and above, with exponential backoff. Set the defaults on the client, or pass `RequestOptions` to a single call:

```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()
)
```

## Common Operations

The examples in this section use the `client` from [Quick Start](#quick-start).

### Create a Checkout Session

Create a checkout session, then redirect the customer to the returned checkout 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()` returns a nullable `String?`. Each checkout URL works once and expires after 24 hours. For every session option, see [Checkout Sessions](/developer-resources/checkout-session).

### Create a Product

Create a monthly subscription product priced at \$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` is in the smallest currency unit. `discountBps` sets the discount in basis points and replaces the deprecated `discount` field.

### Activate License Key

Activate a license key for a device or installation. If the key has reached its activation limit, the API returns `422` and the SDK throws `UnprocessableEntityException`. An inactive key returns `403` (`PermissionDeniedException`), and an unknown key returns `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}")
}
```

### Handle Subscriptions

Create a subscription, then charge it if it's an on-demand subscription.

<Warning>
  `POST /subscriptions` (the SDK's `subscriptions().create()` method) is **deprecated**. It still works for existing integrations, but new integrations should create subscriptions through a [Checkout Session](/developer-resources/checkout-session).
</Warning>

```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()}")
```

<Info>
  `billing` requires only `country`, a two-letter ISO country code. Use `AttachExistingCustomer` to attach an existing customer, or `NewCustomer` to create one. `charge` is for [on-demand subscriptions](/developer-resources/ondemand-subscriptions), and `productPrice` is in the smallest currency unit.
</Info>

## Usage-Based Billing

### Record Usage Events

Send a usage event for a customer. Meters that track the event's `eventName` aggregate it:

```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")
```

The `eventId` is the idempotency key, so give each event a unique value. A request accepts up to 1,000 events.

## Async Operations

### Async Client

The async client has the same methods as the synchronous client, but most of them are `suspend` functions. Call them from a coroutine:

```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()}")
}
```

You can also call `client.async()` on a synchronous client to get its async version.

## Error Handling

For an error status, the SDK throws a subclass of `DodoPaymentsServiceException`, which has `statusCode()`, `headers()`, and `body()`. The subclasses are `BadRequestException` (400), `UnauthorizedException` (401), `PermissionDeniedException` (403), `NotFoundException` (404), `UnprocessableEntityException` (422), `RateLimitException` (429), `InternalServerException` (5xx), and `UnexpectedStatusCodeException` for other statuses, such as 409:

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

Network failures throw `DodoPaymentsIoException`, and responses the SDK can't interpret throw `DodoPaymentsInvalidDataException`. All SDK exceptions extend `DodoPaymentsException`.

### Functional Error Handling

Use `Result` for functional error handling:

```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` catches every exception, including SDK exceptions, and returns them as a failed `Result`.
</Tip>

## Android Integration

The Kotlin SDK is a server SDK. It authenticates with your secret API key, and anyone who has your APK can extract a key compiled into it, so never use it inside an Android app.

To take payments in an Android app:

1. On your server, create the checkout session with this SDK (see [Ktor Integration](#ktor-integration)) and return its `checkout_url`.
2. In the app, fetch that `checkout_url` from your server and open it with the [Android SDK](/developer-resources/sdks/android), which holds no API key.

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

## Response Validation

By default, the SDK throws `DodoPaymentsInvalidDataException` only when you read a property with an unexpected type. To check the whole response up front, enable validation for a request, or call `validate()` on a response:

```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()
```

## Advanced Features

### Proxy Configuration

To send requests through a proxy, pass a `java.net.Proxy` to the 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()
```

### Temporary Configuration

`withOptions` returns a client with modified settings that shares the original client's connection and thread pools. The original client doesn't change:

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

## Ktor Integration

Create the client once and call it from a route:

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

## Resources

<CardGroup cols={2}>
  <Card title="GitHub Repository" icon="github" href="https://github.com/dodopayments/dodopayments-kotlin">
    Source code, releases, and the full method list.
  </Card>

  <Card title="API Reference" icon="book" href="/api-reference/introduction">
    Every endpoint, parameter, and response.
  </Card>

  <Card title="Discord Community" icon="discord" href="https://discord.gg/bYqAp4ayYh">
    Ask questions and talk with other developers.
  </Card>

  <Card title="Report Issues" icon="bug" href="https://github.com/dodopayments/dodopayments-kotlin/issues">
    Report bugs or request features.
  </Card>
</CardGroup>

## Support

For help with the Kotlin SDK:

* **Discord**: Join the [community server](https://discord.gg/bYqAp4ayYh) for real-time help.
* **Email**: Contact [support@dodopayments.com](mailto:support@dodopayments.com).
* **GitHub**: Open an issue on the [repository](https://github.com/dodopayments/dodopayments-kotlin).

## Contributing

To contribute, read the [contributing guidelines](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.