# Kotlin SDK

> com.layerhop:layerhop is a coroutine-based Kotlin client for the JVM and Android, with OkHttp, kotlinx.serialization models, a watch Flow and webhook verification.

Source: https://docs.layerhop.com/sdks/kotlin

`com.layerhop:layerhop` is the Kotlin client for the LayerHop [REST API](/api-reference), for the JVM and Android, mirroring the [Go](/sdks/go) and [TypeScript](/sdk) SDKs. Every call is a `suspend` function built on OkHttp and kotlinx.serialization, and it compiles to Java 8 bytecode, so it runs on Android minSdk 21 without the Android Gradle plugin.

> The SDK never holds keys. It returns **unsigned** transactions; your app's wallet (Reown AppKit / WalletConnect, the Solana Mobile Wallet Adapter, a KMS on your server…) signs and broadcasts them.

## How do I install it?

```kotlin
// build.gradle.kts
dependencies {
    implementation("com.layerhop:layerhop:0.6.0")
}
```

The dependencies are OkHttp 4, kotlinx.coroutines and kotlinx.serialization. The source lives in [`layerhop/layerhop-kotlin`](https://github.com/layerhop/layerhop-kotlin).

## Quickstart

Amounts are **integer strings in base units** (`"100000000"` = 100 USDC).

```kotlin
import com.layerhop.*

val lh = LayerHop(LayerHopOptions(integrator = "myapp")) // apiKey = ..., baseUrl = "http://10.0.2.2:8787"

suspend fun swap() {
    // 1. Quote
    val q = lh.quote(QuoteParams(
        srcChain = "base", srcToken = "USDC", amount = "100000000",
        dstChain = "solana", dstToken = "SOL",
        sender = "0xYourEvmAddress", recipient = "YourSolanaAddress",
    ))
    val best = q.best

    // 2. Build the unsigned transaction
    val tx = lh.buildTx(best.quoteId)

    // 3. Sign + send with YOUR wallet
    val txHash = if (tx.isEvm) {
        val evm = tx.asEvm()   // send evm.approval first when set, then the tx, then evm.extraTxs in order
        signEvm(evm)
    } else {
        val svm = tx.asSvm()   // base64 VersionedTransaction; svm.submitViaApi → sign only, then lh.submit(..)
        signSvm(svm)
    }

    // 4. Report the broadcast
    lh.reportSubmitted(best.quoteId, txHash)

    // 5. Watch until filled / refunded / failed
    lh.watch(best.provider, txHash).collect { println(it.state) }
}
```

Gas on arrival: pass `gasOnArrival = true` (about $2 of native gas for the recipient on the destination chain) or `gasOnArrivalUsd = 5.0`, then check `best.gasOnArrival`. See [Gas on arrival](/guides/gas-on-arrival).

## How do I use it in an Android app?

Add `<uses-permission android:name="android.permission.INTERNET" />` to `AndroidManifest.xml`, create one client (in your `Application` or DI graph) and call it from `viewModelScope` or `lifecycleScope`. Requests are cancelled with the coroutine.

```kotlin
class SwapViewModel : ViewModel() {
    private val lh = LayerHop(LayerHopOptions(integrator = "myapp"))
    val state = MutableStateFlow("idle")

    fun quote(sender: String, recipient: String) = viewModelScope.launch {
        state.value = try {
            val q = lh.quote(QuoteParams("base", "USDC", "25000000", "arbitrum", "ETH", sender, recipient))
            "Receive ${q.best.dstAmount} wei via ${q.best.provider}"
        } catch (e: LayerHopException) {
            "${e.code}: ${e.apiMessage}"   // e.g. NO_ROUTE, see e.docsUrl
        } catch (e: java.io.IOException) {
            "Network error"
        }
    }
}
```

The `tx.asEvm()` fields map directly onto an `eth_sendTransaction` request through Reown AppKit / WalletConnect. `tx.asSvm().serializedTx` is a base64 transaction for the Solana Mobile Wallet Adapter's `signAndSendTransactions` (or `signTransactions` when `submitViaApi` is true).

Do not ship an API key inside an app. Quotes and swaps work without one, and keyed calls (checkout, invoices, subscriptions, deposit addresses) belong on your server. R8 needs no extra rules: the models use kotlinx.serialization's generated serializers, not reflection.

## Which methods are available?

| Method | Endpoint |
|---|---|
| `health()` · `chains()` · `tokens(chain)` · `prices(ids)` | `GET /v1/health` · `/v1/chains` · `/v1/tokens` · `/v1/prices` |
| `quote(QuoteParams)` | `GET /v1/quote` |
| `buildTx(quoteId)` | `POST /v1/orders` → `BuiltTx` with `asEvm()` / `asSvm()` |
| `simulate(quoteId)` | `POST /v1/simulate` |
| `submit(quoteId, signedBase64)` | `POST /v1/orders/submit` (Solana `submitViaApi` only) |
| `reportSubmitted(quoteId, txHash)` | `POST /v1/orders/{quoteId}/submitted` |
| `status(provider, id)` · `listOrders(ListOrdersParams)` | `GET /v1/orders/{provider}/{id}` · `GET /v1/orders` |
| `watch(provider, id, interval, timeout)` | A `Flow<OrderStatus>` of state changes; completes when final, throws `WatchTimeoutException` after 30 min by default |
| `createCheckoutSession` · `getCheckoutSession` · `listCheckoutSessions` · `expireCheckoutSession` · `quoteCheckoutSession` | `/v1/checkout/sessions` |
| `createCheckoutProduct` · `getCheckoutProduct` · `listCheckoutProducts` · `updateCheckoutProduct` · `startProductCheckout` | `/v1/checkout/products` |
| `createRefund(sessionId, CreateRefundParams)` · `listRefunds(sessionId)` | `/v1/checkout/sessions/{id}/refunds` |
| `createInvoice` · `getInvoice` · `listInvoices` · `updateInvoice` · `sendInvoice` · `voidInvoice` · `getPublicInvoice` · `payInvoice` | `/v1/invoices` |
| `createDepositAddress` · `getDepositAddress` · `listDepositAddresses` · `refreshDepositAddress` · `disableDepositAddress` · `simulateDeposit` | `/v1/deposit-addresses` |
| `createSubscription` · `getSubscription` · `listSubscriptions` · `updateSubscription` · `cancelSubscription(id, atPeriodEnd)` · `pauseSubscription` · `resumeSubscription` · `listSubscriptionInvoices` | `/v1/subscriptions` |
| `createLimitOrder` · `submitLimitOrder` · `getLimitOrder` · `listLimitOrders` · `cancelLimitOrder` · `simulateLimitOrder` | `/v1/limit-orders` (Solana, beta) |
| `serviceStatus()` · `triggerWebhookEvent(type)` | `GET /v1/status` · `POST /v1/webhooks/trigger` |

The `update*` params send only the fields you set; `""` clears a nullable field by sending `null`. `createDepositAddress` and `createLimitOrder` default `integrator` and `integratorFeeBps` (and `channel` for limit orders) to the client's options.

## How do I take payments with checkout?

Run this on your server with an API key ([Checkout guide](/guides/checkout)):

```kotlin
val server = LayerHop(LayerHopOptions(apiKey = System.getenv("LAYERHOP_API_KEY")))

val session = server.createCheckoutSession(CreateCheckoutParams(
    amount = "49000000", settleChain = "base", settleToken = "USDC",
    recipient = "0xYourPayoutAddress", successUrl = "https://shop.example.com/thanks",
))
// Send the customer to session.url; fulfil on the checkout.session.completed webhook.

val sub = server.createSubscription(CreateSubscriptionParams(
    customerEmail = "a@example.com", interval = SubscriptionIntervals.MONTH, productId = "prod_123",
))
```

## How do I verify webhooks?

Verify the `LayerHop-Signature` header against the **raw** body before parsing it ([Webhooks guide](/guides/webhooks)). Ktor:

```kotlin
post("/webhooks/layerhop") {
    val raw = call.receiveText()
    val event = try {
        Webhooks.constructEvent(secret, raw, call.request.header(Webhooks.SIGNATURE_HEADER))
    } catch (e: WebhookSignatureException) {
        return@post call.respond(HttpStatusCode.BadRequest)
    }
    if (event.type == WebhookEventTypes.CHECKOUT_SESSION_COMPLETED) fulfil(event.data.session!!)
    call.respond(HttpStatusCode.OK)
}
```

`Webhooks.verifySignature(secret, body, header)` returns a `Boolean` (5-minute tolerance by default, several `v1` values accepted, constant-time comparison). `Webhooks.sign(secret, body, timestamp)` builds a header for your own tests.

## How are errors reported?

Every non-2xx response throws `LayerHopException` with a stable `code` (`QUOTE_EXPIRED`, `NO_ROUTE`…, see [Errors](/errors)), the HTTP `status`, the `requestId` to quote to support, and `docsUrl`. Network failures throw `java.io.IOException`.

## How do I use test mode?

Use an `lh_test_…` key and every call is served by the sandbox: live-priced quotes, placeholder transactions (`tx.sandbox == true`, never broadcast them) and orders that fill in about 10 seconds. See [Test mode](/guides/test-mode).
