Skip to main content

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​

  1. Open your project
  2. Go to File > Add Package Dependencies...
  3. Add local and select KapePlatformSDK folder that was just extracted
  4. Add the modules you need to your targets:
TargetAdd these products
Main AppKapeCore, KapeAuth, KapeVPN, plus KapeVPN-Lightway-Bridge / KapeVPN-OpenVPN-Bridge if you use them (see below)
Network ExtensionKapeVPN, 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:

warning

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.

info

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>
KeyRequiredDescription
BrandIdYesBrand identifier provided to you
EnvironmentYesProduction or Staging
MaxLogLevelNoOff, Error, Warn, Info, Debug, Trace (default: Info)
ClientIdYesOIDC client ID
IssuerURLYesOIDC issuer URL for discovery
RedirectURLYesOIDC redirect URI (must match your URL scheme)
AppGroupIdentifierNoShared App Group for app + extension IPC. Required when using KapeVPN. Omit it if you use only KapeCore + KapeAuth — see below
KeychainServiceYesKeychain service name for token storage
YAMLConfigNameYesName of the YAML config file (without .yaml)
LicenseYesSDK license key provided to you. Which features it grants gates parts of the SDK — see Licensed features below
ServerAddressNoDisplay address in iOS VPN settings
VPNProfileNameNoName iOS shows for the VPN configuration in Settings → General → VPN (default: your app's CFBundleDisplayName, then CFBundleName)
PacketTunnelBundleIdentifierNoBundle ID of your Network Extension
LightwayCertificateNoCA certificate for verifying Lightway server connections. Required when using the Lightway protocol.
OpenVPNCertificateNoCA chain PEM for OpenVPN. Required when using the OpenVPN protocol.
OpenVPNClientCertNoClient certificate PEM for OpenVPN. Required when using the OpenVPN protocol.
OpenVPNClientKeyNoRSA private key matching OpenVPNClientCert. Required when using the OpenVPN protocol.
OpenVPNTlsAuthKeyNoOpenVPN TLS-Auth static key (V1). Required when using the OpenVPN protocol.
FileLoggingEnabledNoYES 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
LogsCollectorNoDictionary of size and retention limits for the Low Level SDK's log files. See Logs Collector
HttpCallBehaviorNoInitial 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.
warning

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 GroupWithout
Keychainshared access group (kSecAttrAccessGroup)app's default access group
Auth stateUserDefaults(suiteName:)UserDefaults.standard
File logsApp 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.

danger

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:

FeatureGates
CategoryDedicatedIpDedicated 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
CategorySimpleDedicatedIpSimple Dedicated IPs in fetchDedicatedIPs(), and reserving one. Without it they are left out of the list, with no error, and reserveSimpleDedicatedIP(at:) throws .notEntitled
CategoryAntiCensorshipThe gated HttpCallBehavior cases — see HTTP Call Behavior below
VpnProtocolLightway, VpnProtocolWireGuard, VpnProtocolOpenVpnWhich protocols selectVpnProtocol(_:) accepts, and which endpoints .automatic attempts
VpnProtocolObfuscationObfuscated endpoints. Unlike the rest, this one filters instead of failing
VpnLightwayBitripplesetBitrippleEnabled(_:)
VpnLightwaySnisetLightwaySNI(_:)
VpnHttpsProxysetProxyConfig(_:promoteUnderAutomatic:)
VpnKillSwitchAdvancedsetKillSwitchMode(.advanced). .standard is always available
VpnSplitTunnelsetSplitTunnel(_:)
VpnCustomDnssetCustomDnsServers(_:)
VpnAutoConnectRulessetConnectOnDemand(_:) and setOnDemandRules(_:)
VpnAdvancedProtectionsetAdvancedProtectionCategories(_:)
VpnLightwayHeartbeatsetHeartbeatEnabled(_:)
VpnLightwayExpresslanesetExpresslaneEnabled(_:)
VpnSessionPauseResumepause(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)
warning

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.

info

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.