NAV Navbar
Java JavaScript Objective-C C#

Token SDKs

The Token SDK allows developers to easily write applications on top of TokenOS. We recommend this way of interacting with the network. To learn more about Token architecture, see the TokenOS Overview.

By means of the Token SDK, a program can transfer funds, get bank account information, and manage access to private information. These operations are all built on the foundation of Smart Token technology.

To enable these features, use the Token SDK to set up a Token member (roughly analogous to a Token user account) and use it to interact with TokenOS.

Android SDK

To use the Token Android SDK, you need Android Studio.

To get the Token Android SDK, source code, and instructions for use go to the token-sdk-android Token Artifactory page. (Choose the latest aar under token-sdk-android to use the latest version of the SDK.)

Maven

<repositories>
    ...
    <repository>
      <url>https://token.jfrog.io/token/public-libs-release-local/</url>
    </repository>
</repositories>
<dependency>
    <groupId>io.token.sdk</groupId>
    <artifactId>tokenio-sdk-android</artifactId>
    <version>1.0.150</version>
</dependency>

Gradle

repositories {
  ...
  maven { url 'https://token.jfrog.io/token/public-libs-release-local/' }
}
compile(group: 'io.token.sdk', name: 'tokenio-sdk-android', version: '1.0.150')

Java SDK

For non-Android Java development, use the Token Java SDK. You need Java Development Kit (JDK) version 7 or later.

To get the Token Java SDK JARs, source code, and instructions for use go to the token-sdk-java Token Artifactory page. (Choose the latest jar under token-sdk-java to use the latest version of the SDK.)

Maven

<repositories>
    ...
    <repository>
      <url>https://token.jfrog.io/token/public-libs-release-local/</url>
    </repository>
</repositories>
<dependency>
    <groupId>io.token.sdk</groupId>
    <artifactId>tokenio-sdk-java</artifactId>
    <version>1.0.150</version>
</dependency>

Gradle

repositories {
  ...
  maven { url 'https://token.jfrog.io/token/public-libs-release-local/' }
}
compile(group: 'io.token.sdk', name: 'tokenio-sdk-java', version: '1.0.150')

JavaScript SDK

The JavaScript SDK builds code usable in Node.js or (via a build tool like webpack or browserify) in the browser. It uses ES7, but builds code usable in ES6.

Token requires a recent version of npm and yarn to build. To install the npm package: npm install token-io

Objective-C SDK

To use the SDK version compatible with Token’s Sandbox testing environment (typical usage), add the appropriate lines to your Podfile:

  # Might already have this line, needed for Protobuf:
  source 'https://github.com/CocoaPods/Specs.git'

  # Add these:
  source 'https://github.com/tokenio/token-cocoa-pods.git'
  pod 'TokenSdk'

Token publishes a new SDK pod when the Sandbox testing environment updates. The source ... token-cocoa-pods.git line above tells pod to watch that git repo for updates. The pod line fetches the latest published pod.

To get the latest Objective-C SDK code, visit https://github.com/tokenio/sdk-objc. This code might be ahead of (incompatible with) the Sandbox testing environment.

C# SDK

The SDK is based on C# 7.0. The target framework is .Net Framework 4.5.1. The package can be found on Nuget.

To use the SDK, add the package as a dependency to your project file:

<ItemGroup>
    <PackageReference Include="Token.SDK.Net" Version="1.0.4" />
</ItemGroup>

Note that the account-linking feature and the notification feature are currently not supported in the C# SDK.

Other Languages

To use a language other than the Token SDK languages (currently Java, JavaScript, and Objective-C, with additional languages to be added), you can use the Token gRPC or HTTP API. Many of the API endpoints require authentication; to authenticate a request, you must compute a cryptographic signature and attach it.

For information about how to do this, contact Token.

Getting Started

Rather than writing a program from scratch, it can be easier to start from working code. Token’s Merchant Quick Checkout sample is a simple web service that uses the SDK. To try out a snippet of code, you can add it to this program.

Get the sample server:

Follow the instructions in the Merchant Quick Checkout sample’s README to get it up and running. It’s a simple web server that calls some Token SDK APIs. This flow is described in more detail on the Token Request page.

As you’ll soon find out, most Token APIs use a logged-in Token member (a Token user account). Where you see a Member object in the sample code, that’s a good place to add code that you want to try out.

How To’s

The following scenarios are typical TokenOS workflows:

Aside: Protocol Buffers

Many important data structures are defined as Protocol Buffer messages.

The build process generates Java code from these. You can learn more about this Java Code. A quick start: If you have a Java object based on a protocol buffer definition, to get the value of the field named tool_name, there are methods named getToolName…; To create a Java object of a class based on a protocol buffer definition named Tool, call Tool.newBuilder().setToolName("Nice tool").build();.

You can get the newest protocol buffer definitions by downloading the most recent jars from https://token.jfrog.io/token/list/public-libs-release-local/io/token/proto/. For protocol buffers, you want the “regular” jars, not javadoc or sources. You can also see them as web pages.

message Signature {
  string member_id = 1;
  string key_id = 2;
  string signature = 3;
}
Signature signature = Signature.newBuilder()
    .setMemberId(memberId)
    .setKeyId(signer.getKeyId())
    .setSignature(signer.sign(update))
    .build();

Many important data structures are defined in terms of Protocol Buffer messages.

For example, smart token signatures are defined in terms of the message Signature.

In Token Javascript, the object corresponding to a protocol buffer message has fields with camel-case names: protocol buffer foo_bar becomes Javascript fooBar.

A protocol buffer string becomes a Javascript string; a protocol buffer number becomes a Javascript number. A protocol buffer enum value becomes a Javascript string; for example, if an enum has a value ENDORSED = 1, in Javascript, this value is 'ENDORSED'. A repeated protocol buffer field becomes a Javascript array. A protocol buffer bytes becomes a Javascript string, the base64 encoding of those bytes.

You can also see the Token SDK protocol buffers as web pages.

message Signature {
  string member_id = 1;
  string key_id = 2;
  string signature = 3;
}
/* signature */
message Signature {
  memberId = "...",
  keyId = "...",
  signature = "..."
}

Many important data structures are defined as Protocol Buffer messages.

The SDK comes with Objective-C code generated from these. You can learn more about this Objective-C code. A quick start: If you have an object based on a protocol buffer definition, to get the value of a field named tool_name, get the property named toolName. If that would give you a property whose name collides with a language keyword (e.g., id), get the property named id_p instead.

The Objective-C comes with copies of the Token public protobuf definitions in its protos/ directory. You can also see them as web pages.

message Signature {
  string member_id = 1;
  string key_id = 2;
  string signature = 3;
}
Signature *signature = [Signature message];
signature.memberId = payer.id_p;
signature.keyId = keyId;
signature.signature = [crypto sign:update].value;

Many important data structures are defined as Protocol Buffer messages.

The build process generates C# code from these. You can learn more about this C# code. A quick start: If you have an object based on a protocol buffer definition, to get the value of a field named tool_name, get the property named ToolName. If that would give you a property whose name collides with the class that owns the property, e.g., Signature property inside a Signature class, get the property named Signature_ instead.

The C# SDK fetches the newest protocol buffer definitions when it is built. You can get the newest protocol buffer definitions by downloading the most recent jars from https://token.jfrog.io/token/list/public-libs-release-local/io/token/proto/. For protocol buffers, you want the “regular” jars, not javadoc or sources. You can also see them as web pages.

message Signature {
  string member_id = 1;
  string key_id = 2;
  string signature = 3;
}
Signature signature = new Signature
{
    MemberId = Payer.Id,
    KeyId = keyId,
    Signature_ = signer.Sign(update)
};

Setup

To use the Token SDK, one creates a client object. This client’s configuration controls the connection to TokenOS.

The most important Token flows involve a Member (a user, merchant, or TPP). A member might have authority over some assets, e.g., a bank account.

Create a Client

The Token client is an API that sends requests to the Token network and gets responses back.

In creating a client, the code specifies which Token environment to use. Here, the code uses the sandbox environment for testing. The sandbox environment uses fake banks and fake money. Once your code works well and is ready for real banks and real money, contact us to discuss next steps for switching to our production environment.

(Token has other environments for newer, less-tested code. Sandbox is good for testing in a relatively stable environment. If you’re trying a new feature, Token might ask you to use another environment.)

When creating a client, the code specifies a developer key, a string associated with an SDK user. If your organization doesn’t already have a developer key, [contact Token]((https://token.io/contact) to get one. You can still connect to our sandbox environment and test without a developer key, however you may encounter rate-limits or restrictions.

The sample below shows how to set up an SDK client that saves your member’s private keys in a directory named keys. If your code runs somewhere with a safer storage place than a directory, you can implement a KeyStore that stores keys elsewhere, perhaps using UnsecuredFileSystemKeyStore as a guide.

The sample below shows how to set up an SDK client that saves your member’s private keys in a directory named keys. If your code runs somewhere with a safer place than a directory, then you can write a crypto engine that stores keys elsewhere, perhaps using UnsecuredFileCryptoEngine (mentioned below) as a guide.

The sample below shows how to set up an SDK client that saves your member’s private keys in the device’s secure enclave. If your code runs somewhere with a safer place, then you can write a TKKeyStore that stores keys elsewhere and use it with TokenIOBuilder.keyStore.

A typical program creates one SDK client instance and uses it for all operations. The client is an abstraction on top of the network connection; it reconnects as needed and kills idle connections.

This code uses the API:

Prerequisites

  • None

What to do next

Create a client

Path keys = Files.createDirectories(Paths.get("./keys"));
TokenIO tokenIO = TokenIO.builder()
        .withKeyStore(new UnsecuredFileSystemKeyStore(keys.toFile()))
        .connectTo(SANDBOX)
        .build();
const Token = new TokenIO({
    env: 'sandbox',
    developerKey,
    keyDir: './keys',
});
TokenIOBuilder *builder = [[TokenIOBuilder alloc] init];
// change the cluster if necessary
builder.tokenCluster = [TokenCluster sandbox];
builder.port = 443;
builder.useSsl = YES;
builder.developerKey = @"4qY7lqQw8NOl9gng0ZHgT4xdiDqxqoGVutuZwrUYQsI";
TokenIO *tokenIO = [builder buildAsync];
var tokenIO = TokenIO.NewBuilder()
    .WithKeyStore(new InMemoryKeyStore())
    .ConnectTo(TokenCluster.SANDBOX)
    .Build();

Create a Member

This code creates a new member (a new Token user account).

If you are a business looking to connect to banks through Token for account information and payment initiation, you will need to create a Token businessMember. If you are a bank setting up your customers in the Token ecosystem, you will set them up as Token members.

The new member automatically gets a member ID, a string that uniquely identifies that member.

A member can also have aliases. An alias is a verifiable identifier for the member that makes sense to humans; for example, an email address. For businesses, your alias will typically be a web domain.

This code shows an alias suitable for a “throwaway” member to use in automated tests. This alias is randomly generated each time; if it were static, then the second time this code was run, it would error out since the alias would already be claimed (an alias can only belong to one member). TokenOS normally makes the member verify ownership before the alias is usable. This wouldn’t normally work for randomly-generated email addresses. In our sandbox environment, TokenOS automatically verifies email aliases that have the text +noverify@, so tests can use these random email addresses. (In many email systems, messages to example+noverify@example.com are delivered to example@example.com; text between the + and @ is ignored.)

In order to interact in live production, your alias will have to be verified. For businesses, verification must be done by Token after undertaking due diligence and granting permissions. For bank customers whose banks have integrated Token APIs, TokenOS verifies their email addresses by sending a message for the user to interact with.

If something goes wrong in this process, call retryVerification to re-send the mail.

If something goes wrong in this process, call `resendAliasVerification` to re-send the mail.

If something goes wrong in this process, call RetryVerification to re-send the mail.

Most of the newly created members’ data “lives” in the Token cloud. E.g., when creating the member, the SDK client uploads the newly-created public keys. However, the private keys remain in local storage and are not uploaded.

To learn more about setting up a Token Business Member, see our page Access Banks

This code uses the APIs:

Prerequisites

What to do next

Create a member

Alias alias = Alias.newBuilder()
        .setType(Alias.Type.EMAIL)
        .setValue(randomAlphabetic(10).toLowerCase()
                + "+noverify@example.com")
        .build();

Member newMember = tokenIO.createMember(alias);

// let user recover member by verifying email if they lose keys
newMember.useDefaultRecoveryRule();

const alias = Alias.create({
    type: 'EMAIL',
    value: 'alias-' + Token.Util.generateNonce() + '+noverify@example.com',
});

// Create a member with keys stored in memory:
const member = await Token.createMember(
    alias,
    Token.UnsecuredFileCryptoEngine);

// let user recover member by verifying email if they lose keys
await member.useDefaultRecoveryRule();
return member;
Alias *alias = [[Alias alloc] init];
// For this test user, we generate a random alias to make sure nobody else
// has claimed it. The "+noverify@" means Token automatically verifies this
// alias (only works in test environments).
alias.value = [[[@"alias-" stringByAppendingString:[TKUtil nonce]]
                stringByAppendingString:@"+noverify@token.io"]
               lowercaseString];
alias.type = Alias_Type_Email;
alias.realm = @"token";
[tokenIO createMember:alias onSuccess:^(TKMember *m) {
    newMember = m; // Use member.
} onError:^(NSError *e) {
    // Something went wrong.
    @throw [NSException exceptionWithName:@"CreateMemberFailedException"
                                   reason:[e localizedFailureReason]
                                 userInfo:[e userInfo]];
}];
var alias = new Alias
{
    Type = Email,
    Value = Util.Nonce() + "+noverify@example.com"
};

var newMember = tokenIO.CreateMember(alias);

// let user recover member by verifying email if they lose keys
newMember.UseDefaultRecoveryRule();

To make payments, a Token member uses a bank account. To use a bank account, a user normally must interact with the bank, perhaps authenticating to a bank web server. At some banks, a Token member can link bank accounts: the member demonstrates authority over some accounts and TokenOS “remembers” this. Later, the member can use the bank account (e.g., retrieve account balance, transfer funds) without a user interacting with the bank each time.

A consumer can use a Token-integrated bank website and the Token app to complete the Token linking process, as follows:

  1. The Token app displays a list of banks that are supported by Token.

  2. The user selects a bank, and the Token app pops up a web view and navigates to the bank linking page.

  3. The user enters their bank credentials and selects accounts to link.

  4. The bank linking flow finishes. The Token app extracts the encrypted account linking payload (the data contained in the request’s response) from the internal Token service to which it has been redirected by the linking flow.

The preferred method of linking real money bank accounts is to follow the Token app steps.

It is also possible to link accounts through the SDK. To simplify the process of navigating the user through the bank linking pages, we use the Browser interface, consisting of the following methods:

  • void goTo(URL url) to navigate to a URL
  • Observable<URL> url() to detect changes in the URL from the user clicking on a link or being redirected to a new page
  • void close() to close the browser

When an implementation of Browser is provided to TokenIO, the method initiateAccountLinking can be called to navigate the user through the bank linking pages in the browser and link accounts with a member. We provide two implementations of Browser, one wrapping an iOS WebView and the other wrapping an Android WebView.

There is also a Member unlinkAccounts method to remove previously-linked accounts.

For test purposes, the Token SDKs provide methods to link with a test bank account.

  • createAndLinkTestBankAccount creates an account in a fake bank and links the account to the Token member.
  • createTestBankAccount creates an account in a fake bank and returns a “bank authorization”. The bank authorization can be used with the method linkAccounts to link the account to the Token member.

Thes above calls work in test environments, but not in production.

This code uses the APIs:

    None. Account linking feature is currently not supported by the C# SDK.

Prerequisites

What to do next

Any of

  • Initiate a payment transfer on the user’s behalf; see Payments.
  • Get information about the user’s bank accounts; see Accounts.
  • Get information about the other users (with permission); see Sharing Account Information.

Create a test bank account and link it to a Token member

return member.createAndLinkTestBankAccount(1000.0, "EUR");
const auth = await member.createTestBankAccount(200, 'EUR');

// Links the account by sending the authorization.
const accounts = await member.linkAccounts(auth);
/* list of linked accounts */
[
{
  "id": "a:ENTBYaHyT8phyoB2rq3mTNT7odhtLfXuJDor4dBST2d7:5zKcENpV",
  "name": "Checking-f3cce4a7-7882-47ad-9633-001f62a73242",
  "bankId": "iron"
}
]
Money *balance = [Money message]; // test account's starting balance
balance.currency = @"EUR";
balance.value = @"5678.00";
[member createTestBankAccount:balance
                    onSuccess:^(OauthBankAuthorization* auth) {
    [member linkAccounts:auth.bankId
             accessToken:auth.accessToken
               onSuccess:^(NSArray<TKAccount*> * _Nonnull accounts) {
        // use accounts
        account = accounts[0];
    } onError:^(NSError * _Nonnull e) {
        // Something went wrong.
        @throw [NSException exceptionWithName:@"LinkAccountException"
                                       reason:[e localizedFailureReason]
                                     userInfo:[e userInfo]];
    }];
} onError:^(NSError * _Nonnull e) {
    // Something went wrong.
    @throw [NSException exceptionWithName:@"TestAccountException"
                                   reason:[e localizedFailureReason]
                                 userInfo:[e userInfo]];
}];
// Account linking feature is currently not supported by the C# SDK.

Load Existing Member

A Member MemberSync Member TKMember object represents a Token member with private keys (and thus the authority to act on behalf of that member). createMember returns this object. To use (“log in” as) an already-created member, call the SDK client’s getMember method. Assuming that the member has keys in the SDK’s keystore, the app can use the logged-in member’s API.

If the SDK does not have the member’s keys, it can (with the relevant user’s permission) get keys:

  • To “move” a member to a new device or app, use Recovery. Recovery of a member uses local keys (“log in”) only after they verify their identity; it un-verifies the member’s aliases and un-links the member’s bank accounts. This is useful, e.g., if a user gets a new mobile phone.

  • To “copy” a member to a new device or app, use Provisioning. Provisioning lets a member use local keys (“log in”) after prompting them to approve. It assumes the member still has a “main” set of keys elsewhere. It’s useful, e.g., for letting the user carry out low-privilege actions in a web browser while their privileged private keys stay on a secure mobile device.

The “log in” phrase, though handy, isn’t quite accurate. “Logging in” doesn’t start a session; there is no “log out” API. It just creates an object locally and tells that object which keys to use.

This code uses the API:

Prerequisites

  • Previously-created account

What to do next

Any of

  • Initiate a payment transfer on the user’s behalf; see Payments.
  • Get information about the user’s bank accounts; see Accounts.
  • Get information about the other users (with permission); see Sharing Account Information.

Use already-stored private key

return tokenIO.getMember(memberId);
member = Token.getMember(Token.UnsecuredFileCryptoEngine, mid);
[tokenIO getMember:memberId onSuccess:^(TKMember *m) {
    loggedInMember = m; // Use member.
} onError:^(NSError *e) {
    // Something went wrong.
    @throw [NSException exceptionWithName:@"LoginMemberFailedException"
                                   reason:[e localizedFailureReason]
                                 userInfo:[e userInfo]];
}];
return tokenIO.GetMember(memberId)

Provisioning

Provisioning lets a member use local keys after prompting them to approve. It assumes the member still has a “main” set of keys elsewhere. It’s useful, e.g., for letting the user carry out low-privilege actions in a web browser while their privileged private keys stay on a secure mobile device.

The steps are

  • Provision: Create keypair[s], tentatively associated with the member.
  • Notify: Send a notification to the member, prompting to approve (or decline) a public key.
  • GetMember Use the approved key.

The sample code here shows how to provision and request approval for a low-privilege key. To let the user carry out higher-privilege operations with this client, provision, getMember, and then request approval for higher-privilege keys.

This code uses the APIs:

Provision, Notify

DeviceInfo deviceInfo = tokenIO.provisionDevice(alias);
Key lowKey = deviceInfo.getKeys().stream().filter(k -> {
    return k.getLevel() == Key.Level.LOW;
}).findFirst().orElse(null);
// ask user (on "regular" device) to approve one of our keys
NotifyStatus status = tokenIO.notifyAddKey(
        alias,
        Collections.singletonList(lowKey),
        DeviceMetadata.newBuilder()
                .setApplication("SDK Sample")
                .build());
const deviceInfo = await Token.provisionDeviceLow(
    alias,
    Token.UnsecuredFileCryptoEngine);
const lowKey = deviceInfo.keys.filter(
    k => k.level === KeyLevel.LOW
)[0];
const deviceMetadata = DeviceMetadata.create({
    application: 'SDK Sample',
});
const notifyStatus = await Token.notifyAddKey(
    alias,
    [lowKey],
    deviceMetadata,
);
if (notifyStatus !== 'ACCEPTED') {
    console.log('notifyAddKey got ' + notifyStatus); // eslint-disable-line
}
[tokenIO provisionDevice:memberAlias
               onSuccess:^(DeviceInfo *di) {
                   for (Key* k in di.keys) {
                       if (k.level == Key_Level_Low) {
                           [tokenIO notifyAddKey:memberAlias
                                         keys:@[k]
                                  deviceMetadata:metadata
                                       onSuccess:^() {
                                           sentKey = k;
                                       } onError:^(NSError *e) {
                                           @throw [NSException exceptionWithName:@"NotifyFailedException"
                                                                          reason:[e localizedFailureReason]
                                                                        userInfo:[e userInfo]];
                                       }];
                           break;
                       }
                   }
               } onError:^(NSError *e) {
                   @throw [NSException exceptionWithName:@"ProvisionDeviceFailedException"
                                                  reason:[e localizedFailureReason]
                                                userInfo:[e userInfo]];
               }];
var deviceInfo = tokenIO.ProvisionDevice(alias);
var lowKey = deviceInfo.Keys
    .Where(k => k.Level.Equals(Low))
    .FirstOrDefault(null);
// ask user (on "regular" device) to approve one of our keys
var status = tokenIO.NotifyAddKey(alias, "SDK Sample", lowKey);

Use keys

String memberId = tokenIO.getMemberId(alias);
// Uses the key that remote member approved (we hope)
Member member = tokenIO.getMember(memberId);
const memberStruct = await Token.resolveAlias(alias);
const localLoggedIn = Token.getMember(
    Token.UnsecuredFileCryptoEngine,
    memberStruct.id);
[tokenIO getMemberId:memberAlias
           onSuccess:^(NSString *id) {
               [tokenIO getMember:id
                          onSuccess:^(TKMember *m) {
                              member = m;
                          } onError:^(NSError *e) {
                              @throw [NSException exceptionWithName:@"LoginFailedException"
                                                             reason:[e localizedFailureReason]
                                                           userInfo:[e userInfo]];
                          }];
           } onError:^(NSError *e) {
               @throw [NSException exceptionWithName:@"FindMemberFailedException"
                                              reason:[e localizedFailureReason]
                                            userInfo:[e userInfo]];
           }];
var memberId = tokenIO.GetMemberId(alias);
// Uses the key that remote member approved (we hope)
var member = tokenIO.GetMember(memberId);

Recovery

Recovery “moves” a member to a new device. It prompts the user to verify their identity through some process configured earlier. E.g., a user with a “normal consumer” member account might verify their identity by verifying ownership of their alias’ email address. As part of the recovery process, TokenOS un-links the member’s bank accounts and un-verifies the member’s aliases.

Members created in the Objective-C SDK are automatically configured with the “normal consumer” recovery process.

Members created in other SDKs don’t start with a configured recovery process. It’s possible to configure one.

The Javascript SDK does not have APIs to carry out the recovery process. It does have an API to set up a recovery rule. Thus, Members in JS programs can be recovered by other programs; but JS programs can’t recover members.

The recovery process has steps (some of which are handled automatically by the Objective-C SDK) (some of which are handled automatically in the default “normal consumer” recovery process, but needed in other recovery process) :

  • Generate privileged keypair The app generates a keypair. This can eventually become the recovered member’s privileged-level key.

  • Begin Recovery This returns a verification ID to the app. If the member has a “normal consumer” recovery rule, this also sends an email to the member’s alias email address; this email contains a short code.

  • Get Recovery Authorizations The client collects authorizations. If the member has a “normal consumer” recovery rule, the client needs the code emailed to the member’s email.

    If the client uses other/additional recovery agents, the client somehow fetches authorizations from those agents.

    If something goes wrong during this stage, call retryVerification to retrieve the verification Id.

  • Complete Recovery The client submits the private key, verification id, and collected authorizations. If the authorizations are OK, then TokenOS associates the member with the privileged key. The member on the client can then re-establish themselves: verify alias (calling verifyAlias passing in the same verification Id used for recovery), approve other keys, re-link bank accounts, etc.

The Objective-C SDK only supports the “normal consumer” recovery rule. It only supports Token’s “default” alias-verifying recovery rule. It does not support multiple agents.

As part of the recovery process, TokenOS deletes the member’s public keys from the Token Cloud and un-verifies all of the member’s aliases. The member’s new privileged key (created as part of the recovery process) is the member’s only working key until more are added. The member can verify the alias used for recovery by calling verifyAlias.

This code uses the APIs:

This code uses the APIs:

This code uses the APIs:

Beginning recovery, asking for a code to be sent:

[tokenIO beginMemberRecovery:self.payerAlias
                   onSuccess:^(NSString *verificationId_) {
                       // prompt user to enter code:
                       verificationId = verificationId_;
                       showPrompt(@"Enter code emailed to you:");
                   } onError:^(NSError *e) {
                       @throw [NSException exceptionWithName:@"BeginRecoveryFailedException"
                                                      reason:[e localizedFailureReason]
                                                    userInfo:[e userInfo]];
                   }];

Completing recovery using the entered code:

[tokenIO verifyMemberRecovery:self.payerAlias
                     memberId:self.payerSync.id
               verificationId:verificationId
                         code:userEnteredCode
                    onSuccess:^() {
                        [tokenIO completeMemberRecovery:self.payerAlias
                                               memberId:self.payerSync.id
                                         verificationId:verificationId
                                                   code:userEnteredCode
                                              onSuccess:^(TKMember *newMember) {
                                                  member = newMember;
                                              } onError:^(NSError *e) {
                                                  @throw [NSException exceptionWithName:@"CompleteRecoveryFailedException"
                                                                                 reason:[e localizedFailureReason]
                                                                               userInfo:[e userInfo]];
                                              }];
                    } onError:^(NSError *e) {
                        @throw [NSException exceptionWithName:@"VerifyRecCodeFailedException"
                                                       reason:[e localizedFailureReason]
                                                     userInfo:[e userInfo]];
                    }];

Beginning recovery for “normal consumer”, asking for a code to be sent:

String verificationId = tokenIO.beginRecovery(alias);

Completing recovery for “normal consumer” after they enter recovery code:

String memberId = tokenIO.getMemberId(alias);

// In the real world, we'd prompt the user to enter the code emailed to them.
// Since our test member uses an auto-verify email address, any string will work,
// so we use "1thru6".
Member recoveredMember = tokenIO.completeRecoveryWithDefaultRule(
        memberId,
        verificationId,
        "1thru6");
// We can use the same verification code to re-claim this alias.
recoveredMember.verifyAlias(verificationId, "1thru6");

Recovery process for a different recovery rule:

String memberId = tokenIO.getMemberId(alias);

CryptoEngine cryptoEngine = new TokenCryptoEngine(memberId, new InMemoryKeyStore());
Key newKey = cryptoEngine.generateKey(PRIVILEGED);

String verificationId = tokenIO.beginRecovery(alias);
Authorization authorization = tokenIO.createRecoveryAuthorization(memberId, newKey);

// ask recovery agent to verify that I really am this member
Signature agentSignature = getRecoveryAgentSignature(authorization);

// We have all the signed authorizations we need.
// (In this example, "all" is just one.)
MemberRecoveryOperation mro = MemberRecoveryOperation.newBuilder()
        .setAuthorization(authorization)
        .setAgentSignature(agentSignature)
        .build();
Member recoveredMember = tokenIO.completeRecovery(
        memberId,
        Arrays.asList(mro),
        newKey,
        cryptoEngine);
// after recovery, aliases aren't verified

// In the real world, we'd prompt the user to enter the code emailed to them.
// Since our test member uses an auto-verify email address, any string will work,
// so we use "1thru6".
recoveredMember.verifyAlias(verificationId, "1thru6");

Beginning recovery for “normal consumer”, asking for a code to be sent:

var verificationId = tokenIO.BeginRecovery(alias);

Completing recovery for “normal consumer” after they enter recovery code:

var memberId = tokenIO.GetMemberId(alias);

// In the real world, we'd prompt the user to enter the code emailed to them.
// Since our test member uses an auto-verify email address, any string will work,
// so we use "1thru6".
var recoveredMember = tokenIO.CompleteRecoveryWithDefaultRule(
    memberId,
    verificationId,
    "1thru6");
// We can use the same verification code to re-claim this alias.
recoveredMember.VerifyAlias(verificationId, "1thru6");

Recovery process for a different recovery rule:

var memberId = tokenIO.GetMemberId(alias);

var cryptoEngine = new TokenCryptoEngine(memberId, new InMemoryKeyStore());
var newKey = cryptoEngine.GenerateKey(Privileged);

var verificationId = tokenIO.BeginRecovery(alias);
var authorization = tokenIO.CreateRecoveryAuthorization(memberId, newKey);

// ask recovery agent to verify that I really am this member
var agentSignature = GetRecoveryAgentSignature(authorization);

// We have all the signed authorizations we need.
// (In this example, "all" is just one.)
var mro = new MemberRecoveryOperation
{
    AgentSignature = agentSignature,
    Authorization = authorization
};
var recoveredMember = tokenIO.CompleteRecovery(
    memberId,
    new List<MemberRecoveryOperation> {mro},
    newKey,
    cryptoEngine);
// after recovery, aliases aren't verified

// In the real world, we'd prompt the user to enter the code emailed to them.
// Since our test member uses an auto-verify email address, any string will work,
// so we use "1thru6".
recoveredMember.VerifyAlias(verificationId, "1thru6");

Payments

Making payments is one of the most fundamental operations in TokenOS and is designed in accordance with PSD2 requirements.

To make a payment, a transfer token is required to authorize the money transfer. For example, a merchant might request a transfer token to collect payment for an online purchase.

In this case, the merchant initiates the request for the transfer token, which is the “smart contract” between the merchant (payee) and the customer (payer). From the customer’s perspective, the terms of the contract are, “I, the paying member, allow a payment from my account #12345 at Iron Bank to pay €100 for online order #79262212. Payable to Online Merchant XYZ. Signed by Paying Member.”

The payer Token member (customer) accepts the terms of the contract by digitally signing (endorsing) the transfer token.

To initiate the actual transfer of funds, the payer or payee redeems the token. In the merchant sample code, the merchant member (payee) redeems the Token. In the case of moving money to a non-Token account, the payer redeems the Token. Other flows are possible; in any flow, there is a payer, who endorses; and a redeemer, who redeems.

Request Payment

The Token SDK notifyPaymentRequest method sends a notification to a member asking them to create a transfer token. (Notice that this is an SDK method, not a Member method.) Typically, this sends a notification to the member’s phone, where the Token client app prompts to create a transfer token.

In the payment request, the “from” and “to” specify the direction of the hoped-for payment. The payer is “from” and the payee is “to”. Don’t confuse this with the direction of the notification.

If you’re using an alias but don’t know its type (perhaps due to a UI that asks the user to enter a string like “address@example.com” but doesn’t have logic to recognize that as an email-alias), use an alias with UNKNOWN type and TokenOS will determine the type.

The refId lets the notifier attach an Id string that will be set on the eventually-created transfer token. If, unlike this sample code, your code has some “retry” logic, use the same refId for each try; TokenOS uses it to detect duplicates and support idempotent requests. This Id is opaque to Token; it uses the Id to detect duplicates, but does not expect any format in particular. If an organization uses Ids to keep track of payment requests, it might make sense to use those Ids here.

When testing notification code with members created just for that test, you might hit an issue: If the member doesn’t have a subscriber (configuration specifying how to deliver notifications), the notification fails. See Subscribing to Notifications to see how to “subscribe” a test user.

This code uses the API:

Prerequisites

  • Create a Token member and link a bank account; see Setup.

What to do next

Send notification requesting payment

TokenPayload paymentRequest = TokenPayload.newBuilder()
        .setDescription("Sample payment request")
        .setFrom(TokenMember.newBuilder()
                .setAlias(payerAlias))
        .setTo(TokenMember.newBuilder()
                .setAlias(payee.firstAlias()))
        .setTransfer(TransferBody.newBuilder()
                .setAmount("100.00")
                .setCurrency("EUR"))
        // if refID not set, the eventually-created
        // transfer token will have random refId:
        .setRefId(cartId)
        .build();

NotifyStatus status = tokenIO.notifyPaymentRequest(paymentRequest);
const paymentRequest = TokenPayload.create({
    description: 'Sample payment request',
    from: {
        alias: payerAlias,
    },
    to: {
        alias: payeeAlias,
    },
    transfer: {
        amount: '100',
        currency: 'EUR',
    },
    // if refID not set, the eventually-created
    // transfer token will have random refId:
    refId: cartId,
});
const status = await Token.notifyPaymentRequest(paymentRequest);
/* returned status is NotifyStatus enum as a string */
"ACCEPTED"
TokenPayload *payload = [TokenPayload message]; // hoped-for payment
payload.description_p = @"lunch";
payload.from.alias = payerAlias;
payload.to.id_p = payee.id;
payload.transfer.lifetimeAmount = @"100";
payload.transfer.currency = @"EUR";

[tokenIO notifyPaymentRequest:payload
                    onSuccess:^ {
                        // Notification sent.
                        waitingForPayment = true;
                    } onError:^(NSError *e) {
                        // Something went wrong.
                        // Maybe we used wrong alias?
                        @throw [NSException exceptionWithName:@"NotifyException"
                                                       reason:[e localizedFailureReason]
                                                     userInfo:[e userInfo]];
                    }
 ];
var paymentRequest = new TokenPayload
{
    Description = "Sample payment request",
    From = new TokenMember
    {
        Alias = payerAlias
    },
    To = new TokenMember
    {
        Alias = payee.FirstAlias()
    },
    Transfer = new TransferBody
    {
        Amount = "100.00",
        Currency = "EUR"
    },
    RefId = cartId
};

var status = tokenIO.NotifyPaymentRequest(paymentRequest);

Create Transfer Token

Creating a transfer token is the first step in the payments process. This transfer token specifies the terms such as amount, conditions, payer, and payee. Later, endorsing the transfer token authorizes the payment.

Transfer tokens can be created with many combinations of a token’s conditions:

  • amount
  • currency (specified by ISO 4217 codes)
  • payer (asset owner) account
  • payee
  • destination of payment
  • free-form description
  • reference Id

The refId lets the creator attach an Id string to the transfer token. If, unlike this sample code, your code has some “retry” logic, use the same refId for each try; TokenOS uses it to detect duplicates and support idempotent requests. This Id is opaque to Token; it uses the Id to detect duplicates, but does not expect any format in particular. If an organization uses Ids to keep track of payments, it might make sense to use those Ids here.

If you’re using an alias to specify the payee but don’t know the alias’ type (perhaps due to a UI that asks the user to enter a string like “address@example.com” but doesn’t have logic to recognize that as an email-alias), use an alias with UNKNOWN type and TokenOS will determine the type.

There might not be a payee member; a transfer token can omit that. The transfer token can specify a list of possible destination accounts, but it can omit it. It’s up to the redeemer to decide where to put the transfer’s funds; if the transfer token has a list of possible destinations, then the redeemer must choose one of those; if the transfer token doesn’t have such a list, then the redeemer can use any account.

Once created, the smart token “lives” in the Token cloud. createTransferToken creates the token and uploads it. Although the token’s core data doesn’t change, its metadata does change. If the source and/or destination bank has integrated with Token, the bank can propose fees and FX transfer rates. As members endorse (or cancel) the Token, that state lives in the cloud. Later, you’ll see how to get a token (with up-to-date endorsement/cancellation data) with getToken.

This code uses the API:

Prerequisites

  • Create a Token member and link a bank account; see Setup.
  • (Optional) Request payment; see Request payment.

What to do next

Any of

Create a transfer (payment) token

Token transferToken = payer.createTransferToken(
        100.0, // amount
        "EUR")  // currency
        // source account:
        .setAccountId(payer.getAccounts().get(0).id())
        // payee token alias:
        .setToAlias(payeeAlias)
        // optional description:
        .setDescription("Book purchase")
        // ref id (if not set, will get random ID)
        .setRefId(purchaseId)
        .execute();
const token = await payer.createTransferTokenBuilder(100.00, 'EUR')
    .setFromId(payer.memberId())
    .setAccountId(accounts[0].id)
    .setToAlias(payeeAlias)
// if not explicitly set, will get random refId:
    .setRefId(purchaseId)
    .execute();
NSDecimalNumber *amount = [NSDecimalNumber decimalNumberWithString:@"100.99"];
TransferTokenBuilder *builder = [payer createTransferToken:amount
                                                  currency:@"EUR"];
builder.accountId = payerAccount.id;
builder.toMemberId = payee.id;
builder.descr = @"Book purchase";
builder.refId = refId;

[builder executeAsync:^(Token *t) {
    // Use token.
    transferToken = t;
} onError:^(NSError *e) {
    // Something went wrong. (We don't just build a structure; we also
    // upload it to Token cloud. So things can go wrong.)
    @throw [NSException exceptionWithName:@"BuilderExecuteException"
                                   reason:[e localizedFailureReason]
                                 userInfo:[e userInfo]];
}];
var transferToken = payer.CreateTransferToken(
        100.0, // amount
        "EUR") // currency
    // source account:
    .SetAccountId(payer.GetAccounts()[0].Id())
    // payee token alias:
    .SetToAlias(payeeAlias)
    // optional description:
    .SetDescription("Book purchase")
    // ref id (if not set, will get random ID)
    .SetRefId(purchaseId)
    .Execute();

Endorse Transfer Token

To authorize moving money, a member endorses a transfer token.

Image showing transfer token creation

The diagram shows the endorsement process for transfer tokens. Steps 1 and 2 correspond to Token SDK calls; the remaining steps are internal Token processes.

  1. An end user creates the transfer token.
  2. The payer end user endorses (signs) the transfer token.
  3. Token cloud authenticates, signs, and persists the transfer token.
  4. Token cloud sends the user-and-Token-signed transfer token to the bank.
  5. Bank signs the transfer token.
  6. Bank returns the fully-signed transfer token (signed by the end user, the Token cloud, and the bank) to the end user.
  7. Transfer token’s ID is sent to the redeemer.

The user endorsing a token does not have to be the token’s creator. A merchant might create (but not endorse) a transfer token and send it to a customer as a request for payment. The customer endorses the token to authorize payment.

This code uses the API:

Prerequisites

What to do next

Any of

Endorse a transfer token

transferToken = payer.endorseToken(
        transferToken,
        Key.Level.STANDARD).getToken();
const result = await payer.endorseToken(token);
/* endorseToken result with token */
{
  "token": {
    "id": "ta:22DfY78YKwc5uStGjN9pZHwKicgWK7HFXFH2Mdhw46Ym:P2XFaVcFf",
    "payload": {
      "version": "1.0",
      "refId": "mxefinfguvnmt4yqausug14iqla7uvdxv6skcg43g1iwzh0k9",
      "issuer": {
        "id": "token",
        "username": "token"
      },
      "from": {
        "id": "m:3u7aXLqQYVKue83ZNUztEuXDtd6x:5zKtXEAq"
      },
      "to": {
        "id": "m:396CJ28UhEmXa26EwF9YEQU6tepB:5zKtXEAq",
        "username": "20kv7bm0syntpegybjits54s4ir1focgx7luaemg0lefa5g66r"
      },
      "access": {
        "resources": [
        {
          "allAccounts": {}
        }
        ]
      }
    },
    "payloadSignatures": [
    {
      "action": "ENDORSED",
      "signature": {
        "memberId": "m:3u7aXLqQYVKue83ZNUztEuXDtd6x:5zKtXEAq",
        "keyId": "qCn4z3-NBBl2PDmq",
        "signature": "eAmUkw8yplepTMin7kR5YFCcHbuGWjsGW5opja9T8A_wa_U3RDrbTE6fmf1kvu6g4AOBU-bxtlbtx16vmYDdCg"
      }
    },
    {
      "action": "ENDORSED",
      "signature": {
        "memberId": "token",
        "keyId": "1x7df4vuFUHYQCa7",
        "signature": "WWn17ZElBRHq327Eg4iaFoDU3rDCZGuduhadPw-MizK58fQLxhimDKGAZ7tbogFCFTJlyKflUUbLSlN3wau5CA"
      }
    }
    ]
  },
  "status": "SUCCESS"
}
[payer endorseToken:transferToken
            withKey:Key_Level_Standard
          onSuccess:^(TokenOperationResult *result) {
              // Update transferToken with newer value:
              // Payload is same; now has signatures attached.
              transferToken = result.token;
          } onError:^(NSError *e) {
              // something went wrong
              @throw [NSException exceptionWithName:@"EndorseException"
                                             reason:[e localizedFailureReason]
                                           userInfo:[e userInfo]];
          }];
transferToken = payer.EndorseToken(transferToken, Standard).Token;

Redeem Transfer Token

To initiate a payment request, a member redeems a transfer token. During the redemption process, the Token cloud verifies the transfer token’s integrity, confirms that the signatures (endorsements) are valid, and ensures that there are sufficient funds in the given account to satisfy the transfer token’s conditions. If all the checks pass, the Token cloud sends the payment request to the bank to perform the actual money transfer.

Image showing transfer token redemption

The diagram shows the redemption process for transfer tokens. Only step 1 is performed by calling the Token SDK; the remaining steps are internal Token processes.

  1. Redeemer redeems the transfer token.
  2. Token cloud verifies the transfer token’s conditions.
  3. Token cloud sends the funds transfer request to the payer’s bank.
  4. Money moves from the payer’s bank to the payee’s bank using legacy payment rails.
  5. Payer’s bank signs a confirmation that money movement was initiated (but cannot confirm whether the money was successfully transferred).
  6. Payer’s bank sends the signed confirmation to the redeemer.

The redeemer can specify a refId reference Id parameter when redeeming. This is different from the transfer token’s refId, though it is used in a similar way: to detect duplicates and support idempotent requests. This Id is opaque to Token; it uses the Id to detect duplicates, but does not expect any format in particular. If not explicitly set, the transfer has a random refId; do not assume it “inherits” the transfer token’s refId.

This code uses the APIs:

Prerequisites

What to do next

Any of

  • Redeem the token again.

  • Cancel the transfer token (see Cancel Transfer token). Typically, the asset owner (payer) is the party who cancels a transfer token, although it can be canceled by either party.

  • Take no action—this token’s workflow is finished.

Redeem a transfer (payment) token

Token transferToken = payee.getToken(tokenId);

// Payee redeems a transfer token.
// Money is transferred to a payee bank account.
Transfer transfer = payee.redeemToken(
        transferToken,
        Destinations.token(payee.memberId(), accountId),
        // if refId not set, transfer will have random refID:
        cartId);

const transferToken = await payee.getToken(tokenId);

// Destination for sending the funds
const destination = TransferEndpoint.create({
    account: {
        sepa: {
            iban: '123',
        },
    },
});

// Payer redeems the token, getting a transfer
const transfer = await payee.redeemToken(
    transferToken,
    5,
    'EUR',
    'lunch',
    [destination],
    cartId);
/* transfer returned by redeem */
{
  "id": "t:6WsTduE14eichtKa3tHyiZiVah39WJ8pHM7EtoW3sv6X:5zKcENpV",
  "transactionId": "6702bca956a44eee994822806560668b",
  "createdAtMs": "1502217232198",
  "payload": {
    "refId": "pni92yqt24se2sqjumsll3di116dm6xowhaons0jsrxvv4pldi",
    "tokenId": "tt:FT4kA1BauFNX6nv2ZnvN1yYV78oCsyodENm8XjqYkmUb:5zKcENpV",
    "amount": {
      "currency": "EUR",
      "value": "5"
    },
    "destinations": [
    {
      "account": {
        "sepa": {
          "iban": "123"
        }
      }
    }
    ],
    "description": "lunch"
  },
  "payloadSignatures": [
  {
    "memberId": "m:36XFB8Cx257rJb265Fa5FAkrXN7M:5zKtXEAq",
    "keyId": "rki_qdKsD9al2BQf",
    "signature": "ocGqWUqlAPzu8Vx3-aVFUsohA9X_yLdmHqmlLn0UCDJYPmafm3tHlzyC7XnQOGT_ZJk6DVhSZDp4ioQ49r9LDA"
  },
  {
    "memberId": "m:s4oZcPiiY3ppzd6PwH5TXprVZfo:5zKtXEAq",
    "keyId": "XxjMIqqSLit4ARKa",
    "signature": "JGFxUd5cdI5D6dJOa4twSPLyk-TqMUGms7koO4cynitYlttiDJ5Ggmo-9hgQEJFwXN3eNQvR9ATENWc9q0PjCg"
  }
  ],
  "status": "SUCCESS"
}
// There are a few ways to specify destination; here we use (fake) IBAN
TransferEndpoint *destination = [[TransferEndpoint alloc] init];
destination.account.sepa.iban = @"123";
[payee getToken:transferToken.id_p
      onSuccess:^(Token *token) {
          [payee redeemToken:token
                      amount:nil // use default
                    currency:nil // use default
                 description:nil // use default
                 destination:destination
                    onSuccess:^(Transfer *t) {
                        // use transfer
                        transfer = t;
                    } onError:^(NSError *e) {
                        // something went wrong
                        @throw [NSException exceptionWithName:@"RedeemException"
                                                       reason:[e localizedFailureReason]
                                                     userInfo:[e userInfo]];
                    }];
      } onError:^(NSError *e) {
          // something went wrong
          @throw [NSException exceptionWithName:@"GetTokenException"
                                         reason:[e localizedFailureReason]
                                       userInfo:[e userInfo]];
      }];
var transferToken = payee.GetToken(tokenId);

// Payee redeems a transfer token.
// Money is transferred to a payee bank account.
var transfer = payee.RedeemToken(
    transferToken,
    new TransferEndpoint
    {
        Account = new BankAccount
        {
            Token = new Token
            {
                MemberId = payee.MemberId(),
                AccountId = accountId
            }
        }
    },
    // if refId not set, transfer will have random refID:
    cartId);

Cancel Transfer Token

A transfer token’s payer or payee can cancel a transfer token anytime, which cancels any future payments that the transfer token authorized. Neither previous payments nor pending payments are reversed or canceled.

This code uses the APIs:

Prerequisites

What to do next

  • No actions. This token’s workflow is finished.

Cancel a transfer (payment) token

Token transferToken = grantor.getToken(tokenId);

// Cancel transfer token.
return grantor.cancelToken(transferToken);
const transferToken = await payer.getToken(tokenId);

// Payer cancels the token
return await payer.cancelToken(transferToken);
/* result with canceled token. It has both ENDORSED
and CANCELED payloadSignatures */
{
  "token": {
    "id": "tt:7ECKSivyfXzQzp8fAw9Z4aRkedPCKfPPuNH1MiprEmig:5zKcENpV",
    "payload": {
      "version": "1.0",
      "refId": "jh3y2pckfore57u4qp747k3xrqmix3i7kp9irtdtjmdixgk3xr",
      "issuer": {
        "username": "iron"
      },
      "from": {
        "id": "m:2YLns17mwhzbNYMofToaP34kFAYx:5zKtXEAq"
      },
      "expiresAtMs": "1502303565723",
      "transfer": {
        "redeemer": {
          "id": "m:2X16B4ZTwqejz9YC3so9JDkbXL6d:5zKtXEAq",
          "username": "iqzsaszle5shuscndt8wfusorrxj8xrvi5fs0l2q5fdb6e0zfr"
        },
        "instructions": {
          "source": {
            "account": {
              "token": {
                "memberId": "m:2YLns17mwhzbNYMofToaP34kFAYx:5zKtXEAq",
                "accountId": "a:GBTQ7DEbbKyo4wKarkkS3NaAMJiMbPnzf595dAELEaWW:5zKcENpV"
              }
            }
          }
        },
        "currency": "EUR",
        "lifetimeAmount": "100",
        "pricing": {
          "sourceQuote": {
            "id": "17aa3c61992f4061bdf04b7bb48eef33",
            "accountCurrency": "EUR",
            "feesTotal": "0.25",
            "fees": [
            {
              "amount": "0.17",
              "description": "Transaction Fee"
            },
            {
              "amount": "0.08",
              "description": "Initiation Fee"
            }
            ],
            "expiresAtMs": "1502303565723"
          },
          "instructions": {
            "feesPaidBy": "SHARED_FEE",
            "fxPerformedBy": "SHARED_FX"
          }
        }
      }
    },
    "payloadSignatures": [
    {
      "action": "ENDORSED",
      "signature": {
        "memberId": "m:2YLns17mwhzbNYMofToaP34kFAYx:5zKtXEAq",
        "keyId": "EVknJb1X_wqpyVc8",
        "signature": "1fO5tXvfB27P4_lYgerjxTyYX1q-n-TUdTmn0w-awEBuG0ml-6jEhkQFQxnDFTzGHlYVodIaggtxegsSV60lCg"
      }
    },
    {
      "action": "ENDORSED",
      "signature": {
        "memberId": "m:36XFB8Cx257rJb265Fa5FAkrXN7M:5zKtXEAq",
        "keyId": "rki_qdKsD9al2BQf",
        "signature": "EE3Zk5VF257RFKcWs0JxyrXnkOCXB3EagATzfOSA_q7eltqTq_mUIL-zP8jT2r-vmRPHd_3BBEVTQtxgFuhjDw"
      }
    },
    {
      "action": "CANCELLED",
      "signature": {
        "memberId": "m:2YLns17mwhzbNYMofToaP34kFAYx:5zKtXEAq",
        "keyId": "EVknJb1X_wqpyVc8",
        "signature": "drkAuAUhQx8m3enpmjyrrv7WSxDmZAPkWOE-AQbSIdyPjOcxKLXNTpKMZpoaRQjNsmAS7uBi9yr1VxnrrSXJDw"
      }
    },
    {
      "action": "CANCELLED",
      "signature": {
        "memberId": "m:36XFB8Cx257rJb265Fa5FAkrXN7M:5zKtXEAq",
        "keyId": "rki_qdKsD9al2BQf",
        "signature": "ZX6jpS6iax7DOydg1586Nb0SUu631ATolelWU28dcdAwqGlKW6P1fXHXmCF_sVf5Xqy0elLkda18T8Q88-QQDw"
      }
    }
    ]
  },
  "status": "SUCCESS"
}
[payer cancelToken:transferToken
         onSuccess:^(TokenOperationResult *result) {
             // token now has more signatures; in this case, at least
             // one is a cancellation signature
             transferToken = result.token;
         } onError: ^(NSError *e) {
             // Something went wrong
         }];
var transferToken = grantor.GetToken(tokenId);

// Cancel transfer token.
return grantor.CancelToken(transferToken);

Multi-Use Transfer Token

A member can redeem a transfer token more than once. You can specify a charge amount on a transfer token. The charge amount limits how much a member can redeem each time. For example, to set up a Transfer token for a year of monthly 10-Euro payments, the lifetime amount would be 120 Euros and the charge amount wold be 10 Euros.

This code uses the API: TransferTokenBuilder.setChargeAmount: set the charge (vs lifetime) amount

This code uses the API: TransferTokenBuilder.setChargeAmount: set the charge (vs lifetime) amount

This code uses the API: TransferTokenBuilder.chargeAmount

This code uses the API: TransferTokenBuilder.SetChargeAmount: set the charge (vs lifetime) amount

Multi-use: charge amount < lifetime amount

Token transferToken = payer.createTransferToken(120.00, "EUR")
        ...
    .setChargeAmount(10.0)
    .execute();
const token = await payer.createTransferToken(120.00, 'EUR')
        ...
    .setChargeAmount(10.0)
    .execute();
var transferToken = payer.CreateTransferToken(120.00, "EUR")
        ...
    .SetChargeAmount(10.0)
    .Execute();

TransferTokenBuilder *builder = [payer createTransferToken:120.00
                                                   currency:@"EUR"];
        ...
builder.chargeAmount = 20.00;
Token *token = [builder execute];

Transfer Token Options

You’ve seen the basic payment flow with a simple transfer Token. When creating a transfer token, you can set options on it to change its “rules” or attach information to it.

Purpose of Payment

Token transferToken = payer.createTransferToken(120.0, "EUR")
        ...
    .setPurposeOfPayment(PERSONAL_EXPENSES)
    .execute();
const token = await payer.createTransferToken(120.00, 'EUR')
        ...
    .setPurposeOfPayment('PERSONAL_EXPENSES')
    .execute();
TransferTokenBuilder *builder = [payer createTransferToken:120.00
                                                  currency:@"EUR"];
        ...
builder.purposeOfPayment = PurposeOfPayment_PersonalExpenses;
Token *token = [builder execute];
var transferToken = payer.CreateTransferToken(120.0, "EUR")
        ...
    .SetPurposeOfPayment(PERSONAL_EXPENSES)
    .Execute();

Effective-at Time, Expiration Time

Token transferToken = payer.createTransferToken(120.0, "EUR")
        ...
    // effective in one second:
    .setEffectiveAtMs(now + 1000)
    // expires in 300 seconds:
    .setExpiresAtMs(now + (300 * 1000))
    .execute();
const token = await payer.createTransferToken(120.00, 'EUR')
        ...
    // effective in one second:
    .setEffectiveAtMs(now + 1000)
    // expires in 300 seconds:
    .setExpiresAtMs(now + (300 * 1000))
    .execute();
TransferTokenBuilder *builder = [payer createTransferToken:120.00
                                                  currency:@"EUR"];
        ...
// effective in 1 second:
builder.effectiveAtMs = [[NSDate date] timeIntervalSince1970] * 1000.0 + 1000.0;
// expires in 300 seconds:
builder.expiresAtMs = [[NSDate date] timeIntervalSince1970] * 1000.0 + 300000.0;
Token *token = [builder execute];
var transferToken = payer.CreateTransferToken(120.0, "EUR")
        ...
    // effective in one second:
    .SetEffectiveAtMs(now + 1000)
    // expires in 300 seconds:
    .SetExpiresAtMs(now + (300 * 1000))
    .Execute();

Destination Account

In the Redeem Transfer Token example, you saw how the redeemer can specify a destination bank account for a transfer when redeeming. The redeemer doesn’t have to specify an account, though. When creating a transfer token, you can add destinations.

A transfer token payload has a list of destinations in priority order. When a bank determines where to transfer money, it considers the first destination first. If the bank can transfer money to that destination, it does so. If the bank can’t transfer money to that destination, it considers the next one, and so on.

When the redeemer specifies a destination, that destination is added at the end of the list.

This code uses the API:

Token transferToken = payer.createTransferToken(100.0, "EUR")
        ...
    .addDestination(
        Destinations.sepa("DE89 3704 0044 0532 0130 00"))
    .execute();
// Payer creates the token with the desired terms
const token = await payer.createTransferToken(100.00, 'EUR')
    .setAccountId(accounts[0].id)
    .setToAlias(payeeAlias)
    .addDestination({
        account: {
            sepa: {
                iban: 'DE89 3704 0044 0532 0130 00',
            },
        },
    })
    .execute();
TransferEndpoint *destination = [[TransferEndpoint alloc] init];
destination.account.token.accountId = payeeAccount.id;
destination.account.token.memberId = payee.id;
NSArray<TransferEndpoint *> *destinations = @[destination];

TransferTokenBuilder *builder = [payer createTransferToken:120.00
                                                  currency:@"EUR"];
        ...
builder.destinations = destinations;
Token *token = [builder execute];
var transferToken = payer.CreateTransferToken(100.0, "EUR")
        ...
    .AddDestination(
        new TransferEndpoint
        {
            Account = new BankAccount
            {
                Sepa = new Sepa
                {
                    Bic = "XUIWC2489",
                    Iban = "DE89 3704 0044 0532 0130 00"
                }
            }
        })
    .Execute();

Reference Id / Idempotency

Different financial institutions have different ways to keep track of transfers. The reference Id specifies an Id attached to the transfer token; the token creator might use this to associate the token with an item in some local database. This reference ID is opaque to Token; Token uses it to de-duplicate requests and support idempotent charge requests, but does not expect any particular string format.

This code uses the API: TransferTokenBuilder.setRefId: set the reference ID

This code uses the API: TransferTokenBuilder.setRefId: set the reference ID

This code uses the API: TransferTokenBuilder.refId: reference ID

This code uses the API: TransferTokenBuilder.SetRefId: set the reference ID

Token transferToken = payer.createTransferToken(120.0, "EUR")
        ...
    .setRefId("a713c8a61994a749")
    .execute();
const token = await payer.createTransferToken(120.00, 'EUR')
        ...
    .setRefId('a713c8a61994a749')
    .execute();
var transferToken = payer.CreateTransferToken(120.0, "EUR")
        ...
    .SetRefId("a713c8a61994a749")
    .Execute();
TransferTokenBuilder *builder = [payer createTransferToken:120.00
                                                  currency:@"EUR"];
        ...
builder.refId = @"purchase:2017-11-01:28293336394ffby";
Token *token = [builder execute];

Attachments

A user can provide an attachment, roughly equivalent to a file, with a token. For example, buyer might include an image of a scanned import permit with their payment; a service provider might provide an invoice image along with a request for payment.

When building a transfer token, you can add attachments. For each attachment, provide a file name, MIME type (e.g., image/jpeg), and contents. The Token SDK APIs have attachments and blobs. Here, an attachment might not have the “file” contents. To download the contents, you want the blob associated with that attachment.

The Token SDK code uploads the attachment to Token’s network along with the transfer token. TokenOS uses the token’s permissions to control access to the attachment. Only those who can access the token can access the attachment.

This code uses the APIs:

Token with an attachment

Attachment attachment = payer.createBlob(
        payeeId,
        "image/jpeg",
        "invoice.jpg",
        loadImageByteArray("invoice.jpg"),
        DEFAULT);

// Create a transfer token.
Token transferToken =
        payer.createTransferToken(100.0, "EUR")
                .setAccountId(payer.getAccounts().get(0).id())
                .setToMemberId(payeeId)
                .setDescription("Invoice payment")
                .addAttachment(attachment)
                .execute();
const attachment = await payer.createBlob(
    payer.memberId(),
    'image/jpeg',
    'invoice.jpg',
    getImageData('invoice.jpg'));

const token = await payer.createTransferToken(100.00, 'EUR')
    .setAccountId(accounts[0].id)
    .setToAlias(payeeAlias)
// attach reference to token:
    .addAttachment(attachment)
    .execute();
payer.createTransferToken(100.0, "EUR")
        .setAccountId(payer.getAccounts().get(0).id())
        .setToAlias(payeeAlias)
        .setDescription("Invoice payment")
        .addAttachment(
                payer.memberId(),
                "image/jpeg",
                "invoice.jpg",
                loadImageByteArray("invoice.jpg"))
        .execute();
const token = await payer.createTransferTokenBuilder(100.00, 'EUR')
    .setFromId(payer.memberId())
    .setAccountId(accounts[0].id)
    .setToAlias(payeeAlias)
    .addAttachmentData(
        payer.memberId(),
        'image/jpeg',
        'invoice.jpg',
        getImageData('invoice.jpg'))
    .execute();
[payer createBlob: payer.id
         withType: @"image/jpeg"
         withName: @"invoice.jpg"
         withData: loadImage(@"invoice.jpg")
        onSuccess: ^(Attachment *a) {
            NSDecimalNumber *amount = [NSDecimalNumber decimalNumberWithString:@"100.99"];
            TransferTokenBuilder *builder = [payer createTransferToken:amount
                                                              currency:@"EUR"];
            builder.accountId = payerAccount.id;
            builder.toMemberId = payee.id;
            builder.attachments = @[a]; // associate attachment with token

            [builder executeAsync:^(Token *t) {
                // TransferToken exists and has been uploaded.
                // Payee cannot see blob until payer endorses token (not shown here).
                transferToken = t;
            } onError:^(NSError *e) {
                @throw [NSException exceptionWithName:@"BuilderExecuteException"
                                               reason:[e localizedFailureReason]
                                             userInfo:[e userInfo]];
            }];
        } onError: ^(NSError *e) {
            // Something went wrong. (We don't create a blob; we also
            // upload it to Token cloud. So things can go wrong.)
            @throw [NSException exceptionWithName:@"CreateBlobException"
                                           reason:[e localizedFailureReason]
                                         userInfo:[e userInfo]];
        }];
var transferToken = payer.CreateTransferToken(
        100.0, // amount
        "EUR") // currency
    // source account:
    .SetAccountId(payer.GetAccounts()[0].Id())
    // payee token alias:
    .SetToAlias(payeeAlias)
    // optional description:
    .SetDescription("Book purchase")
    // ref id (if not set, will get random ID)
    .SetRefId(purchaseId)
    .Execute();
Token transferToken = payee.getToken(tokenId);

List<Attachment> attachments = transferToken
        .getPayload()
        .getTransfer()
        .getAttachmentsList();
for (Attachment attachment : attachments) {
    // Attachment has some metadata (name, type)
    // but not the "file" contents.
    if (attachment.getType().startsWith("image/")) {
        // Download the contents for the attachment[s]
        // we want:
        Blob blob = payee.getTokenBlob(tokenId, attachment.getBlobId());
        // Use the attachment data.
        showImage(
                blob.getPayload().getName(),  // "invoice.jpg"
                blob.getPayload().getType(),  // "image/jpeg"
                // byte[] of contents:
                blob.getPayload().getData().toByteArray());
    }
}
const transferToken = await payee.getToken(tokenId);

const allContents = [];

const transferBody = transferToken.payload.transfer;
for (let ix = 0; ix < transferBody.attachments.length; ix++) {
    // attachments have metadata but not the 'file' content
    const att = transferBody.attachments[ix];
    // download the content of the attachment[s] we want
    const blob = await payee.getTokenBlob(tokenId, att.blobId);
    const blobContents = blob.payload.data;
    allContents.push(blobContents);
}
/* Attachment: */
{
    "blobId": "b:4owPaRQh9xW1JJWt6qS9GyHjGEF8yk8uKjqVcXX8HWjd:5zKtXEAq",
    "type": "text",
    "name": "randomFile.txt"
  }

/* corresponding Blob (most data omitted here) */
{
    "id": "b:4owPaRQh9xW1JJWt6qS9GyHjGEF8yk8uKjqVcXX8HWjd:5zKtXEAq",
    "payload": {
      "ownerId": "m:46EuLaHeaQZS2Ymah4NWd4Hn4vpm:5zKtXEAq",
      "type": "text",
      "name": "randomFile.txt",
      "data": "pLtt0nnTv/E ...long base64 string... u6PTiIPDAMXA=="
    }
  }
[payee getToken:tokenId
      onSuccess:^(Token *t) {
          // Token comes with attachments: the metadata for a blob
          // (MIME type, etc). To download the blob's "file" contents:
          [payee getTokenBlob:t.id_p
                   withBlobId:t.payload.transfer.attachmentsArray[0].blobId
                    onSuccess:^(Blob *b) {
                        // use data from blob:
                        displayImage(b.payload.data);
                    } onError:^(NSError *e) {
                        // Something went wrong.
                        @throw [NSException exceptionWithName:@"GetBlobException"
                                                       reason:[e localizedFailureReason]
                                                     userInfo:[e userInfo]];
                    }];
      } onError:^(NSError *e) {
          // Something went wrong.
          @throw [NSException exceptionWithName:@"GetTokenException"
                                         reason:[e localizedFailureReason]
                                       userInfo:[e userInfo]];
      }];
var transferToken = payee.GetToken(tokenId);

var attachments = transferToken
    .Payload
    .Transfer
    .Attachments;
foreach (var attachment in attachments)
{
    // Attachment has some metadata (name, type)
    // but not the "file" contents.
    if (attachment.Type.StartsWith("image/"))
    {
        // Download the contents for the attachment[s]
        // we want:
        var blob = payee.GetTokenBlob(tokenId, attachment.BlobId);
        // Use the attachment data.
        ShowImage(
            blob.Payload.Name, // "invoice.jpg"
            blob.Payload.Type, // "image/jpeg"
            // byte[] of contents:
            blob.Payload.Data.ToByteArray());
    }
}

Accounts

A member can have some linked bank accounts. An app can get information about the state of a member’s bank accounts. A logged-in member can access its own linked accounts. Later, you will learn how one member can access another member’s account information by means of an access token.

Get Accounts

Get Accounts

List<Account> accounts = member.getAccounts();
for (Account account : accounts) {
    Money balance = member.getCurrentBalance(account.id(), STANDARD);
    sums.put(
            balance.getCurrency(),
            Double.parseDouble(balance.getValue())
                    + sums.getOrDefault(
                    balance.getCurrency(), 0.0));
}
const accounts = await member.getAccounts();
for (let i = 0; i < accounts.length; i++) {
    const balanceResponse = await member.getBalance(accounts[i].id, config.KeyLevel.STANDARD);
    const currency = balanceResponse.balance.available.currency;
    sums[currency] = (sums[currency] || 0) +
        parseFloat(balanceResponse.balance.available.value);
}
/* list of accounts from getAccounts */
[
    {
      "id": "a:CwXU68QcNJABqckmPMxxqCA3NFYmRKEHjygFg5n9Qnng:5zKcENpV",
      "name": "Checking-d8672606-0143-42a4-a33b-d9dd1dbed5be",
      "bankId": "iron"
    }
]
[member getAccounts:^(NSArray<TKAccount *> *accounts) {
    for (TKAccount *a in accounts) {
        [a getBalance:^(TKBalance *b) {
            sums[b.available.currency] = @([sums[b.available.currency] floatValue] + [b.available.value floatValue]);
        } onError:^(NSError *e) {
            // Something went wrong.
            @throw [NSException exceptionWithName:@"GetBalanceException"
                                           reason:[e localizedFailureReason]
                                         userInfo:[e userInfo]];
        }];
    }
} onError: ^(NSError *e) {
    // Something went wrong.
    @throw [NSException exceptionWithName:@"GetAccountsException"
                                   reason:[e localizedFailureReason]
                                 userInfo:[e userInfo]];
}];
var accounts = member.GetAccounts();

Get Account Balance

Account Balances

List<Account> accounts = member.getAccounts();
for (Account account : accounts) {
    Money balance = member.getCurrentBalance(account.id(), STANDARD);
    sums.put(
            balance.getCurrency(),
            Double.parseDouble(balance.getValue())
                    + sums.getOrDefault(
                    balance.getCurrency(), 0.0));
}
const accounts = await member.getAccounts();
for (let i = 0; i < accounts.length; i++) {
    const balanceResponse = await member.getBalance(accounts[i].id, config.KeyLevel.STANDARD);
    const currency = balanceResponse.balance.available.currency;
    sums[currency] = (sums[currency] || 0) +
        parseFloat(balanceResponse.balance.available.value);
}
/* returned balance: */
{
    "current": {
      "currency": "EUR",
      "value": "200.0000"
    },
    "available": {
      "currency": "EUR",
      "value": "200.0000"
    }
  }
[member getAccounts:^(NSArray<TKAccount *> *accounts) {
    for (TKAccount *a in accounts) {
        [a getBalance:^(TKBalance *b) {
            sums[b.available.currency] = @([sums[b.available.currency] floatValue] + [b.available.value floatValue]);
        } onError:^(NSError *e) {
            // Something went wrong.
            @throw [NSException exceptionWithName:@"GetBalanceException"
                                           reason:[e localizedFailureReason]
                                         userInfo:[e userInfo]];
        }];
    }
} onError: ^(NSError *e) {
    // Something went wrong.
    @throw [NSException exceptionWithName:@"GetAccountsException"
                                   reason:[e localizedFailureReason]
                                 userInfo:[e userInfo]];
}];
var accounts = member.GetAccounts();
foreach (var account in accounts)
{
    var balance = member.GetCurrentBalance(account.Id(), Standard);
    if (sums.ContainsKey(balance.Currency))
    {
        sums[balance.Currency] += Convert.ToDouble(balance.Value);
    }
    else
    {
        sums[balance.Currency] = Convert.ToDouble(balance.Value);
    }
}

Get Transactions

The Member get-Transactions method gets a list of transactions for an account. These transactions come from the bank associated with the account; when an app gets transactions, TokenOS relays the request to the bank. This API fetches a “page” of results at a time. You can specify the offset at which to start fetching and how many items to fetch.

If you want only one transaction and you know its ID, use the Member getTransaction method to fetch it.

A transaction has a status, which can change. For example, when first fetched a transaction might have PROCESSING status, but have SUCCESS status when fetched again.

The Account class has getTransactions and getTransaction methods that get the same information as the Member methods; the Account methods don’t take an account ID parameter.

The TKAccount class has getTransactionsOffset and getTransaction methods that get the same information as the TKMember methods; the TKAccount methods don’t take an account ID parameter.

The AccountSync class has GetTransactions and GetTransaction methods that get the same information as the MemberSync methods; the AccountSync methods don’t take an account ID parameter.

This code uses the APIs:

Get Transactions

String accountId = accounts.get(0).id();
for (Transaction transaction :
        payer.getTransactions(accountId, null, 10, STANDARD).getList()) {
    displayTransaction(
            transaction.getAmount().getCurrency(),
            transaction.getAmount().getValue(),
            transaction.getType(), // debit or credit
            transaction.getStatus());
}
String transactionId = transfer.getTransactionId();
Transaction transaction = payer.getTransaction(accountId, transactionId, STANDARD);
const pagedResult = await payer.getTransactions(
    accountId,
    '',
    10,
    config.KeyLevel.STANDARD);
return pagedResult.data;
const transactionId = transfer.transactionId;
const transaction = await payer.getTransaction(
    accountId,
    transactionId,
/* paged list of transactions */
{
    "transactions": [
      {
        "id": "c040fd209bf849408d14dcd7c7670e25",
        "type": "DEBIT",
        "status": "SUCCESS",
        "amount": {
          "currency": "EUR",
          "value": "5.0000"
        },
        "description": "lunch",
        "tokenId": "tt:tK79P3TQ6HBJkBkWoUn1NkLjb72v6bQcWfL7vQTd95S:5zKcENpV",
        "tokenTransferId": "t:GP8fUsSRb8z7buvstio9HdF54DHY7QKzeWwBdMJckdt2:5zKcENpV"
      }
    ],
    "offset": "oDyYrW19Capds1ZWghbyY8A8xD42gEvZvVywmSsV1hPo5rcNS1f3GD"
}
[payer getTransactionsOffset:NULL // NULL: get first "page" of results
                       limit:10
                  forAccount:payerAccount.id
                     withKey:Key_Level_Low
                   onSuccess:^(PagedArray<Transaction *> *ary) {
                       for (Transaction *tr in ary.items) {
                           // use transactions
                           displayMoney(tr.amount.currency, tr.amount.value);
                       }
                   } onError:^(NSError *e) {
                       // Something went wrong.
                       @throw [NSException exceptionWithName:@"GetTransactionsException"
                                                      reason:[e localizedFailureReason]
                                                    userInfo:[e userInfo]];
                   }];
var accountId = accounts[0].Id();
foreach (var transaction in payer.GetTransactions(accountId, null, 10, Standard).List)
{
    DisplayTransaction(
        transaction.Amount.Currency,
        transaction.Amount.Value,
        transaction.Type, // debit or credit
        transaction.Status);
}
var transactionId = transfer.TransactionId;

Get Account from ID

A member can get an Account for one of their accounts by calling getAccount on that account’s ID. This gets a structure with the account’s bank ID and name.

This code uses the API: Member.getAccount: get one account by ID

This code uses the API: Member.getAccount: get one account by ID

This code uses the API: TKMember getAccount:onSuccess:onError: get one account by ID

This code uses the API: MemberSync.GetAccount: get one account by ID

Get One Account

Account account = payer.getAccount(accountId);
const account = await payer.getAccount(accountId);
/* returned account */
{
    "id": "a:GcJ2xSrEbv1mFehcAofi8TKH4USQQnx8G9fYcFQYkivY:5zKcENpV",
    "name": "Checking-95fdfb33-2a05-4583-861f-f897bcbbfacd",
    "bankId": "iron"
}
[payer getAccount:accountId
        onSuccess:^(TKAccount *a) { ... }
          onError:^(NSError *e) { ... }];
var account = payer.GetAccount(accountId);

Get Tokens

Get Tokens

for (Token token : payer.getTransferTokens(null, 10)
        .getList()) {
    TransferBody transferBody = token.getPayload().getTransfer();
    displayTransferToken(
            transferBody.getCurrency(),
            transferBody.getLifetimeAmount());
}
const pagedResult = await payer.getTransfers('', '', 10);
return pagedResult.data;
const pagedResult = await payer.getTransferTokens('', 10);
return pagedResult.data;
/* paged list of transfers */
{
  "transfers": [
    {
      "id": "t:5y5iERaFokPus4ZYhEaqNURWS9LFRUpfPPM8kReerT8q:5zKcENpV",
      "transactionId": "525727b136d448d197d9dd3ec2694d3c",
      "createdAtMs": "1502217213312",
      "payload": {
        "refId": "3uo5og17rz4zclu1rafpnl8frymo9ir5tpmttvhyssugdxtj4i",
        "tokenId": "tt:GH7zJANLCZebA9TpA9YACU6qdaArWQHqTqcwg1LPH8ZU:5zKcENpV",
        "amount": {
          "currency": "EUR",
          "value": "5"
        },
        "destinations": [
          {
            "account": {
              "sepa": {
                "iban": "123"
              }
            }
          }
        ],
        "description": "lunch"
      },
      "payloadSignatures": [
        {
          "memberId": "m:2fSwQUjTwWWCtJqgGvMQghFyr2Z4:5zKtXEAq",
          "keyId": "PcYr6-8UqokzzwHX",
          "signature": "wh_ZOQ4ivXYlISL0kIsEJaDGvfKTXGsU4tjfDl1Z8Jg15eUF-uvvQqkCUZu3wNhomXXLc22C7X6qYPlirzuwDA"
        },
        {
          "memberId": "m:36XFB8Cx257rJb265Fa5FAkrXN7M:5zKtXEAq",
          "keyId": "rki_qdKsD9al2BQf",
          "signature": "UNhuuKNAolsIW0a4hJsetuAHgiGai8taKklHn_5t4IzTXqVin9s8aQkAZdKNfMu9InR8O9EeExCJBaAN0FPYBg"
        }
      ],
      "status": "SUCCESS"
    }
  ],
  "offset": "2ysouReAu3N7BVZ6GGWqrR"
}

/* paged list of transfer tokens */
{
  "tokens": [
    {
      "id": "tt:2nve9FjkxQ7a9ji2HXWZbV4wGs7nnXiwG19M61MvqYEc:5zKcENpV",
      "payload": {
        "version": "1.0",
        "refId": "a0itn8ftx86clxd1718i5uq5mis943myvi1d5umon7vz1hpk3xr",
        "issuer": {
          "username": "iron"
        },
        "from": {
          "id": "m:3c49aaaeSwXcz6MDNpjiGx9ZEjBo:5zKtXEAq"
        },
        "expiresAtMs": "1502303621079",
        "transfer": {
          "redeemer": {
            "id": "m:4EF5TAjqRNxMQsUup3eQDWBBeQpN:5zKtXEAq",
            "username": "m4vv5zn8h2cxsj7v7pzdj9k9lrsyi8h5tz2euiih6epjmunmi"
          },
          "instructions": {
            "source": {
              "account": {
                "token": {
                  "memberId": "m:3c49aaaeSwXcz6MDNpjiGx9ZEjBo:5zKtXEAq",
                  "accountId": "a:DoeGcPQgFZ9bHg3qKMppKx1jfZXXaGDNGnApLfBSmteQ:5zKcENpV"
                }
              }
            }
          },
          "currency": "EUR",
          "lifetimeAmount": "100",
          "pricing": {
            "sourceQuote": {
              "id": "15bd02568f3f477ebf71e9b8d9f891e9",
              "accountCurrency": "EUR",
              "feesTotal": "0.25",
              "fees": [
                {
                  "amount": "0.17",
                  "description": "Transaction Fee"
                },
                {
                  "amount": "0.08",
                  "description": "Initiation Fee"
                }
              ],
              "expiresAtMs": "1502303621079"
            },
            "instructions": {
              "feesPaidBy": "SHARED_FEE",
              "fxPerformedBy": "SHARED_FX"
            }
          }
        }
      },
      "payloadSignatures": [
        {
          "action": "ENDORSED",
          "signature": {
            "memberId": "m:36XFB8Cx257rJb265Fa5FAkrXN7M:5zKtXEAq",
            "keyId": "rki_qdKsD9al2BQf",
            "signature": "oF7kE0eC7N78i1IDjrvQtunts_3JLYjA461VH5QGysA9tamnPOTJFXxL7R3v3sbMs5LqbC7fTE59vrxVixqXCw"
          }
        },
        {
          "action": "ENDORSED",
          "signature": {
            "memberId": "m:3c49aaaeSwXcz6MDNpjiGx9ZEjBo:5zKtXEAq",
            "keyId": "oy1gfHXIJExNYeEV",
            "signature": "e11sYoOJnf-6RJi8Hs-ASLNYcQ1hFnQMXO_IT-mUqdQHevOfH1X2Nfo3nIYaN3g8vcmObnSd82-ZQ-UqjssIBA"
          }
        }
      ]
    }
  ],
  "offset": "2ysouReAu3N7BVZ6GGWqrR"
}

[payer getTransferTokensOffset:NULL // NULL: get first "page" of results
                         limit:10
                     onSuccess:^(PagedArray<Token*> *ary) {
                         for (Token *tt in ary.items) {
                             // use the tokens
                             displayMoney(tt.payload.transfer.currency, tt.payload.transfer.amount);
                         }
                     } onError:^(NSError *e) {
                         // Something went wrong.
                         @throw [NSException exceptionWithName:@"GetTransferTokensException"
                                                        reason:[e localizedFailureReason]
                                                      userInfo:[e userInfo]];
                     }];
foreach (var token in payer.GetTransferTokens(null, 10).List)
{
    var transferBody = token.Payload.Transfer;
    DisplayTransferToken(
        transferBody.Currency,
        transferBody.LifetimeAmount);
}

Get Transfers

Get Transfers

for (Transfer transfer :
        payer.getTransfers(null, 10, null).getList()) {
    displayTransfer(
            transfer.getStatus(),
            transfer.getPayload().getDescription());
}
const pagedResult = await payer.getTransfers('', '', 10);
return pagedResult.data;
/* paged list of transfers */
{
  "transfers": [
    {
      "id": "t:5y5iERaFokPus4ZYhEaqNURWS9LFRUpfPPM8kReerT8q:5zKcENpV",
      "transactionId": "525727b136d448d197d9dd3ec2694d3c",
      "createdAtMs": "1502217213312",
      "payload": {
        "refId": "3uo5og17rz4zclu1rafpnl8frymo9ir5tpmttvhyssugdxtj4i",
        "tokenId": "tt:GH7zJANLCZebA9TpA9YACU6qdaArWQHqTqcwg1LPH8ZU:5zKcENpV",
        "amount": {
          "currency": "EUR",
          "value": "5"
        },
        "destinations": [
          {
            "account": {
              "sepa": {
                "iban": "123"
              }
            }
          }
        ],
        "description": "lunch"
      },
      "payloadSignatures": [
        {
          "memberId": "m:2fSwQUjTwWWCtJqgGvMQghFyr2Z4:5zKtXEAq",
          "keyId": "PcYr6-8UqokzzwHX",
          "signature": "wh_ZOQ4ivXYlISL0kIsEJaDGvfKTXGsU4tjfDl1Z8Jg15eUF-uvvQqkCUZu3wNhomXXLc22C7X6qYPlirzuwDA"
        },
        {
          "memberId": "m:36XFB8Cx257rJb265Fa5FAkrXN7M:5zKtXEAq",
          "keyId": "rki_qdKsD9al2BQf",
          "signature": "UNhuuKNAolsIW0a4hJsetuAHgiGai8taKklHn_5t4IzTXqVin9s8aQkAZdKNfMu9InR8O9EeExCJBaAN0FPYBg"
        }
      ],
      "status": "SUCCESS"
    }
  ],
  "offset": "2ysouReAu3N7BVZ6GGWqrR"
}
[payer getTransfersOffset:NULL // NULL: get first "page" of results
                    limit:10
                  tokenId:NULL // NULL: don't filter by token
                onSuccess:^(PagedArray<Transfer*> *ary) {
                    for (Transfer *t in ary.items) {
                        // use the transfers
                        displayMoney(t.payload.amount.currency, t.payload.amount.value);
                    }
                } onError:^(NSError *e) {
                    // Something went wrong.
                    @throw [NSException exceptionWithName:@"GetTransfersException"
                                                   reason:[e localizedFailureReason]
                                                 userInfo:[e userInfo]];
                }];
foreach (var transfer in payer.GetTransfers(null, 10, null).List)
{
    DisplayTransfer(
        transfer.Status,
        transfer.Payload.Description);
}

Sharing Account Information

Token members can allow other Token members to access their normally private data/information or to perform operations on their behalf. This supports information retrieval as defined by PSD2.

Access tokens provide the required authorization. For example, a Token member might allow an AISP to access the transaction history of one or more bank accounts. The resultant access token might be specified as, “This AISP can access my checking account at Iron Bank until July 31, 2017 to retrieve balance and transaction history under this account. Signed Granting Member.”

Access tokens are multi-use; that is, they authorize access an unlimited number of times, until they are canceled or until they expire (90 days).

Create Access Token

Creating an access token is the first step in the information retrieval process.

If you’re using an alias to specify the grantee but don’t know the alias’ type (perhaps due to a UI that asks the user to enter a string like “address@example.com” but doesn’t have logic to recognize that as an email-alias), use an alias with UNKNOWN type and TokenOS will determine the type.

Once created, the smart token “lives” in the Token cloud. createAccessToken creates the token and uploads it. Although the token’s core data doesn’t change, as members endorse (or cancel) the Token, that state lives in the cloud. Later, you’ll see how to get a token (with up-to-date endorsement/cancellation data) with getToken.

This code uses the API:

Prerequisites

What to do next

Any of

Create an access (information) token

Token accessToken = grantor.createAccessToken(
        AccessTokenBuilder
                .create(granteeAlias)
                .forAccount(accountId)
                .forAccountBalances(accountId));
const token = await grantor.createAccessToken(
    granteeAlias,
    [
        Resource.create({allAccounts: {}}), // user can call getAccounts
        Resource.create({allBalances: {}}), // for each account, can getBalance
    ]);
AccessTokenConfig *access = [AccessTokenConfig createWithToId:self.payeeSync.id];
[access forAllAccounts];
[access forAllBalances];

[grantor createAccessToken:access
                 onSuccess:^(Token *at) {
                     // created (and uploaded) but not yet endorsed
                     accessToken = at;
                 } onError:^(NSError *e) {
                     // Something went wrong.
                     @throw [NSException exceptionWithName:@"GrantAccessException"
                                                    reason:[e localizedFailureReason]
                                                  userInfo:[e userInfo]];
                 }];
var accessToken = grantor.CreateAccessToken(AccessTokenBuilder
    .Create(granteeAlias)
    .ForAllAccounts() // user can call getAccounts()
    .ForAllBalances() // for each account, can call getBalance()
    .Build());

Endorse Access Token

Endorsing an access token is the process of digitally signing it.

Image showing access token creation

The diagram shows the endorsement process for access tokens. Steps 1 and 2 correspond to Token SDK calls; the remaining steps are internal Token processes.

  1. End user creates the access token.
  2. End user endorses (signs) the access token.
  3. Token cloud authenticates, signs, and persists the access token.
  4. Token cloud returns the fully-signed access token (signed by the end user and the Token cloud) to the end user.
  5. Access token’s ID is sent to the redeemer.

This code uses the API:

Prerequisites

What to do next

Any of

Endorse an access token

accessToken = grantor.endorseToken(
        accessToken,
        Key.Level.STANDARD).getToken();
const result = await grantor.endorseToken(token);
/* result with endorsed access token */
{
    "token": {
      "id": "ta:22DfY78YKwc5uStGjN9pZHwKicgWK7HFXFH2Mdhw46Ym:P2XFaVcFf",
      "payload": {
        "version": "1.0",
        "refId": "mxefinfguvnmt4yqausug14iqla7uvdxv6skcg43g1iwzh0k9",
        "issuer": {
          "id": "token",
          "username": "token"
        },
        "from": {
          "id": "m:3u7aXLqQYVKue83ZNUztEuXDtd6x:5zKtXEAq"
        },
        "to": {
          "id": "m:396CJ28UhEmXa26EwF9YEQU6tepB:5zKtXEAq",
          "username": "20kv7bm0syntpegybjits54s4ir1focgx7luaemg0lefa5g66r"
        },
        "access": {
          "resources": [
            {
              "allAccounts": {}
            }
          ]
        }
      },
      "payloadSignatures": [
        {
          "action": "ENDORSED",
          "signature": {
            "memberId": "m:3u7aXLqQYVKue83ZNUztEuXDtd6x:5zKtXEAq",
            "keyId": "qCn4z3-NBBl2PDmq",
            "signature": "eAmUkw8yplepTMin7kR5YFCcHbuGWjsGW5opja9T8A_wa_U3RDrbTE6fmf1kvu6g4AOBU-bxtlbtx16vmYDdCg"
          }
        },
        {
          "action": "ENDORSED",
          "signature": {
            "memberId": "token",
            "keyId": "1x7df4vuFUHYQCa7",
            "signature": "WWn17ZElBRHq327Eg4iaFoDU3rDCZGuduhadPw-MizK58fQLxhimDKGAZ7tbogFCFTJlyKflUUbLSlN3wau5CA"
          }
        }
      ]
    },
    "status": "SUCCESS"
  }
[grantor endorseToken:accessToken
              withKey:Key_Level_Standard
            onSuccess:^(TokenOperationResult *result) {
                accessToken = result.token;
            } onError:^(NSError *e) {
                // Something went wrong.
                @throw [NSException exceptionWithName:@"EndorseAccessException"
                                               reason:[e localizedFailureReason]
                                             userInfo:[e userInfo]];
            }];
accessToken = grantor.EndorseToken(accessToken, Standard).Token;

Redeem Access Token

To retrieve information or perform an operation on a Token member’s behalf, you perform a series of operations that together make up the redemption process for access tokens. (This is different from redeeming a transfer token, which requires just a single SDK call.) During the access token redemption process, the Token cloud verifies the access token’s integrity, confirms that the signatures (endorsements) are valid, and checks for expiration (90 days). If all the checks pass, the Token cloud provides the requested information or performs the specified operation.

Because access tokens are multi-use, the redemption process can be repeated indefinitely until the token expires or is canceled.

A typical operation sequence would be for an AISP to call forAccessToken which creates a Representable that acts as the grantor, perform a getAccounts or getAccount operation to obtain the grantor’s account(s) and finally get information about those accounts.

Image showing access token redemption

The diagram shows the redemption process for access tokens. Only step 1 is performed by calling the Token SDK; the remaining steps are internal Token processes.

In this example, the action in step 4 is to get the balance of the grantor’s bank account. For other actions, the remaining steps might be different.

  1. Redeemer uses (applies) the access token to create a Representable that acts as the grantor.
  2. Redeemer, acting as the grantor, performs an action (such as retrieving an account balance) by making a Token SDK call.
  3. Token cloud verifies the access token’s conditions.
  4. Token cloud sends the request for the action to the appropriate Token system participant.
  5. Grantor’s bank signs the response that contains the balance information.
  6. Grantor’s bank sends the signed response to the redeemer.

This code uses the APIs:

Prerequisites

What to do next

Any of

  • To prevent future access or operations, cancel the access token; see Cancel Access Token.
  • Otherwise, this token’s workflow is finished.

Redeem an access token

Representable grantor = grantee.forAccessToken(tokenId, customerInitiated);
List<Account> accounts = grantor.getAccounts();

// Get the data we want
Money balance0 = accounts.get(0).getCurrentBalance(STANDARD);
const grantor = grantee.forAccessToken(tokenId);
const accounts = await grantor.getAccounts();

// Get information we want:
const balance0 = await grantor.getBalance(accounts[0].id, config.KeyLevel.LOW);
/* List of accounts from getAccounts: */
[
    {
      "id": "a:B3EphxmCi9VfcJevZ8Ad9PLMf41onD6ENEp6aHBHFito:5zKcENpV",
      "name": "Checking-714790e2-220a-4017-830c-316cefeff371",
      "bankId": "iron"
    }
]
id<TKRepresentable> representable = [grantee forAccessToken:accessTokenId]; // future requests will behave as if we were grantor
[representable getAccounts:^(NSArray <TKAccount *> *ary) {
    // use accounts
    [ary[0] getBalance:^(TKBalance * b) {
        balance0 = b.current;
    } onError:^(NSError *e) {
        @throw [NSException exceptionWithName:@"AccessBalanceException"
                                       reason:[e localizedFailureReason]
                                     userInfo:[e userInfo]];
    }];
} onError:^(NSError *e) {
    @throw [NSException exceptionWithName:@"UseAccessException"
                                   reason:[e localizedFailureReason]
                                 userInfo:[e userInfo]];
}];
var grantor = grantee.ForAccessToken(tokenId, customerInitiated);
var accounts = grantor.GetAccounts();

// Get the data we want
var balance0 = accounts[0].GetCurrentBalance(Standard);

Cancel Access Token

An access token’s grantor or grantee can cancel an access token at any time. This prevents future information retrieval an any operations associated with the access token.

This code uses the APIs:

Prerequisites

What to do next

  • No actions. This token’s workflow is finished.

Cancel an access token

Token accessToken = grantor.getToken(tokenId);

// Cancel access token.
return grantor.cancelToken(accessToken);
const accessToken = await grantor.getToken(tokenId);

// Grantor cancels the token
return await grantor.cancelToken(accessToken);
/* Result with canceled access token */
{
    "token": {
      "id": "ta:EUFcgxq7fCUQUbScvKjhzDLobrcjmAFZUKyyatwSDyKU:P2XFaVcFf",
      "payload": {
        "version": "1.0",
        "refId": "se1i8np9so1n0sard5f80k9qksfv4gd0x77c00e22jqkhuxr",
        "issuer": {
          "id": "token",
          "username": "token"
        },
        "from": {
          "id": "m:B7Sf1z6s2Z6YFN4nPPSbs6Mjzwd:5zKtXEAq"
        },
        "to": {
          "id": "m:2EzHiCDzR5HaM6x7pbApypxWYhPW:5zKtXEAq",
          "username": "b5aqdyo5b6o1ptw0dc1w3tyb9jx72pcui4nnoa0fhr1oiggb9"
        },
        "access": {
          "resources": [
            {
              "allAccounts": {}
            }
          ]
        }
      },
      "payloadSignatures": [
        {
          "action": "ENDORSED",
          "signature": {
            "memberId": "m:B7Sf1z6s2Z6YFN4nPPSbs6Mjzwd:5zKtXEAq",
            "keyId": "LTSDBQJu1mmpCBM2",
            "signature": "VBhVS7il_aROn8Bd0Hy1RxNdwd7F1nvajAk6gAVBKe-xu37gnzMha-gSCMI3jfE635kHJHl1Tk3FTWSma_2JBA"
          }
        },
        {
          "action": "ENDORSED",
          "signature": {
            "memberId": "token",
            "keyId": "1x7df4vuFUHYQCa7",
            "signature": "YzF30OedzJ4JAKUf0iilwrx6yt9HN5bilgm_PRVgl-6w9ppbMjVFZ2f7nAq-OjKDsvRkdLdHQytF_cqtMJSABA"
          }
        },
        {
          "action": "CANCELLED",
          "signature": {
            "memberId": "m:B7Sf1z6s2Z6YFN4nPPSbs6Mjzwd:5zKtXEAq",
            "keyId": "LTSDBQJu1mmpCBM2",
            "signature": "8ndAmqHj3qkZKv7uzMPYkjynX7LSSTEp9nWCnmHrJLUGd8aNRsTEF1RYYd_eH3s17mSH4kZbeWXFnsQX5SbeBw"
          }
        },
{
          "action": "CANCELLED",
          "signature": {
            "memberId": "token",
            "keyId": "1x7df4vuFUHYQCa7",
            "signature": "Jtfgmx_hoL5VJdbdq27VNamR69EjYoT5VBmpD0ZHIOPnS5kgi7BpKSrR7BEM8Qe1MeovctehJDuaqO3dVJGHBQ"
          }
        }
      ]
    },
    "status": "SUCCESS"
}
[grantor cancelToken:accessToken
           onSuccess:^(TokenOperationResult *result) {
               // token now has more signatures, including a CANCEL signature
               accessToken = result.token;
           } onError:^(NSError *e) {
               // something went wrong
               @throw [NSException exceptionWithName:@"CancelAccessException"
                                              reason:[e localizedFailureReason]
                                            userInfo:[e userInfo]];
           }];
var accessToken = grantor.GetToken(tokenId);

// Cancel access token.
return grantor.CancelToken(accessToken);

Replace Access Token

If member A tries to create an access token for member B but there already is an A→B access token, TokenOS throws an error: there can be at most one access token from one member to another. To grant another member different or additional rights, replace the old token.

If you don’t have the old token, you can find it by calling the Member getAccessTokens method to get a “paged” list of all access tokens and then finding the one with the correct to.

To grant different/additional rights, create a new access token, call the Member replaceAndEndorseAccessToken method, passing in the old token and new tokens. (The replaceAccessToken method replaces the old token but does not endorse the new one; you probably want replaceAndEndorseAccessToken instead.)

This code uses the APIs:

Find an existing access token

return grantor.getActiveAccessToken(granteeMemberId);
return await grantor.getActiveAccessToken(granteeMemberId);
foundToken = [self.payerSync getActiveAccessToken:self.payeeSync.id];
foreach (var token in grantor.GetAccessTokens(null, 100).List)
{
    var toAlias = token.Payload.To.Alias;
    if (toAlias.Equals(granteeAlias))
    {
        return token;
    }
}

Replace and endorse in one call:

TokenOperationResult status = grantor.replaceAndEndorseAccessToken(
        oldToken,
        AccessTokenBuilder
                .fromPayload(oldToken.getPayload())
                .forAccount(accountId));
const result = await grantor.replaceAndEndorseAccessToken(
    oldToken,
    [
        Resource.create({allAccounts: {}}),
        Resource.create({allBalances: {}}),
        Resource.create({allAddresses: {}}),
    ]
);
return result.token;
AccessTokenConfig *newAccess = [AccessTokenConfig fromPayload:foundToken.payload];
[newAccess forAllAccounts];
[newAccess forAllBalances];
[newAccess forAllAddresses];

[grantor replaceAndEndorseAccessToken:foundToken
                    accessTokenConfig:newAccess
                            onSuccess:^(TokenOperationResult *result) {
                                accessToken = result.token;
                            } onError:^(NSError *e) {
                                // something went wrong
                                @throw [NSException exceptionWithName:@"ReplaceAccessTokenException"
                                                               reason:[e localizedFailureReason]
                                                             userInfo:[e userInfo]];
                            }];
var status = grantor.ReplaceAndEndorseAccessToken(
    oldToken,
    AccessTokenBuilder
        .FromPayload(oldToken.Payload)
        .ForAllAccounts()
        .ForAllBalances()
        .ForAllAddresses()
        .Build());

Replace but don’t endorse

// (replaceAndEndorseAccessToken is much safer.
// The "find" code doesn't see unendorsed tokens,
// so if the unendorsed token needs replacing,
// it can't be "found").
TokenOperationResult status = grantor.replaceAccessToken(
        oldToken,
        AccessTokenBuilder
                .fromPayload(oldToken.getPayload())
                .forAccount(accountId));
const replaceResult = await grantor.replaceAccessToken(
    oldToken,
    [
        Resource.create({allAccounts: {}}),
        Resource.create({allBalances: {}}),
        Resource.create({allAddresses: {}}),
    ]
);
const endorseResult = await grantor.endorseToken(
    replaceResult.token);
return endorseResult.token;
foundToken = [self.payerSync getActiveAccessToken:self.payeeSync.id];
AccessTokenConfig *newAccess = [AccessTokenConfig fromPayload:foundToken.payload];
[newAccess forAllAccounts];
[newAccess forAllBalances];
[newAccess forAllAddresses];

[grantor replaceAccessToken:foundToken
          accessTokenConfig:newAccess
                  onSuccess:^(TokenOperationResult *result) {
                      accessToken = result.token;
                  } onError:^(NSError *e) {
                      @throw [NSException exceptionWithName:@"ReplaceAccessTokenException"
                                                     reason:[e localizedFailureReason]
                                                   userInfo:[e userInfo]];
                  }
 ];
// (replaceAndEndorseAccessToken is much safer.
// The "find" code doesn't see unendorsed tokens,
// so if the unendorsed token needs replacing,
// it can't be "found").
var status = grantor.ReplaceAccessToken(
    oldToken,
    AccessTokenBuilder
        .FromPayload(oldToken.Payload)
        .ForAllAccounts()
        .ForAllBalances()
        .ForAllAddresses()
        .Build());

Members

The SDK lets you manage a logged-in member’s data.

Keys

A member has a few cryptographic keys. When a member cryptographically signs something (e.g., endorses a Smart Token), TokenOS makes sure that the Member “owns” the signing key.

When you create a member via the SDK’s createMember method, it automatically gets three key pairs; the three public keys are uploaded to the Token cloud.

To add more keys, call the Member approveKey or Member approveKeys method. This uploads public keys to the Token cloud and associates them with the member.

Call Member removeKey or Member removeKeys to un-associate some previously-uploaded keys. Future signature checks using these keys will fail.

The Member approveKey[s] and Member removeKey[s] methods control keys in the Token Cloud. They do not affect which keys are stored locally.

This code uses the APIs:

const keypair4 = await Token.Crypto.generateKeys('LOW');
keypair4.publicKey = Util.strKey(keypair4.publicKey);
await member.approveKey(Key.create(keypair4));
const keypair5 = await Token.Crypto.generateKeys('STANDARD');
const keypair6 = await Token.Crypto.generateKeys('PRIVILEGED');
keypair5.publicKey = Util.strKey(keypair5.publicKey);
keypair6.publicKey = Util.strKey(keypair6.publicKey);
await member.approveKeys([Key.create(keypair5), Key.create(keypair6)]);

await member.removeKey(keypair4.id);
await member.removeKeys([keypair5.id, keypair6.id]);

Local Key Storage

TokenOS member security rests on keys. To use member keys (other than low-privilege keys), a user must authenticate using two factors. To enforce this, applications that manage member keys must store them securely. The SDK provides some ways to store keys.

The SDK’s built-in key storage systems are useful for low-privilege keys, but not for other keys. E.g., the SDK provides a key storage system that stores keys in files in a directory. Assuming that directory isn’t secured in some way, it wouldn’t be appropriate to store high-privilege keys there. If you’re using a platform that has an API to store things securely, you can write your own key storage system to use that API.

To configure where the client stores Member private keys, choose a *CryptoEngine type and pass it as a parameter to Token.createMember, Token.login, Token.provisionDevice, and Token.provisionDeviceLow.

The Token SDK client class provides some built-in key storage CryptoEngine types:

  • Token.MemoryCryptoEngine Keeps keys in memory and “forgets” them on restart. Useful for unit tests, but not for persistent members.
  • Token.UnsecuredFileCryptoEngine Keeps keys in files in a directory; specify which directory as a parameter when creating the Token SDK client.
  • Token.BrowserCryptoEngine Keeps keys in browser’s localStorage. Useful for web clients.

You can define another CryptoEngine key storage type. The implementation should probably look something like the BrowserCryptoEngine implementation, which is a thin wrapper around BrowserKeyStore, which stores keys. Use the KeyStoreCryptoEngine helper class to ease defining a similar thin wrapper.

For more information about implementing a custom CryptoEngine, see the SDK’s src/security/engines/README.md.

This code uses the APIs:

The SDK’s built-in key storage systems are useful for low-privilege keys, but not for other keys. E.g., the SDK provides a key storage system that stores keys in files in a directory. Assuming that directory isn’t secured in some way, it wouldn’t be appropriate to store high-privilege keys there. If you’re using a platform that has an API to store things securely, you can write your own key storage system to use that API.

To configure where the client stores Member private keys, choose a KeyStore implementation and pass it as a parameter to TokenIO.builder().withKeyStore().

The Token SDK client class provides basic built-in KeyStore classes:

  • InMemoryKeyStore Keeps keys in memory, “forgets” on restart. Useful for unit tests, but not for persistent members.
  • UnsecuredFileSystemKeyStore Keeps keys in files in a directory; specify which directory as a constructor parameter.

You can define another KeyStore class, perhaps using InMemoryKeyStore‘s implementation for inspiration.

By default, the SDK stores member private keys in the device’s Secure Enclave. If the user attempts to sign something (using a private key), the device prompts the user to authenticate with Face ID or Touch ID.

This default behavior is good for “real world” use cases. It’s not good for all automated tests, however. For example, if a test creates a member with one SDK client and then tries to log in as the member in another client, the test fails because the it can’t authenticate. When creating an SDK client for automated tests, you can set the SDK builder’s keyStore property to an instance of TKInMemoryKeyStore. (In Objective-C, builder.keyStore = [[TKInMemoryKeyStore alloc] init];) A TKInMemoryKeyStore uses keys in memory instead of persisting them. Thus, tests can use its keys without authenticating; when the program stops running, it “forgets” the keys.

To use a different system for local key storage, implement the TKKeyStore protocol. When creating an SDK client, set the builder’s keyStore property to an instance of this key store.

The SDK’s built-in key storage systems are useful for low-privilege keys, but not for other keys. E.g., the SDK provides a key storage system that stores keys in files in a directory. Assuming that directory isn’t secured in some way, it wouldn’t be appropriate to store high-privilege keys there. If you’re using a platform that has an API to store things securely, you can write your own key storage system to use that API.

To configure where the client stores Member private keys, choose a KeyStore implementation and pass it as a parameter to TokenIO.NewBuilder().WithKeyStore().

The Token SDK client class provides basic built-in KeyStore classes:

  • InMemoryKeyStore Keeps keys in memory, “forgets” on restart. Useful for unit tests, but not for persistent members.
  • UnsecuredFileSystemKeyStore Keeps keys in files in a directory; specify which directory as a constructor parameter.

You can define another KeyStore class, perhaps using InMemoryKeyStore’s implementation for inspiration.

Aliases

A member’s aliases identify the member in a way that makes sense to humans. (A member ID also identifies a member, but long hash strings aren’t great for UI.) For example, an email address can be a member’s alias. A member can claim an alias. TokenOS attempts to verify the alias; for example sending a message to a claimed email address. An alias identifies at most one member; if one member has claimed an alias, another member can’t claim it.

Adding, Removing

The Member addAlias and Member addAliases methods attempt to add aliases for a member. This attempt takes a while, and may fail. If one member has already claimed an alias, then another member can’t add it. Until TokenOS can verify the alias, you can’t use that alias to refer to a member; it’s in a special “unverified” state. (To find out a member’s successfully-verified aliases, call the Member aliases method.)

The Member removeAlias and Member removeAliases methods remove aliases from a member.

This code uses the APIs:

Verifying

When an alias of type EMAIL is added, a verification email will be sent to the address, providing a link to complete verification. DOMAIN aliases need to be manually verified by us. To register a domain alias, please add it using Member addAlias then send us a message with the member ID and desired alias to complete verification. The member ID can be easily retrieved using the Member memberId method.

This code uses the APIs:

Fetching

Call the Member aliases method to get a list of the member’s aliases. There’s a Member firstAlias convenience method that returns the first verified alias.

This code uses the APIs:

Resolving

You can look up a member’s Id by their alias. This Id can be used to look up that member’s profile. It can also be useful to determine that the alias does actually refer to some user, for example to catch when a user is about to make a payment to a typo; the aliasExists convenience method can help here.

This code uses the APIs:

const alias1 = (await member.aliases())[0]; // or member.firstAlias();
const alias2 = Alias.create({
    type: 'EMAIL',
    value: 'alias2-' + Token.Util.generateNonce() + '+noverify@token.io',
});
await member.addAlias(alias2);

const alias3 = Alias.create({
    type: 'EMAIL',
    value: 'alias3-' + Token.Util.generateNonce() + '+noverify@token.io',
});
const alias4 = Alias.create({
    type: 'EMAIL',
    value: 'alias4-' + Token.Util.generateNonce() + '+noverify@token.io',
});
await member.addAliases([alias3, alias4]);

await member.removeAlias(alias1);
await member.removeAliases([alias2, alias3]);

const resolved = await Token.resolveAlias(alias4);
/* resolved alias has id and alias fields: */
{
  "id": "m:4AaVmfKfY9DQ9tuqWJcEwq9VkXpo:5zKtXEAq",
  "alias": {
    "type":"EMAIL",
    "value":"alias4-v6rpfo+noverify@token.io"}
  }
}

Recovery Rules

A member’s recovery rules specify how the user can move that member to a new device. The default “normal consumer” recovery rule lets a user recover their member by means of a code emailed to the member’s email address. More complex recovery rules are possible; the Java SDK can configure a member to use other or additional rules.

The Java SDK has a Member.useDefaultRecoveryRule method that configures a member to use the default “normal consumer” recovery rule.

The Java SDK also has a method to configure a member to use a more complex rule, Member.addRecoveryRule. This rule consists of recovery agents. A recovery agent is a Token member, capable of approving things (by digitally signing them). A recovery rule can have one primary agent and some number of secondary agents. Recovery requires approval by the primary agent; if the rule has secondary agents, then recovery also requires approval by one of them.

Token’s default recovery agent approves recovery based on “normal consumer” process: sends an email with a special URL to the member’s alias email, listens for visits to that URL, and approves recovery if it has seen such a visit. Call Member.getDefaultAgent to get the default recovery agent’s Member Id, suitable for use in a recovery rule.

Banks or other institutions can define other recovery agents that use other processes. Such an agent will need a Token member. “Customer” members use the agent’s member Id in their recovery rules. When the agent wants to approve a recovery attempt, they can receive MemberRecoveryOperation.Authorization protocol buffers and call Member.authorizeRecovery to generate a signature to return.

The Javascript SDK has a Member.useDefaultRecoveryRule method that sets up a member to use the default “normal consumer” recovery rule.

The Objective-C SDK automatically sets up a member to use the default “normal consumer” recovery rule when creating that member.

This code uses the APIs:

The C# SDK has a Member.UseDefaultRecoveryRule method that configures a member to use the default “normal consumer” recovery rule.

The C# SDK also has a method to configure a member to use a more complex rule, Member.AddRecoveryRule. This rule consists of recovery agents. A recovery agent is a Token member, capable of approving things (by digitally signing them). A recovery rule can have one primary agent and some number of secondary agents. Recovery requires approval by the primary agent; if the rule has secondary agents, then recovery also requires approval by one of them.

Token’s default recovery agent approves recovery based on “normal consumer” process: sends an email with a special URL to the member’s alias email, listens for visits to that URL, and approves recovery if it has seen such a visit. Call Member.GetDefaultAgent to get the default recovery agent’s Member Id, suitable for use in a recovery rule.

Banks or other institutions can define other recovery agents that use other processes. Such an agent will need a Token member. “Customer” members use the agent’s member Id in their recovery rules. When the agent wants to approve a recovery attempt, they can receive MemberRecoveryOperation.Authorization protocol buffers and call Member.AuthorizeRecovery to generate a signature to return.

This code uses the API:

Specifying that a member should use the “normal consumer” recovery rule:

member.useDefaultRecoveryRule();

Specify a “normal consumer” recovery rule:

member.useDefaultRecoveryRule();

Specify a custom recovery rule:

// Someday in the future, this user might ask the recovery agent
// "Please tell Token that I am the member with ID m:12345678 ."
// While we're setting up this new member, we need to tell the
// recovery agent the new member ID so the agent can "remember" later.
tellRecoveryAgentMemberId(newMember.memberId());

String agentId = tokenIO.getMemberId(agentAlias);
RecoveryRule recoveryRule = RecoveryRule.newBuilder()
        .setPrimaryAgent(agentId)
        // This example doesn't call .setSecondaryAgents ,
        // but could have. If it had, then recovery would have
        // required one secondary agent authorization along with
        // the primary agent authorization.
        .build();
newMember.addRecoveryRule(recoveryRule);

Recovery agent behavior:

// "Remember" whether this person who claims to be member with
// the ID m:12345678 really is:
boolean isCorrect = checkMemberId(authorization.getMemberId());
if (isCorrect) {
    return agentMember.authorizeRecovery(authorization);
}
throw new RuntimeException("I don't authorize this");

Specify a “normal consumer” recovery rule:

member.UseDefaultRecoveryRule();

Specify a custom recovery rule:

// Someday in the future, this user might ask the recovery agent
// "Please tell Token that I am the member with ID m:12345678 ."
// While we're setting up this new member, we need to tell the
// recovery agent the new member ID so the agent can "remember" later.
TellRecoveryAgentMemberId(newMember.MemberId());

var agentId = tokenIO.GetMemberId(agentAlias);
var recoveryRule = new RecoveryRule {PrimaryAgent = agentId};
// This example doesn't call .setSecondaryAgents ,
// but could have. If it had, then recovery would have
// required one secondary agent authorization along with
// the primary agent authorization.

newMember.AddRecoveryRule(recoveryRule);

Recovery agent behavior:

// "Remember" whether this person who claims to be member with
// the ID m:12345678 really is:
var isCorrect = CheckMemberId(authorization.MemberId);
if (isCorrect)
{
    return agentMember.AuthorizeRecovery(authorization);
}

throw new Exception("I don't authorize this");

Address

// This sample code uses a few of the fields available in
// an address; for full list (place, province, ...), see
// https://developer.token.io/sdk/pbdoc/io_token_proto_common_address.html
const address1 = Address.create({
    houseNumber: '221B',
    street: 'Baker St',
    city: 'London',
    postCode: 'NW1 6XE',
    country: 'UK',
});
const addressRecord1 = await member.addAddress('Home', address1);
await member.deleteAddress(addressRecord1.id);
const address2 = Address.create({
    houseNumber: '16/17',
    street: 'Osloer Strasse',
    city: 'Berlin',
    postCode: 'D-13359',
    country: 'Germany',
});
await member.addAddress('Office', address2);
const addresses = await member.getAddresses();
/* Member.getAddresses returns a list of AddressRecords: */
[ { id: 'a0223d59f2654ee3837437423bda8c81',
    name: 'Office',
    address: 
     { houseNumber: '16/17',
       street: 'Osloer Strasse',
       postCode: 'D-13359',
       city: 'Berlin',
       country: 'Germany' },
    addressSignature: 
     { memberId: 'm:AZY5JEygU3QNThZEEbtUD9aBiJW:5zKtXEAq',
       keyId: 'TyUcVdarNeA5-53u',
       signature: '4ku9b-f2UIQ4VIfyzCAKS5BL9ug_4UelNBdgMb4fLMHFaV8ObuQtSeAToqntVeFn0Ht0ScF9vfQO7I8irGXCCw' } } ]

Profile

Each member has a profile: a display name and picture.

This profile can be useful in UI: a user might not recognize a member’s email address, but could recognize a display name. However, Members can choose their display names and pictures; Token does not verify these. Thus, in many contexts it makes sense to show other, verified, information about a Member beyond the profile. E.g., Token’s Quick Checkout flow for merchant websites shows the URL of the website that triggered the flow along with the merchant member’s display name and profile picture.

Getting the Profile

To get a member’s profile information, call Member getProfile. This gets a Profile data structure containing the member’s display name and blob Ids for the member’s profile picture. There can be a few blob Id fields, one for each profile picture size. If the original profile picture was smaller than expected for a profile picture size. For example, if the original profile picture was 500x500, TokenOS creates a scaled small image, but not a scaled medium image.

Profile pictures live in blobs, the same data structures used for transfer token attachments. Unlike most transfer token attachments, profile picture blobs are public; any member can fetch these blobs. Call Member getBlob to fetch a profile picture blob.

If your code caches profile pictures, it’s good to know that if a picture’s blob Id hasn’t changed, then the picture data hasn’t changed.

This code uses the APIs:

Setting the Profile

To set a member’s display name, call Member setProfile. This method takes a Profile data structure but ignores the picture blob Id fields.

To set a member’s profile picture, call Member setProfilePicture. This method takes a MIME type and bytes of image data. It uploads the image, scales it to a few sizes, and creates public blobs.

This code uses the APIs:

const name = Profile.create({
    displayNameFirst: 'Tycho',
    displayNameLast: 'Nestoris',
});
await member.setProfile(name);
const jpeg = loadPicture('tycho.jpg'); // file contents as byte array
await member.setProfilePicture('image/jpeg', jpeg);

const profile = await member.getProfile(member.memberId());
/* Profile structure: */
{
  displayNameFirst: 'Tycho',
  displayNameLast: 'Nestoris',
  originalPictureId: 'b:2aT7GiHkqhDzpLw...rfBaag4jw:5zKtXEAq'
}

Default Bank Account

A member can have a default bank account. A transfer token can specify a member Id recipient without specifying an account. In this case, TokenOS uses the recipient’s default account.

This code uses the APIs:

Notifications

TokenOS sends messages and prompts to members by means of notifications. A notification is a structured object containing information sent to a member, normally to a member’s mobile app.

For example, a member might want to purchase goods at an online merchant website. The member’s account is configured to require strong customer authentication, but the website doesn’t have a way to get that authentication. The website sends a payment request notification (as shown at Request Payment). TokenOS delivers the notification to the member’s mobile app. The mobile app prompts the user to authenticate and approve or deny the payment request.

You can use Token SDK APIs to poll for notification. Testing this requires some setting up: if you create a test member via TokenIO.createMember, that member isn’t automatically configured to receive notifications; you need to subscribe it.

Polling for Notifications

To fetch a member’s recent notifications, call the Member getNotifications method.

This fetches a “paged” list of notifications. As with other “paged” lists, pass a null offset to get recent notifications and a next-offset string. Pass that next-offset string as offset to get the next batch of notifications.

If the member has more than one subscriber, getNotifications returns more than one Notification for each “message”: one for each subscriber with a status field indicating whether that notification has been delivered yet.

The getNotification method gets information about one notification.

This code uses the APIs:

    None. Notification feature is currently not supported by the C# SDK
PagedList<Notification, String> pagedList =
        member.getNotifications(null, 10);
List<Notification> notifications = pagedList.getList();
if (!notifications.isEmpty()) {
    Notification notification = notifications.get(0);
    switch (BodyCase.valueOf(notification.getContent().getType())) {
        case PAYEE_TRANSFER_PROCESSED:
            System.out.printf("Transfer processed: %s", notification);
            break;
        default:
            System.out.printf("Got notification: %s", notification);
            break;
    }
    return Optional.of(notification);
}
const pagedList = await member.getNotifications(null, 10);
if (pagedList.data.length > 0) {
    const notification = pagedList.data[0];
    switch (notification.content.type) {
    case 'PAYEE_TRANSFER_PROCESSED':
        // console.log('Transfer Processed: ', JSON.stringify(notification));
        break;
    default:
        // console.log('Got Notification: ', JSON.stringify(notification));
        break;
    }
    return;
}
[payee getNotificationsOffset:NULL
                        limit:10
                    onSuccess:^(PagedArray<Notification *> *ary) {
                        for (Notification *n in ary.items) {
                            if ([n.content.type isEqualToString:@"PAYEE_TRANSFER_PROCESSED"]) {
                                // use notification
                                notification = n;
                            }
                        }
                    } onError:^(NSError *e) {
                        // Something went wrong.
                        @throw [NSException exceptionWithName:@"PollException"
                                                       reason:[e localizedFailureReason]
                                                     userInfo:[e userInfo]];
                    }];
/* returned paged list of notifications */
{
  "data": [
    {
      "id": "9X6uFlvq1NuNuhkCzXQs",
      "subscriberId": "2uITiHf1ITrMv2SpRbGa",
      "content": {
        "type": "PAYMENT_REQUEST",
        "title": "Payment request",
        "body": "test-h41m requested 100,00 €.",
        "payload": "{\"payload\":{\"description\":\"Sample payment request\",\"from\":{\"alias\":{\"type\":\"USERNAME\",\"value\":\"test-6x7r\"},\"id\":\"m:23PPht2VGk43beMai489LvVnDa4P:5zKtXEAq\"},\"refId\":\"70v134mkjzgs0kafw15csg7gb9zw0jqj1hwk7l4fhohdkc07ldi\",\"to\":{\"alias\":{\"type\":\"USERNAME\",\"value\":\"test-h41m\"},\"id\":\"m:3c5t5mUKepHfdN5rUdBFb3iwS7bQ:5zKtXEAq\"},\"transfer\":{\"amount\":\"100\",\"currency\":\"EUR\"}}}",
        "createdAtMs": "1506015949630"
      },
      "status": "DELIVERED"
    }
  ],
  "offset": "CsFxV"
}

Subscribing to Notifications

A subscriber specifies how to deliver notifications to a member.

When a user creates a member using Token’s mobile app, the app subscribes to the member’s notifications so that TokenOS knows which mobile phone to deliver to.

A bank can use its own app for notifications instead of the Token app. Such a bank subscribes to the user’s notifications. (For details about the relevant bank API, see Bank Integration: Notification Handling).

If some other program, for example a unit test, creates a member, that member doesn’t automatically have any subscribers. Sending notifications to such a member fails. You can subscribe them to the "iron" test bank.

This code uses the API:

    None. Notification feature is currently not supported by the C# SDK

Subscribe


[payee subscribeToNotifications:@"iron"
            handlerInstructions:(NSMutableDictionary<NSString *, NSString *> *) [NSMutableDictionary dictionary]
                      onSuccess:^(Subscriber *s) {
                          subscriber = s;
                      } onError:^(NSError *e) {
                          // Something went wrong.
                          @throw [NSException exceptionWithName:@"SubscribeException"
                                                         reason:[e localizedFailureReason]
                                                       userInfo:[e userInfo]];
                      }];
Subscriber sub = member.subscribeToNotifications("iron");

const subscription = await member.subscribeToNotifications('iron');
// Notification feature is currently not supported by the C# SDK

Errors

This section describes runtime errors thrown by the Token SDKs, such as program exceptions.

Java SDK Errors

The Token Java SDK throws the standard gRPC exceptions; for more information, refer to grpc-java Status.java. You will see the enum names in stack dumps.

Error Enum Value Meaning
OK 0 Operation completed successfully.
CANCELLED 1 Operation was canceled (typically by the caller).
UNKNOWN 2 Unknown errror; for example, a status value was received from an unknown errror-space, or an API call returned an error with incomplete information.
INVALID_ARGUMENTS 3 Client specified an invalid argument.
DEADLINE_EXCEEDED 4 Deadline expired before operation could complete.
NOT_FOUND 5 Requested entity (such as a file or directory) was not found.
ALREADY_EXISTS 6 Entity that you attempted to create (such as a file or directory) already exists.
PERMISSION_DENIED 7 Caller does not have permission to execute the specified operation.
RESOURCE_EXHAUSTED 8 A resource, such as a per-user quota or the file system is out of space, has been exhausted.
FAILED_PRECONDITION 9 Operation was rejected because the system is not in a state required for the operation’s execution.
ABORTED 10 Operation was aborted, typically due to a concurrency issue.
OUT_OF_RANGE 11 Operation was attempted past the valid range; for example, seeking or reading past the end of a file.
UNIMPLEMENTED 12 Operation is not implemented or not supported/enabled.
INTERNAL 13 Internal error.
UNAVAILABLE 14 Service is unavailable, most likely due to a transient condition that might be corrected by retrying.
DATA_LOSS 15 Unrecoverable data loss or corruption.
UNAUTHENTICATED 16 Request does not have valid authentication credentials for the operation.

Objective-C Errors

The Token Objective-C SDK throws the standard gRPC exceptions; for more information, refer to grpc status.h.

Error Enum Value Meaning
GRPC_STATUS_OK 0 Operation completed successfully.
GRPC_STATUS_CANCELLED 1 Operation was canceled (typically by the caller).
GRPC_STATUS_UNKNOWN 2 Unknown errror; for example, a status value was received from an unknown errror-space, or an API call returned an error with incomplete information.
GRPC_STATUS_INVALID_ARGUMENTS 3 Client specified an invalid argument.
GRPC_STATUS_DEADLINE_EXCEEDED 4 Deadline expired before operation could complete.
GRPC_STATUS_NOT_FOUND 5 Requested entity (such as a file or directory) was not found.
GRPC_STATUS_ALREADY_EXISTS 6 Entity that you attempted to create (such as a file or directory) already exists.
GRPC_STATUS_PERMISSION_DENIED 7 Caller does not have permission to execute the specified operation.
GRPC_STATUS_RESOURCE_EXHAUSTED 8 A resource, such as a per-user quota or the file system is out of space, has been exhausted.
GRPC_STATUS_FAILED_PRECONDITION 9 Operation was rejected because the system is not in a state required for the operation’s execution.
GRPC_STATUS_ABORTED 10 Operation was aborted, typically due to a concurrency issue.
GRPC_STATUS_OUT_OF_RANGE 11 Operation was attempted past the valid range; for example, seeking or reading past the end of a file.
GRPC_STATUS_UNIMPLEMENTED 12 Operation is not implemented or not supported/enabled.
GRPC_STATUS_INTERNAL 13 Internal error.
GRPC_STATUS_UNAVAILABLE 14 Service is unavailable, most likely due to a transient condition that might be corrected by retrying.
GRPC_STATUS_DATA_LOSS 15 Unrecoverable data loss or corruption.

JavaScript SDK Errors

The Token JavaScript SDK throws different types of HTTP errors. All errors are wrapped in an error object, with a message that contains the SDK method that failed, along with the reason for failure.

C# SDK Errors

The Token C# SDK throws System.AggregateException that wraps the standard gRPC exceptions; for more information, refer to grpc-csharp StatusCode.cs.

Error Enum Value Meaning
OK 0 Operation completed successfully.
Cancelled 1 Operation was canceled (typically by the caller).
Unknown 2 Unknown error.
InvalidArgument 3 Client specified an invalid argument.
DeadlineExceeded 4 Deadline expired before operation could complete.
NotFound 5 Requested entity (such as a file or directory) was not found.
AlreadyExists 6 Entity that you attempted to create (such as a file or directory) already exists.
PermissionDenied 7 Caller does not have permission to execute the specified operation.
ResourceExhausted 8 A resource, such as a per-user quota or the file system is out of space, has been exhausted.
FailedPrecondition 9 Operation was rejected because the system is not in a state required for the operation’s execution.
Aborted 10 Operation was aborted, typically due to a concurrency issue.
OutOfRange 11 Operation was attempted past the valid range; for example, seeking or reading past the end of a file.
Unimplemented 12 Operation is not implemented or not supported/enabled.
Internal 13 Internal error.
Unavailable 14 Service is unavailable, most likely due to a transient condition that might be corrected by retrying.
DataLoss 15 Unrecoverable data loss or corruption.
Unauthenticated 16 Request does not have valid authentication credentials for the operation.

Copyright © 2018 Token, Inc. All Rights Reserved