Unity LevelPlay
- Generate a private key yourself: at least 8 characters without spaces, ideally a random string of 32 or more. Store it in Kasskeep together with the LevelPlay App Key:
npx @kasskeep/cli ads credentials --id app_xxx --levelplay-private-key-file - --levelplay-app-key <App Key> < ./levelplay-private-key.txtLevelPlay has one App Key per platform. If both your iOS and Android apps report to the same Kasskeep app, give both keys separated by a comma (--levelplay-app-key <iOS key>,<Android key>, at most 10). Without a private key callbacks return 422 levelplay_not_configured.
- In LevelPlay, Apps, open the actions of your app and choose Set S2S callback:
- Callback URL, with the bracketed macros exactly as written:
https://api.kasskeep.com/v1/ads/levelplay/<app_id>/s2s?appUserId=[USER_ID]&rewards=[REWARDS]&eventId=[EVENT_ID]&itemName=[ITEM_NAME]&placementName=[PLACEMENT_NAME]&adNetwork=[AD_NETWORK]&appKey=[APP_KEY]&auctionId=[AUCTION_ID]LevelPlay appends timestamp and signature itself, and custom parameters with a custom_ prefix.
- Private key: the same string as in step 1.
- Run Callback test before saving; LevelPlay activates the callback only after a correct answer. The test
eventIdis recorded like a real reward, so test against a development app or before mapping ad units.
- Map the ad unit, one id per platform:
npx @kasskeep/cli ads unit-set --id app_xxx --name revive --levelplay ios:<iOS ad unit id> --levelplay android:<Android ad unit id>- On the client, initialize LevelPlay with the App Key and the user id, and before each show set the dynamic user id and the custom parameters
ad_unit,ad_unit_idanddata. The provider below does this.
Verification
Section titled “Verification”signaturemust equal hexmd5(timestamp + eventId + appUserId + rewards + privateKey), withappUserIdURL-decoded. Otherwise400 invalid_signature.timestamp(YYYYMMDDHHMM, read as UTC; 10-digit seconds and 13-digit milliseconds are also accepted) must be within ±1 hour of the server clock:400 timestamp_out_of_windowor400 invalid_timestamp. Change the window with--levelplay-timestamp-window <seconds>(60 to 604800).- When
app_keyis set and the callback carriesappKey, it must be one of the configured keys:400 app_key_mismatch. - Optional source IP allowlist:
--levelplay-ip-allowlist levelplayallows LevelPlay’s published callback addresses; you can also list IPs and IPv4 CIDRs. Others get403 ip_not_allowed. An empty value turns it off. - Every verified callback, including duplicates and unmapped ones, answers
200with"ack": "<eventId>:OK"in the JSON body, which is what LevelPlay looks for.
LevelPlay retries callbacks that get no correct answer (20 times within 24 hours, then daily for 7 days).
| LevelPlay parameter | Event field |
|---|---|
dynamicUserId, else appUserId |
app_user_id (a 32-digit hex dynamic id is restored to a UUID) |
rewards |
reward_amount |
itemName |
reward_type |
eventId |
network_transaction_id |
custom_data |
custom_data |
timestamp |
occurred_at |
Ad unit matching
Section titled “Ad unit matching”The client sends ad_unit=<your name>, ad_unit_id=<LevelPlay ad unit id> and data=<customData>; they return as custom_ad_unit, custom_ad_unit_id and custom_data. Kasskeep looks up the mapping by custom_ad_unit_id, then by placementName, then by the name in custom_ad_unit. If the mapped name differs from custom_ad_unit, the callback is treated as unmapped.
Provider example
Section titled “Provider example”import { LevelPlay, LevelPlayInitRequest, LevelPlayRewardedAd } from "unity-levelplay-mediation";import { Kasskeep, type RewardedProvider } from "@kasskeep/react-native";
const ads = new Map<string, LevelPlayRewardedAd>();
const levelplayRewarded: RewardedProvider = { network: "levelplay", init: ({ appKey, appUserId }) => new Promise((resolve, reject) => { const request = LevelPlayInitRequest.builder(appKey!).withUserId(appUserId).build(); LevelPlay.init(request, { onInitSuccess: () => resolve(), onInitFailed: (e) => reject(new Error(String(e?.errorMessage ?? e))) }); }), setUserId: (appUserId) => LevelPlay.setDynamicUserId(appUserId.replace(/-/g, "")), showRewarded: ({ networkAdUnitId, serverParams }) => new Promise((resolve) => { const ad = ads.get(networkAdUnitId); if (!ad) return resolve({ status: "failed" }); let rewarded = false; ad.setListener({ onAdRewarded: () => (rewarded = true), onAdClosed: () => setTimeout(() => resolve({ status: rewarded ? "completed" : "skipped" }), 500), onAdDisplayFailed: () => resolve({ status: "failed" }), onAdLoaded: () => {}, onAdLoadFailed: () => {}, onAdDisplayed: () => {}, }); LevelPlay.setMetaData("LevelPlay_Rewarded_Server_Params", Object.entries(serverParams).map(([k, v]) => `${k}=${v}`)); ad.showAd(); }),};
Kasskeep.configure({ apiKey, baseUrl, appUserId, ads: { rewardedProvider: levelplayRewarded, appKey: "<App Key>", adUnits: { revive: "<ad unit id>" }, testAds: __DEV__ },});Create and loadAd() each LevelPlayRewardedAd after init and keep it in ads; reload after every show. onAdRewarded can arrive after onAdClosed, hence the short delay. Check method names against the plug-in version you pin.