Skip to Content
SDK IntegrationKotlin (Android & JVM)

Android / Kotlin SDK

Integrate TrustPin into your Android or JVM application for native certificate pinning.

Current version: cloud.trustpin:kotlin-sdk 6.3.0

Platform Requirements

PlatformMinimum Version
AndroidAPI 25+ (full feature support)
JVMJava 11+

Kotlin Version: 2.3.0+

Note: The Maven Central artifact is an Android AAR. For server-side JVM, desktop, or Compose Multiplatform targets, request access to the hardened JVM JAR via support@trustpin.cloud.


Installation

Gradle (Kotlin DSL)

Add TrustPin to your build.gradle.kts:

dependencies { implementation("cloud.trustpin:kotlin-sdk:6.3.0") }

Gradle (Groovy)

Add to your build.gradle:

dependencies { implementation 'cloud.trustpin:kotlin-sdk:6.3.0' }

Maven

Add to your pom.xml:

<dependency> <groupId>cloud.trustpin</groupId> <artifactId>kotlin-sdk</artifactId> <version>6.3.0</version> </dependency>

Optional Client Adapters: OkHttp / Ktor

Thin, optional integration artifacts published to Maven Central alongside the SDK, versioned in lockstep with it. Each contains only public-API glue, and neither pulls in the SDK or the HTTP client transitively. Your app supplies both:

dependencies { implementation("cloud.trustpin:trustpin-okhttp:6.3.0") // OkHttp implementation("cloud.trustpin:trustpin-ktor:6.3.0") // Ktor (OkHttp engine); includes trustpin-okhttp }

Quick Start

1. Get Your Credentials

Sign in to the TrustPin Dashboard  and retrieve:

  • Organization ID
  • Project ID
  • Public Key (Base64-encoded)

2. Initialize TrustPin

Ship a trustpin.json asset in your app and load it with TrustPinConfiguration.fromAssets(context). Credentials stay out of source.

Place trustpin.json at app/src/main/assets/trustpin.json:

{ "organization_id": "your-org-id", "project_id": "your-project-id", "public_key": "your-base64-public-key", "mode": "strict" }

Build-variant overrides follow standard Android source-set merging. Drop a different file under src/debug/assets/trustpin.json or src/staging/assets/trustpin.json to use per-flavor credentials.

KeyTypeRequiredNotes
organization_idStringYesNon-empty
project_idStringYesNon-empty
public_keyStringYesBase64-encoded ECDSA P-256 public key
modeStringNo"strict" (default) or "permissive"
configuration_urlStringNoMust be HTTPS. Overrides the default CDN endpoint
embedded_configuration_assetStringNoAsset name of a bundled signed configuration, read from assets/. See Embedded Configuration

Then load it during Application.onCreate():

import android.app.Application import cloud.trustpin.kotlin.sdk.TrustPin import cloud.trustpin.kotlin.sdk.TrustPinConfiguration import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch class MyApplication : Application() { override fun onCreate() { super.onCreate() CoroutineScope(Dispatchers.IO).launch { try { val config = TrustPinConfiguration.fromAssets(this@MyApplication) TrustPin.setup(config) TrustPin.awaitConfiguration() println("TrustPin initialized") } catch (e: Exception) { println("TrustPin setup failed: ${e.message}") } } } }

setup() is non-blocking. It starts loading the configuration and returns without waiting. TrustPin.awaitConfiguration() is the fail-closed gate: call it once after setup, immediately before constructing any HTTP client that depends on pinning, to suspend until a validated configuration is loaded (default timeout: 30s) and throw if it didn’t. For synchronous call sites use TrustPin.awaitConfigurationBlocking(), or check TrustPin.isConfigurationLoaded for a non-throwing status read.

Don’t forget to register your Application class in AndroidManifest.xml:

<application android:name=".MyApplication" ...> </application>

3. Add Network Permission

Ensure your AndroidManifest.xml includes:

<uses-permission android:name="android.permission.INTERNET" />

Embedded Configuration

TrustPin fetches its signed pinning configuration online and keeps the last validated one on the device. For the one case where neither exists, an app’s very first start while every configuration source is unreachable, you can ship a signed configuration inside the app as a last-resort fallback.

Download the signed configuration for your project from the dashboard  and place it at app/src/main/assets/trustpin-seed.b64. With trustpin.json, reference it by adding "embedded_configuration_asset": "trustpin-seed.b64" and leave the call site unchanged. Programmatically, pass an EmbeddedConfiguration:

import cloud.trustpin.kotlin.sdk.EmbeddedConfiguration // Android: the asset is read by the Context-aware setup path. TrustPin.setup( TrustPinConfiguration( organizationId = "your-org-id", projectId = "your-project-id", publicKey = "your-base64-public-key", embeddedConfiguration = EmbeddedConfiguration.Asset("trustpin-seed.b64"), ).withAndroidStorage(context) ) // JVM: a classpath resource, or a file inside the installation. TrustPin.setup( TrustPinConfiguration( organizationId = "your-org-id", projectId = "your-project-id", publicKey = "your-base64-public-key", embeddedConfiguration = EmbeddedConfiguration.File("config/trustpin-seed.b64"), ) )
VariantResolves againstAvailable on
EmbeddedConfiguration.Asset(name)Android assets/, or the JVM classpathAndroid and JVM
EmbeddedConfiguration.File(path)A filesystem pathJVM only

On Android, EmbeddedConfiguration.Asset requires one of the Context-aware setup paths (fromAssets(context) or .withAndroidStorage(context)).

Requirements

  • Use it only in apps protected by RASP (runtime application self-protection) that guards bundled resources against modification. An unprotected app must not ship an embedded configuration.
  • The file must be the unmodified signed payload downloaded from the dashboard for this project. It is verified against publicKey during setup, and setup() throws TrustPinError.InvalidProjectConfig if the file is not a bundled resource, cannot be read, or does not verify.
  • Regenerate it in CI on every release, so it is never older than the app that ships it. Pins expire on their own schedule, and an embedded configuration whose pins have all expired is equivalent to having no fallback. trustpin-cli projects jws writes the currently published payload to a file, so a release pipeline can refresh the bundled copy on every build.

Behaviour

  • It is never preferred over an online source, or over a configuration the SDK has already fetched and validated.
  • It is subject to the same integrity checks as any other configuration: a device that has already trusted a newer configuration will not accept an older embedded one.
  • The iOS, Flutter, and React Native SDKs expose the equivalent option. The file format is identical across platforms.

Integration Approaches

ApproachBest ForSetup Complexity
OkHttp Integration (Recommended)Most Android apps🟢 Low
Retrofit IntegrationREST API clients (uses OkHttp under the hood)🟢 Low
Ktor Client IntegrationKtor-based apps and KMP shared modules🟡 Medium
Manual VerificationCustom transports, non-OkHttp stacks🟠 High

With the optional trustpin-okhttp adapter , one line replaces the manual SSL wiring and guarantees the factory/trust-manager pair belongs to the same TrustPin instance:

val client = OkHttpClient.Builder() .trustPin() // or .trustPin(TrustPin.instance("payments")) .connectTimeout(30, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) .build()
// Java OkHttpClient client = TrustPinOkHttp.trustPin(new OkHttpClient.Builder()).build();

Call after TrustPin.setup(...).

Without the adapter, wire the SSL socket factory manually. TrustPinSSLSocketFactory.create() returns an SSL socket factory wired to your TrustPin configuration. Pass it, along with its trust manager, to OkHttpClient.Builder:

import cloud.trustpin.kotlin.sdk.ssl.TrustPinSSLSocketFactory import okhttp3.OkHttpClient import java.util.concurrent.TimeUnit val sslSocketFactory = TrustPinSSLSocketFactory.create() val httpClient = OkHttpClient.Builder() .connectTimeout(30, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) .writeTimeout(30, TimeUnit.SECONDS) .sslSocketFactory(sslSocketFactory, sslSocketFactory.trustManager()) .build()

Retrofit Integration

Retrofit uses OkHttp under the hood, so share the same TrustPin-backed client:

class ApiClient { private val okHttpClient by lazy { val sslSocketFactory = TrustPinSSLSocketFactory.create() OkHttpClient.Builder() .connectTimeout(30, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) .sslSocketFactory(sslSocketFactory, sslSocketFactory.trustManager()) .build() } private val retrofit by lazy { Retrofit.Builder() .baseUrl("https://api.example.com/") .client(okHttpClient) .addConverterFactory(GsonConverterFactory.create()) .build() } }

Ktor Client Integration

With the optional trustpin-ktor adapter (OkHttp engine only, since TLS configuration is engine-specific in Ktor):

import io.ktor.client.* import io.ktor.client.engine.okhttp.* val ktorClient = HttpClient(OkHttp) { engine { trustPin() } }

A Ktor preconfigured OkHttpClient bypasses engine configuration. Pin it directly with the OkHttp adapter instead.

Without the adapter, plug TrustPin into Ktor’s OkHttp engine manually:

import io.ktor.client.* import io.ktor.client.engine.okhttp.* import okhttp3.OkHttpClient private val httpClient by lazy { val sslSocketFactory = TrustPinSSLSocketFactory.create() HttpClient(OkHttp) { engine { preconfigured = OkHttpClient.Builder() .sslSocketFactory(sslSocketFactory, sslSocketFactory.trustManager()) .build() } } }

Manual Verification

For custom transports, validate a domain/certificate pair directly with the suspending API:

import java.security.cert.X509Certificate val certificate: X509Certificate = /* ... */ TrustPin.verify("api.example.com", certificate)

Named Instances (Multi-Tenant)

The SDK supports independent named instances via TrustPin.instance(id) for apps that talk to multiple TrustPin projects (for example, separate consumer and admin backends). Because the bundled-asset path loads a single trustpin.json per build, multi-tenant setups currently require the programmatic configuration API. See the upstream Kotlin SDK docs  for usage.


Logging

Set the desired verbosity before calling setup() to capture initialization logs:

import cloud.trustpin.kotlin.sdk.TrustPinLogLevel TrustPin.setLogLevel(TrustPinLogLevel.INFO)

Available levels: NONE, ERROR, INFO, DEBUG.

Custom Log Sink

To route SDK log output into your own logging pipeline, install a global TrustPinLogSink. One sink serves all instances and receives every message after per-instance level filtering, tagged with the producing instance id:

TrustPin.setLogSink { level, instanceId, message -> myLogger.log("[$instanceId] $message") } TrustPin.setLogSink(null) // restore the default sink

Sinks are called synchronously from SDK internals, including TLS-handshake threads: keep them fast and non-blocking, don’t perform I/O inline, and never call back into TrustPin from a sink.


Monitoring Pin Validation

To feed pin-validation verdicts into your security monitoring, for example reporting suspected MITM attempts to your backend, install a global TrustPinValidationListener:

import cloud.trustpin.kotlin.sdk.TrustPinValidationListener import java.security.cert.X509Certificate TrustPin.setValidationListener(object : TrustPinValidationListener { override fun onValidationFailure( instanceId: String, domain: String, error: TrustPinError, presentedCertificate: X509Certificate, ) { // Fires only for definitive verdicts: PinsMismatch, AllPinsExpired, // DomainNotRegistered (strict mode). `presentedCertificate` is the // leaf as received from the network. Treat it as untrusted input. } override fun onValidationSuccess(instanceId: String, domain: String) { // Optional. The default implementation does nothing. } }) TrustPin.setValidationListener(null) // detach

The listener is observe-only: it is invoked strictly after the verdict is decided and cannot veto, approve, or alter a connection. Transient conditions (configuration fetch failures, timeouts) and permissive-mode connections to unregistered domains produce no callbacks. Like log sinks, listeners are called synchronously from TLS-handshake threads, so keep them non-blocking and never call back into TrustPin.


Error Handling

TrustPinError is a sealed class with the following variants:

VariantMeaning
DomainNotRegisteredStrict mode and the host isn’t in the configuration
PinsMismatchServer certificate doesn’t match any active pin
AllPinsExpiredConfiguration is stale. Rotate pins in the dashboard
InvalidServerCertServer returned an unparseable certificate
InvalidProjectConfigBad credentials or invalid configuration
ErrorFetchingPinningInfoNetwork failure while loading the configuration
ConfigurationValidationFailedJWS signature didn’t verify against the project’s public key
ConfigIntegrityErrorConfiguration failed an integrity check. Hard stop
NotInitializedAn API was called before setup() completed successfully
AlreadyInitializedsetup() was called a second time on the same instance
SetupInProgressAn operation raced a setup() that hadn’t finished yet
LockTimeoutAn internal lock couldn’t be acquired in time
TimeoutAn operation (e.g. awaitConfiguration) exceeded its timeout
SSLContextSetupFailedThe pinned SSLContext / socket factory couldn’t be created
UnsupportedDeviceThe runtime environment doesn’t support the required security primitives
try { TrustPin.setup(config) } catch (e: TrustPinError.InvalidProjectConfig) { // Bad credentials } catch (e: TrustPinError.ErrorFetchingPinningInfo) { // Network failure during setup } catch (e: TrustPinError.NotInitialized) { // setup() wasn't called, or it failed silently. Guard with awaitConfiguration() } catch (e: TrustPinError) { // Any other TrustPin failure (UnsupportedDevice, etc.) }

Best Practices

Setup & Initialization

  1. Initialize in Application.onCreate() for app-wide coverage.
  2. Use a coroutine scope for async setup. The API is suspend-first (blocking variants are available for synchronous call sites).
  3. Call TrustPin.awaitConfiguration() after setup(), before constructing any HTTP client that depends on pinning. setup() is non-blocking.
  4. Set the log level before setup() to capture initialization output.
  5. Handle setup errors gracefully. Don’t block app launch.

Security

  1. Use TrustPinMode.STRICT in production.
  2. Prefer SPKI pinning; rotate pins in the dashboard before they expire.
  3. Monitor pin validation failures via TrustPin.setValidationListener(...) or logging.
  4. Keep credentials outside source control. Prefer TrustPinConfiguration.fromAssets(context) with per-flavor trustpin.json files, or fetch them at runtime and use withAndroidStorage(context).
  5. Use HTTPS for all pinned domains.
  6. Ship an embedded configuration only if the app is protected by RASP, and regenerate it in CI on every release.

Performance

  1. Configuration is cached for 10 minutes with a stale-while-revalidate fallback.
  2. Reuse OkHttpClient instances rather than creating one per request.
  3. Use minimal log levels in production.

Complete Documentation

For the full API reference, ProGuard/R8 rules, and additional integration patterns, visit:

TrustPin Kotlin API Reference 


Resources