NAV Navbar
gRPC/Protobuf HTTP/JSON

Token Bank Integration

TokenOS interacts with banks via APIs. A bank can provide tighter integration with TokenOS by implementing TokenOS’s bank integration API. (If a bank doesn’t implement this API, but does offer a public API, then TokenOS might use that other API. Implementing TokenOS’ bank integration API enables more features and smoother user experience for that bank.) By means of the TokenOS bank integration API, a bank can

The Token Bank Integration Java SDK simplifies handling the TokenOS bank integration API. It handles network requests by calling Java methods; this allows programmers to concentrate on business logic instead of remote procedure call logic.

A Bank’s steps to integrating with TokenOS:

  1. Get the Token Bank Integration SDK. Get the sample bank server implementation.

  2. Generate keys and certificates for secure communication; keep track of them.

  3. Set up fake environment/data for testing. This probably means setting up an environment with some fake accounts.

  4. Get the sample bank server up and running. Normally this just works, but there are troubleshooting tips.

  5. Ask Token to send test requests to the server. As the Bank gets some parts working with real functionality, Token can turn on more tests.

  6. Replace fake parts of the sample bank server implementation with real implementations.

  7. Set up a web interaction that authenticates a Bank customer and lets them choose accounts to link to Token.

The bank sets up a server that opens a port and listens for connections from TokenOS. Rather than implementing a server “from scratch”, a bank can start with the sample implementation. The sample responds to requests with preconfigured data; it doesn’t persist state, but instead “forgets” everything when reset.

API Overview

The bank server implementation handles TokenOS requests by defining methods. The Token Bank Integration SDK defines some interfaces. The bank implements these interfaces; when TokenOS sends a request to the bank, the SDK turns that request into an interface method call.

To handle these requests, a bank implements some interfaces. Depending on which features the bank will support, it must implement some or all of these:

Many parts of the Bank Integration API work with a bank’s accounts: ask account balance, request transfer, and so on. A few parts of this API also refer to Token members, IDs of Token users. Where you see “account”, think bank account; where you see “member”, think Token member.

In TokenOS, an account has exactly one currency. Banks might support multicurrency bank accounts; to work with TokenOS, treat such an account as multiple Token “accounts”, one per currency. A TokenOS bank account has metadata. To “tag” one TokenOS account for one currency and another TokenOS for another currency, use metadata.

Moving money involves Transfers and Tokens. A Transfer represents a request to move money from one account to another. A transfer can have two Transactions associated with it: a debit at Bank A and a credit at Bank B. A Token is an authorization to access an underlying asset; when TokenOS keeps track of whether a payer authorizes a payment, it uses a Token to do so. You can learn more about Tokens.

// TokenOS treats these as distinct accounts:
BankAccount eurAcct = BankAccount.newBuilder()
  .setSwift(swift)
  .setAccountFeatures(features)
  .putMetadata("currency", "EUR")
  .build();
BankAccount gbpAcct = BankAccount.newBuilder()
  .setSwift(swift) // same
  .setAccountFeatures(features) // same
  .putMetadata("currency", "GBP") // different
  .build();

Account Linking Web Flow

The user sets up Token to link her bank account(s) with her Token member. Until the user has at least one bank account associated with her Token member, she can’t do any useful banking things.

The user interacts with the bank’s website to demonstrate authority over accounts; the user’s application sends proof of this interaction to Token; Token sends this proof to the Bank’s server, which responds with account linking information.

Overview / UI Flow

When a user wants to set up Token with her bank accounts at a Bank…

  1. The user launches the Token app on her phone.

  2. The Token app shows a list of banks. The user chooses her Bank.

  3. The Token app shows a screen that prompts the user to log in to her Bank. This screen is a web view showing a web page provided by the Bank. When onboarding, the Bank told Token the URL of this page. Token passes in memberId=token member id and redirect_uri=uri as URL parameters.
    Sample Bank website login

  4. The user logs in, sees a list of her link-able accounts; she checks those accounts she wants to link. This is a web form provided by the Bank web site.
    Sample Bank website login

  5. The user OKs the web form. The Bank’s web site stores information about the linked accounts, associating that information with a “bearer token”: a short, unguessable string.

  6. The Bank’s web site redirects to the location in the earlier-specified redirect_uri parameter, appending #access_token=bearer token string

    When the web view’s location changes to this URL, the app gets the bearer token string and stops showing the web view.

  7. The app uses the Token SDK to send this bearer token string to TokenOS.

  8. Using the bearer token string, TokenOS requests linking information from the bank’s RPC server. The bank’s AccountLinkingService handles the request, returning a BankAuthorization structure containing the member ID and account information.

  9. TokenOS links the accounts to the Token member. (The Bank does not need to write code to handle this request, unless it’s configured to not store customer data in the Token Cloud, in which case it implements StorageService.)

To get all this working, the Bank’s code must do a few things:

It’s normal to associate more than one Token member with a bank account. For example, if the bank account is a joint account, then more than one user might link their Token members with it.

Bank Web Site

As part of enabling Token linking, the Bank must set up some forms on the Bank web site, generate some data that links a Token member ID to their account(s), store that account-choice information, and redirect. (This implements the Implicit Grant part of the OAuth2.0 spec.)

  1. Prompt the user to sign in; authenticate the user. Once she authenticates…

  2. Present a form with a checkbox for each of the user’s bank accounts. If the user has some accounts which shouldn’t be Token-linkable, don’t show those accounts. If the user wants to Cancel, pretend she didn’t select any accounts and chose OK. If the user OKs…

  3. Store the account choice information. Later, the Bank service will need to retrieve this information using a bearer token string. Thus, it might make sense to store this information in a database indexed on the bearer token string.

  4. Redirect as described below.

The start of this flow should be an endpoint whose path ends in /authorize, e.g. https://sample-bank.com/token/authorize.

The Token app sets ?memberId=string&redirect_uri=string URL parameters when it shows the Bank’s sign-in web page. Keep track of these values, which are needed later. The member ID string is a Token member ID; it probably is not the same as the user’s Bank web site user name.

To respond to the client app with the bearer token string, redirect to the URI: redirect_uri#access_token=bearer token string. E.g., if redirect_uri is https://token.io/ and the bearer token string is ZmpkCmF, then redirect to https://token.io/#access_token=ZmpkCmF.

To instead respond to the client app with an error, redirect to the URI: redirect_uri?error=error string, where error string is one of invalid_request, access_denied, server_error, or temporarily_unavailable.

https//token.io?error=access_denied

Bank Integration SDK

The Bank programs a service to listen and respond to Token requests from the Internet. Token provides a Bank Integration Java SDK to ease this task. The Java SDK defines Java interfaces for the Bank to implement. It provides some code to convert structures sent over the network into simpler structures. Bank programmers can thus focus on core business logic.

The Sample implementation uses the Bank Integration SDK and uses its APIs.

The SDK defines a few interfaces. To support various Token features, a bank can implement some of these interfaces. If a bank will not support all Token features, it need not implement all of these interfaces. The interfaces are described in more detail below.

To get the latest Bank Integration Java SDK, browse https://token.jfrog.io/token/webapp/#/artifacts/browse/tree/General/public-libs-release-local/io/token/sdk/tokenio-sdk-integration and choose the latest version.

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-integration</artifactId>
    <version>2.1.5</version>
</dependency>

Gradle

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

Server Builder

Token Integration SDK provides a framework for wiring service implementations abstractions into a single Server running on a given port.

This example uses io.token.sdk.ServerBuilder to run a custom implementation of the Token Bank Service integrated with the Core Banking System of a partner bank.

This example starts a server listening on port 1234 and ready to serve requests from TokenOS. Its connectivity configuration was set up as part of onboarding.

You can optionally register an interceptor to execute some code at the start or end of any RPC request the server receives. Call the registerInterceptor method; this method uses InterceptorFactory which uses SimpleInterceptor.

This code uses the APIs:

ServerBuilder server = ServerBuilder
        .forPort(args.port)
        .reportErrorDetails()
        .withAccountService(factory.accountService())
        .withAccountLinkingService(factory.accountLinkingService())
        .withTransferService(factory.transferService())
        .withStorageService(factory.storageService());
if (args.useSsl) {
    server.withTls(
            args.configPath("tls", "cert.pem"),
            args.configPath("tls", "key.pem"),
            args.configPath("tls", "trusted-certs.pem"));
}

// You will need to Ctrl-C to exit.
server
        .build()
        .start()
        .await();

Service Interface Definitions

The Token Bank Integration SDK defines some interfaces. Much of the programming effort in integrating a bank with Token goes into implementing these interfaces; much of the rest of this page describes them.

Common Objects

To program a service that handles Token requests, the Bank implements some interfaces using the Token Bank Integration SDK. Some data structures are used in a few methods.

Many important data structures are defined as Protocol Buffers. 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. To see these files as web pages, please see the Bank Protocol Buffer Reference.

Money

An object to represent a monetary value in a specific currency.

message Money {
  string currency = 1;
  string value = 2;         // double amount in the string format.
}
Money = {
  "currency": "EUR",
  "value": "92.21"
}

Bank Accounts

The BankAccount structure describes Bank accounts. When TokenOS requests that the bank act upon some account, it specifies that account with a BankAccount.

The TransferEndpoint structure has a BankAccount and some customer information (e.g., name, address). When proposing a money transfer, TokenOS uses a TransferEndpoint.

When linking a bank account to Token (described below), the account’s Account Features tell TokenOS what the account can do.

BankAccount.newBuilder()
    // set one of the account types (in this example, SWIFT):
    .setSwift(Swift.newBuilder().setBic(bic).setAccount(accountNumber))

    .setAccountFeatures(
        AccountProtos.AccountFeatures.newBuilder()
           .setSupportsInformation(true) // AccountService working
           .setSupportsPayment(true))    // Can initiate transfers

    ... optionally set metadata ...
    
    .build()
message BankAccount {
  // Token account Destination. Useful as source or destination
  // for a transfer; doesn't make sense for a bank to "link" this.
  message Token {
    string member_id = 1;
    string account_id = 2;
  }

  // Deprecated; unused
  message TokenAuthorization {
    option deprecated = true;
    io.token.proto.banklink.BankAuthorization authorization = 1;
  }

  // Source account managed by a coop bank
  message Bank {
    string bank_id = 1;
  }

  // SWIFT transfer
  message Swift {
    option (io.token.proto.extensions.message.redact) = true;
    string bic = 1;     // BIC code AAAABBCCDD
    string account = 2;
  }

  // SEPA transfer.
  message Sepa {
    option (io.token.proto.extensions.message.redact) = true;
    string iban = 1;    // International Bank Account Number
    string bic = 2;     // Bic code. Optional, except for non EEA countries
  }

  // ACH transfer
  message Ach {
    option (io.token.proto.extensions.message.redact) = true;
    string routing = 1; // Routing number
    string account = 2;
  }

  // Faster Payments Service transfer (UK)
  message FasterPayments {
    option (io.token.proto.extensions.message.redact) = true;
    string sort_code = 1;
    string account_number = 2;
  }

  // Custom authorization
  message Custom {
    option (io.token.proto.extensions.message.redact) = true;
    string bank_id = 1;
    string payload = 2;
  }

  oneof account {
    Token token = 1;
    TokenAuthorization token_authorization = 2 [deprecated = true];
    Swift swift = 3;
    Sepa sepa = 4;
    Ach ach = 5;
    Bank bank = 6;
    FasterPayments faster_payments = 9;
    Custom custom = 10;
  }

  map<string, string> metadata = 7 [(io.token.proto.extensions.field.redact) = true];
  AccountFeatures account_features = 8;
}
message AccountFeatures {
  bool supports_payment = 1;            // **DEPRECATED** can both send and receive payments
  bool supports_information = 2;        // can get info, e.g., get balance
  bool requires_external_auth = 3;
  bool supports_send_payment = 4;       // can send payments from account
  bool supports_receive_payment = 5;    // can receive payments to account
}
message TransferEndpoint {
  io.token.proto.common.account.BankAccount account = 1; // Account identifier, e.g., SWIFT transfer info
  CustomerData customer_data = 2;                        // Customer data: name and address
}
message CustomerData {
  option (io.token.proto.extensions.message.redact) = true;
  repeated string legal_names = 1;                   // Repeated in case of joint account holders.
  io.token.proto.common.address.Address address = 2; // Physical address
}
// Bank Account representation.
// There is more than one way to refer to a bank account;
// this structure's contents depend on the chosen reference.
{
  "token": {
    "memberId": "m:2xIIp:5mXgKRNd",
    "accountId": "a:7LAe9:5mXpRAcI"
  }
}

{
  "tokenAuthorization": {
    "authorization": {
      "bankId": "iron",
      "accounts": [{
        "ciphertext": "CBiZSBlbmNpcGhlcmVkIGF1dGggZGF0YQo="
      }]
    }
  }
}

{
  "swift": {
    "bic": "IRONUSCA000",
    "account": "iban:3814827"
  }
}

{
  "sepa": {
    "iban": "IE64BOFI90583812345678"
  }
}

{
  "ach": {
    "routing": "067014822",
    "account": "2589016585911"
  }
}

Transactions

A Transaction object represents a transaction (debit or credit) of an account. If the transaction is a Token transaction, it contains the ID of the corresponding Smart Token and the ID of the transfer in TokenOS. TransactionStatus represents which stage a given transaction has reached.

enum TransactionType {
  INVALID_TYPE = 0;
  DEBIT = 1;
  CREDIT = 2;
}

enum TransactionStatus {
  INVALID_STATUS = 0;                   // invalid status
  PENDING = 1;                          // the transaction is pending submission
  PROCESSING = 7;                       // the transaction is being processed
  SUCCESS = 2;                          // the transaction has been successful
  PENDING_EXTERNAL_AUTHORIZATION = 15;  // the transaction requires authorization by the user to complete
  FAILURE_CANCELED = 10;                // the transaction has been canceled, rolled back
  FAILURE_INSUFFICIENT_FUNDS = 3;       // the transaction has failed due to insufficient funds
  FAILURE_INVALID_CURRENCY = 4;         // the transaction has failed due to currency mismatch
  FAILURE_PERMISSION_DENIED = 6;        // the transaction has failed due to access violation
  FAILURE_QUOTE_EXPIRED = 11;           // the transaction has failed because the quote has expired
  FAILURE_INVALID_AMOUNT = 12;          // the transaction has failed due to invalid amount
  FAILURE_INVALID_QUOTE = 13;           // the transaction has failed due to invalid quote (wrong fx rate)
  FAILURE_EXPIRED = 14;                 // the transaction has failed to complete within alotted time
  FAILURE_GENERIC = 5;                  // the transaction has failed due to other reasons
  SENT = 16;                            // legacy transfers only: the transaction has been sent but has not been acknowledged by the bank
  INITIATED = 17;                       // the transaction has been initiated but the result is unknown, this may be the final status and may not get updated later
}

message Transaction {
  string id = 1;                                // Transaction ID
  TransactionType type = 2;                     // Debit or credit
  TransactionStatus status = 3;                 // Status. For example, SUCCESS or FAILURE_CANCELED
  io.token.proto.common.money.Money amount = 4; // Transaction amount.
  string description = 5 [(io.token.proto.extensions.field.redact) = true]; // Optional description

  string token_id = 6;                          // Points to the token, only set for Token transactions.
  string token_transfer_id = 7;                 // Points to the token transfer, only set for Token transactions.
  int64 created_at_ms = 8;                      // Creation time

  map<string, string> metadata = 9 [(io.token.proto.extensions.field.redact) = true]; // Additional fields. Optional.
}

message Balance {
  string account_id = 1;
  io.token.proto.common.money.Money current = 2;
  io.token.proto.common.money.Money available = 3;
  int64 updated_at_ms = 4;
  repeated TypedBalance other_balances = 5; // optional

  message TypedBalance {
    string type = 1;
    io.token.proto.common.money.Money amount = 2;
    int64 updated_at_ms = 3;
  }
}

enum RequestStatus {
  INVALID_REQUEST = 0;
  SUCCESSFUL_REQUEST = 1;     // success
  MORE_SIGNATURES_NEEDED = 2; // failed, needed to use a PRIVILEGED key
}
Transaction = {
  "id": "dd29e7b6ab953d5b3fb44c1dd9ac7efd",
  "type": "DEBIT",
  "status": "PROCESSING",
  "amount": {
    "currency": "EUR",
    "value": "12.34"
  },
  "description": "Sample transaction",
  "tokenId": "tt:gOkIOWYHK:5mXpRAcI",
  "tokenTransferId": "t:UpoJIJbvdOKXQWka:5mXpRAcI"
}

// Transaction Status
INVALID_STATUS
PENDING
PROCESSING
SUCCESS
FAILURE_INSUFFICIENT_FUNDS
FAILURE_INVALID_CURRENCY
FAILURE_PERMISSION_DENIED
FAILURE_GENERIC

Account Balance

A Balance Java object represents an account’s balance.

package io.token.sdk.api;

import com.google.auto.value.AutoValue;
import io.token.proto.common.transaction.TransactionProtos.Balance.TypedBalance;

import java.math.BigDecimal;
import java.util.List;

/**
 * Represents account balance.
 */
@AutoValue
public abstract class Balance {
    /**
     * Creates new balance instance.
     *
     * @param currency currency
     * @param available available balance
     * @param current current balance
     * @param updatedAtMs date of last update
     * @param otherBalances other optional typed balances
     * @return new balance instance
     */
    public static Balance create(
            String currency,
            BigDecimal available,
            BigDecimal current,
            long updatedAtMs,
            List<TypedBalance> otherBalances) {
        return new AutoValue_Balance(currency, available, current, updatedAtMs, otherBalances);
    }

    /**
     * Returns account currency.
     *
     * @return account currency
     */
    public abstract String getCurrency();

    /**
     * Returns available balance.
     *
     * @return available balance
     */
    public abstract BigDecimal getAvailable();

    /**
     * Returns current balance.
     *
     * @return current balance
     */
    public abstract BigDecimal getCurrent();

    /**
     * Returns date of last update.
     *
     * @return date of last update
     */
    public abstract long getUpdatedAtMs();

    /**
     * Returns list of other optional typed balances.
     *
     * @return list of other balances
     */
    public abstract List<TypedBalance> getOtherBalances();
}

Account Linking Service

TokenOS uses AccountLinkingService to get information about bank accounts a user wishes to link with a Member. A user has navigated the bank’s account linking web flow to authenticate as a bank customer and choose accounts to link. The user’s Token app received a bearer token string from that flow and passed that to TokenOS. TokenOS calls the AccountLinkingService getBankAuthorization method to get information about the chosen bank accounts: currency, supported features, etc.

This uses the API:

public interface AccountLinkingService {
    /**
     * Exchanges OAuth Implicit Grant access token for bank authorization payload.
     *
     * @param accessToken OAuth access token
     * @return bank authorization payload
     */
    BankAuthorization getBankAuthorization(String accessToken);
}

BankAccount Structure

A BankAccount structure describes a bank account.

When linking an account, the bank uses a BankAccount structure.

Later, when TokenOS refers to this account in requests, it will use the BankAccount value that the bank provided.

This structure has fields:

  • account: Account information useful for transfers. E.g., this can be SEPA transfer information.
  • account_features: Flags TokenOS uses to know which features the account supports.
  • metadata: string:string map.

Here, account is a “destination” of some inter-bank transfer method. A bank might support more than one such transfer method. When linking a bank account to TokenOS, an implementor might wonder: If we use SEPA information to link to TokenOS, how will TokenOS know this account accepts transfers via both SEPA and SWIFT? This happens via the resolveTransferDestination method, described later.

message BankAccount {
  // Token account Destination. Useful as source or destination
  // for a transfer; doesn't make sense for a bank to "link" this.
  message Token {
    string member_id = 1;
    string account_id = 2;
  }

  // Deprecated; unused
  message TokenAuthorization {
    option deprecated = true;
    io.token.proto.banklink.BankAuthorization authorization = 1;
  }

  // Source account managed by a coop bank
  message Bank {
    string bank_id = 1;
  }

  // SWIFT transfer
  message Swift {
    option (io.token.proto.extensions.message.redact) = true;
    string bic = 1;     // BIC code AAAABBCCDD
    string account = 2;
  }

  // SEPA transfer.
  message Sepa {
    option (io.token.proto.extensions.message.redact) = true;
    string iban = 1;    // International Bank Account Number
    string bic = 2;     // Bic code. Optional, except for non EEA countries
  }

  // ACH transfer
  message Ach {
    option (io.token.proto.extensions.message.redact) = true;
    string routing = 1; // Routing number
    string account = 2;
  }

  // Faster Payments Service transfer (UK)
  message FasterPayments {
    option (io.token.proto.extensions.message.redact) = true;
    string sort_code = 1;
    string account_number = 2;
  }

  // Custom authorization
  message Custom {
    option (io.token.proto.extensions.message.redact) = true;
    string bank_id = 1;
    string payload = 2;
  }

  oneof account {
    Token token = 1;
    TokenAuthorization token_authorization = 2 [deprecated = true];
    Swift swift = 3;
    Sepa sepa = 4;
    Ach ach = 5;
    Bank bank = 6;
    FasterPayments faster_payments = 9;
    Custom custom = 10;
  }

  map<string, string> metadata = 7 [(io.token.proto.extensions.field.redact) = true];
  AccountFeatures account_features = 8;
}
message AccountFeatures {
  bool supports_payment = 1;            // **DEPRECATED** can both send and receive payments
  bool supports_information = 2;        // can get info, e.g., get balance
  bool requires_external_auth = 3;
  bool supports_send_payment = 4;       // can send payments from account
  bool supports_receive_payment = 5;    // can receive payments to account
}

BankAuthorization structure

This structure contains information that links a member to BankAccount structures. The bank cryptographically signs these.

  • The Bank’s ID.
  • For each account to link:
    • BankAccount structure by which TokenOS should refer to this account later
    • A “pretty” account name, shown to the user.
    • The value of the memberId URL parameter mentioned before.
    • An expiration, in ms since Jan 1, 1970.

The BankAccountAuthorizer class eases creation and signing.

message BankAuthorization {
  string bank_id = 1;                                                 // Bank ID, e.g., "iron"
  repeated io.token.proto.common.security.SealedMessage accounts = 2; // Encrypted link info
}

Here, the SealedMessage “seals” a PlaintextBankAuthorization

message PlaintextBankAuthorization {
  string member_id = 1;    // Token member id
  string account_name = 2 [(io.token.proto.extensions.field.redact) = true]; // e.g., "Checking account with # ending 5678"
  BankAccount account = 3 [(io.token.proto.extensions.field.redact) = true]; // Account info by some method, e.g., SEPA
  int64 expiration_ms = 4; // Expiration time
}

Account Service

TokenOS uses AccountService methods to ask a bank for information about the bank’s customers and customer accounts.

Account Balance

TokenOS queries the bank for an account’s current balance, current and available. It might do this, for example, when the user lists their accounts in the Token client application.

This uses the API:

Balance getBalance(BankAccount account);

Transaction History

TokenOS queries the bank for an account’s recent transactions (getTransactions) or information about one transaction (getTransaction).

The getTransactions method is a “paged” query. It should only return up to limit transactions, starting from the most recent. It also returns offset, which marks the place in the full transaction history. The list of transactions together with the offset constitute a PagedList. For example, the offset could be the ID of the last transaction returned. Then, when TokenOS calls getTransactions with that transaction ID as the offset parameter, the implementation should skip that transaction as well as every transaction more recent than it. The offset parameter does not need to be comprehensible–only the bank needs to understand it.

TokenOS might call getTransactions because a customer views their transaction history in the Token mobile app. If, say, the user scrolls to the bottom of their transaction history, then TokenOS might make a series of paged getTransactions calls.

This uses the APIs:

default PagedList<Transaction, String> getTransactions(
        BankAccount account,
        String offset,
        int limit) {
Optional<Transaction> getTransaction(BankAccount account, String transactionId);
message Transaction {
  string id = 1;                                // Transaction ID
  TransactionType type = 2;                     // Debit or credit
  TransactionStatus status = 3;                 // Status. For example, SUCCESS or FAILURE_CANCELED
  io.token.proto.common.money.Money amount = 4; // Transaction amount.
  string description = 5 [(io.token.proto.extensions.field.redact) = true]; // Optional description

  string token_id = 6;                          // Points to the token, only set for Token transactions.
  string token_transfer_id = 7;                 // Points to the token transfer, only set for Token transactions.
  int64 created_at_ms = 8;                      // Creation time

  map<string, string> metadata = 9 [(io.token.proto.extensions.field.redact) = true]; // Additional fields. Optional.
}

Transfer Destination Choices

A bank might support more than one way of transferring funds; e.g., a bank might support both SEPA and SWIFT. When TokenOS “plans” a transfer, it asks the destination bank (the “credit” side) for a list of choices. TokenOS then gives this list of choices to the source bank (the “debit” side). The source bank can choose a transfer method.

When TokenOS calls this method, it passes in the BankAccount the bank used when linking the account.

This uses the APIs:

List<TransferEndpoint> resolveTransferDestination(BankAccount account);

Transfer Service

TokenOS can request a bank to transfer funds by means of SEPA, SWIFT, or FPS.

Token OS invokes only the payer’s bank’s Transfer Service.

TransferService has one required method, transfer that takes a Transfer object parameter.

To find out which account is debited, call the Transfer object’s getAccount method. It will return the BankAccount structure that linked the account to TokenOS.

This uses the API:

/**
 * Creates a transaction for the specified transfer request.
 *
 * @param transfer the token transfer request
 * @return bank transaction id for the requested transfer
 * @throws TransferException in case transfer fails
 */
String transfer(Transfer transfer) throws TransferException;

/**
 * Creates a batch of transactions for the specified transfer request that must either all
 * succeed or all fail. Implementation is only required if the bank writes a Smart Contract
 * that requests a batch of transfers to be executed.
 *
 * @param transfers the token transfer requests
 * @return bank transaction id for the requested transfers
 * @throws TransferException in case transfer fails
 */
default String batchTransfer(List<Transfer> transfers) throws TransferException {
    throw new StatusRuntimeException(UNIMPLEMENTED);
}

Notification Service

Token notifies members of important events. For example, if a merchant uses Token to ask a member for a payment, Token notifies the member to ask for approval. A Token client using the Token client SDK (different from the Token Bank Integration SDK) can register to deliver these notifications. We say it “subscribes” to notifications. Thus, if a Bank has some way to deliver notifications to a Token member, it can register to do so by subscribing to the member’s notifications. TokenOS uses the Bank Integration SDK’s NotificationService to push these notifications to the Bank. The Bank can then deliver these notifications to the member.

If a bank does not subscribe to notifications, then Token will not normally call the Bank’s NotificationService and it is fine if the Bank’s implementation does nothing.

The bank’s NotificationService has one method, notify. There are two parameters: a Subscriber with the information needed to deliver the notification; and a Notification with the notification’s content.

Calling subscriber.getHandlerInstructionsMap() gets the (optional) key-value pairs set when subscribing to the member’s notifications. Thus if the Bank subscribed to a member with handler instructions {"bankAccountNumber":"CH1234567"}, the Bank’s notify method might call subscriber.getHandlerInstructionsMap().get("bankAccountNumber") and contact the member accordingly.

This uses the API:

public interface NotificationService {
    void notify(Notification notification, Subscriber subscriber);
}

Onboarding

As a bank integrates with TokenOS, it needs to provide (and get) some information. If you’re setting up integration, you might not have all of this information ready at first; please provide it as you set it up.

Information Banks Provide

(This list last updated 2017-12-19)

Information Token Provides

Establishing TLS (SSL)

The communication between Token Bank Integration SDK and Token Cloud is secured by means of a mutual TLS authentication. As a part of bank onboarding process, Token provides an X.509 certificate the Bank’s server needs to trust. Likewise, Token requires an X.509 certificate from the Bank to establish mutual trust.

The sample implementation has scripts to generate certificates and code to use those certificates when communicating with the Token Cloud.

To generate a self-signed Server Certificate and key:

openssl req -x509 -newkey rsa:4096 -keyout key_temp.pem -out cert.pem -days 365

To convert private key to PKCS8 syntax:

openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in key_temp.pem -out key.pem

Once you have set up certificates and keys, see Server Builder to configure your server to use them.

Troubleshooting the Sample Bank

Jetty ALPN/NPN has not been properly configured.

Despite the wording of this gRPC error message, the sample bank doesn’t use Jetty. It uses netty’s Tomcat Native, netty-tcnative-boringssl-static. See its documentation for troubleshooting tips; e.g., on a Mac that doesn’t already have openssl installed, install openssl.

Testing TLS, Certs with openssl

When Token’s servers and a bank server “talk” with each other, they use TLS. The bank server has certificates from Token; Token servers have a certificate from the bank. Using these, the servers can communicate securely.

You can use the openssl tool to make sure that certificates are set up correctly. You’re trying to determine: if Token has these certificates from us, can their servers talk to ours?

To try this out:

# first go to config/tls directory in bank-sample-java project
$ cd ./config/tls

# If you haven't already, edit regenerate-certs
# and replace "demo.example.com" with your domain name
$ vim regenerate-certs

# If you haven't already, generate certificates for your
# domain:
$ ./regenerate-certs # you will get new “cert.pem” and “key.pem”

# Set up a directory for our client (a.k.a. “pretend token”) certificates
# (You can use whatever directory you like.)
$ mkdir ../tmp; cd ../tmp  

# run regenerate-certs to generate your client certificate
# (you should see “cert.pem” and “key.pem” in your current directory)
$  ../tls/regenerate-certs pretend.io 

# rename them for readability
$ mv cert.pem cert-client.pem ; mv key.pem key-client.pem

$ cat ../tls/cert.pem >> trusted-certs-client.pem # client, trust bank cert
$ cat ./cert-client.pem >> ../tls/trusted-certs.pem # bank, trust client cert

$ ...Deploy the sample server...
$ ...Remember to pass the --ssl command-line parameter...

# Now check the connection with openssl from the directory
# with client certificate:
$ openssl s_client -connect your domain name:your port number -CAfile trusted-certs-client.pem -cert cert-client.pem -key key-client.pem

The simple openssl s_client client program prints diagnostic information. Hopefully it says many things and finishes with Verify return code: 0 (ok) closed. If it doesn’t, it should print out something that helps track down the problem.

API

This is the network part of the Bank Integration API, the protocols by which TokenOS communicates with a Bank Server. It is shown in two ways, in two tabs. The first is gRPC protocol using protobuf to define and encode the messages. The second is the HTTP API with requests and responses in JSON. This network API is similar to, but not quite the same as, the API a bank implements. The Token Bank Integration SDK comes with some code that eases integration tasks. This code converts some data from protocol buffers to simpler structures.

Account API

Gets basic account information.

Get Transaction

Get information about one transaction.

message GetTransactionRequest {
  string transaction_id = 1;
  io.token.proto.common.account.BankAccount account = 2;
}
message GetTransactionResponse {
  io.token.proto.common.transaction.Transaction transaction = 1;
}

In AccountService

rpc GetTransaction (GetTransactionRequest) returns (GetTransactionResponse) {
  option (google.api.http) = {
      get: "/accounts/transactions?transaction_id={transaction_id}&account.swift.bic={account.swift.bic}&account.swift.account={account.swift.account}&account.sepa.iban={account.sepa.iban}&account.sepa.bic={account.sepa.bic}&account.ach.routing={account.ach.routing}&account.ach.account={account.ach.account}&account.faster_payments.sort_code={account.faster_payments.sort_code}&account.faster_payments.account_number={account.faster_payments.account_number}&account.custom.bank_id={account.custom.bank_id}&account.custom.payload={account.custom.payload}"
  };
}
GET '/transactions/{transaction_id}'

response = {
  "transaction": {
    "id": "dd29e7b6ab953d5b3fb44c1dd9ac7efd",
    "type": "DEBIT",
    "status": "PROCESSING",
    "amount": {
      "currency": "EUR",
      "value": "12.34"
    },
    "description": "Sample transaction",
    "tokenId": "tt:gOkIOWYHK:5mXpRAcI",
    "tokenTransferId": "t:UpoJIJbvdOKXQWka:5mXpRAcI",
    "createdAtMs": "1499301354256"
  }
}

Get Transactions

Gets transactions of an account. This can be limited only to accounts that have been enabled with Token. Banks can define whatever accountId format they want. (“checking1”, IBAN, or a proprietary format). Note that transactions are different than transfers.

message GetTransactionsRequest {
  io.token.proto.common.account.BankAccount account = 1;
  int32 integer_offset = 2 [deprecated=true];  // use offset instead
  int32 limit = 3;
  string offset = 4;
}
message GetTransactionsResponse {
  repeated io.token.proto.common.transaction.Transaction transactions = 1;
  string offset = 2;
}

In AccountService

rpc GetTransactions (GetTransactionsRequest) returns (GetTransactionsResponse) {
  option (google.api.http) = {
      get: "/accounts/transactions?offset={offset}&limit={limit}&account.swift.bic={account.swift.bic}&account.swift.account={account.swift.account}&account.sepa.iban={account.sepa.iban}&account.sepa.bic={account.sepa.bic}&account.ach.routing={account.ach.routing}&account.ach.account={account.ach.account}&account.faster_payments.sort_code={account.faster_payments.sort_code}&account.faster_payments.account_number={account.faster_payments.account_number}&account.custom.bank_id={account.custom.bank_id}&account.custom.payload={account.custom.payload}"
  };
}
GET "/accounts/{account.bic}/{account.account}/transactions?offset={offset}&limit={limit}"

response = {
  "transactions": [{
    "id": "dd29e7b6ab953d5b3fb44c1dd9ac7efd",
    "type": "DEBIT",
    "status": "PROCESSING",
    "amount": {
      "currency": "EUR",
      "value": "12.34"
    },
    "description": "Sample transaction",
    "tokenId": "tt:gOkIOWYHK:5mXpRAcI",
    "tokenTransferId": "t:UpoJIJbvdOKXQWka:5mXpRAcI",
    "createdAtMs": "1499301354256"
  }]
}

Get Balance

Fetches balance of an account

message GetBalanceRequest {
  io.token.proto.common.account.BankAccount account = 1;
}
message GetBalanceResponse {
  io.token.proto.common.money.Money current = 1;
  io.token.proto.common.money.Money available = 2;
  int64 updated_at_ms = 3;
  repeated io.token.proto.common.transaction.Balance.TypedBalance other_balances = 4; // optional
}

In AccountService

rpc GetBalance (GetBalanceRequest) returns (GetBalanceResponse) {
  option (google.api.http) = {
      get: "/accounts/balance?account.swift.bic={account.swift.bic}&account.swift.account={account.swift.account}&account.sepa.iban={account.sepa.iban}&account.sepa.bic={account.sepa.bic}&account.ach.routing={account.ach.routing}&account.ach.account={account.ach.account}&account.faster_payments.sort_code={account.faster_payments.sort_code}&account.faster_payments.account_number={account.faster_payments.account_number}&account.custom.bank_id={account.custom.bank_id}&account.custom.payload={account.custom.payload}"
  };
}
GET '/accounts/{account.bic}/{account.account}/balance'

response = {
  "current": {
    "currency": "EUR",
    "value": "123.45"
  },
  "available": {
    "currency": "EUR",
    "value": "123.45"
  }
}

Transfer API

Transfers money from one account to another, from a source account at the bank, and returns a transaction. This call is used for internal transfers (intra-bank) as well as push transactions (SEPA credit, FPS), as defined by the “method”.

message TransferRequest {
  string token_ref_id = 10;
  string transfer_ref_id = 11;
  string transfer_id = 1;
  io.token.proto.common.money.Money requested_amount = 2;
  io.token.proto.common.money.Money transaction_amount = 3;
  io.token.proto.common.account.BankAccount source = 4;
  repeated io.token.proto.common.transferinstructions.TransferEndpoint destinations = 5;
  string description = 9; // optional transfer description
  io.token.proto.common.transferinstructions.TransferInstructions.Metadata metadata = 12;
}
message TransferResponse {
  string transaction_id = 1;
  StatusCode status = 2;
  string status_description = 3;
}

In TransferService

rpc Transfer (TransferRequest) returns (TransferResponse) {
  option (google.api.http) = {
      post: "/transfers"
  };
}
POST '/transfers'

body = {
  "transferId": "t:I58bxf:5mXpRAcI",
  "requestedAmount": {
    "currency": "USD",
    "value": "100.00"
  },
  "transactionAmount": {
    "currency": "USD",
    "value": "100.00"
  },
  "source": {
    "swift": {
      "bic": "IRONUSCA000",
      "account": "iban:3814827"
    }
  },
  "destinations": [{
    "account": {
      "swift": {
        "bic": "IRONUSCA000",
        "account": "iban:3814827"
      }
    },
    "customerData": {
      "legalNames": ["John Smith"],
      "address": {
        "houseNumber": "332C",
        "street": "Cook St"
      }
    }
  }],
  "feeResponsibility": "SHARED_FEE",
  "description": "Sample Transfer"
}

response = {
  "transactionId": "458b10fd2a637fc289372baa428e8747",
  "status": "PROCESSING",
  "statusDescription": "PROCESSING"
}

Storage API

Some Banks store Token’s data about them. They do so via these protocols.

Set Value

Sets a value in a key value store. The key value store should map string keys to string values. This will be used for storing a small amount of bank related data. For example, the mapping between a Token accountId and an internal accountId.

message SetValueRequest {
  enum ContentCategory {
    INVALID = 0;
    ACCOUNT_INFO = 1;
    TOKEN_INFO = 2;
  }

  string key = 1;
  bytes value = 2 [(io.token.proto.extensions.field.redact) = true];
  ContentCategory category = 3;
}
message SetValueResponse {
  bytes previous = 1 [(io.token.proto.extensions.field.redact) = true];
}

In StorageService

rpc SetValue (SetValueRequest) returns (SetValueResponse) {
  option (google.api.http) = {
      put: "/storage/{key}"
  };
}
PUT "/storage/{key}"

body = {
  "key": "m:88Enz2:5zKtXEAq/a:UP4PwzxDuKbK:5zKcENpV",
  "value": "Wm05MWNpQm1hWFpsSUhOcGVBbz0=",
  "category": "ACCOUNT_INFO"
}

response = {
  "previous": "YjI1bElIUjNieUIwYUhKbFpRbz0="
}

Get Value

Gets a value in the key value store. The key value store should map string keys to string values. This will be used for storing a small amount of bank related data. For example, the mapping between a Token accountId and an internal accountId.

message GetValueRequest {
  string key = 1;
}
message GetValueResponse {
  bytes value = 1 [(io.token.proto.extensions.field.redact) = true];
}

In StorageService

rpc GetValue (GetValueRequest) returns (GetValueResponse) {
  option (google.api.http) = {
      get: "/storage/{key}"
  };
}
GET "/storage/{key}"

response = {
  "value": "Wm05MWNpQm1hWFpsSUhOcGVBbz0="
}

Remove Value

Removes a value from the key store.

message RemoveValueRequest {
  string key = 1;
}
message RemoveValueResponse {
}

In StorageService

rpc RemoveValue (RemoveValueRequest) returns (RemoveValueResponse) {
  option (google.api.http) = {
      delete: "/storage/{key}"
  };
}
DELETE "/storage/{key}"

response = {}

Notification API

Some Banks register to transmit notifications to their customers. These Banks do so via this protocol.

Notify

Requests that the bank relay a notification to a subscriber. For more information, see Notification Service.

syntax = "proto3";
package io.token.proto.common.notification;

import "banklink.proto";
import "security.proto";
import "token.proto";
import "member.proto";
import "extensions/field.proto";

option java_outer_classname = "NotificationProtos";
option csharp_namespace = "Tokenio.Proto.Common.NotificationProtos";

// Metadata for a notification
message DeviceMetadata {
  string application = 1;   // Name of the application to add keys to (e.g. Token, Chrome)
  string application_version = 2; // Application version (e.g. 2.0)
  string device = 3;  // Device the application resides on (e.g. Mac, iPhone X), to support multiple devices
  double longitude = 4 [(io.token.proto.extensions.field.redact) = true]; // Longitude of the user's location to signal where the request is coming from
  double latitude = 5 [(io.token.proto.extensions.field.redact) = true];  // Latitude of the user's location
}

// A notification to the payer that a transfer was successfully processed.
message PayerTransferProcessed {
  string transfer_id = 1; // transfer ID
}

// A notification to the payee that a transfer was successfully processed.
message PayeeTransferProcessed {
  string transfer_id = 1; // transfer ID
}

// A notification to the payer that a transfer failed.
message PayerTransferFailed {
  string transfer_id = 1; // transfer ID
}

// A generic notification that a transfer was successfully processed.
message TransferProcessed {
  string transfer_id = 1; // transfer ID
}

// A generic notification that a transfer failed.
message TransferFailed {
  string transfer_id = 1; // transfer ID
}

// A notification that a bank wants to be linked.
message LinkAccounts {
  io.token.proto.banklink.BankAuthorization bank_authorization = 1;
}

// A notification to step up / endorse a token.
// E.g., perhaps user tried to endorse in browser with only LOW-privilege
// key available but needs a HIGH-privilege key signature.
message StepUp {
  string token_id = 1; // ID of Token to endorse
}

// A notification to step up a get balance(s) request
message BalanceStepUp {
  repeated string account_id = 1; // Account IDs
}

// A notification to step up a transaction request
message TransactionStepUp {
  string account_id = 1;     // Account ID
  string transaction_id = 2; // Transaction ID
}

// A notification to notify a member that a recovery process has completed
message RecoveryCompleted {}

// A notification that a key wants to be added to a member. Clients should timeout the notification
// and screen, once the expires_ms has passed
message AddKey {
  int64 expires_ms = 3;                       // Expiration time
  repeated io.token.proto.common.security.Key keys = 4; // List of new keys to add
  DeviceMetadata device_metadata = 5;
}

// A notification that a bank wants to be linked, and keys want to be added.
message LinkAccountsAndAddKey {
  LinkAccounts link_accounts = 1;
  AddKey add_key = 2;
}

// A notification to request a payment
message PaymentRequest {
  io.token.proto.common.token.TokenPayload payload = 1; // requested payment
}

// A notification that a token was cancelled
message TokenCancelled {
  string token_id = 1; // Token ID
}

message EndorseAndAddKey {
  option deprecated = true;
  io.token.proto.common.token.TokenPayload payload = 1;
  AddKey add_key = 2;
  string token_request_id = 3;    // Optional token request ID
  string bank_id = 4;             // Optional bank ID
  string state = 5;               // Optional token request state for signing
  io.token.proto.common.member.ReceiptContact contact = 6;    //Optional receipt contact
}

// A notification that a token needs to be created/endorsed
message CreateAndEndorseToken {
  io.token.proto.common.token.TokenRequestPayload payload = 1;
  io.token.proto.common.token.TokenRequestOptions options = 2;
  AddKey add_key = 3;                                               // Optional key to add
  io.token.proto.common.member.ReceiptContact contact = 4;          // Optional receipt contact
}

// A notification to indicate that a previously sent notification was invalidated
message NotificationInvalidated {
  string previous_notification_id = 1;
}

// A data that goes in a NotifyRequest
message NotifyBody {
  oneof body {
    io.token.proto.common.notification.PayerTransferProcessed payer_transfer_processed = 1;
    io.token.proto.common.notification.LinkAccounts link_accounts = 2;
    io.token.proto.common.notification.StepUp step_up = 3;
    io.token.proto.common.notification.AddKey add_key = 4;
    io.token.proto.common.notification.LinkAccountsAndAddKey link_accounts_and_add_key = 5;
    io.token.proto.common.notification.PayeeTransferProcessed payee_transfer_processed = 6;
    io.token.proto.common.notification.PaymentRequest payment_request = 7;
    io.token.proto.common.notification.PayerTransferFailed payer_transfer_failed = 8;
    io.token.proto.common.notification.TransferProcessed transfer_processed = 9;
    io.token.proto.common.notification.TransferFailed transfer_failed = 10;
    io.token.proto.common.notification.TokenCancelled token_cancelled = 11;
    io.token.proto.common.notification.BalanceStepUp balance_step_up = 12;
    io.token.proto.common.notification.TransactionStepUp transaction_step_up = 13;
    io.token.proto.common.notification.EndorseAndAddKey endorse_and_add_key = 14 [deprecated = true];
    io.token.proto.common.notification.RecoveryCompleted recovery_completed = 15;
    io.token.proto.common.notification.NotificationInvalidated notification_invalidated = 16;
    io.token.proto.common.notification.CreateAndEndorseToken create_and_endorse_token = 17;
  }
}

// The status returned when sending a notification. ACCEPTED means the notification was initiated, but
// not necessarily successfully delivered
enum NotifyStatus {
  INVALID = 0;
  ACCEPTED = 1;
  NO_SUBSCRIBERS = 2;
}

// The record of a notification. Retrieved from notification service
message Notification {
  // A unique ID given to this notification
  string id = 1;

  // The subscriber that was or will be notified
  string subscriber_id = 2;

  // Contents of the notification
  NotificationContent content = 3;

  enum Status {
    INVALID = 0;
    PENDING = 1;
    DELIVERED = 2;
    COMPLETED = 3;      // The member has completed the notification
    INVALIDATED = 4;    // the notification has been invalidated
  }
  Status status = 4;
}

// The contents of a notification that was sent or will be sent
message NotificationContent {
  string type = 1;
  string title = 2;
  string body = 3;
  string payload = 4;
  int64 created_at_ms = 5;
  string loc_key = 6;
  repeated string loc_args = 7;
}
message NotifyRequest {
  io.token.proto.common.notification.Notification notification = 1;
  io.token.proto.common.subscriber.Subscriber subscriber = 2;
}
message NotifyResponse {}

In NotificationService

rpc Notify (NotifyRequest) returns (NotifyResponse) {
  option (google.api.http) = {
      post: "/notifications"
  };
}
POST "/nofifications"

body = {
  "notification": {
    "id": "ZVPhQLaYnQMcWYcNjMYT",
    "subscriberId": "5MDo6YbGu6M0qRRBZECl",
    "content": {
      "type": "LINK_ACCOUNTS",
      "title": "Add accounts to Token",
      "body": "Confirm adding your Iron Bank accounts to Token",
      "payload": "Zm91ciBmaXZlIHNpeAo=",
      "createdAtMs": "1499301354256",
      "locKey": "LINK_ACCOUNTS",
      "locArgs": ["Iron Bank"]
    },
    "status": "PENDING"
  }
}

response = {}

Copyright © 2018 Token, Inc. All Rights Reserved