Native iOS apps protected by Kasada send a header most integrations hardcode: x-kpsdk-dv. With Kasada's iOS SDK i-2.x that header changed shape, and a value copied from an older build no longer works.
This is a follow-up to Kasada Mobile SDK v2: The User-Agent Is Now an MD5 Hash. The user-agent describes the app; x-kpsdk-dv describes the device the app runs on, and since i-2.x it describes the app too.
Key Takeaways
- On iOS the Kasada SDK sends
x-kpsdk-dvon the very first request to the protected site, before any challenge is solved.- It is a small JSON object, XORed with a key fixed in the SDK and base64 encoded.
- Since SDK
i-2.x(thex-kpsdk-vvalue) the object has four fields:osVersion,hardwareId,appIdandappVersion. Older builds sent only the first two.- On an app running
i-2.0.1, the two-field header was rejected on every attempt (0 of 18). The four-field header was accepted on every attempt (49 of 49).appIdandappVersionare the same values the hashed user-agent is built from, so build both from one set of inputs.- Pass the same value to our payload API as
dv, and the payload is generated on a real iPhone with that model's screen and iOS version.
What x-kpsdk-dv contains
Before encoding, the header is a single JSON object:
{"osVersion":"18.0.1","hardwareId":"iPhone13,2","appId":"com.example.app","appVersion":"9.15.0"}
The SDK XORs those bytes with a key fixed in the Kasada SDK and base64 encodes the result, which gives:
QkZWEmcDRUBEDloaAg8FDhpTAwhEGxFFWRFcQAVCVHFXFAlBUDRRDl8DBgABUxYUGkxERn0HDwNEVFxAFgZAVglAXV0dV0MTG0gbAEEWYVZfEl1XVg8OFA1NHAxIBxFQ
There is no timestamp and no nonce. The same device and app always produce the same value.
The four fields
- osVersion: the iOS version,
UIDevice.systemVersion, such as18.0.1. - hardwareId: the model identifier from
utsname.machine, such asiPhone13,2for an iPhone 12. It is not the marketing name. - appId: the app's bundle ID, such as
com.example.app. - appVersion: the app's version string, such as
9.15.0.
What changed in i-2.x
Builds before i-2.x sent only the device: osVersion and hardwareId. From i-2.x the SDK adds the app's bundle ID and version.
That matters because the old two-field value is what most integrations hardcoded. On a Kasada-protected app running i-2.0.1, we sent the same flow with the two-field header and with the four-field header the app itself sends. The two-field header was rejected every time; the four-field header was accepted every time. Nothing else changed between the runs.
If your mobile flow started failing without any change on your side, check the x-kpsdk-v value the app sends. If it starts with i-2., your x-kpsdk-dv needs all four fields.
Keep it consistent with the user-agent
In the v2 mobile SDK the user-agent is an MD5 hash of <OS version> / <bundle ID> <app version>. Three of its inputs are also in x-kpsdk-dv: osVersion, and the bundle ID and app version as appId and appVersion.
So the two headers describe the same app on the same OS. Build both from one set of values. A user-agent hashed from one app version next to an x-kpsdk-dv naming another is inconsistent by construction.
A payload that matches the device
x-kpsdk-dv reaches Kasada before our API is involved, so the payload you post to /tl has to agree with it: the screen it reports should be the screen of the model the header names, on the iOS version it names.
The payload API now takes the header as an optional dv field. With it, the payload is generated on a real iPhone, drawn from a pool of more than 4,000 collected devices, with the screen of the model hardwareId names and the WebKit behaviour of the iOS version osVersion names. Without it, the API picks a device from the iOS version in the user-agent, which the hashed user-agent does not contain.
SDK support
The Go, Python and JavaScript SDKs generate the header locally, with no API call, from the same values as the user-agent:
Go (hyper-sdk-go v4.2.0):
import "github.com/Hyper-Solutions/hyper-sdk-go/v4/kasada"
userAgent := kasada.GenerateUserAgent("18.0.1", "com.example.app", "9.15.0")
dv := kasada.GenerateDV("18.0.1", "iPhone13,2", "com.example.app", "9.15.0")
res, err := session.GenerateKasadaPayload(ctx, &hyper.KasadaPayloadInput{
UserAgent: userAgent,
IpsLink: ipsLink,
Script: script,
AcceptLanguage: "en-US,en;q=0.9",
IP: ip,
Dv: dv, // the x-kpsdk-dv value you send to the site
})
Python (hyper_sdk 4.2.0):
from hyper_sdk import KasadaPayloadInput, generate_kasada_dv, generate_kasada_user_agent
user_agent = generate_kasada_user_agent("18.0.1", "com.example.app", "9.15.0")
dv = generate_kasada_dv("18.0.1", "iPhone13,2", "com.example.app", "9.15.0")
result = session.generate_kasada_payload(KasadaPayloadInput(
user_agent, ips_link, script, "en-US,en;q=0.9", ip,
dv=dv, # the x-kpsdk-dv value you send to the site
))
JavaScript / TypeScript (hyper-sdk-js 5.2.0):
import { KasadaPayloadInput, generateKasadaDv, generateKasadaPayload, generateKasadaUserAgent } from 'hyper-sdk-js';
const device = {
osVersion: '18.0.1',
hardwareId: 'iPhone13,2',
bundleId: 'com.example.app',
appVersion: '9.15.0',
};
const userAgent = generateKasadaUserAgent(device);
const dv = generateKasadaDv(device);
const result = await generateKasadaPayload(session, new KasadaPayloadInput(
userAgent, ipsLink, script, ip, 'en-US,en;q=0.9', false, dv,
));
Calling the API directly, add dv to the /payload body. It is ignored for desktop user-agents, and a value that does not decode is rejected with invalid dv.
If you captured x-kpsdk-dv from the app you are emulating, send that exact value instead of generating one.
The full reference is in the Mobile User-Agent documentation.
Questions about your mobile flow?
If your mobile Kasada flow behaves differently from what is described here, come and talk to us on Discord. We are happy to look at a capture with you.
Working examples for every language and TLS client are in the examples repo, and the step-by-step Kasada integration guide is in our documentation.
Bypass Kasada with a single API call
Skip the browser. Generate the sensor data, tokens and cookies you just read about over plain HTTP, with a free one-week trial.