Skip to main content

Installation

KapePlatformSDK is hosted on a private Cloudsmith maven registry. Downloading from Cloudsmith requires a entitlement token, reach your contact at Kape if you have not received an entitlement token.

Gradle (Kotlin DSL)​

Add the Kape Maven repository and dependency to your build files:

// settings.gradle.kts
dependencyResolutionManagement {
repositories {
maven {
url = uri("https://dl.cloudsmith.io/basic/expressvpn/kp_platform_sdks/maven/")
credentials {
username = "token"
password = "<YOUR_ENTITLEMENT_TOKEN>"
}
}
}
}
// app/build.gradle.kts
dependencies {
implementation("com.kape.platformsdk:kape-platform-sdk:<version>")
// One per VPN protocol the app supports — pass the matching object to KapeVpnManager
implementation("com.kape.platformsdk:kape-platform-sdk-wireguard:<version>")
implementation("com.kape.platformsdk:kape-platform-sdk-lightway:<version>")
implementation("com.kape.platformsdk:kape-platform-sdk-openvpn:<version>")
}

Gradle (Groovy DSL)​

// settings.gradle
dependencyResolutionManagement {
repositories {
maven {
url "https://dl.cloudsmith.io/basic/expressvpn/kp_platform_sdks/maven/"
credentials {
username "token"
password "<YOUR_ENTITLEMENT_TOKEN>"
}
}
}
}
// app/build.gradle
dependencies {
implementation 'com.kape.platformsdk:kape-platform-sdk:<version>'
// One per VPN protocol the app supports — pass the matching object to KapeVpnManager
implementation 'com.kape.platformsdk:kape-platform-sdk-wireguard:<version>'
implementation 'com.kape.platformsdk:kape-platform-sdk-lightway:<version>'
implementation 'com.kape.platformsdk:kape-platform-sdk-openvpn:<version>'
}
info

The Low Level SDK (KapeClientSDK) AAR is included as a transitive dependency. You do not need to add it separately.

Choosing VPN protocols​

kape-platform-sdk contains no VPN protocol. Each protocol is a separate artifact carrying that protocol's tunnel and native libraries, so an app ships only the protocols it declares:

ProtocolArtifactPass to KapeVpnManager(protocols = ...)
WireGuardkape-platform-sdk-wireguardKapeWireGuardSupport
Lightwaykape-platform-sdk-lightwayKapeLightwaySupport
OpenVPNkape-platform-sdk-openvpnKapeOpenVpnSupport

Use the same <version> for all of them; each depends on kape-platform-sdk. See VPN Protocol for what happens when the selected protocol is not bundled.

Depending on a single module​

kape-platform-sdk is a fused library containing every module except the VPN protocols, which is what most apps want. If your product needs OpenVPN endpoint configuration but does not run the SDK's tunnel — handing the config to your own backend, to a third-party OpenVPN client, or to a file — depend on the discovery artifact alone instead:

dependencies {
implementation("com.kape.platformsdk:kape-vpn-discovery:<version>")
}

It depends on kape-core only, so an app that depends on it links no WireGuard, OpenVPN or Lightway native library and ships none of them in its APK. See OpenVPN Endpoint Discovery.

Configuration File​

Create res/xml/kape_sdk_settings.xml and add it to your app module:

<?xml version="1.0" encoding="utf-8"?>
<kape-sdk-settings
brand-id="your-brand-id"
environment="Production"
max-log-level="Info"
client-id="your-oidc-client-id"
issuer-url="https://auth.example.com/realms/your-tenant"
redirect-url="your-app-scheme://callback"
license="your-license-key"
yaml-config-name="your_config" />
AttributeRequiredDescription
brand-idYesBrand identifier provided to you
environmentYesProduction or Staging
max-log-levelNoOff, Error, Warn, Info, Debug, Trace (default: Info)
client-idYesOIDC client ID
issuer-urlYesOIDC issuer URL for discovery
redirect-urlYesOIDC redirect URI (must match your manifest intent filter)
licenseNoSDK license key provided to you. Which features it grants gates parts of the SDK — see Licensed features below
yaml-config-nameYesName of the YAML config file in assets/ (without .yaml)
lightway-certificate-assetNoassets/ file name of the Lightway CA certificate PEM. Required when using the Lightway protocol.
openvpn-ca-certificate-assetNoassets/ file name of the OpenVPN CA chain PEM. Required when using the OpenVPN protocol.
openvpn-client-certificate-assetNoassets/ file name of the OpenVPN client certificate PEM. Required when using the OpenVPN protocol.
openvpn-client-key-assetNoassets/ file name of the client private key PEM. Required when using the OpenVPN protocol.
openvpn-tls-auth-key-assetNoassets/ file name of the OpenVPN TLS-Auth static key PEM. Required when using the OpenVPN protocol.
storage-nameNoName of the on-device storage holding the SDK session, auth state and cache (default: kape_sdk_storage). Give two configurations different names to keep separate sessions in one app — e.g. a production and a staging build variant. Letters, digits, ., _ and - only. Changing it on an existing install starts a new, signed-out session. The Android counterpart of the iOS KeychainService key.
file-logging-enabledNotrue writes the SDK's logs to disk, so they survive the process and can be uploaded or exported with KapeLogsCollector (default: false). Needs max-log-level Info or more verbose. See Logs Collector
prelog-max-mb, prelog-max-minutes, log-file-max-mb, log-retention-hoursNoSize and retention limits for the Low Level SDK's log files. See Logs Collector

Each *-asset attribute has an inline counterpart without the -asset suffix (lightway-certificate, openvpn-ca-certificate, …) that carries the PEM body directly. Inline takes precedence when both are set. Prefer the *-asset form: XML attribute values are newline- normalised, so a multi-line PEM pasted inline is corrupted on read.

Load the settings in code using:

val settings = KapeSDKSettings.fromXml(context, R.xml.kape_sdk_settings)

YAML Configuration​

Place the YAML configuration file provided to you in your app's assets/ directory. The file name must match the yaml-config-name attribute in kape_sdk_settings.xml (e.g. your_config.yaml).

Licensed features​

Your SDK license carries the set of features it grants. Seventeen of them gate SDK behaviour:

FeatureGates
CategoryDedicatedIpDedicated IP. Without it every KapeDipAccountService read reports the same thing a subscription with no Dedicated IP licenses reports — KapeDipSetupStage.NotEntitled, empty location lists — setup calls return DipSetupResult.NotEntitled, and KapeVpnManager.connect(location) on a Dedicated IP returns ConnectResult.DipError(KapeVpnTunnelError.DipNotEntitled). Nothing to handle beyond what you already handle for an unentitled subscription
CategorySimpleDedicatedIpSimple Dedicated IPs in fetchDedicatedIps, and reserving one. Without it they are left out of the list, with no error, and reserveSimpleDip returns NotEntitled
VpnProtocolLightwaysetSelectedProtocol(Lightway), and Lightway endpoints under Automatic
VpnProtocolWireGuardsetSelectedProtocol(Wireguard), and WireGuard endpoints under Automatic
VpnProtocolOpenVpnsetSelectedProtocol(OpenVPN), and OpenVPN endpoints under Automatic
VpnProtocolObfuscationObfuscated endpoints. Filters rather than refuses — see below
VpnLightwayBitripplesetBitrippleEnabled
VpnLightwaySnisetLightwaySNI
VpnHttpsProxysetProxyConfig
VpnKillSwitchAdvancedsetKillSwitchMode(Advanced) only
VpnSplitTunnelsetSplitTunnelAppMode and the per-package edits
VpnCustomDnssetCustomDnsServers
VpnAdvancedProtectionsetAdvancedProtectionCategories
VpnLightwayHeartbeatsetHeartbeatEnabled
VpnLightwayExpresslanesetExpresslaneEnabled
VpnSessionPauseResumepause. resume is never gated
VpnAutoConnectRulessetConnectOnDemand, setOnDemandRules and the rule edit helpers, plus setConnectOnStartup

See Licensed VPN features for what each refusal looks like and how a license downgrade or expiry is handled.

KapeSdkHandle.hasLicensedFeature(feature) reports what your license grants, so you can hide UI for a feature you are not licensed for. The verdict comes from the Low Level SDK's own licenseStatus() — it has verified the license signature and read its expiry date, and it remains the authority, enforcing licensing itself on every request.

It is not available synchronously at startup. licenseStatus() needs the Low Level SDK's manager, which KapeSdkHandle builds in the background, so hasLicensedFeature returns false until that finishes. Bind UI to KapeSdkHandle.licenseState instead, which starts at KapeLicenseState.Resolving and emits KapeLicenseState.Resolved once the verdict is known — a bare false cannot tell "not known yet" from "not granted". awaitLicensedFeature(feature) is the suspending one-shot form.

when (val state = handle.licenseState.collectAsState().value) {
is KapeLicenseState.Resolving -> showPlaceholder()
is KapeLicenseState.Resolved -> {
showSplitTunnelUi(state.isLicensed(LicensedFeature.VpnSplitTunnel))
// Any problem grants nothing — an expired or revoked license reports it here.
state.problems.forEach { log(it.toString()) }
}
}

Manifest Setup​

AppAuth's RedirectUriReceiverActivity must be able to intercept your redirect URI scheme. There are two ways to configure this.

Option A: Gradle placeholder (recommended). Set appAuthRedirectScheme in your build.gradle.kts. AppAuth's own manifest entry already uses this placeholder, so no changes to AndroidManifest.xml are needed:

// app/build.gradle.kts
android {
defaultConfig {
manifestPlaceholders["appAuthRedirectScheme"] = "your-app-scheme"
}
}

The value must be the scheme part of your redirect-url (e.g. "myapp" for myapp://callback).

Option B: Manual intent filter. Declare the intent filter directly in AndroidManifest.xml:

<activity
android:name="net.openid.appauth.RedirectUriReceiverActivity"
android:exported="true"
tools:node="replace">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="your-app-scheme" android:host="callback" />
</intent-filter>
</activity>

The scheme and host must match the redirect-url value in kape_sdk_settings.xml.

VPN Service Setup​

VPN connectivity (the KapeVpn module) runs the tunnel inside an android.net.VpnService. The service is an ordinary component of your app. Register it and declare the supporting permissions in your AndroidManifest.xml.

Declare the permissions:

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<!-- Android 14 (API 34)+ -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<!-- Android 13 (API 33)+, for the foreground-service notification -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

Register the service. The system requires the BIND_VPN_SERVICE permission on the <service> element and the android.net.VpnService intent filter:

<service
android:name=".KapeVpnTunnelService"
android:permission="android.permission.BIND_VPN_SERVICE"
android:foregroundServiceType="specialUse"
android:exported="false">
<intent-filter>
<action android:name="android.net.VpnService" />
</intent-filter>
</service>
info

Reading the current WiFi SSID for Smart Locations and SSID-based on-demand rules additionally requires the ACCESS_FINE_LOCATION and ACCESS_BACKGROUND_LOCATION runtime permissions (and, on API 33+, NEARBY_WIFI_DEVICES). These are requested by your app only if you use those features. They are not bundled in the SDK's manifest — only the non-sensitive ACCESS_WIFI_STATE is. See Server Locations and Network Protection.

The SDK also contributes one component of its own to your merged manifest: KapeOnDemandService, a foreground service that keeps Connect on Demand watching while no tunnel is running. It does nothing until Connect on Demand is enabled. Reacting to a reboot is your own receiver's job — see Network Protection.

See VPN Service for the KapeVpnTunnelService implementation.

OpenVPN Build Configuration​

When using the OpenVPN protocol, the OpenVPN binary (libovpnexec.so) is executed as a subprocess and must be extracted to the filesystem on install. Enable legacy JNI packaging in your build.gradle.kts:

// app/build.gradle.kts
android {
packaging {
jniLibs {
useLegacyPackaging = true
}
}
}

Without this, libovpnexec.so remains compressed inside the APK and cannot be executed, causing OpenVPN connections to fail at startup.