Insights
Insights gives you a structured financial profile of a user, derived from their bank transactions. Instead of working with raw transaction data, you receive pre-calculated statistics and high-level indicators that are ready to use directly in your business logic — credit decisions, affordability assessments, mortgage eligibility checks, or any other financial evaluation.
What you can build with this
Insights turns a user's raw bank transactions into a structured financial summary — aggregated amounts per category per time window, ready to use directly in your application.
A typical output looks like this:
| Category | July | August | September | Total | Count | Average per transaction | … |
|---|---|---|---|---|---|---|---|
| Salary | €2,841 | €2,921 | €2,980 | €8,742 | 3 | €2,914 | |
| Mortgage repayment | −€1,150 | −€1,150 | −€1,150 | −€3,450 | 3 | −€1,150 | |
| Groceries | −€412 | −€388 | −€396 | −€1,196 | 14 | −€85 | |
| … |
You choose the time window and the category scope. Insights handles the aggregation.
Requesting insights
GET {baseUrl}/enrichment/insights?type=user&categoryId=Gambling%20%26%20lotteries
Authorization: Bearer <access token for the user>
All query parameters are optional and can be combined:
| Query parameter | Values | Meaning |
|---|---|---|
type |
user | account |
user: all of the user's accounts together. account: one insight per account. Without it, both are returned. |
accountId |
an account id | Only the insight of this account. |
categoryId |
a configured category id | Only this category. * is the total over all categories. |
periodId |
a period id (see Time windows) or YYYY-MM |
Only this period. An unknown period returns an empty list, not an error. |
categoryId and periodId filter the categories and their statistics only. financialIndicators are always returned in full, regardless of these filters.
Request insights once insightsStatus on the account status endpoint is COMPLETED, and only use an insight whose processing.status is COMPLETED (see Status). The response contains a pagination object that is currently always empty: all matching insights are returned in a single response.
What the API returns
Here is a complete response for a user, filtered to all transactions (categoryId=*) in December 2025:
GET /enrichment/insights?type=user&categoryId=*&periodId=2025-12
{
"data": [
{
"schemaVersion": 3,
"insightId": "8b1f0c2e-4a7d-4e1b-9c55-2f6a1d0e7b31",
"type": "user",
"userId": "7d41c9e2-1b3a-4f60-8e2d-5a9c0b7f3e14",
"tenantId": "tenant-a",
"tenantAppId": "app-1",
"tenantUserId": "customer-48213",
"accountIds": [
"31b0a6d4-9e2f-4c71-b8a3-0d5e6f7a8b9c",
"8c2e4f1a-7b3d-4e9c-a6f0-1d2e3c4b5a69"
],
"providers": ["ING", "RABOBANK"],
"created": "2026-10-13T07:41:02.000Z",
"lastUpdated": "2026-10-13T07:41:02.000Z",
"processing": {
"status": "COMPLETED",
"created": "2026-10-13T07:40:11.000Z",
"lastUpdated": "2026-10-13T07:41:02.000Z"
},
"financialIndicators": [
{
"name": "INCOME_SALARY_OR_PENSION",
"periodId": "12-MONTHS",
"period": {
"id": "12-MONTHS",
"type": "PERIOD",
"unit": "months",
"amount": 12,
"fullPeriodsOnly": false
},
"result": 3650.0
}
],
"categories": [
{
"id": "*",
"type": "external",
"statistics": [
{
"categoryId": "*",
"categoryType": "external",
"periodId": "2025-12",
"period": {
"type": "INDIVIDUAL-MONTHS",
"unit": "months",
"amount": 1,
"fullPeriodsOnly": false,
"periodStart": "2025-12-01",
"periodEnd": "2025-12-31",
"firstTransactionDate": "2025-12-01T00:00:00.000Z",
"lastTransactionDate": "2025-12-31T00:00:00.000Z"
},
"transactions": {
"count": 74,
"negativeCount": 61,
"positiveCount": 13,
"minAmount": -1250.0,
"maxAmount": 2950.0,
"totalAmount": 237.13,
"averageAmount": 3.2,
"totalPositiveAmount": 3650.0,
"averagePositiveAmount": 280.77,
"totalNegativeAmount": -3412.87,
"averageNegativeAmount": -55.95
},
"recurringTransactions": {
"count": 9,
"negativeCount": 8,
"positiveCount": 1,
"minAmount": -1250.0,
"maxAmount": 2950.0,
"totalAmount": 962.5,
"averageAmount": 106.94,
"totalPositiveAmount": 2950.0,
"averagePositiveAmount": 2950.0,
"totalNegativeAmount": -1987.5,
"averageNegativeAmount": -248.44
}
}
]
}
]
}
],
"pagination": {}
}
data contains one insight per scope: one for the user and, without a type filter, one per account. Each entry in categoriesrepresents one transaction category. Within it, statistics contains one entry per time window. Each statistic gives you the aggregated figures for that category in that period.
In this example, the user received 13 incoming payments in December, totalling €3,650. Only one of them — €2,950 — was identified as recurring. If you are assessing structural income, recurringTransactions.totalPositiveAmount gives you €2,950, not €3,650. The remaining €700 arrived as incidental deposits.
How to read the response
User level vs. account level
Insights are available at two scopes:
- User level (
type: user) — statistics aggregated across all connected bank accounts. Use this for a complete financial picture. A user insight lists the included accounts inaccountIdsand the banks inproviders. - Account level (
type: account) — statistics scoped to a single account. Use this when you need to analyse one bank separately, for example to distinguish a salary account from a savings account. An account insight contains a singleaccountIdandprovider.
Filter using the type and accountId query parameters. Without type, both the user insight and the account insights are returned.
Categories
Which categories an insight contains is configured per tenant. A category is either:
- internal — Ockto's own classification, or
- external — your own category scheme.
The type of each category (and categoryType of each statistic) tells you which one it is. The category * is always the total over all transactions.
Example configuration:
| Category id | Meaning |
|---|---|
* |
Total over all transactions |
Gambling & lotteries |
Gambling and lottery spend |
Debt collection, Bailiff & seizure |
Debt collection and bailiff payments |
Recurring savings transfer, To savings account, From savings account |
Savings |
To investment account, From investment account |
Investments |
To crypto account, From crypto account |
Crypto |
Category ids can contain spaces and special characters, so URL-encode them in the categoryId parameter — for example Gambling%20%26%20lotteries.
Time windows
Every statistic is linked to a periodId that defines the time window it covers. You can request all periods at once or filter to one with the periodId query parameter. Which periods are available is configured per tenant. The default set is:
periodId |
Window |
|---|---|
YYYY-MM |
One statistic per calendar month, for up to the last 12 completed months. The current month is left out; months without transactions are filled with zeros. |
90-DAYS |
Rolling 90 days |
180-DAYS |
Rolling 180 days |
12-MONTHS |
Rolling 12 months |
FULL-HISTORY |
All collected data, from the first to the last booking date |
The period object on each statistic describes the window:
| Field | Meaning |
|---|---|
type |
INDIVIDUAL-MONTHS (monthly statistics), PERIOD (rolling windows) or FULL-HISTORY |
unit / amount |
Length of the window, for example days / 90 or months / 1 |
fullPeriodsOnly |
Whether only complete periods are included |
periodStart / periodEnd |
First and last day of the window, both inclusive. Use these to see exactly which dates are covered. Can be null for FULL-HISTORY on an account without transactions. |
firstTransactionDate / lastTransactionDate |
Date and time of the first and last transaction within the window, or null if there are none |
The difference between the two pairs matters: periodStart and periodEnd tell you which dates the aggregation covers, while firstTransactionDate and lastTransactionDate tell you how much actual data falls inside that window.
All transactions vs. recurring
Each statistic contains two parallel sets of figures:
transactions— all transactions in the category and period, andrecurringTransactions— only the transactions identified as recurring.
The difference matters when you want to distinguish structural patterns from incidental ones. A salary that arrives every month appears identically in both sets. A one-off payment of the same amount only appears in transactions. Use recurringTransactions when structural regularity is what you are testing for.
Available statistics per category
For each category and period, both transactions and recurringTransactions contain:
| Field | Meaning |
|---|---|
count |
Number of transactions |
positiveCount / negativeCount |
Number of incoming / outgoing transactions |
minAmount / maxAmount |
Smallest and largest amount |
totalAmount |
Net sum of all amounts (income + expenses) |
averageAmount |
Mean amount per transaction |
totalPositiveAmount / averagePositiveAmount |
Sum and mean of incoming amounts |
totalNegativeAmount / averageNegativeAmount |
Sum and mean of outgoing amounts (negative) |
Amounts are null when there are no transactions to aggregate — for example averagePositiveAmount in a category with only expenses, or all amounts in a month without transactions.
Financial indicators
Besides category statistics, each insight contains financialIndicators: high-level figures calculated over a given period. For example:
"financialIndicators": [
{
"name": "INCOME_SALARY_OR_PENSION",
"periodId": "12-MONTHS",
"period": {
"id": "12-MONTHS",
"type": "PERIOD",
"unit": "months",
"amount": 12,
"fullPeriodsOnly": false
},
"result": 3650.0
}
]
Each indicator has a name, the periodId and period it covers, and a result. Depending on the indicator, result is a number, a boolean, a string or null.
Financial indicators are not affected by the categoryId and periodId filters: they are always returned in full.
Example: combining statistics
Because every statistic has the same shape, you can combine them directly. Requesting categoryId=Gambling%20%26%20lotteries returns, among others, this statistic for December 2025:
{
"categoryId": "Gambling & lotteries",
"categoryType": "external",
"periodId": "2025-12",
"period": {
"type": "INDIVIDUAL-MONTHS",
"unit": "months",
"amount": 1,
"fullPeriodsOnly": false,
"periodStart": "2025-12-01",
"periodEnd": "2025-12-31",
"firstTransactionDate": "2025-12-07T00:00:00.000Z",
"lastTransactionDate": "2025-12-25T00:00:00.000Z"
},
"transactions": {
"count": 2,
"negativeCount": 2,
"positiveCount": 0,
"minAmount": -30.0,
"maxAmount": -20.4,
"totalAmount": -50.4,
"averageAmount": -25.2,
"totalPositiveAmount": 0,
"averagePositiveAmount": null,
"totalNegativeAmount": -50.4,
"averageNegativeAmount": -25.2
},
"recurringTransactions": {
"count": 0,
"negativeCount": 0,
"positiveCount": 0,
"minAmount": null,
"maxAmount": null,
"totalAmount": null,
"averageAmount": null,
"totalPositiveAmount": null,
"averagePositiveAmount": null,
"totalNegativeAmount": null,
"averageNegativeAmount": null
}
}
Combined with the * statistic for the same month from the response above, gambling spend in December is €50.40 of €3,650.00 incoming (1.4%).
Status
Insights are calculated after the full enrichment pipeline — categorisation and recurring detection — has completed for all of a user's accounts. The calculation is triggered automatically — you do not need to initiate it.
Checking readiness
The cleanest way to know when insights are available is the account status endpoint (GET /processing-status). Its insightsStatus field tells you where the calculation stands:
insightsStatus |
Meaning |
|---|---|
NOT_STARTED |
The calculation has not started yet |
PROCESSING |
Calculation is in progress — do not use yet |
COMPLETED |
Insights are available |
FAILED |
Calculation could not complete |
Insights are only available once insightsStatus is COMPLETED. There is no webhook for insight completion: poll the status endpoint until it reaches COMPLETED or FAILED.
Status of an insight
Each insight also carries its own processing.status. For a user insight, this is the lowest status across the user's accounts, so it only becomes COMPLETED once every account is done.
processing.status |
Meaning |
|---|---|
RETRIEVING |
Transactions are still being retrieved |
PROCESSING |
Transactions are being enriched or calculated |
COMPLETED |
The insight is complete and ready to use |
ERROR |
Processing failed |
Always check that processing.status is COMPLETED before using the data.

