NAV Navbar
Android iOS

Embedded SDK

What are Smart Tokens?

Smart tokens are at the core of TokenOS.

There are two kinds of smart tokens:

Example:

A business (the payee) requests a member (the payer) to authorize a smart token to pay for an online purchase: “Allow Business XYZ to initiate a payment from my account at Iron Bank to pay €224 for order 79262212.“

Example:

A member (the grantor) authorizes a service (the grantee) to access and aggregate their account information.

Tokens are created by the Token app. To begin this process you must create a TokenRequest which will be sent to the app. The created token will mostly consist of details specified on the TokenRequest.

Prerequisites

Getting Started

In build.gradle, add the Token Maven repo.

repositories {
  maven {
    url https://token.jfrog.io/token/public-libs-release-local
  }
}

Add the dependency.

implementation(io.token.sdk:tokenio-sdk-android:2.12.23) {
 exclude group: com.google.api.grpc, module: proto-google-common-protos
}

Add the Android settings.

android {
  defaultConfig {
    minSdkVersion 21
    compileSdkVersion 28
    multiDexEnabled = true
  }
  packagingOptions {
    pickFirst "**"
  }
  compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
  }
}

Pagination

Token may provide a PagedList response for GET operations that return multiple records. All the Token pagination APIs require an offset and limit:

  1. The offset indicates the starting offset of the page. Use ‘null’ for the first page.
  2. The limit indicates the number of records per page. It should be less than 100.

A PagedList object will be returned if the API processes successfully. It contains:

  1. A list of records per the limit.
  2. An offset for requesting the next page.

If the size of the records is smaller than the request limit, it means the API has reached the last page.

TokenClient

TokenClient is the Token SDK’s unauthenticated call interface. It is used to:

User Authentication Store

UserAuthenticationStore is used to persist the app authentication status. Whenever the user has authenticated, the status must be saved in this shared store so the Token SDK knows the user has been authenticated. After the status is set there is a short window of time for the app to make authenticated calls.

If the status is not authenticated or has expired, the authenticated calls will throw a TokenAuthenticationException.

// Set authentication duration to 10 seconds.
UserAuthenticationStore persistedAuthenticationStore
= new UserAuthenticationStore(10);

// The application context
Context applicationContext;

CryptoEngineFactory engine = new AKSCryptoEngineFactory(
   applicationContext,
   persistedAuthenticationStore);

If you are using Android Fingerprint for authentication, save the status in the onAuthenticationSucceeded method.

class FingerprintUiHelper extends FingerprintManagerCompat.AuthenticationCallback {
  ...

  @Override
  public void onAuthenticationSucceeded(
    FingerprintManagerCompat.AuthenticationResult result) {
    persistedAuthenticationStore.authenticateUser();
  }
}

Creating the TokenClient

Now that we have an instance of the CryptoEngineFactory, we can create a TokenClient object with the builder. This will be the main interface from which to make unauthenticated calls.

TokenClient tokenClient = TokenClient
   .builder()
   .withCryptoEngine(engine)
   .connectTo(TokenCluster.SANDBOX)
   .build();
TokenClientBuilder *builder = [[TokenClientBuilder alloc] init];
// Change the cluster if necessary
builder.tokenCluster = [TokenCluster sandbox];
builder.port = 443;
builder.useSsl = YES;
builder.developerKey = @"Please request one from Token directly";
TokenClient *tokenClient = [builder build];

Note: The developer key in the above sample is for testing only. If you are using the SDK for your application, please request one from Token directly.

Member

Member is the interface to make authenticated API calls on behalf of a user. Use it to:

TKMember is the interface to make authenticated API calls on behalf of the user. Use it to:

The Token SDK will generate keys and store them in Android Keystore. All the authenticated calls are signed with the keys, so Token can confirm the user’s identity.

The Token SDK will generate keys and store them in Secure Enclave. All authenticated calls are signed with the keys, so Token can confirm the user’s identity.

There are three levels of keys in Token:

Whenever the call requires a signature from PRIVILEGED or STANDARD-level keys, the UserAuthenticationStore will check that the user has achieved the sufficient level of authentication required.

Alias

In creating a user member, you must create an custom alias. Please provide a short description to Token. This description will show in the web-app when users are entering aliases.

The Token web-app will resolve aliases with “token” realm by default. Please contact Token for configuring it to your realm after your app is ready.

EXAMPLE:

Alias alias = Alias.newBuilder()
   .setValue("example@token.io")
   .setType(Type.EMAIL)
   // The Realm is required.
   .setRealm(bankId)
   .build();
Alias *alias = [[Alias alloc] init];
alias.value = @"example@token.io"
alias.type = Alias_Type_Email;
// The Realm is required.
alias.realm = @"token";

Aliases and member IDs can be looked up using resolveAlias or getMemberId. If you don’t know the type of alias you are searching for, you can set it to UNKNOWN, and it will be resolved by Token.

Aliases and member IDs can be searched using getTokenMember. If you don’t know the type of alias you are searching for, you can set it to Alias_Type_Unknown, and it will be resolved by Token.

tokenClient.resolveAlias(alias)
  .subscribe(tokenMember -> {
    Alias resolved = tokenMember.getAlias();
    String memberId = tokenMember.getId();
  });
[tokenClient getTokenMember:alias
                  onSuccess:^(TokenMember *tokenMember) {
                      if (tokenMember != nil) {
                          // Exists.
                      } else {
                          // Doesn’t exist.
                      }
                  } onError:^(NSError *e) {
                      // Something went wrong.
                  }];

Create a New Member

Create a member for a new user.

For banks using the Token app, create a member for a new user as mentioned below.

// User authentication is required.
tokenClient.createMember(alias).subscribe(member -> {});
// User authentication is required.
[tokenClient createMember:alias
                onSuccess:^(TKMember *m) {
                    // Use member.
                    newMember = m;
                } onError:^(NSError *e) {
                    // Something went wrong.
                }];

In the bankId realm, you need to use your own recovery agent. While creating a new user member, you can use the below call to specify the recovery agent who is entitled to sign the recovery authorization on behalf of the user. Alias is the user member’s alias, and recoveryAgent is, for example, the member ID of a Bank member.

NOTE: You MUST specify a recovery agent with your member and alias, or you will not be able recover your member at a later time.

// User authentication is required.
tokenClient.createMember(alias, recoveryAgent).subscribe(member -> {});
// User authentication is required.
[tokenClient createMember:alias
            recoveryAgent: bankMemberId
                onSuccess:^(TKMember *m) {
                    // Use member.
                    newMember = m;
                } onError:^(NSError *e) {
                    // Something went wrong.
                }];

In case the recovery agent member ID is not available, it can be fetched by resolving the agent’s alias.

Alias agentAlias = Alias.newBuilder()
   .setValue(BankID)
   .setType(Type.BANK)
   .build();

tokenClient.resolveAlias(agentAlias)
  .subscribe(tokenMember -> {
     String recoveryAgent = tokenMember.getId();
  });
Alias *agentAlias = [[Alias alloc] init];
agentAlias.value = @"bankId";
agentAlias.type = Alias_Type_Bank;

[tokenClient getMemberId:agentAlias
                onSuccess:^(NSString *agentId) {
                    recoveryAgent = agentId;
                } onError:^(NSError *e) {
                    // Something went wrong.
                }];

Check whether the alias has been verified.

member
  .aliases()
  .subscribe(aliases -> {
    if (aliases.contains(alias)) {
      // Verified.
    }});
[newMember getAliases:alias
            onSuccess::^(NSArray<Alias *> *aliasArray) {
                  if ([aliasArray containsObject:alias]) {
                      // Verified.
                  } else {
                      // Not yet Verified.
                  }
            } onError:^(NSError *e) {
                // Something went wrong.
            }];

If the Bank is managing the alias’s realm, call verifyAliasBlocking to verify the alias with the Bank SDK.

bankMember.verifyAliasBlocking(recoveredMember.memberId(), alias);
bankMember.verifyAliasBlocking(recoveredMember.memberId(), alias);

After the alias is verified, the member is ready to use. Make sure to persist the member ID for future use.

Get an Existing Member

Get the Member object on a device with valid keys.

tokenClient
  .getMember(memberId)
  .subscribe(member -> {});
[tokenClient getMember:memberId
             onSuccess:^(TKMember *m) {
                 // Use member.
                 loginMember = m;
             } onError:^(NSError *e) {
                 // Something went wrong.
             }];

Recover a Member

If the valid device was lost or the app was deleted and there is no existing device that holds a user’s valid keys, the member will need to be recovered. All old keys will be invalidated after recovery and all the bank accounts linked to the recovered member will be marked as locked. The user member will need to relink their accounts to unlock them.

Remember, you must have already specified a recovery agent when you created a member and alias. If you have not done so, see Create a New Member to create a member with a recovery agent under the bankId realm.

The diagram below shows the flow of the member recovery:

Image showing the member recovery flow

Before recovering the user, a new PRIVILEGED key will need to be generated, and an authorization using that key created(1). Use CryptoEngine to create a new key. After that, you can generate the authorization for the entitled agent to sign(2).

Before recovering the user, a new PRIVILEGED key will need to be generated, and an authorization using that key created(1). Use TKCrypto to create a new key. After that, you can generate the authorization for the entitled agent to sign(2).

CryptoEngine cryptoEngine
               = new AKSCryptoEngineFactory(
                    Context,
                    new UserAuthenticationStore(),
                    Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
                    .create(userMemberId);

Key newKey = cryptoEngine.generateKey(PRIVILEGED);
Authorization authorization = tokenClient
       .createRecoveryAuthorizationBlocking(userMemberId, newKey);
// The member id to recover
NSString *userMemberId;

TKCrypto *crypto = [tokenClient createCrypto:userMemberId];
Key *privilegedKey = [crypto generateKey:Key_Level_Privileged];

[tokenClient
createRecoveryAuthorization:userMemberId
key:privilegedKey
onSuccess:^(MemberRecoveryOperation_Authorization *authorization) {
    // Sends the authorization to recovery agent.
} onError:^(NSError *e) {
    // Something went wrong.
}];

The entitled Agent must sign the authorization(3) and use it to generate a MemberRecoveryOperation(4). For example, if you are using the bank member as the recovery agent, the bank needs to sign the authorization using the Bank SDK.

// This is a java example code that the bank member signs the authorization
Signature agentSignature = bankMember
       .authorizeRecoveryBlocking(authorization);

MemberRecoveryOperation mro =
     MemberRecoveryOperation.newBuilder()
       .setAuthorization(authorization)
       .setAgentSignature(agentSignature)
       .build();

Finally, the user member can be recovered by calling completeRecoveryBlocking with tokenClient(5).

Finally, the user member can be recovered by calling completeRecovery with tokenClient(5).

Member recoveredMember = tokenClient.completeRecoveryBlocking(
        userMemberId,
        Arrays.asList(mro),
        newKey,
        cryptoEngine);

// The MemberRecoveryOperation signed by the recovery agent.
MemberRecoveryOperation *mro;
// The member id to recovery
NSString *userMemberId;

// The privileged key for the MemberRecoveryOperation.
Key *privilegedKey;

// The crypto you used to generate the privileged key.
TKCrypto *crypto = [tokenClient createCrypto:userMemberId];
    [tokenClient completeRecovery:userMemberId
               recoveryOperations:[NSArray arrayWithObject:mro]
                    privilegedKey:privilegedKey
                           crypto:crypto
                        onSuccess:^(TKMember *recoveredMember) {
                            // Use member.
                            newMember = recoveredMember;
                        } onError:^(NSError *e) {
                            // Something went wrong.
                        }];

A recovered member will be returned after TokenOS verifies the recovery agent’s signature(6).

Remember, all aliases of the user will be invalidated after recovery. The bank member must call verifyAliasBlocking again.

bankMember.verifyAliasBlocking(recoveredMember.memberId(), alias);
bankMember.verifyAliasBlocking(recoveredMember.memberId(), alias);

Provision a New Device

If you want to load an existing member onto a new device, you will need approval from one of the old devices with the valid keys.

Generate new keys and send them to the old device via notification.

DeviceMetadata metadata = DeviceMetadata.newBuilder()
  .setApplication("Token")
  .setDevice("Android")
  .build();

tokenClient
   .provisionDevice(alias)
   .flatMap(deviceInfo -> tokenClient.notifyAddKey(
       alias,
       deviceInfo.getKeys(),
       metadata))
    .subscribe(notifyStatus -> {});
DeviceMetadata *metadata = [DeviceMetadata message];
metadata.application = @“Token;
metadata.device = @“iPhone;

[tokenClient provisionDevice:alias
                  onSuccess:^(DeviceInfo *deviceInfo) {
                      // Generate the keys.
                      persistedMemberId = deviceInfo.memberId;
                      keys = deviceInfo.keys;
                      [tokenClient notifyAddKey:alias
                                           Keys:keys
                                 deviceMetadata:metadata
                                      onSuccess:^() {
                                          // Notification sent.
                                      } onError:^(NSError *e) {
                                          // Something went wrong.
                                      }];
                  } onError:^(NSError *e) {
                      // Something went wrong.
                  }];

Approve the new keys on the old device with the valid keys. The code below is run on the approved device.

// The notification object you received
Notification notification;

try {
 BodyCase bodyCase =
   BodyCase.valueOf(notification.getContent().getType());
 String payload = notification.getContent().getPayload();
 if (bodyCase.equals(ADD_KEY) ) {
   AddKey.Builder builder = AddKey.newBuilder();
   JsonFormat.parser().merge(payload, builder);
   AddKey addkey = builder.build();
   // Approve the keys
   // User authentication required
   member
     .approveKeys(addkey.getKeysList())
     .subscribe();
 }
} catch (Exception e) {
 // Something went wrong.
}
// The notification object you received
Notification *notification;

if ([notification.content.type isEqualToString:@"ADD_KEY"]) {
    AddKey *content = [TKJson
                       deserializeMessageOfClass:[AddKey class]
                       fromJSON:notification.content.payload];
    // User authentication is required.
    [member approveKeys:content.keysArray
              onSuccess:^() {
                  // Approved.
              } onError:^(NSError *e) {
                  // Something went wrong.
              }];
}

Get the member on the new device.

tokenClient
  .getMember(memberId)
  .subscribe(member -> {});
[tokenClient getMember:persistedMemberId
             onSuccess:^(TKMember *m) {
                 // Use member.
                 loginMember = m;
             } onError:^(NSError *e) {
                 // Something went wrong.
             }];

For receiving and handling notifications, refer to the Notification section of this document.

Profile

Users cannot determine who the member is from a member ID alone. There is the option to set up a profile for the member to make identification easier.

member
  .setProfile(Profile.newBuilder()
    .setDisplayNameFirst("Jon")
    .setDisplayNameLast("Snow")
    .build())
  .subscribe(profile -> {})
// Your profile picture
byte[] picture;

member
  .setProfilePicture("png", picture)
  .subscribe()
Profile *profile = [[Profile alloc] init];
profile.displayNameFirst = @"Jon";
profile.displayNameLast = @"Snow";

[member setProfile:profile
         onSuccess:^(Profile *p) {
             // Success.
         } onError:^(NSError *e) {
             // Something went wrong.
         }];

[member setProfilePicture:member.id
                 withType: @"image/gif"
                 withName: @"selfie.gif"
                 withData: loadImage(@"selfie.gif")
                onSuccess:^() {
                    // Success.
                 } onError:^(NSError *e) {
                     // Something went wrong.
                 }];

You can also look up other members’ profiles with their member ID.

// Other member’s id.
String *otherMemberId;

member
  .getProfile(otherMemberId)
  .subscribe(profile -> {});

member
  .getProfilePicture(otherMemberId, ProfilePictureSize.SMALL)
  .subscribe(blob -> {
     // The picture data.
     byte[] picture = blob.getPayload().getData().toByteArray();
  });
// Other members’ id.
NSString *otherMemberId;

[member getProfile:otherMemberId
         onSuccess:^(Profile *p) {
             // Success.
         } onError:^(NSError *e) {
             // Something went wrong.
         }];

[member getProfilePicture:otherMemberId
                     size:ProfilePictureSize_Small
                onSuccess:^(Blob *blob) {
                    displayImage(blob.data);
                } onError:^(NSError *e) {
                    // Something went wrong.
                }];

Bank Account Linking

Members can easily link and manage their bank accounts. This process can be repeated at a later time to add more accounts from the same bank.

Account linking

To link accounts to a Member, you will need the BankAuthorization. Banks integrated with Token can construct the BankAuthorization in their backend.

Accounts can then be linked.

// The bank authorization of the accounts.
BankAuthorization bankAuthorization;
member
     .linkAccounts(bankAuthorization)
     .subscribe(accounts -> {});
// The bank authorization of the accounts.
BankAuthorization *bankAuthorization;

[member linkAccounts:bankAuthorization
            onSuccess:^(NSArray<TKAccount *> *accountList) {
                // Success.
            } onError:^(NSError *e) {
                // Something went wrong.
            }];
}

Get Bank Accounts

Get Accounts

member
  .getAccounts()
  .subscribe(accounts -> {}); // List<Account>
[member getAccounts:^(NSArray<TKAccount *> *accounts) {
     // Success.
} onError:^(NSError *e) {
     // Something went wrong.
}];

Get Balances

Use the Account object to get balances.

Use the TKAccount object to get balances.

// The account you want to look up.
Account account;

account
  .getBalance(Level.LOW)   // Key Level
  .subscribe(balance -> {
    // Balance object includes current and available balance.
    Money money = balance.getCurrent();
  });
// The account you want to look up.
TKAccount * account;

[account getBalance:^(TKBalance *balance) {
     // TKBalance includes current and available balance.
     Money *money = balance.current;
} onError:^(NSError *e) {
     // Something went wrong.
}];

Get Transactions

You can access the user member’s transactions using pagination. Set the limit to any number less than 100 and offset to null. The result is a PagedList class containing a list of transactions (starting with the most recent) and a cursor with a new offset inside. You can use this new offset for the next page of transactions. See Pagination for more information on paged lists.

You can now filter transactions using the startDate and endDate parameters. startDate is an optional inclusive lower bound of the transaction booking date and endDate is an optional inclusive upper bound of the transaction booking date. Each new page of transactions will use the same date range specified in the original request. (Currently this feature is only supported in Java. It will soon be available in other languages.)

// The account you want to look up.
Account account;

// NULL: get first "page" of results.
account
  .getTransactions(
     null,       // Offset
     10,         // Per page
     Level.LOW)  // Key Level
  .subscribe(transactionPagedList -> {
     List<Transaction> transactions =
              transactionStringPagedList.getList();
        });
// NULL: get first "page" of results.
[member getTransactionsOffset:NULL
                        limit:10
                   forAccount:account.id
                      withKey:Key_Level_Low
                    onSuccess:^(PagedArray<Transaction *> *transactions) {
                        for (Transaction *t in transactions.items) {
                            // Use transactions.
                        }
                    } onError:^(NSError *e) {
                        // Something went wrong.
}];

Notifications

App push notification services (FCM, APNS etc.) require a secret to send notifications; therefore Token is not able to directly notify user members.

What Token does do, is notify the bank’s backend via the Bank Integration SDK Notification service. The bank’s backend can then forward the notification to the bank’s apps. To enable this feature, the users’ app needs to subscribe to the notification with the member’s bank ID.

Subscriber subscriber = member.subscribeToNotificationsBlocking("bankId");
NSMutableDictionary<NSString *,NSString *> *handlerInstructions = [NSMutableDictionary dictionary];
   [member subscribeToNotifications:@“bankId handlerInstructions:handlerInstructions onSuccess:^(Subscriber *subscriber) {
       // Success.
   } onError:^(NSError * error) {
       // Something went wrong.
   }];

Receiving Notifications

After the embedded app receives the notification ID from push notification services, the Notification can be fetched as shown below:

// FCM notification.
public class NotificationService extends FirebaseMessagingService {

  @Override public void onMessageReceived(RemoteMessage remoteMessage) {
    super.onMessageReceived(remoteMessage);
    String notificationId = remoteMessage.getData().get("notification-id");
    // Continue
  }
}

member
  .getNotification(notificationId)
  .subscribe(notification -> {});  // Notification object.
// APNS notification userInfo.
NSDictionary * userInfo;

NSString * notificationId = [userInfo valueForKey:@"notification-id"];
[member getNotification:notificationId
              onSuccess:^(Notification *notification) {
                  // Success.
                  receivedNotification = notification;
              } onError:^(NSError *e) {
                  // Something went wrong.
              }];

Or using notification polling:

member
  .getNotifications(
    null,  // Offset.
    10)    // Limit.
  .subscribe(notificationPagedList -> {
    List<Notification> notifications = notificationPagedList.getList();
    if (notifications.size() > 0) {
      Notification notification = notifications.get(0);
      // Continue.
    }
  });
[member
 getNotificationsOffset:NULL
 limit:10
 onSuccess:^(PagedArray<Notification *> *notifications) {
     // Success.
     if (notifications.items.count > 0) {
         receivedNotification = [notifications.items objectAtIndex:0];
     }
 } onError:^(NSError *e) {
     // Something went wrong.
 }];

The polling notifications list is sorted in descending order and by the time created.

IMPORTANT: In order for the push notifications to be sent to your device, your server must be set up with a corresponding backend notification service that can forward Token notifications to your app.

Handling Notifications

Notifications have different types that specify the payload and its content.

You can look up the type in notification.content.type. The payload of the notification is stored at notification.content.payload and serialized in JSON. You can deserialize it with JsonFormat. TKJson.

// The notification object you received.
Notification notification;

try {
  BodyCase bodyCase =
    BodyCase.valueOf(notification.getContent().getType());
  String payload = notification.getContent().getPayload();
  if (bodyCase.equals(ADD_KEY) ) {
    // AddKey is the notification content object for "ADD_KEY" notification.
    AddKey.Builder builder = AddKey.newBuilder();
    JsonFormat.parser().merge(payload, builder);
    AddKey addkey = builder.build();
    // Continue handling AddKey object
  }
} catch (Exception e) {
 // Something went wrong.
}
// AddKey is the notification content object for "ADD_KEY" notification.
 if ([notification.content.type isEqualToString:@"ADD_KEY"]) {
     AddKey *content = [TKJson
                        deserializeMessageOfClass:[AddKey class]
                        fromJSON:notification.content.payload];
     // Continue handling the AddKey object.
}

Here is the list of Notifications for embedded apps:

Type Category User Action Require Note
AddKey Member Management Yes e.g. Provisioning a new device
RecoveryCompleted Member Management No A notification to notify a member that a recovery process has completed
CreateAndEndorseToken PISP/AISP Yes A notification that a token needs to be created/endorsed.
TransferProcessed PISP No A notification that a transfer was successfully processed in PISP.
TransferFailed PISP No A notification that a transfer failed in PISP.
PaymentRequest P2P Yes A notification to request a payment.
PayerTransferProcessed P2P No A notification to the payer that a transfer was successfully processed.
PayerTransferFailed P2P No A notification to the payer that a transfer failed.
PayeeTransferProcessed P2P No A notification to the payee that a transfer was successfully processed.
NotificationInvalidated Generic No A notification to indicate that a previously sent notification was invalidated.

Below are the protocol buffers for notifications.

Updating Notification Status

Each Notification has its status. If no user action is required for the notification, the status will be NO_ACTION_REQUIRED. Otherwise, the initial status is PENDING.

You can update the PENDING notifications status as COMPLETED or DECLINED depends on users’ actions. Thereby, a device can avoid to handle a processed notification. Once the status is updated, it will be immutable.

// The ID of notification to update.
String notificationId;

member.updateNotificationStatusBlocking(notificationId, DECLINED);

// The ID of notification to update.
NSString *notificationId;

[member updateNotificationStatus:notificationId
                          status:Notification_Status_Declined
                       onSuccess:^ {
                          [expectation2 fulfill];
                       } onError:^(NSError *e) {
                           // Something went wrong.
                       }];

Merchant Checkout

When a user makes a purchase they will need to approve payment to the merchant. Token will send a CreateAndEndorseToken notification containing a TokenRequest payload. The app will use the information in the TokenRequest, indicated by the BodyCase, to create a transfer token.

// The received notification.
Notification *notification;

try {
  BodyCase bodyCase =
    BodyCase.valueOf(notification.getContent().getType());
  String payload = notification.getContent().getPayload();
  if (bodyCase.equals(CREATE_AND_ENDORSE_TOKEN) ) {
    CreateAndEndorseToken.Builder builder = CreateAndEndorseToken
        .newBuilder();
    JsonFormat.parser().merge(payload, builder);
    CreateAndEndorseToken content = builder.build();
    If (content.getTokenRequest()
               .getRequestPayload()
               .getRequestBodyCase == RequestBodyCase.TRANSFER_BODY) {
      // Merchant checkout notification.
      // Continue.
    }
  }
} catch (Exception e) {
  // Something went wrong.
}
// Received notification.
Notification *notification;

if ([notification.content.type
     isEqualToString:@"CREATE_AND_ENDORSE_TOKEN"]) {
    CreateAndEndorseToken *content = notification
    [TKJson deserializeMessageOfClass:[CreateAndEndorseToken class]
                             fromJSON:notification.content.payload];
    if (content.tokenRequest.requestPayload.requestBodyOneOfCase ==
        TokenRequestPayload_RequestBody_OneOfCase_TransferBody) {
        // Merchant checkout notification.
        // Continue.
    }
}

The transfer token can now be created and endorsed. After the token is endorsed, the user should be prompted to sign the token request state so the merchant can redeem the token.

Below is the protocol buffer for CreateAndEndorseToken.

Create the transfer token.

// The CreateAndEndorseToken content you received.
CreateAndEndorseToken content;
// The ID of account you want to use.
String accountId;

// Create the transfer token from the token request
member
    .createTransferToken(content.getTokenRequest())
    .setAccountId(accountId)
    .execute()
    .subscribe(token -> {});
// The CreateAndEndorseToken content you received.
CreateAndEndorseToken *content;
// The ID of the account you want to use.
NSString *accountId;

TransferTokenBuilder *builder =
[member createTransferToken:content.tokenRequest];
builder.accountId = accountId;

[builder executeAsync:^(Token *token) {
    //continue to endorse token
} onError:^(NSError *e) {
    // Something went wrong.
}];

Endorse the transfer token.

// The transfer token you created.
Token token;

member
 // User authentication required.
  .endorseToken(token, STANDARD)
  .subscribe(tokenOperationResult -> {
      //Continue to sign token request state.
  });
// User authentication required.
[member endorseToken:token
             withKey:Key_Level_Standard
           onSuccess:^(TokenOperationResult *result) {
               //Continue to sign token request state.
           } onError:^(NSError *e) {
               // Something went wrong.
           }];

Sign the token request state.

// The token operation result from endorse token
TokenOperationResult tokenOperationResult;

member.signTokenRequestState(
       content.getTokenRequest().getId(),
       tokenOperationResult.getToken().getId(),
       content.getTokenRequest().getRequestPayload().getCallbackState()))
   .subscribe(ignore -> {});
[member signTokenRequestState:content.tokenRequestId
                      tokenId:result.token.id_p
                        state:content.state
                    onSuccess:^(Signature *ignore) {
                        // Success.
                    } onError:^(NSError *e) {
                        // Something went wrong.
                    }];

Below are the protocol buffers for the Token object and the Token Request object.

Future-Dated Payments

When a request for a single future-dated payment is received, the execution_date field in the transfer token request will be populated with the specific date on which the transfer is to occur. The date is formatted using the string, “YYYY-MM-DD”.

Future-dated Payment Sample (Currently available only in Java)

public static Token createTransferTokenScheduled(
        Member payer,
        Alias payeeAlias) {

Standing Order Tokens

The standing order token is used for recurring payments-that is, payments that happen at regular intervals. The frequency of these standing orders can be initiated in the following frequencies: daily (DAIL), weekly (WEEK), twice-weekly (TOWK), monthly, (MNTH), twice-monthly (TOMN), quarterly (QUTR), semiannually (SEMI), or yearly (YEAR).

You must specify the startDate and in most cases, the endDate of each standing order. For example, if you create a standing order from, 2019-01-01 to 2019-02-28 with the frequency TOMN, it is an order for 4 payments, 2 for the month of January and 2 for February. If you do not specify an end date, the payment will happen indefinitely.

When a token request for a standing order is received, create a Member StandingOrderTokenBuilder object with the tokenrequest parameter. Those parameters listed in the table below as required must be included in the request received. The others are optional.

member Required. Payer of the token
amount Required. Amount per charge of the standing order token
currency Required. Currency for each charge in the standing order
frequency Required. ISO 20022 code for the frequency of the standing order: DAIL, WEEK, TOWK, MNTH, TOMN, QUTR, SEMI, YEAR
startDate Required. Start date of the standing order: ISO 8601 YYYY-MM-DD or YYYYMMDD.
endDate Optional. End date of the standing order: ISO 8601 YYYY-MM-DD or YYYYMMDD. If not specified, the standing order will occur indefinitely.
destination Required. Adds a transfer destination to a transfer token request.
ProviderTransferMetadata Optional. Adds metadata for a specific provider.
destinationCountry Optional. Sets the destination country in order to narrow down the country selection in the web-app UI.

The TPP is responsible for initiating the standing order, but it is the responsibility of the bank to execute each payment. The bank will not initiate a standing order until the token has been redeemed by the TPP that requested it.

Creating a Token on Behalf of Another Party

If TPPs have been given permission by Token to do so, they can create a token on behalf of another party. The actingAs field on the token payload will be set with the following properties describing the intended recipient:

display_name Name of recipient, to be shown to user
ref_id Optional. The reference ID of the recipient. Opaque to Token
logo_url URL pointing to recipient’s logo
secondary_name Optional. Domain or email of the recipient, to be shown to user along with display_name

NOTE: If acting_as is present, we suggest you display that to the user instead of the profile/profile picture of the member the payment is being sent to.

Accessing Accounts

Access Tokens

NOTE: In this document and elsewhere, access token refers to a Token-specific concept. It is a smart token that accesses end-user bank account information.

When a user wants to share their account information with a third party, they need to approve an access token request. Token will send a CreateAndEndorseToken notification to the app, and use the information in the TokenRequest, indicated by the BodyCase, to create an access token.

// The received notification
Notification *notification;

try {
  BodyCase bodyCase =
    BodyCase.valueOf(notification.getContent().getType());
  String payload = notification.getContent().getPayload();
  if (bodyCase.equals(CREATE_AND_ENDORSE_TOKEN) ) {
    CreateAndEndorseToken.Builder builder = CreateAndEndorseToken
        .newBuilder();
    JsonFormat.parser().merge(payload, builder);
    CreateAndEndorseToken content = builder.build();
        If (content.getTokenRequest()
               .getRequestPayload()
               .getRequestBodyCase == RequestBodyCase.ACCESS_BODY) {
      // It is an access token request notification
      // Continue
    }
  }
} catch (Exception e) {
  // Something went wrong.
}
// The received notification.
Notification *notification;

if ([notification.content.type
     isEqualToString:@"CREATE_AND_ENDORSE_TOKEN"]) {
    CreateAndEndorseToken *content =
    [TKJson deserializeMessageOfClass:[CreateAndEndorseToken class]
                             fromJSON:notification.content.payload];
    if (content.tokenRequest.requestPayload.requestBodyOneOfCase ==
        TokenRequestPayload_RequestBody_OneOfCase_AccessBody) {
        // It is an access token request notification.
        // Continue.
    }
}

The AccessTokenBuilder is used to create access tokens. After creating the AccessTokenBuilder object, you can add resources for each account the user wants to share.

// The CreateAndEndorseToken content you received.
CreateAndEndorseToken content;
// The ID of the account you want to share.
String accountId;


AccessTokenBuilder builder = AccessTokenBuilder.fromTokenRequest(
    content.getTokenRequest());

builder.forAccount(accountId);
builder.forAccountTransactions(accountId);
builder.forAccountBalances(accountId);

member
    .createAccessToken(builder)
    .subscribe(token -> {});
// The CreateAndEndorseToken content you received.
CreateAndEndorseToken *content;
// The ID of the account you want to share.
NSString *accountId;

AccessTokenBuilder *builder = [[AccessTokenBuilder alloc]
                                initWithTokenRequest:content.tokenRequest];
[builder forAccount:accountId];
[builder forAccountBalances:accountId];
[builder forAccountTransactions:accountId];

[member createAccessToken:builder
                onSuccess:^(Token *token) {
                    //Continue to endorse token.
                } onError:^(NSError *e) {
                    // Something went wrong.
                }];

Only one active token is allowed per member/TPP pair at a time; therefore you need to to check the active access token (if any) before you replace it.

If you want to edit the resources of an access token, you can replace the old token with new AccessTokenBuilder too.

// The TPP member ID. Can be found in the TokenRequest.
String tppMemberId;

// The new access token builder
AccessTokenBuilder builder;

Token activeAccessToken = member.getActiveAccessTokenBlocking(tppMemberId);

TokenOperationResult result = member.replaceAccessTokenBlocking(activeAccessToken, builder);

Token newToken = result.getToken();
// The TPP member ID. Can be found in the TokenRequest.
NSString *tppMemberId;

// The new access token builder
AccessTokenBuilder *builder;

[member getActiveAccessToken:tppMemberId onSuccess:^(Token *activeAccessToken) {
    [member replaceAccessToken:activeAccessToken
            accessTokenBuilder:builder
                     onSuccess:^(TokenOperationResult * result) {
                          newToken = result.token
                     } onError:^(NSError *e) {
                          // Something went wrong.
                     }];
                  } onError:^(NSError *e) {
                      // Something went wrong.
                  }];

Endorse the access token.

// The transfer token you created.
Token token;

member
 // User authentication required.
  .endorseToken(token, STANDARD)
  .subscribe(tokenOperationResult -> {
      //Continue to sign token request state.
  });
// User authentication required.
[member endorseToken:token
             withKey:Key_Level_Standard
           onSuccess:^(TokenOperationResult *result) {
               //Continue to sign token request state
           } onError:^(NSError *e) {
               // Something went wrong.
           }];

Sign the token request state.

// The token operation result from endorse token
TokenOperationResult tokenOperationResult;

member.signTokenRequestState(
       content.getTokenRequest().getId(),
       tokenOperationResult.getToken().getId(),
       content.getTokenRequest().getRequestPayload().getCallbackState()))
   .subscribe(ignore -> {});
[member signTokenRequestState:content.tokenRequestId
                      tokenId:result.token.id_p
                        state:content.state
                    onSuccess:^(Signature *ignore) {
                        // Success.
                    } onError:^(NSError *e) {
                        // Something went wrong.
                    }];

Fetching Active Access Tokens

You can fetch the active access tokens by pagination. The results list is sorted in descending order by the time created.

PagedList pagedList = member.getAccessTokensBlocking(null, 10);

accessTokenList = pagedList.getList();
newOffset = pagedList.getOffset();
[member getAccessTokensOffset:nil
                        limit:10
                    onSuccess:^(PagedArray<Token *> * pagedArray) {
                        newOffset = pagedArray.offset;
                        accessTokenList = pagedArray.items;
                    } onError:^(NSError * _Nonnull) {
                        // Something went wrong.
                    }];

P2P Payment

Users can pay other Token users and legacy users with transfer tokens. Token users will receive payments into their default account.

Create Transfer Token

Create a transfer token to transfer funds to other Token users.

// the member ID of payee.
String payeeId;
// The ID of account you want to use.
String accountId;
// The amount you want to send.
double amount;

member
    .createTransferToken(amount, "EUR")
    .setToMemberId(payeeId)
    .setAccountId(accountId)
    .setDescription("Book purchase")
    .setEffectiveAtMs(System.currentTimeMillis())
    .execute()
    .subscribe(token -> {});
// the member ID of the payee.

NSString *payeeId;
// The ID of account you want to use.
NSString *accountId;
// The amount you want to send.
NSDecimalNumber *amount;

TransferTokenBuilder *builder =
    [member createTransferToken:amount currency: @"EUR"];
builder.toMemberId = payeeId;
builder.accountId = accountId;
builder.descr = @"Book purchase";

// This token can only be endorsed before this effective time.
builder.effectiveAtMs = [[NSDate date] timeIntervalSince1970] * 1000.0;

[builder executeAsync:^(Token *token) {
    // Continue to endorse token.
} onError:^(NSError *e) {
    // Something went wrong.
}];

To create a transfer token to users without using Token aliases (e.g. using SWIFT scheme), construct the TransferEndpoint object. Instead of setting toMemberId in TransferTokenBuilder, set the destination directly.

// The id of the account you want to use.
String accountId;
// The amount you want to send.
double amount;

// payee's SWIFT account
String iban = "";
// payee's SWIFT BIC
String bic = "";
// payee's legal name
String legalName = "";
// payee's country
String country = "";

member
    .createTransferToken(amount, "EUR")
    .addDestination(TransferEndpoint.newBuilder()
        .setAccount(BankAccount.newBuilder()
            .setSwift(Swift.newBuilder()
                .setAccount(iban)
                .setBic(bic)))
        .setCustomerData(CustomerData.newBuilder()
            .addLegalNames(legalName)
            .setAddress(
                AddressProtos.Address.newBuilder()
                    .setCountry(country)
                    .build())
            .build())
        .build())
    .setAccountId(accountId)
    .setDescription("Book purchase")
    .setEffectiveAtMs(System.currentTimeMillis())
    .execute()
    .subscribe(token -> {});
// Payee's SWIFT account.
NSString *iban;
// Payee's SWIFT BIC.
NSString *bic;
// The id of account you want to use.
NSString *accountId;
// The amount you want to send.
NSDecimalNumber *amount;

TransferTokenBuilder *builder =
    [member createTransferToken:amount currency: @"EUR"];
builder.accountId = accountId;
TransferEndpoint *endpoint = [[TransferEndpoint alloc] init];
endpoint.account.swift.account = iban;
endpoint.account.swift.bic = bic;
builder.destinations = @[endpoint];

// This token can only be endorsed before this effective time.
builder.effectiveAtMs = [[NSDate date] timeIntervalSince1970] * 1000.0;

[builder executeAsync:^(Token *token) {
    // Continue to endorse token.
} onError:^(NSError *e) {
    // Something went wrong.
}];

Payment Request

Users can request payments from other Token users.

The payee notifies the payer of the payment request.

// Payee member object.
Member payee;
// Payer's member Id.
String payerId;

TokenPayload payload = TokenPayload.newBuilder()
        .setDescription("Lunch")
        .setFrom(TokenMember.newBuilder().setId(payerId).build())
        .setTo(TokenMember.newBuilder().setId(payee.memberId()).build())
        .setTransfer(TransferBody.newBuilder()
            .setAmount("100")
            .setCurrency("EUR")
            .build())
        .build();
    tokenClient.notifyPaymentRequest(payload).subscribe(notifyStatus -> {});
// Payee member object.
TKMember *payee;
// Payer's member Id.
NSString *payerId;

TokenPayload *payload = [TokenPayload message];
payload.description_p = @"lunch";
payload.from.id_p = payer.id;
payload.to.id_p = payee.id;
payload.transfer.lifetimeAmount = @"100";
payload.transfer.currency = @"EUR";

[tokenClient notifyPaymentRequest:payload
                        onSuccess:^ {
                            // Notification sent.
                        } onError:^(NSError *e) {
                            // Something went wrong.
                        }];

Below is the protocol buffer for the Token payload.

The payer receives a notification.

// The received notification.
Notification notification;

try {
 BodyCase bodyCase =
     BodyCase.valueOf(notification.getContent().getType());
 String payload = notification.getContent().getPayload();
 if (bodyCase.equals(PAYMENT_REQUEST) ) {
   PaymentRequest.Builder builder = PaymentRequest.newBuilder();
   JsonFormat.parser().merge(payload, builder);
   PaymentRequest content = builder.build();
   // The same payload from payee.
   TokenPayload payload = content.getPayload();
   // Continue.
 }
} catch (Exception e) {
 // Something went wrong.
}
// The received notification.
Notification *notification;

if ([notification.content.type isEqualToString:@"PAYMENT_REQUEST"]) {
    PaymentRequest *content =
    [TKJson deserializeMessageOfClass:[PaymentRequest class]
                             fromJSON:notification.content.payload];
    // The same payload from payee.
    TokenPayload *receivedPayload = content.payload.
    // Continue.
}

Now the payer has enough information to create a transfer token.

// The id of account you want to use.
String accountId;
// The payload you want received.
TokenPayload receivedPayload;

member
  .createTransferToken(
    receivedPayload.getTransfer().getLifetimeAmount(),
    receivedPayload.getTransfer().getCurrency())
  .setToMemberId(receivedPayload.getTo().getId())
  .setAccountId(accountId)
  .setDescription(receivedPayload.getDescription())
  .setEffectiveAtMs(System.currentTimeMillis())
  .execute()
  .subscribe(token -> {});
// The id of account you want to use.
NSString *accountId;
// The payload you want received.
TokenPayload *receivedPayload;


NSDecimalNumber *amount = [NSDecimalNumber decimalNumberWithString:
                           receivedPayload.transfer.lifetimeAmount];

TransferTokenBuilder *builder =
    [member createTransferToken:amount
                       currency:receivedPayload.transfer.currency];
builder.fromMemberId = receivedPayload.from.id_p;
builder.toMemberId = receivedPayload.to.id_p;
builder.accountId = accountId;
builder.descr = receivedPayload.description_p;
// This token can only be endorsed before this effective time.
builder.effectiveAtMs = [[NSDate date] timeIntervalSince1970] * 1000.0;

[builder executeAsync:^(Token *token) {
    // Continue to endorse token.
} onError:^(NSError *e) {
    // Something went wrong.
}];

Endorse and Redeem Transfer Token

After redemption, the transfer will proceed between both users’ accounts.

member.endorseToken(token, STANDARD)
   .flatMap(tokenOperationResult ->
       member.redeemToken(tokenOperationResult.getToken()))
   .subscribe(transfer -> {});
// User authentication required.
[member endorseToken:token
             withKey:Key_Level_Standard
           onSuccess:^(TokenOperationResult *result) {
               // Continue redeem token.
           } onError:^(NSError *e) {
               // Something went wrong.
           }];
[member redeemToken:token
          onSuccess:^(Transfer *transfer) {
              // Success.
          } onError:^(NSError *e) {
              // Something went wrong.
          }];

Copyright © 2019 Token, Inc. All Rights Reserved