Skip to main content
Skip table of contents

Getting Started - Checks

Introduction

Checks in finAPI Data Intelligence are quick verifications of transactions or accounts with a specific focus on a single decision.

To generate a check, a data source is always required.

Prerequisites

TL:TR

Used Systems

See Environments page for detailed information.

Used Endpoints

To create a report, at least the following endpoints are required:

Description

HTTP Method

Process Controller Endpoint

Link to API Doc

Synchronize Data Source

POST

/api/v1/dataSources/bankConnections/synchronization

finAPI API Documentation

Get status of the Data Source
(for polling if no callback was used)

GET

/api/v1/dataSources/{dataSourceId}/status

finAPI API Documentation

One (or more) of the following checks

Create a Account Holder Type Check

POST

/api/v1/checks/accountHolderType

finAPI API Documentation

Create a Balance Check

POST

/api/v1/checks/balancesCheck

finAPI API Documentation

Create a Children Check

POST

/api/v1/checks/childrenCheck

finAPI API Documentation

Create a Top List Check

POST

/api/v1/checks/topListCheck

finAPI API Documentation

Process Overview

cURL Example

Translated into cURL it looks like the following:

Step 1 - Synchronize Data Source (cURL)

This step is an extract from the Guide for Cases. More information can be found at Getting Started - Synchronize a Data Source.

Before you can synchronize data in Data Intelligence, it must already have been imported via finAPI WebForm 2.0 or finAPI OpenBanking Access.

For synchronization, it is recommended that the callback is used to avoid unnecessary poll requests that can stress client and server systems.

Synchronization request example for full synchronization of the user:

BASH
curl --location 'https://di-sandbox.finapi.io/api/v1/dataSources/bankConnections/synchronization' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer <access_token>' \
--data '{
  "callbackUrl": "https://domain.tld",
  "callbackHandle": "38793e87-499d-4860-b947-2c2c8ab10322"
}'

The response looks like this:

JSON
{
  "bankConnections": [
    {
      "dataSourceId": "4e760145-2e65-4242-ac33-488943528c93",
      "creationDate": "2020-01-01 00:00:00.000",
      "lastUpdate": "2020-01-01 00:00:00.000",
      "externalId": 123456,
      "bic": "COBADEFFXXX",
      "bankName": "Commerzbank",
      "bankStatus": "IN_PROGRESS",
      "updateRequired": false,
      "accounts": [
        {
          "accountId": "5f660145-2e65-4242-ac33-488943528c93",
          "creationDate": "2020-01-01 00:00:00.000",
          "lastUpdate": "2020-01-01 00:00:00.000",
          "externalId": 123456,
          "iban": "DE13700800000061110500",
          "accountType": "CHECKING",
          "status": "UPDATED"
        }
      ]
    }
  ]
}

Step 1.5 - Polling the status of the Data Source (cURL)

It is not recommended to poll the status. If you still want to do it, please make sure that there is at least a 200ms pause between requests.

Request to the Data Source status:

BASH
curl --location 'https://di-sandbox.finapi.io/api/v1/dataSources/{dataSourceId}/status' \
--header 'Authorization: Bearer <access_token>'

The response looks like this:

JSON
{
  "status": "IN_PROGRESS",
  "code": "SYNC_IN_PROGRESS",
}

As long as the status is not SUCCESSFUL, you cannot proceed with report creation.

Step 2 - Create a Check (cURL)

A Check requires a data source as input to know, which accounts should be checked.

Request for creating a Check:

BASH
curl --location 'https://di-sandbox.finapi.io/api/v1/checks/balancesCheck' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer <access_token>' \
--data '{
  "dataSourceId": "4e760145-2e65-4242-ac33-488943528c93",
  "checkAmount": 100,
  "includeOverdraft": false
}'

The result looks like this:

JSON
{
  "checkData": [
    {
      "checkPassed": true,
      "latestBalanceDate": "2023-01-01 00:00:00.000",
      "overdraftAvailable": false,
      "accountId": "4e760145-2e65-4242-ac33-488943528c93",
      "accountIban": "DE13700800000061110500"
    }
  ]
}

Implementation Guide

See a full working project here: finAPI Data Intelligence Product Platform Examples (Bitbucket)
Code from this guide can be found here: finAPI Data Sources(Bitbucket), finAPI Checks (Bitbucket)
Environment overview can be found here: Environments

The guidelines and the example project are written in Kotlin with Spring Boot. But it is easily adoptable for other languages and frameworks.
For the HTTP connection, we are using here plain Java HttpClient, to be not restrictive for the client selection and that everything is transparent.

Only the actual functionality is discussed in this guideline. Used helper classes or the models can be looked up in the source code.

However, we always recommend using a generated client from our API, which reduces the effort of creating models and functions to access the endpoints and eliminates potential sources of error.

To learn how to generate and apply the API client using the Process Controller "Create user and exchange with access_token" as an example, please see the Getting Started - Code Generator (SDK) Integration.

Please also note that the code presented here and also the repository is only an illustration of a possible implementation. Therefore, the code is kept very simple and should not be used 1:1 in a production environment.

Step 1 - Synchronize Data Source

This step is an extract from the Guide for Cases. More information can be found at Getting Started - Synchronize a Data Source.

Data sources are a central component and not just bound to reports. Therefore we create them in a neutral datasourcespackage: finAPI Data Sources(Bitbucket) (Data Source)

In this step, we create a simple service that starts the synchronization. In addition, we use a callback so that we don't have to poll for synchronization status.

First, we create a new class named DataSourceService. This gets the URL of finAPI Data Intelligence (incl. the base path /api/v1) and the domain of the own service for the callback.

As further classes we get the DataSourcesRepository and DiProcessesRepository passed. These classes are example database repositories. In the example, these classes are not discussed further because they only perform the required tasks in the example.

Last but not least we have an ObjectMapper which is used to map JSON into objects and vice versa.

The first function we create is createPostRequest(). This is passed the user access_token, the URI of finAPI Data Intelligence, and the body.

From the URI and the diURL, which we get via the constructor and the configuration, the method builds the final URL.

KOTLIN
@Service
class DataSourceService(
    @Value("\${finapi.instances.dataintelligence.url}") private val diUrl: String,
    @Value("\${exampleapp.instances.callback.url}") private val callbackUrl: String,
    private val dataSourcesRepository: DataSourcesRepository,
    private val diProcessesRepository: ReportProcessesRepository,
    private val objectMapper: ObjectMapper
) {
    /**
     * Create a POST HttpRequest.
     */
    private fun createPostRequest(
        accessToken: String,
        endpointUri: String,
        body: String
    ): HttpRequest {
        return HttpRequest.newBuilder()
            // build URL for https://<data_intelligence><endpointUri>
            .uri(URI.create("${diUrl}${endpointUri}"))
            // set Content-Type header to application/json
            .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
            .header(HttpHeaders.AUTHORIZATION, "Bearer $accessToken")
            // add the body
            .POST(
                // use empty body to sync all accounts
                HttpRequest.BodyPublishers.ofString(body)
            ).build()
    }

    companion object {
        private val log = KotlinLogging.logger { }
    }
}

Next, we add to the class the method that will start the synchronization.

Since synchronizing a Data Source is an asynchronous process, we must first create an internal processId which we will use as a callbackHandle.
We can use this later to identify and continue our process again via the callback.

Since some calls require parameters, we should also store these at the processId before synchronizing the data source, so that the callback knows directly what it has to do.

For this we have the object DiProcessEntity, which we pass to the syncDataSource() function.

You can find the code of those classes in the repository.

After that, we send the request and use the previously created function createPostRequest() to create the request.

The result is checked in this example with a simple static function to see if it matches 2xx. Otherwise, an exception is thrown and the process is completely aborted.

If this check is also successful, the body of the response is mapped into an object, and the data is saved.

In this example, we save the access_token as well, because we use it later for the callback.
In a production application, this should be encrypted so that no one can gain access to the client's data.

The syncDataSource() method gets the callbackPath from outside.

Reason for that is, that we can create different callback paths for different scenarios (e.g. Reports or Checks).

KOTLIN
@Service
class DataSourceService(
    @Value("\${finapi.instances.dataintelligence.url}") private val diUrl: String,
    @Value("\${exampleapp.instances.callback.url}") private val callbackUrl: String,
    private val dataSourcesRepository: DataSourcesRepository,
    private val diProcessesRepository: DiProcessesRepository,
    private val objectMapper: ObjectMapper
) {
    /**
     * Synchronize the data sources.
     */
    @Throws(RuntimeException::class)
    fun syncDataSource(
        accessToken: String,
        callbackPath: String,
        processEntity: DiProcessEntity
    ): BankConnectionsResultModel {
        val client = HttpClient.newBuilder().build()

        // create the body for the request
        val body = DataSourceSyncRequestModel(
            callbackUrl = URI("${callbackUrl}${callbackPath}"),
            callbackHandle = processEntity.processId
        )

        // create a request object and send it to DI
        val response = client.send(
            // create request URI for https://<data_intelligence>/api/v1/dataSources/bankConnections/synchronization
            createPostRequest(
                accessToken = accessToken,
                endpointUri = CreateAndGetReportService.URI_DATA_SOURCE,
                body = objectMapper.writeValueAsString(body)
            ),
            HttpResponse.BodyHandlers.ofString()
        )

        // check for status code is 2xx or log and throw an exception
        StatusCodeCheckUtils.checkStatusCodeAndLogErrorMessage(
            response = response,
            errorMessage = "Unable to sync data source."
        )

        // return the object of the mapped result
        val bankConnectionsResult = objectMapper.readValue(response.body(), BankConnectionsResultModel::class.java)

        // save all data sources and the current token to the database.
        // the token is required when the asynchronous request of the callback is received to continue the process.
        if (bankConnectionsResult.bankConnections != null) {
            processEntity.status = EnumProcessStatus.SYNC_STARTED

            val dataSources: ArrayList<DataSourceEntity> = ArrayList()
            bankConnectionsResult.bankConnections.forEach {
                dataSources.add(
                    DataSourceEntity(
                        userId = accessToken,
                        dataSourceId = it.dataSourceId
                    )
                )
            }
            val dataSourceEntities = dataSourcesRepository.saveAll(dataSources)
            processEntity.dataSources.addAll(dataSourceEntities)

            // save the accessToken, so that the callback can continue.
            // please encrypt this in reality!
            processEntity.accessToken = accessToken
            diProcessesRepository.save(processEntity)
        }
        log.info("[${processEntity.processId}] Synchronization with Data Intelligence started")
        return bankConnectionsResult
    }

    [...]
}

With this, we would now be able to create the Data Sources.

Example to sync data sources for Reports:

KOTLIN
// create a process entity, which will be stored, if the sync call was successful
val processEntity = DiProcessEntity(
    processId = UUID.randomUUID().toString(),
    status = EnumProcessStatus.SYNC_CREATED
)

// start synchronizing data sources
dataSourceService.syncDataSource(
    accessToken = accessToken,
    callbackPath = "${ReportCallbackApi.URI_BASEPATH}${ReportCallbackApi.URI_CB_DATASOURCE_REPORTS}",
    processEntity = processEntity
)

Example to sync data sources for Checks with parameters:

KOTLIN
// create a process entity, which will be stored, if the sync call was successful
val processEntity = DiProcessEntity(
    processId = UUID.randomUUID().toString(),
    status = EnumProcessStatus.SYNC_CREATED,
    amount = BigDecimal.ONE
)

// start synchronizing data sources
dataSourceService.syncDataSource(
    accessToken = accessToken,
    callbackPath = "${CheckCallbackApi.URI_BASEPATH}${CheckCallbackApi.URI_CB_DATASOURCE_CHECKS}",
    processEntity = processEntity
)

It is important to note that we are referring to an implementation with callback here. Without callback, the state of the data source must be polled. This should always happen with a pause of at least 200ms. Only when the status is SUCCESSFUL may the creation of a case be continued.

Step 2 - Create a Check

Now it is time to create the check.

First, we create a CheckService which gets the DiProcessesRepository and the ObjectMapper.

Via the DiProcessRepository we can later get the values we need for the check.

The first function in the class is again to create a request object.

KOTLIN
@Service
@Suppress("TooGenericExceptionCaught")
class CheckService(
    @Value("\${finapi.instances.dataintelligence.url}") private val diUrl: String,
    private val diProcessesRepository: DiProcessesRepository,
    private val objectMapper: ObjectMapper
) {
    /**
     * Create a POST HttpRequest.
     */
    private fun createPostRequest(
        accessToken: String,
        endpointUri: String,
        body: String
    ): HttpRequest {
        return HttpRequest.newBuilder()
            // build URL for https://<data_intelligence><endpointUri>
            .uri(URI.create("${diUrl}${endpointUri}"))
            // set Content-Type header to application/json
            .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
            .header(HttpHeaders.AUTHORIZATION, "Bearer $accessToken")
            // add the body
            .POST(
                // use empty body to sync all accounts
                HttpRequest.BodyPublishers.ofString(body)
            ).build()
    }

    companion object {
        private val log = KotlinLogging.logger { }
        const val URI_CHECK_BALANCE = "/checks/balancesCheck"
    }
}

Next, we create the function createBalanceCheck().

This first loads the entity via the processId (which we used as callbackHandle) to get the data.

In the case of the balance check, we always need an amount. If the entity or the amount is empty, we abort here with an exception.

From the passed data and the amount we now create a BalanceCheckRequestModel to be able to make the request correctly.

After that, the request is created and sent using the createPostRequest() function created before.

We now check the response with the StatusCodeCheckUtils if it corresponds to the value 2xx. Otherwise, an exception is thrown.
In this case, we still write the status of our process into the database and rethrow the exception.

If all this has been successful, we can map the result of the check and process it further.
We also store the successful status in the process table so that any frontend processes can continue.

Now the result of the check can be saved or otherwise processed and the check itself is completed.

KOTLIN
@Service
@Suppress("TooGenericExceptionCaught")
class CheckService(
    @Value("\${finapi.instances.dataintelligence.url}") private val diUrl: String,
    private val diProcessesRepository: DiProcessesRepository,
    private val objectMapper: ObjectMapper
) {
    /**
     * Create a balance check.
     */
    @Throws(RuntimeException::class)
    @Suppress("TooGenericExceptionThrown")
    fun createBalanceCheck(
        accessToken: String,
        processId: String,
        dataSourceId: String,
        amount: BigDecimal,
        includeOverdraft: Boolean? = null
    ) {
        log.info("[${processId}] Starting create a check...")
        val client = HttpClient.newBuilder().build()

        // get the process entity
        val processEntity = diProcessesRepository.findFirstByProcessId(processId = processId)
        if (processEntity?.amount == null) {
            throw RuntimeException("Unable to create balance check. No Process Entity or amount found.")
        }

        // create body for case
        val body = BalanceCheckRequestModel(
            dataSourceId = dataSourceId,
            checkAmount = processEntity.amount!!,
            includeOverdraft = includeOverdraft
        )

        // send request
        val response = client.send(
            // create request URI for https://<data_intelligence>/api/v1/checks/balancesCheck
            createPostRequest(
                accessToken = accessToken,
                endpointUri = URI_CHECK_BALANCE,
                body = objectMapper.writeValueAsString(body)
            ),
            HttpResponse.BodyHandlers.ofString()
        )

        // check for status code is 2xx or log and throw an exception
        try {
            StatusCodeCheckUtils.checkStatusCodeAndLogErrorMessage(
                response = response,
                errorMessage = "Unable to create a balance check."
            )
        } catch (ex: RuntimeException) {
            processEntity.status = EnumProcessStatus.CHECK_FAILED
            diProcessesRepository.save(processEntity)
            throw ex
        }

        // map the result
        val checkResponse = objectMapper.readValue(response.body(), BalanceCheckResponseModel::class.java)

        // save successful state for check creation
        processEntity.status = EnumProcessStatus.CHECK_FINISHED
        diProcessesRepository.save(processEntity)

        log.info("[${processId}] Finished create a balance check...")

        // save the result of the check in the database and/or inform the UI about the result
    }

    [...]
}

In the last step, we now only need to create the callback so that the flow works completely.

Step 2.5 - Callback API for the Check

The callback is relevant to avoid possible polling for the data source and to start the Check creation as fast as possible.

Basically, the callback service in the Check context is similar to the callback service for Reports.

However, we need a different URL here, since we do not want to run Reports, but Checks.

Now that all domain-oriented calls are ready, the callback can be implemented.

The callback is sent as a POST request. Therefore we build a @RestController with a @PostMapping to our base URL.

Since we get the callback handle in the body we don`t need a dynamic URL for handling different users. However, you should be able to clearly distinguish from other callbacks and only handle callbacks for Check creation here.

Our code must first check if the body contains a callbackHandle, which is our created processId. If it does not, we cannot associate it with any synchronization and must abort.
For this, we throw an exception so that this can at least be logged.

After that, we fetch the process object belonging to the callback handle from the database and check again if an instance was found. If this is null an exception is also thrown.

As a last validation of the input data we check if the request.datasource.status is set to SUCCESSFUL (see validateDataSourceRequest() function).

If this was successful, the status of the Data Source is updated.

It may happen that multiple Data Sources need to be updated.

Therefore, we need a function that checks in the database all Data Sources belonging to the handle whether they all have the status SUCCESSFUL.
In this example, this has been greatly simplified by only setting the callbackReceived flag to true should the status of the Data Source be SUCCESSFUL.
The isAllCallbacksReceived() function then simply checks through whether all data sources belonging to the process have received their callback.

Only if this condition is successful, the Check is generated.

KOTLIN
@RestController
class CheckCallbackApi(
    private val checkService: CheckService,
    private val dataSourcesRepository: DataSourcesRepository,
    private val diProcessesRepository: DiProcessesRepository
) {
    @PostMapping("${URI_BASEPATH}${URI_CB_DATASOURCE_CHECKS}")
    @Suppress("TooGenericExceptionThrown")
    fun dataSourceCallbackForChecks(@RequestBody body: CallbackRequestModel) {
        // if the body does not contain a callback handle we throw an exception,
        // because we cannot assign it to another request
        if (body.callbackHandle == null) {
            throw RuntimeException("No callback handle provided.")
        }

        // if we could not find the handle we do the same
        val processEntity = diProcessesRepository.findFirstByProcessId(
            processId = body.callbackHandle
        ) ?: throw RuntimeException("Unable to find the callback handle.")

        // validate if the request contains a successful state or something else
        if (validateDataSourceRequest(body)) {
            // that we've received a data source
            val dataSources = processEntity.dataSources
            dataSources.forEach {
                if (it.dataSourceId == body.datasourceId) {
                    it.callbackReceived = true
                    dataSourcesRepository.save(it)
                }
            }

            // if everything was updated, we can create the check
            if (isAllCallbacksReceived(dataSources = dataSources)) {
                // figure out all data source ids
                processEntity.dataSources.forEach {
                    // call the "create balance check" function
                    checkService.createBalanceCheck(
                        accessToken = processEntity.accessToken!!,
                        processId = body.callbackHandle,
                        dataSourceId = it.dataSourceId,
                        amount = BigDecimal.ONE,
                    )
                }
            }
        } else {
            log.error("""
                [${body.callbackHandle}] has received the status [${body.datasource?.status}]
                with the code [${body.datasource?.code}]
                and message [${body.datasource?.message}]
                """.trimIndent()
            )
            // if the callback was not successful, we store it as error status
            processEntity.status = EnumProcessStatus.CALLBACK_FAILED
            diProcessesRepository.save(processEntity)
        }
    }

    private fun isAllCallbacksReceived(dataSources: List<DataSourceEntity>): Boolean {
        var result = true
        dataSources.forEach {
            if (!it.callbackReceived) {
                result = false
            }
        }

        return result
    }

    private fun validateDataSourceRequest(request: CallbackRequestModel): Boolean {
        return (request.callbackHandle != null &&
                request.datasourceId != null &&
                request.datasource != null &&
                EnumStatus.SUCCESSFUL == request.datasource.status)
    }

    companion object {
        private val log = KotlinLogging.logger { }
        const val URI_BASEPATH = "/callbacks"
        const val URI_CB_DATASOURCE_CHECKS = "/dataSource/checks"
    }
}
JavaScript errors detected

Please note, these errors can depend on your browser setup.

If this problem persists, please contact our support.