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

iss

The Issuer value in your Dotdigital push notification profile
e.g. https://api.comapi.com/defaultauth

aud

The Audience value in your Dotdigital push notification profile
e.g. https://api.comapi.com

sub

A unique and consistent identifier for the user, such as a user id, email address

nonce

The nonce is used to randomise the ciphers for added security

iat

Token issued-at time as seconds since the Unix epoch (RFC 7519 NumericDate). Not milliseconds.

exp

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.

🚧

iat and exp are seconds, not milliseconds. A common Android bug is passing System.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 typed issuedAt(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.

2059

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.

2059

Using a random, unique, consistent value for the sub claim per user

🚧

Ensure your sub claim is unique and consistent per user

The 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 note

Do 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(...) throws WeakKeyException. 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.time requires API 26 or library desugaring. java.time.Instant and java.time.temporal.ChronoUnit are only available natively from API 26. If your app's minSdk is below 26, enable core library desugaring in your module's build.gradle (coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:2.1.4" plus compileOptions { coreLibraryDesugaringEnabled true }). Otherwise, substitute new Date() and System.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 hex

The 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.


What’s Next

Setup your API user