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>'
}
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:
| Protocol | Artifact | Pass to KapeVpnManager(protocols = ...) |
|---|---|---|
| WireGuard | kape-platform-sdk-wireguard | KapeWireGuardSupport |
| Lightway | kape-platform-sdk-lightway | KapeLightwaySupport |
| OpenVPN | kape-platform-sdk-openvpn | KapeOpenVpnSupport |
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" />
| Attribute | Required | Description |
|---|---|---|
brand-id | Yes | Brand identifier provided to you |
environment | Yes | Production or Staging |
max-log-level | No | Off, Error, Warn, Info, Debug, Trace (default: Info) |
client-id | Yes | OIDC client ID |
issuer-url | Yes | OIDC issuer URL for discovery |
redirect-url | Yes | OIDC redirect URI (must match your manifest intent filter) |
license | No | SDK license key provided to you. Which features it grants gates parts of the SDK — see Licensed features below |
yaml-config-name | Yes | Name of the YAML config file in assets/ (without .yaml) |
lightway-certificate-asset | No | assets/ file name of the Lightway CA certificate PEM. Required when using the Lightway protocol. |
openvpn-ca-certificate-asset | No | assets/ file name of the OpenVPN CA chain PEM. Required when using the OpenVPN protocol. |
openvpn-client-certificate-asset | No | assets/ file name of the OpenVPN client certificate PEM. Required when using the OpenVPN protocol. |
openvpn-client-key-asset | No | assets/ file name of the client private key PEM. Required when using the OpenVPN protocol. |
openvpn-tls-auth-key-asset | No | assets/ file name of the OpenVPN TLS-Auth static key PEM. Required when using the OpenVPN protocol. |
storage-name | No | Name 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-enabled | No | true 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-hours | No | Size 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:
| Feature | Gates |
|---|---|
CategoryDedicatedIp | Dedicated 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 |
CategorySimpleDedicatedIp | Simple Dedicated IPs in fetchDedicatedIps, and reserving one. Without it they are left out of the list, with no error, and reserveSimpleDip returns NotEntitled |
VpnProtocolLightway | setSelectedProtocol(Lightway), and Lightway endpoints under Automatic |
VpnProtocolWireGuard | setSelectedProtocol(Wireguard), and WireGuard endpoints under Automatic |
VpnProtocolOpenVpn | setSelectedProtocol(OpenVPN), and OpenVPN endpoints under Automatic |
VpnProtocolObfuscation | Obfuscated endpoints. Filters rather than refuses — see below |
VpnLightwayBitripple | setBitrippleEnabled |
VpnLightwaySni | setLightwaySNI |
VpnHttpsProxy | setProxyConfig |
VpnKillSwitchAdvanced | setKillSwitchMode(Advanced) only |
VpnSplitTunnel | setSplitTunnelAppMode and the per-package edits |
VpnCustomDns | setCustomDnsServers |
VpnAdvancedProtection | setAdvancedProtectionCategories |
VpnLightwayHeartbeat | setHeartbeatEnabled |
VpnLightwayExpresslane | setExpresslaneEnabled |
VpnSessionPauseResume | pause. resume is never gated |
VpnAutoConnectRules | setConnectOnDemand, 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>
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.