Installation
Swift Package Manager
KapePlatformSDK is hosted on a private Cloudsmith Swift registry. Downloading from Cloudsmith requires a entitlement token, reach your contact at Kape if you have not received an entitlement token.
Download KapePlatformSDK package
curl -u "token:<YOUR_ENTITLEMENT_TOKEN>" -1sL 'https://dl.cloudsmith.io/basic/expressvpn/kp_platform_sdks/swift/KapePlatformSDK/<VERSION>/KapePlatformSDKPackage.zip' -o "KapePlatformSDKPackage.zip"
unzip KapePlatformSDKPackage.zip
In Xcode
- Open your project
- Go to File > Add Package Dependencies...
- Add local and select KapePlatformSDK folder that was just extracted
- Add the modules you need to your targets:
| Target | Add these products |
|---|---|
| Main App | KapeCore, KapeAuth, KapeVPN, plus KapeVPN-Lightway-Bridge / KapeVPN-OpenVPN-Bridge if you use them (see below) |
| Network Extension | KapeVPN, plus one KapeVPN-<Protocol>-Bridge per VPN protocol you support |
Each VPN protocol is opt-in, so your app ships only the tunnel libraries it uses:
Also add KapeVPN-Lightway-Bridge and KapeVPN-OpenVPN-Bridge to the main app target when you
use them. Their libraries ship as dynamic frameworks, and Xcode embeds a package's frameworks only in
an app target, never in an app extension — the extension loads them from the app bundle. Without
this, the extension is killed at launch with Library not loaded: @rpath/Lightway.framework (or
OpenSSL.framework). KapeVPN-WireGuard-Bridge links statically and needs no such step.
It is possible to add Cloudsmith as a remote SwiftPM registry to avoid registering KapePlatformSDK as a local package but private registries don't play nice with CI systems like Github actions or XCode cloud as the registry credentials needs to be stored in the keychain which might be difficult to manipulate on CI systems. Having a pre-build step to download the package (with credentials potentially stored in environement variables) and use that package as a local Swift package bypass this issue.
Configuration File
Create a KapeSDK.plist file and add it to both your app target and Network Extension target (if you need to connect to the VPN):
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>BrandId</key>
<string>your-brand-id</string>
<key>Environment</key>
<string>Production</string>
<key>MaxLogLevel</key>
<string>Info</string>
<key>ClientId</key>
<string>your-oidc-client-id</string>
<key>IssuerURL</key>
<string>https://auth.example.com/realms/your-tenant</string>
<key>RedirectURL</key>
<string>your-app-scheme://callback</string>
<!-- Omit AppGroupIdentifier entirely if you do not use KapeVPN. -->
<key>AppGroupIdentifier</key>
<string>group.com.your.app</string>
<key>KeychainService</key>
<string>com.your.app.sdk-storage</string>
<key>YAMLConfigName</key>
<string>your_config</string>
<key>License</key>
<string>your-license-key</string>
<!-- Required only when connecting to the VPN -->
<key>ServerAddress</key>
<string>vpn.example.com</string>
<key>VPNProfileName</key>
<string>Your App</string>
<key>PacketTunnelBundleIdentifier</key>
<string>com.your.app.PacketTunnel</string>
<!-- Required only when using the Lightway protocol -->
<key>LightwayCertificate</key>
<string>-----BEGIN CERTIFICATE-----
...
-----END CERTIFICATE-----</string>
<!-- Required only when using the OpenVPN protocol -->
<key>OpenVPNCertificate</key>
<string>-----BEGIN CERTIFICATE-----
... OpenVPN CA chain ...
-----END CERTIFICATE-----</string>
<key>OpenVPNClientCert</key>
<string>-----BEGIN CERTIFICATE-----
... client certificate ...
-----END CERTIFICATE-----</string>
<key>OpenVPNClientKey</key>
<string>-----BEGIN RSA PRIVATE KEY-----
... matching client private key ...
-----END RSA PRIVATE KEY-----</string>
<key>OpenVPNTlsAuthKey</key>
<string>-----BEGIN OpenVPN Static key V1-----
... TLS-Auth static key ...
-----END OpenVPN Static key V1-----</string>
<!-- Optional: initial HTTP call behavior, e.g. for licensed anti-censorship routing -->
<key>HttpCallBehavior</key>
<string>InternalHttpClient</string>
</dict>
</plist>
| Key | Required | Description |
|---|---|---|
BrandId | Yes | Brand identifier provided to you |
Environment | Yes | Production or Staging |
MaxLogLevel | No | Off, Error, Warn, Info, Debug, Trace (default: Info) |
ClientId | Yes | OIDC client ID |
IssuerURL | Yes | OIDC issuer URL for discovery |
RedirectURL | Yes | OIDC redirect URI (must match your URL scheme) |
AppGroupIdentifier | No | Shared App Group for app + extension IPC. Required when using KapeVPN. Omit it if you use only KapeCore + KapeAuth — see below |
KeychainService | Yes | Keychain service name for token storage |
YAMLConfigName | Yes | Name of the YAML config file (without .yaml) |
License | Yes | SDK license key provided to you. Which features it grants gates parts of the SDK — see Licensed features below |
ServerAddress | No | Display address in iOS VPN settings |
VPNProfileName | No | Name iOS shows for the VPN configuration in Settings → General → VPN (default: your app's CFBundleDisplayName, then CFBundleName) |
PacketTunnelBundleIdentifier | No | Bundle ID of your Network Extension |
LightwayCertificate | No | CA certificate for verifying Lightway server connections. Required when using the Lightway protocol. |
OpenVPNCertificate | No | CA chain PEM for OpenVPN. Required when using the OpenVPN protocol. |
OpenVPNClientCert | No | Client certificate PEM for OpenVPN. Required when using the OpenVPN protocol. |
OpenVPNClientKey | No | RSA private key matching OpenVPNClientCert. Required when using the OpenVPN protocol. |
OpenVPNTlsAuthKey | No | OpenVPN TLS-Auth static key (V1). Required when using the OpenVPN protocol. |
FileLoggingEnabled | No | YES writes the SDK's logs to disk, so they survive the process and can be uploaded or exported with KapeLogsCollector (default: NO). Needs MaxLogLevel Info or more verbose. See Logs Collector |
LogsCollector | No | Dictionary of size and retention limits for the Low Level SDK's log files. See Logs Collector |
HttpCallBehavior | No | Initial HTTP call behavior. Case name matched case-insensitively, e.g. DssAutoDiscover. Leave unset to get InternalHttpClient, or — on a license with CategoryAntiCensorship — automatic anti-censorship detection. See HTTP Call Behavior below. |
The same KapeSDK.plist must be included in both your app target and Network Extension target. The extension reads it
to initialize the SDK with identical settings.
To read a differently named plist — one per environment, for example — pass its name, without the
extension: KapeSDKSettings(plistName: "KapeSDK-staging"). The extension then needs to read the same
one; see Switching Environment.
Integrations without an App Group
AppGroupIdentifier exists so the app and its Network Extension can share state. If you use only
KapeCore + KapeAuth — no VPN, no extension — omit the key. KapeSDKSettings then reports
appGroupIdentifier and keychainAccessGroup as nil, and the SDK uses app-private storage:
| With an App Group | Without | |
|---|---|---|
| Keychain | shared access group (kSecAttrAccessGroup) | app's default access group |
| Auth state | UserDefaults(suiteName:) | UserDefaults.standard |
| File logs | App Group container (its Library/Caches on tvOS) | app's own Application Support directory (Caches on tvOS, where Application Support is not writable) |
Your app then needs no App Group registration and no com.apple.security.application-groups
entitlement. Examples/AuthDemo ships this way, with no entitlements file at all.
Calling a KapeVPN API without an App Group throws a KapeError naming AppGroupIdentifier.
Removing an AppGroupIdentifier you have already shipped is a one-way door. Existing keychain
items stay in the old access group and become unreadable — signed-in users are signed out. Decide
before your first release.
YAML Configuration
Place the YAML configuration file provided to you in your app bundle. The file name must match the YAMLConfigName
value in KapeSDK.plist (e.g. your_config.yaml).
Entitlements
If you need to be able to connect to the VPN, both your app and Network Extension require these entitlements:
<key>com.apple.developer.networking.networkextension</key>
<array>
<string>packet-tunnel-provider</string>
</array>
<key>com.apple.security.application-groups</key>
<array>
<string>group.com.your.app</string>
</array>
Network Extension Target
If you need to be able to connect to the VPN, create a Network Extension target (Packet Tunnel Provider) in your Xcode project with a minimal subclass:
import KapeVPN
import KapeVPN_Lightway_Bridge
import KapeVPN_WireGuard_Bridge
class PacketTunnelProvider: KapePacketTunnelProvider {
override var vpnProtocols: [KapeVPNProtocolSupport] { [.lightway, .wireGuard] }
}
List every protocol whose KapeVPN-<Protocol>-Bridge product you linked: .wireGuard, .lightway,
.openVPN. The base class handles everything else: SDK initialization, endpoint discovery, tunnel
management, and connection lifecycle. See VPN Protocol for
what happens when the selected protocol is not listed.
Licensed features
Your SDK license carries the set of features it grants. These gate SDK behaviour:
| Feature | Gates |
|---|---|
CategoryDedicatedIp | Dedicated IP. Without it every fetchDip* call reports the same thing an account with no Dedicated IP licenses reports — KapeDipSetupStage.notEntitled, empty location lists — and connect(to:) with a Dedicated IP location throws KapeVPNError.dipNotEntitled. Nothing to handle beyond what you already handle for an unentitled account |
CategorySimpleDedicatedIp | Simple Dedicated IPs in fetchDedicatedIPs(), and reserving one. Without it they are left out of the list, with no error, and reserveSimpleDedicatedIP(at:) throws .notEntitled |
CategoryAntiCensorship | The gated HttpCallBehavior cases — see HTTP Call Behavior below |
VpnProtocolLightway, VpnProtocolWireGuard, VpnProtocolOpenVpn | Which protocols selectVpnProtocol(_:) accepts, and which endpoints .automatic attempts |
VpnProtocolObfuscation | Obfuscated endpoints. Unlike the rest, this one filters instead of failing |
VpnLightwayBitripple | setBitrippleEnabled(_:) |
VpnLightwaySni | setLightwaySNI(_:) |
VpnHttpsProxy | setProxyConfig(_:promoteUnderAutomatic:) |
VpnKillSwitchAdvanced | setKillSwitchMode(.advanced). .standard is always available |
VpnSplitTunnel | setSplitTunnel(_:) |
VpnCustomDns | setCustomDnsServers(_:) |
VpnAutoConnectRules | setConnectOnDemand(_:) and setOnDemandRules(_:) |
VpnAdvancedProtection | setAdvancedProtectionCategories(_:) |
VpnLightwayHeartbeat | setHeartbeatEnabled(_:) |
VpnLightwayExpresslane | setExpresslaneEnabled(_:) |
VpnSessionPauseResume | pause(for:). resume() is never gated |
The fifteen Vpn* features are covered in full — including what each setter throws and what the
tunnel does with a value it is not licensed for — in
Licensed VPN features.
KapeSdkHandle.hasLicensedFeature(_:) reports what your license grants, so you can hide UI for a
feature you are not licensed for. KapeSdkHandle.licenseProblems reports anything wrong with the
license itself — expired, revoked, unsigned — and a license carrying any problem grants nothing.
Both are resolved from the Low Level SDK's verified view before construction returns; see
Licensed VPN features. The Low Level SDK enforces
licensing itself, per request, and remains the authority.
HTTP Call Behavior
HttpCallBehavior controls how the SDK routes its own API requests. .default, .callback, and
.internalHttpClient (the default) work on any license. Every other case — DSS, NT, and geo-unblock
anti-censorship request routing — requires the CategoryAntiCensorship entitlement on your SDK
license.
Automatic anti-censorship detection
If you leave the HttpCallBehavior plist key unset and your license grants
CategoryAntiCensorship, the SDK picks an initial behavior for you — you don't need to detect
censorship yourself to get anti-censorship routing on a licensed client. At launch, before the SDK
manager is constructed (so it's already in effect for your very first SDK call), it reads the
device's App Store storefront country to apply the HTTP behavior that is more likely to work in that country.
Licenses without CategoryAntiCensorship are unaffected by this — they always start on
.internalHttpClient, same as before.
This only ever applies the initial behavior. Setting the HttpCallBehavior plist key always
takes precedence — set it explicitly if you want a specific mode (e.g. DssEnforce) pinned from
launch rather than the auto-detected one.
Manual control
Read the current behavior, or switch it live at runtime (e.g. in response to your own censorship
detection, or to turn anti-censorship routing off again), via KapeSdkHandle:
// Read the current behavior
let current = handle.httpCallBehavior
// Switch on DSS auto-discovery for licensed clients
handle.setHttpCallBehavior(.dssAutoDiscover)
Setting a gated behavior without the CategoryAntiCensorship entitlement does not fail
immediately — but every subsequent SDK call (login, endpoint discovery, subscriptions, ...) throws
KapeError (FeatureNotLicensed) until you set the behavior back to .default, .callback, or
.internalHttpClient.
Calling setHttpCallBehavior(_:) right after constructing KapeSdkHandle — rather than in
response to some later event — can race the automatic detection above if the plist key is unset:
whichever of the two finishes last wins. Set the plist key instead if you need a specific behavior
guaranteed from the very start.