Getting started with the API
Get started with Dotdigital API - you'll be up and running in no time
Our mobile SDKs use JSON Web Tokens (JWT) to authenticate your app. These are a commonly used method for providing authentication and authorisation using one simple token.
In your code, you need to create a function that generates a valid JWT using the JWT authorisation details provided by Dotdigital in your push notification profile, and a cryptographic nonce which we pass. We refer to this as the challenge function.
Required JWT Claims
Whichever provider or method you use to create a JWT, the JWT's claims must include the following with values provided in the Authentication fields in your push notification profile you setup in Dotdigital:
JWT Claim | Value |
|---|---|
| The Issuer value in your Dotdigital push notification profile |
| The Audience value in your Dotdigital push notification profile |
| A unique and consistent identifier for the user, such as a user id, email address |
| The nonce is used to randomise the ciphers for added security |
| Token issued-at time as seconds since the Unix epoch (RFC 7519 NumericDate). Not milliseconds. |
| Token expiry as seconds since the Unix epoch. Keep this short — minutes to hours rather than days — to limit the blast radius if a token leaks. |
iatandexpare seconds, not milliseconds. A common Android bug is passingSystem.currentTimeMillis()into these claims directly. JWT NumericDate is seconds, so a millisecond value produces a malformed token — typically rejected with 403 Invalid JWT at session start. Use a JWT library's typedissuedAt(Date)/expiration(Date)setters where available (they handle the conversion), or divide by 1000 manually.
The role of the sub claim
The sub claim in the JWT is used to identify the push profile that is later associated with your Dotdigital contact when you set the email address on the profile. It needs to be unique and preferably consistent per user, so that if a user uses your app on multiple devices all these devices are grouped under the same push profile id, and when you send a push to the contact it is sent to all their devices.

Using a unique and consistent value per user for the sub claim
If you cannot allocate a unique and consistent sub claim for a user, possibly because your app allows anonymous users, then you must use a unique and consistent identifier for the device, such as a GUID which you store in the app, then contacts are restricted to a single device for pushes.

Using a random, unique, consistent value for the sub claim per user
Ensure yoursubclaim is unique and consistent per userThe value you pass in the sub claim is the unique user id and cannot be changed later. It is really important that the values are unique per user, and ideally don't change between installs.
Which push profile is associated with my contact?A PUSHOPTIN_xxx field is automatically created for your Dotdigital contacts when you setup your push notification channel. This contains the push profile id that is associated with a contact.
Signing the JWT
JWTs are digitally signed to prove they were issued by a trusted party. You must sign the JWT with the value of the Shared secret field from your push notification profile and the nonce we pass your function to prove it was issued by you.
Nonces
A cryptographic nonce must also be used by your JWT provider when creating the token to ensure that tokens always appear different even if they contain the same claims; this is a common security practice with cryptography. The SDK passes this nonce as an argument to your challenge function during initialisation of the SDK where you create or retrieve a JWT for your app user.
Important security noteDo not hard-code your shared secret on the client side. Anyone who decompiles your APK can recover it and impersonate your users. Sign the JWT on your backend, then have the app fetch the signed token over HTTPS using the nonce supplied to the challenge function. The Android samples below show the signing flow for completeness, but in production the secret should not be on the device.
Sample Code
The following example code demonstrates how to create a challenge function and a self-issued JWT for Android, iOS, and JavaScript.
Android JWT code sample
JWT provider: Java JWT (jjwt) — currently 0.13.0 on Maven Central.
Any class that you create to generate a JWT token must extend the ComapiAuthenticator abstract class.
Any logic that creates the JWT token must be inside the onAuthenticationChallenge() method. This method takes two parameters: AuthClient and ChallengeOptions.
Get the value of the nonce by calling the ChallengeOptions.getNonce() method (camelCase — Java getter convention).
When your JWT token is returned, pass it to the authenticateWithToken() method of the AuthClient object. If the token fetch fails, call authenticateWithToken(null) so the SDK can surface the failure rather than hang waiting for a token.
Add the following dependencies to your module-level Gradle build file:
dependencies {
api('io.jsonwebtoken:jjwt-api:0.13.0')
runtimeOnly('io.jsonwebtoken:jjwt-impl:0.13.0')
runtimeOnly('io.jsonwebtoken:jjwt-orgjson:0.13.0') {
exclude(group: 'org.json', module: 'json') // provided by Android natively
}
}dependencies {
api("io.jsonwebtoken:jjwt-api:0.13.0")
runtimeOnly("io.jsonwebtoken:jjwt-impl:0.13.0")
runtimeOnly("io.jsonwebtoken:jjwt-orgjson:0.13.0") {
exclude(group = "org.json", module = "json") // provided by Android natively
}
}
Shared-secret length. HS256 in JJWT 0.12+ requires a key of at least 256 bits (32 bytes). If your shared secret is shorter,Keys.hmacShaKeyFor(...)throwsWeakKeyException. Generate a secret of at least 32 ASCII characters in your push profile, or use a base64-decoded longer secret.
This is the sample class for a ChallengeHandler. It uses JJWT's typed builder methods (subject, issuer, audience, issuedAt, expiration) so the timestamps are emitted as seconds rather than milliseconds:
package com.example.testapp;
import com.comapi.ComapiAuthenticator;
import com.comapi.internal.network.AuthClient;
import com.comapi.internal.network.ChallengeOptions;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Date;
import javax.crypto.SecretKey;
public class ChallengeHandler extends ComapiAuthenticator {
@Override
public void onAuthenticationChallenge(AuthClient authClient, ChallengeOptions challengeOptions) {
try {
// <Shared secret> must match the 'Shared secret' field in your push notification profile.
// In production, fetch the signed token from your backend rather than holding the secret here.
byte[] data = "<Shared secret>".getBytes(StandardCharsets.UTF_8);
SecretKey key = Keys.hmacShaKeyFor(data);
Instant now = Instant.now();
Instant exp = now.plus(15, ChronoUnit.MINUTES); // short-lived token
String token = Jwts.builder()
.header().type("JWT").and()
// ID claim. The claim name must match the 'ID claim' field in your push profile
// (default 'sub'). The value must be a consistent unique identifier for the app user.
.subject("<Unique consistent app user id>")
// <Audience> must match the 'Audience' field in your push notification profile.
.audience().add("<Audience>").and()
// <Issuer> must match the 'Issuer' field in your push notification profile.
.issuer("<Issuer>")
.claim("nonce", challengeOptions.getNonce())
.issuedAt(Date.from(now))
.expiration(Date.from(exp))
.signWith(key, Jwts.SIG.HS256)
.compact();
authClient.authenticateWithToken(token);
} catch (Exception e) {
e.printStackTrace();
// Authorisation failed — tell the SDK so it can surface the error rather than hang.
authClient.authenticateWithToken(null);
}
}
}package com.example.testapp
import com.comapi.ComapiAuthenticator
import com.comapi.internal.network.AuthClient
import com.comapi.internal.network.ChallengeOptions
import io.jsonwebtoken.Jwts
import io.jsonwebtoken.security.Keys
import java.time.Instant
import java.time.temporal.ChronoUnit
import java.util.Date
class ChallengeHandler : ComapiAuthenticator() {
override fun onAuthenticationChallenge(
authClient: AuthClient,
challengeOptions: ChallengeOptions
) {
try {
// <Shared secret> must match the 'Shared secret' field in your push notification profile.
// In production, fetch the signed token from your backend rather than holding the secret here.
val data = "<Shared secret>".toByteArray(Charsets.UTF_8)
val key = Keys.hmacShaKeyFor(data)
val now = Instant.now()
val exp = now.plus(15, ChronoUnit.MINUTES) // short-lived token
val token = Jwts.builder()
.header().type("JWT").and()
// ID claim — must match the 'ID claim' field in your push profile (default 'sub').
.subject("<Unique consistent app user id>")
// <Audience> must match the 'Audience' field in your push profile.
.audience().add("<Audience>").and()
// <Issuer> must match the 'Issuer' field in your push profile.
.issuer("<Issuer>")
.claim("nonce", challengeOptions.nonce)
.issuedAt(Date.from(now))
.expiration(Date.from(exp))
.signWith(key, Jwts.SIG.HS256)
.compact()
authClient.authenticateWithToken(token)
} catch (e: Exception) {
e.printStackTrace()
// Authorisation failed — tell the SDK so it can surface the error rather than hang.
authClient.authenticateWithToken(null)
}
}
}
java.timerequires API 26 or library desugaring.java.time.Instantandjava.time.temporal.ChronoUnitare only available natively from API 26. If your app'sminSdkis below 26, enable core library desugaring in your module'sbuild.gradle(coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:2.1.4"pluscompileOptions { coreLibraryDesugaringEnabled true }). Otherwise, substitutenew Date()andSystem.currentTimeMillis() + TimeUnit.MINUTES.toMillis(15).
Fetching the JWT from your backend (recommended)
In production the shared secret should never ship in the APK. The Android sample below fetches a pre-signed JWT from your backend using the supplied nonce, then hands it to the SDK. The Kotlin variant uses coroutines so the network call doesn't block the SDK's challenge thread.
public class ChallengeHandler extends ComapiAuthenticator {
private final TokenApi tokenApi; // your backend client (Retrofit, etc.)
private final Executor executor;
public ChallengeHandler(TokenApi tokenApi, Executor executor) {
this.tokenApi = tokenApi;
this.executor = executor;
}
@Override
public void onAuthenticationChallenge(AuthClient authClient, ChallengeOptions options) {
executor.execute(() -> {
try {
String token = tokenApi.fetchJwt(options.getNonce()); // HTTPS POST
authClient.authenticateWithToken(token);
} catch (Throwable t) {
Log.e("Comapi", "Token fetch failed", t);
authClient.authenticateWithToken(null);
}
});
}
}class ChallengeHandler(
private val tokenService: TokenService,
private val scope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
) : ComapiAuthenticator() {
override fun onAuthenticationChallenge(
authClient: AuthClient,
options: ChallengeOptions
) {
scope.launch {
val token = runCatching { tokenService.fetchJwt(nonce = options.nonce) }
.getOrElse { t ->
Log.e("Comapi", "Token fetch failed", t)
null
}
authClient.authenticateWithToken(token)
}
}
}iOS JWT code sample
Here's an example implementation of a token generator in Objective-C and Swift using JWT.
#import "CMPAuthenticationManager.h"
#import <JWT/JWT.h>
@implementation CMPAuthenticationManager
+ (NSString *)generateTokenForNonce:(NSString *)nonce profileID:(NSString *)profileID issuer:(NSString *)issuer audience:(NSString *)audience secret:(NSString *)secret {
NSDate *now = [NSDate date];
NSDate *exp = [NSCalendar.currentCalendar dateByAddingUnit:NSCalendarUnitDay value:30 toDate:now options:0];
NSDictionary *headers = @{@"typ" : @"JWT"};
/* Claims notes:
ID claim — this claim name must match the 'ID claim' field in your push profile
(default 'sub'). The value must be a consistent unique value for the app user.
'aud' must match the 'Audience' field in your push notification profile.
'iss' must match the 'Issuer' field in your push notification profile.
*/
NSDictionary *payload = @{@"nonce" : nonce,
@"sub" : profileID,
@"iss" : issuer,
@"aud" : audience,
@"iat" : [NSNumber numberWithDouble:now.timeIntervalSince1970],
@"exp" : [NSNumber numberWithDouble:exp.timeIntervalSince1970]};
NSData *secretData = [secret dataUsingEncoding:NSUTF8StringEncoding];
id<JWTAlgorithm> algorithm = [JWTAlgorithmFactory algorithmByName:@"HS256"];
NSString *token = [JWTBuilder encodePayload:payload].headers(headers).secretData(secretData).algorithm(algorithm).encode;
return token;
}
@end
/* Note that this should preferably be generated by your backend; the app should retrieve the token over HTTPS. */import JWT
class JWTokenGenerator {
struct AuthHeaders {
static let HeaderType = "JWT"
}
static func generate(tokenFor nonce: String, profileId: String, issuer: String, audience: String, secret: String) -> String {
let now = Date()
let exp = Calendar.current.date(byAdding: .day, value: 30, to: now)!
let base64SecretKey = secret.data(using: .utf8)!
let headers = ["typ": NSString(string: AuthHeaders.HeaderType)] as [AnyHashable: Any]
/* Claims notes:
ID claim — must match the 'ID claim' field in your push profile (default 'sub').
'aud' must match the 'Audience' field in your push notification profile.
'iss' must match the 'Issuer' field in your push notification profile.
*/
let claims = ["nonce": NSString(string: nonce),
"sub": NSString(string: profileId),
"iss": NSString(string: issuer),
"aud": NSString(string: audience),
"iat": NSNumber(value: now.timeIntervalSince1970),
"exp": NSNumber(value: exp.timeIntervalSince1970)] as [AnyHashable: Any]
let algorithm = JWTAlgorithmFactory.algorithm(byName: "HS256")
let e = JWTBuilder.encodePayload(claims)!
let h = e.headers(headers)!
let s = h.secretData(base64SecretKey)!
let b = s.algorithm(algorithm)!
let token = b.encode
return token!
}
}
/* Note that this should preferably be generated by your backend; the app should retrieve the token over HTTPS. */JavaScript JWT code sample
JWT provider: jsrsasign library.
Get the value of the nonce by using the nonce property of the first parameter.
When your JWT token is returned, pass it to the answerAuthenticationChallenge() function (second parameter of your challengeHandler() function).
function challengeHandler (options, answerAuthenticationChallenge) {
// Header
var oHeader = { alg: 'HS256', typ: 'JWT' };
// Payload
var tNow = KJUR.jws.IntDate.get('now');
var tEnd = KJUR.jws.IntDate.get('now + 1day');
var oPayload = {
// ID claim — must match the 'ID claim' field in your push profile (default 'sub').
sub: "<Unique consistent app user id>",
nonce: options.nonce,
// 'aud' must match the 'Audience' field in your push notification profile.
aud: "<Audience>",
// 'iss' must match the 'Issuer' field in your push notification profile.
iss: "<Issuer>",
iat: tNow,
exp: tEnd
};
var sHeader = JSON.stringify(oHeader);
var sPayload = JSON.stringify(oPayload);
// <Shared secret> must match the 'Shared secret' field in your push notification profile.
var sJWT = KJUR.jws.JWS.sign("HS256", sHeader, sPayload, { utf8: "<Shared secret>" });
answerAuthenticationChallenge(sJWT);
}
Be careful with the secret value being automatically cast to hexThe jsrsasign library automatically checks the secret field to see if it is a hexadecimal number being passed. We always use string-based secrets, so to stop this unwanted automatic cast please ensure that the secret value is always cast to a UTF-8 string by using the following cast operation:
{ utf8: <Your secret value> }.Common indications of this issue are 403 - Invalid JWT errors when trying to start a session with the SDK.
Updated 2 days ago
