App integration
The three calls a mobile app makes, with code in eight languages and the response of each.
Your app makes three calls. None needs a secret. Each step has the code (pick your language once and every step follows), then the response you get back.
1. Register the app install
On first launch the app registers with the campaign's public code. Send the person's user fields (your own externalId, plus email or phone if you have them) to link their devices, or leave them out for an anonymous device. The answer holds a deviceId and a refreshToken: store both securely (Keychain on iOS, EncryptedSharedPreferences on Android). Keep the accessToken in memory only.
To mark a user as verified, your server computes userHash and sends it to the app. Never compute it, or hold the launch-key secret, inside the app.
# VALUES - replace these
API_BASE="https://api.wtn.live"
CAMPAIGN_CODE="YOUR_CAMPAIGN_CODE"
curl -sS -X POST "$API_BASE/api/v1/apps/register" \
-H 'Content-Type: application/json' \
-d '{
"campaignCode": "'"$CAMPAIGN_CODE"'",
"appId": "com.example.app",
"platform": "android",
"appVersion": "1.0.0",
"deviceModel": "Pixel 8",
"osVersion": "15",
"locale": "en-IN",
"language": "en",
"vendorDeviceId": "a1b2c3d4e5f60718",
"user": { "externalId": "user-1234", "email": "person@example.com", "phone": "+919876543210" }
}'
# Keep deviceId and refreshToken (secure storage); accessToken lives in memory. Leave "user" out for an anonymous
# device. A new refreshToken replaces the previous one.
# --- SERVER ONLY (never in the app): userHash proves externalId is yours (identity "verified") ---
# Your server sends it to the app, which adds it as user.userHash.
# printf '%s' "user-1234" | openssl dgst -sha256 -hmac "$WTN_LAUNCH_SECRET" -hex | sed 's/^.* //'
{
"deviceId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"userId": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5c",
"identity": "verified",
"accessToken": "EXAMPLE_ACCESS_TOKEN",
"accessTokenExpiresAt": "2026-10-20T10:00:00.000Z",
"refreshToken": "EXAMPLE_REFRESH_TOKEN",
"refreshTokenExpiresAt": "2027-04-17T10:00:00.000Z",
"heartbeatIntervalSecs": 900,
"installId": "EXAMPLE_INSTALL_ID",
"expiresAt": "2027-04-17T10:00:00.000Z"
}identity is anonymous, self_declared or verified. userId is null for an anonymous device.
2. Send a heartbeat
Call this on every app start and every heartbeatIntervalSecs seconds while the app is open. Send the deviceId and refreshToken and keep the new accessToken. On 401 DEVICE_AUTH_INVALID the refresh token is no longer valid: call register again.
# VALUES - replace these
API_BASE="https://api.wtn.live"
DEVICE_ID="DEVICE_ID_FROM_REGISTER"
REFRESH_TOKEN="REPLACE_WITH_REFRESH_TOKEN"
curl -sS -X POST "$API_BASE/api/v1/apps/heartbeat" \
-H 'Content-Type: application/json' \
-d '{"deviceId":"'"$DEVICE_ID"'","refreshToken":"'"$REFRESH_TOKEN"'"}'
# Run on every app start and every heartbeatIntervalSecs. Keep the new accessToken in memory.
# 401 DEVICE_AUTH_INVALID: the refresh token is no longer valid, register again.
{
"ok": true,
"accessToken": "EXAMPLE_ACCESS_TOKEN",
"accessTokenExpiresAt": "2026-10-20T10:00:00.000Z",
"refreshTokenExpiresAt": "2027-04-17T10:00:00.000Z",
"heartbeatIntervalSecs": 900,
"installId": "EXAMPLE_INSTALL_ID",
"expiresAt": "2027-04-17T10:00:00.000Z"
}{
"code": "DEVICE_AUTH_INVALID",
"title": "Device not recognised",
"description": "The refresh token is unknown, wrong, expired or revoked. Call register again."
}Every refusal has the same { code, title, description } shape (the title and description wording is an example).
3. Open the feedback page
Open the campaign's public link in the system browser with the language (lang) and the accessToken after a # (#dt=). The token sits in the fragment, so it never reaches a server log or a Referer; the page removes it from the address bar and sends it with the feedback. A missing or expired token never blocks the person, the feedback is just kept anonymous.
Use Chrome Custom Tabs on Android, and SFSafariViewController or ASWebAuthenticationSession on iOS. Do not use a WebView: microphone permission in a WebView is unreliable.
# VALUES - replace these
SURVEY_BASE="https://survey.wtn.live"
CAMPAIGN_CODE="YOUR_CAMPAIGN_CODE"
LANG_CODE="en"
ACCESS_TOKEN="REPLACE_WITH_ACCESS_TOKEN"
# The token goes after # so it never reaches a server log or a Referer header.
echo "$SURVEY_BASE/p/$CAMPAIGN_CODE?lang=$LANG_CODE#dt=$ACCESS_TOKEN"
# Open this link in the phone's system browser (not a WebView).
https://survey.wtn.live/p/YOUR_CAMPAIGN_CODE?lang=en#dt=EXAMPLE_ACCESS_TOKENThe token sits after the # so it never reaches a server log. Optional check: GET /api/v1/public/pulse/<code>/access.
{
"requireSigned": false,
"state": "NOT_REQUIRED",
"refusal": null,
"language": "en"
}Checklist
- The campaign code is the only campaign value in the app.
deviceIdandrefreshTokenare stored in the platform's secure storage.- Heartbeat runs on every start.
- The page opens in the system browser.