Embedded SDK
What are Smart Tokens?
Smart tokens are at the core of TokenOS.
There are two kinds of smart tokens:
- Transfer tokens authorize payments, or the transfer of assets or funds from the payer to the payee, and function as programmable money.
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.“
- Access tokens authorize the access of member data. The type of access depends on the conditions of the access token, and can include “who,” “what,” and “how” that data can be accessed.
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
- Android version 5.0 or higher
- Android device with a secure hardware keystore
- Install Gradle
- XCode 8.3.3 or higher
- Target of iOS 9.0 or higher
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
}
}
1. Install CocoaPods 1.5.0 or later.
2. In your project’s Podfile, add the following sources:
- https://github.com/tokenio/token-cocoa-pods.git
- https://github.com/CocoaPods/Specs.git
3. Add pod TokenSdk under the target as the dependency.
4. Run pod update from the command line.
5. Use the generated .xcworkspace file to work on your project.
- Objective-C: add #import TokenSdk/TokenSdk.h to your source code to import Objective‑C and make it available for use in your code
- Swift: add import TokenSdk at the top of your Swift source code
Pagination
Token may provide a PagedList
response for GET
operations that return multiple records. All the Token pagination APIs require an offset and limit:
- The offset indicates the starting offset of the page. Use ‘null’ for the first page.
- 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:
- A list of records per the limit.
- 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:
- Set up the API environment
- Create or get the Member object
- Make unauthenticated calls, such as retrieving the list of Token supported banks
- Set up the API environment
- Create or get the TKMember object
- Make unauthenticated calls, such as retrieving the list of Token supported banks
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:
- Manage user aliases, keys, and profile
- Link bank accounts
- Create access and transfer tokens
- Sign access and transfer tokens
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:
- PRIVILEGED
- STANDARD
- LOW
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:
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. |
- Protobuf:
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.
- Protobuf:
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.
}];
- Protobuf:
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.
ACCOUNTS
- Provide third-party access to the account information of the userBALANCES
- Provide third-party access to the user’s account balancesTRANSACTIONS
- Provide third-party access to the user’s account transaction historyTRANSFER_DESTINATIONS
- Provide third-party access to payment rails which can be used to transfer money (the BIC and account number for a SWIFT account)FUNDS_CONFIRMATIONS
- Provide third-party access to check whether a user has sufficient permission to make a given paymentSTANDING_ORDERS
- Provide third-party access to view the user’s standing orders
// 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.
}];
- Protobuf:
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