Account Information

How to start using the Exthand:Gateway with this nuget package?

  1. Register a user.

Before being able to get transactions or initiate payement, you have to send to Exthand:Gateway (E:G) information about your user (PSU). See PSU registration in Gateway.
For AIS, we require first name, last name, date of birth, email address and version of the Terms and Conditions accepted by the PSU.

🚧

Multiple users with same email or mobile phone.

We use the email address or mobile phone to identify uniquely the PSU. If you call multiple times the CreateUserAsync method with the same email address, you'll always receive the same userContext.

Call GetTCAsync

Retrieves the latest version of the Terms and Conditions and Privacy Notice.
If doing AIS, you have to show or provide a link to those two files, and collect the consent (click on checkbox). The consent means: Me, as a PSU, I accept Exthand shares my banking data with your company.
If doing PIS, forget this, consent is not required.

Once you get the consent of the PSU, you have to register him on E:G.

Call CreateUserAsync

See above to know how much data you have to provide to this method.
This will return a UserRegisterResponse object.
Normal case, action property should be == "OK", then you have to store the userContext property with your PSU data.
You will need userContext for all operations, it's important to store it attached to your user and be able to provide it to E/G.

  1. Getting Access to Bank Account (AIS)

Account Information is a 5 steps process:

  • You get the bank list supported for a given country.
  • You get the bank connector behaviour (options) by calling GetBankAccessOptionsAsync
  • You call the Init SCA method and provide a callback URL.
  • You receive most of the time a redirection URL, redirect the user and wait for your callback URL to be called.
  • Finalize the SCA.

Getting the bank list.

Call GetBanksAsync with a countryCode like 'BE', 'FR', etc (ISO Code 2 chars). This call will send you back a list of banks supported for the given country.

Call GetBankAccessOptionsAsync

This call returns information, that helps you build the request access object for a specific connector (you got the connectorID from the bank list previously received).

Options received will mainly indicate you if you have to provide none, one or multiple IBAN accounts when initiating the SCA process toward the bank. (see: https://docs.exthand.com/reference/get_ob-ais-access-options-connectorid )

Call RequestBankAccessAsync

To be able to get a list of transactions or balances for one or more bank accounts, you have to get a token (consent) from the end user (PSU). That consent is given by the bank after a successful authentication and validation process (SCA).

This call needs a BankRequestAccess object completed as follow.

The goal of this call is to initiate the request for bank account access. If successful, you will receive a BankAccessResponse object.

List<BankAccount> bankAccounts = new List<BankAccount>();
bankAccounts.Add(new BankAccount()
{
    currency = "EUR",
    iban = "BE931234567890123"
});

BankAccessRequest bankAccessRequest = new BankAccessRequest()
{
    connectorId = 1, // ING in Belgium
    userContext = person.userContext, // Previously stored userContext you received when registrting the user.
    tppContext = new TppContext()
    {
        TppId = _options.TPPName, // Your name
        App = _options.AppName,  // Your app name
        Flow = flow.Id.ToString()  // Id if your internal flow for debugging on our side.
    },
    accountsAccessRequest = new AccountsAccessRequest()
    {
        flowId = flow.Id.ToString(),
        redirectUrl = redirectURL, // Your redirection URL, called after sucessfull SCA on bank side.
        psuIp = IP,
        singleAccount = new BankAccount()
        {
            currency = "EUR",
            iban = iban.BankAccount,
        },
        transactionAccounts = bankAccounts,
        balanceAccounts = bankAccounts
    }
};

BankAccessResponse bankAccessResponse = await _gatewayService.RequestBankAccessAsync(bankAccessRequest);

BankAccessResponse object contains a resultStatus code which indicates either if an error occured, if you have to redirect the user, etc.

Save the FlowContext included in the result. It will be needed in the next step.
Most of the time, Status should be REDIRECT, and redirect url can be found in dataString property like in the following example:

switch ((ResultStatus)flow.ResponseInitStatus)
{
  case ResultStatus.UNKNOW:
    break;
  case ResultStatus.DONE:
    break;
  case ResultStatus.REDIRECT:
    return Redirect(flow.ResponseInitDataString);
  case ResultStatus.DECOUPLED:
    return RedirectToPage("/bank/handlerSCA", new { id = flow.Id });
  case ResultStatus.PASSWORD:
    return RedirectToPage("/bank/handlerSCA", new { id = flow.Id });
  case ResultStatus.MORE_INFO:
    return RedirectToPage("/bank/handlerSCA", new { id = flow.Id });
  case ResultStatus.SELECT_OPTION:
    return RedirectToPage("/bank/handlerSCA", new { id = flow.Id });
  case ResultStatus.ERROR:
    throw new BusinessException("BANKCTRL-01", "INIT SCA FAILED");
}

Once you redirect your user (PSU) to the bank, he's going out of your scope and you just have to wait for your callback url to be called.

❗️

Finding the correct flow ID

In your call back URL code, get the parameters and ask for the flowID by calling: FindFlowIdAsync like this

// We have to find the FlowId in the queryString of your callback URL.
string flowId = await _gatewayService.FindFlowIdAsync(queryString);

Call FinalizeRequestBankAccessAsync

This call will finalize the process of getting access (SCA) to bank accounts. You call it by passing a BankAccessRequestFinalize object like this:

BankAccessRequestFinalize bankAccessRequestFinalize = new()
{
  flow = flow.ResponseInitFlowContext, // Pay attention to the fact we are speaking now about FlowContext and not Flow's ID.
  tppContext = new()
  {
    TppId = _options.TPPName,
    App = _options.AppName,
    Flow = flow.Id.ToString()
    },
  userContext = person.userContext,
  dataString = queryString
  };

if (!string.IsNullOrEmpty(flow.ResponseFinalizeFlowContext))
  bankAccessRequestFinalize.flow = flow.ResponseFinalizeFlowContext;

BankAccessResponseFinalize bankAccessResponseFinalize = await _gatewayService.FinalizeRequestBankAccessAsync(bankAccessRequestFinalize);

Once the call executed, expect a BankAccessResponseFinalize object.

switch ((ResultStatus)flow.ResponseFinalizeStatus)
{
  case ResultStatus.UNKNOW:
    break;
  case ResultStatus.DONE:
    // This is Ok.
    // We retrieve the list of Bank accounts connected by the user.
    BankAccountsResponse bankAccountsResponse = await _exthandService.GetBankAccountsAsync(flow, user);

    foreach (BankAccountResponse bankAccountResponse in bankAccountsResponse.accounts)
    {
      // WE ADD HERE NEW BANK ACCOUNTS OR UPDATE CONSENT OF EXISTING ONE.
      await _bankService.AddUpdateIBANAsync(bankAccountResponse, user.Id, flow.CountryId, flow.BankId);
    }
    return RedirectToAction("Index");
  case ResultStatus.REDIRECT:
    return Redirect(flow.ResponseFinalizeDataString);
  case ResultStatus.DECOUPLED:
    return RedirectToPage("/bank/handlerSCA", new { id = flow.Id });
  case ResultStatus.PASSWORD:
    return RedirectToPage("/bank/handlerSCA", new { id = flow.Id });
  case ResultStatus.MORE_INFO:
    return RedirectToPage("/bank/handlerSCA", new { id = flow.Id });
  case ResultStatus.SELECT_OPTION:
    return RedirectToPage("/bank/handlerSCA", new { id = flow.Id });
  case ResultStatus.ERROR:
    throw new BusinessException("BANKCTRL-02","FINALIZE SCA FAILED (" + query + ")");
}

Where GetBankAccountsAsync is:

public async Task<BankAccountsResponse> GetBankAccountsAsync(Flow flow, Person user)
{
  BankAccountsRequest bankAccountsRequest = new()
  {
    connectorId = flow.BankId,
    tppContext = new TppContext()
    {
      TppId = _options.TPPName,
      App = _options.AppName,
      Flow = flow.Id.ToString()
      },
    userContext = user.userContext
    };

  return await _gatewayService.GetBankAccountsAsync(bankAccountsRequest);
}

Consent status of accounts

When you get a consent for an account, it should last 90 days (at time of writing).
You can find the limit of usage in structure sent back by the call to get accounts list, for transactions in accounts[x].transactionsConsent.validUntil and for balances in accounts[x].balancesConsent.validUntil.

The consent may be invalidated by the bank. To report it to you, we have added the fields status and statusAt.

status is an integer. A value of 0 means no problem so far with the consent. A value of 100 means that the consent is not valid anymore and should be renewed with the PSU. This is the only value for the moment.

statusAt is a date-time. A value of null (status is then 0) means no problem so far with the consent. If status is not 0, it contains the timestamp of the status change.

In the following example, there is one account with the same consent for balances and transactions (same consentId). The consent should have been valid until November 11, 2022 (validUntil) but it has been invalidated (status = 100) on the September 5, 2022 (statusAt).

Get Accounts example:

{
    "accounts": [
        {
            "id": "BE123456789EUR",
            "currency": "EUR",
            "iban": "BE123456789",
            "description": "JOHN DOE",
            "transactionsConsent": {
                "consentId": "azertyuiop",
                "validUntil": "2022-11-29T18:39:59",
                "status": 100,
                "statusAt": "2022-09-05T11:22:14Z"
            },
            "balancesConsent": {
                "consentId": "azertyuiop",
                "validUntil": "2022-11-29T18:39:59",
                "status": 100,
                "statusAt": "2022-09-05T11:22:14Z"
            }
        }
    ]
}
  1. Getting list of transactions.

Once you have a consent after the SCA phase, you're eligible to request list of transactions or get balances. Pay attention to the fact the end user (PSU) must confirm, during SCA, that you can get those information. PSU might give you a consent for accessing balances but not transactions and vice-versa.

PSD2 limitation and attended mode

PSD2 has defined a limitation in the number of time the balances or transactions can be fetched in a 24h timeframe. Currently, this limitation is 4 times in 24h.

In Direct mode, you have to be sure not to go over this limit. In Gateway mode, balances and transactions are fetched automatically by our system 3 times a day (8h15, 12h15 and 17h15 BE).

PSD2 allows also you to work in attended mode. This means that the end user is currently interacting with your app and you need to have fresher transactions or balances as  apposed to a batch or backend job fetching them on its own without the end user.
In this attended mode, there are no limitations.
To enable this mode, you have to give us the public IP address of the connected user in the field /accountsAccessRequest/psuIp when you call get transactions or get balances.

Call GetTransactionsAsync

To get a list of transactions, call GetTransactionsAsync and give it the bank account identifier and a TransactionRequest object as below:

transactionRequest = new()
{
  ConnectorId = 1,   // 1 For ING Belgium
  psuIp = ourIP, // IP Address of your end user or IP of your back end server.
  TppContext = new()
  {
      TppId = _options.TPPName,
      App = _options.AppName,
    Flow = flow.Id.ToString()
    },
  UserContext = person.userContext
  };

transactionResponse = await gatewayService.GetTransactionsAsync(iban.RemoteId, transactionRequest);

The answer will be a TransactionResponse object which is a paging object to browse pages of answers sent by the bank.

In the TransactionResponse, you'll find a Transaction object which is a bank statement.

🚧

UserContext reminder!

Every time the response object you get back from the server contains a userContext, you have to check if it's null or not. If not null, update it in your DB/Storage.

Browsing pages.

Browsing pages is done by:

  1. Checking if isLastPage of your TransactionResponse is True or not.
  2. Calling GetTransactionsNextAsync with a TransactionPagingRequest object quite similar to the TransactionRequest object plus the PagerContext.

You can find the PagerContext object in the TransactionResponse you received the step before.

while (!transactionResponse.isLastPage)
{

  transactionPagingRequest = new()
  {
    ConnectorId = 1,   // 1 For ING Belgium
    psuIp = ourIP, // IP Address of your end user or IP of your back end server.
    TppContext = new()
    {
        TppId = _options.TPPName,
        App = _options.AppName,
      Flow = flow.Id.ToString()
      },
    UserContext = person.userContext,
    PagerContext = transactionResponse.pagerContext
    };

  transactionResponse = await gatewayService.GetTransactionsNextAsync(iban.RemoteId, transactionPagingRequest);
  
  [...]
}

Transactions unique identifier and sort key (Gateway mode only)

We have added a unique identifier for each transaction. It is composed of two fields GwAccountId and GwSequence.

Those are null in TPP/Direct mode.

The unique identifier for one transaction is GwAccountId + GwSequence. You can use it to check which one you already have received.

🚧

Some banks return empty (null) transaction id. In some banks, it's not unique...

You'd better not count on it in you logic!!

GwSequence is composed of two integer side by side separated by a hyphen. It allows you to sort the transaction of the same GwAccountId in execution date descending order (the more recent transaction comes first). You always receive the transaction from the API in that order.

{
    "transactions": [
        {
            "id": "2022091420220914170550636925",
            "amount": -25.15,
            "GwAccountId": "AT1234567890EUR",
            "GwSequence": "79780626-000000",
            ...