Insights
Overview
Insights are AI-generated financial observations and recommendations produced by the API when strategy runs analyze a user’s transaction history. Each insight contains a structured payload with a title, body text, and optional recommended action.
Insights come in two forms:
- Passive insights — informational observations (e.g. “Your grocery spending increased 18% this month”). No user action required.
- Actionable insights — recommendations with a call to action (e.g. “Switch your subscription billing to your rewards card to save $12/month”). The user can accept or dismiss these.
Insights are generated asynchronously during strategy runs. Once created, they appear in the user’s insight feed and can be filtered, expanded with related transaction data, and acted upon.
Quick Start
1. List insights
curl /v0/insights/ \
-H "x-juno-jwt: <token>"
Returns a paginated list of insights for the authenticated user (200 OK).
2. Read a single insight with transactions
curl "/v0/insights/42?expand=transactions" \
-H "x-juno-jwt: <token>"
Returns the insight details with the transactions that triggered it (200 OK).
3. Accept an actionable insight
curl -X POST /v0/insights/42/accept \
-H "x-juno-jwt: <token>"
Transitions the insight to accepted status (200 OK).
4. Dismiss an actionable insight
curl -X POST /v0/insights/42/dismiss \
-H "x-juno-jwt: <token>"
Transitions the insight to dismissed status (200 OK).
Insight Model
Each insight contains:
{
"insight": {
"id": 42,
"user_strategy_run_id": 7,
"call_to_action": true,
"status": "pending",
"actioned_at": null,
"payload": {
"title": "Reduce subscription overlap",
"body": "You have two active streaming subscriptions billed within 3 days of each other. Consolidating could save you money.",
"icon": "credit-card",
"category": "spending",
"expected_benefit": {
"amount": 1499,
"period": "monthly",
"label": "Save $14.99/mo"
},
"action": {
"type": "cancel_subscription",
"params": { "subscription_id": "sub_abc123" }
},
"if_accepted": "We'll flag this subscription for cancellation review."
},
"created_at": 1706140800000,
"transactions": null
},
"_links": {
"self": { "href": "/v0/insights/42" },
"user": { "href": "/v0/user/me" },
"transactions": { "href": "/v0/insights/42?expand=transactions" },
"strategy-run": { "href": "/v0/user/strategies/str_ext_1/runs/run_ext_7" },
"strategy": { "href": "/v0/user/strategies/str_ext_1" },
"accept": { "href": "/v0/insights/42/accept", "method": "POST" },
"dismiss": { "href": "/v0/insights/42/dismiss", "method": "POST" }
}
}
| Field | Type | Description |
|---|
id | integer | Unique insight identifier |
user_strategy_run_id | integer or null | The strategy run that generated this insight |
call_to_action | boolean | true if the insight is actionable, false if passive |
status | string | Current lifecycle state (see Status Values) |
actioned_at | integer or null | Epoch milliseconds when the user accepted or dismissed |
payload | object or null | Structured insight content (see Payload Types) |
created_at | integer | Epoch milliseconds when the insight was generated |
transactions | array or null | Related transactions (only present when ?expand=transactions is used) |
Payload Types
Insight payloads follow one of two schemas depending on how the insight was generated.
AI Insight Payload — generated by strategy runs analyzing transaction data:
| Field | Type | Description |
|---|
title | string | Short headline for the insight |
body | string or null | Detailed explanation |
icon | string or null | Icon identifier for display |
category | string or null | Topic category (e.g. "spending", "savings") |
expected_benefit | object or null | Projected savings — contains amount (integer, minor units), period, and label |
action | object or null | Recommended action — contains type (string) and optional params (object) |
if_accepted | string or null | Human-readable description of what happens on accept |
Onboarding CTA Payload — generated during onboarding to guide users toward features:
| Field | Type | Description |
|---|
insight | string | The insight message |
call_to_action | string or null | CTA button text |
app_action | string or null | In-app navigation target (open_banking_connect, create_budget, explore_strategies) |
source | string or null | Origin identifier |
Endpoints
List Insights
Returns a paginated list of the authenticated user’s insights, ordered by creation date (newest first).
Query parameters:
| Parameter | Type | Default | Description |
|---|
call_to_action | boolean | — | Filter by actionable (true) or passive (false) |
status | string | — | Filter by status: pending, accepted, dismissed, expired |
offset | integer | 0 | Pagination offset |
limit | integer | 20 | Results per page (1—100) |
Response: 200 OK
{
"insights": [
{
"id": 42,
"user_strategy_run_id": 7,
"call_to_action": true,
"status": "pending",
"actioned_at": null,
"payload": {
"title": "Reduce subscription overlap",
"body": "You have two active streaming subscriptions...",
"category": "spending"
},
"created_at": 1706140800000,
"transactions": null
},
{
"id": 41,
"user_strategy_run_id": 7,
"call_to_action": false,
"status": "pending",
"actioned_at": null,
"payload": {
"title": "Grocery spending up 18%",
"body": "Your grocery spending increased compared to the previous month.",
"category": "spending"
},
"created_at": 1706140700000,
"transactions": null
}
],
"total": 2,
"offset": 0,
"limit": 20,
"_links": {
"self": { "href": "/v0/insights?offset=0&limit=20" },
"user": { "href": "/v0/user/me" }
}
}
Pagination links (prev, next) appear when applicable and preserve any active filters.
Get Insight
GET /v0/insights/{insight_id}
Returns a single insight by ID.
Query parameters:
| Parameter | Type | Default | Description |
|---|
expand | string (repeated) | — | Pass expand=transactions to include related transaction data |
Response: 200 OK
{
"insight": {
"id": 42,
"user_strategy_run_id": 7,
"call_to_action": true,
"status": "pending",
"actioned_at": null,
"payload": {
"title": "Reduce subscription overlap",
"body": "You have two active streaming subscriptions billed within 3 days of each other.",
"category": "spending",
"expected_benefit": {
"amount": 1499,
"period": "monthly",
"label": "Save $14.99/mo"
},
"action": { "type": "cancel_subscription", "params": { "subscription_id": "sub_abc123" } },
"if_accepted": "We'll flag this subscription for cancellation review."
},
"created_at": 1706140800000,
"transactions": [
{
"external_id": "txn_ext_101",
"type": "debit",
"status": "settled",
"direction": "outgoing",
"source": "bank",
"amount": 1499,
"merchant_name": "StreamCo",
"category": "entertainment",
"created_at": 1706054400000,
"updated_at": 1706054400000
}
]
},
"_links": {
"self": { "href": "/v0/insights/42" },
"user": { "href": "/v0/user/me" },
"transactions": { "href": "/v0/insights/42?expand=transactions" },
"accept": { "href": "/v0/insights/42/accept", "method": "POST" },
"dismiss": { "href": "/v0/insights/42/dismiss", "method": "POST" }
}
}
Accept Insight
POST /v0/insights/{insight_id}/accept
Marks an actionable insight as accepted. Sets status to "accepted" and records actioned_at.
Response: 200 OK — returns the updated insight.
Only insights with call_to_action: true and status: "pending" can be accepted. Attempting to accept a passive insight returns 400 Bad Request. Attempting to accept an already-actioned insight returns 409 Conflict.
Dismiss Insight
POST /v0/insights/{insight_id}/dismiss
Marks an actionable insight as dismissed. Sets status to "dismissed" and records actioned_at.
Response: 200 OK — returns the updated insight.
The same constraints apply as accept: the insight must have call_to_action: true and status: "pending". Non-actionable insights return 400, already-actioned insights return 409.
Integration Patterns
Building an Insight Feed
- Fetch insights with
GET /v0/insights/?limit=20
- Render each insight using
payload.title and payload.body
- For insights where
call_to_action is true and status is "pending", show accept/dismiss buttons
- When the user taps accept or dismiss, call the corresponding endpoint
- The response contains the updated insight — re-render in place
insights = GET /v0/insights/?limit=20
for insight in insights:
render(insight.payload.title, insight.payload.body)
if insight.call_to_action and insight.status == "pending":
on_accept → POST /v0/insights/{id}/accept
on_dismiss → POST /v0/insights/{id}/dismiss
Filtering Actionable Insights
To show only pending items that need user attention:
curl "/v0/insights/?call_to_action=true&status=pending" \
-H "x-juno-jwt: <token>"
Expanding Transactions
When displaying insight detail, expand the related transactions to show what triggered the insight:
curl "/v0/insights/42?expand=transactions" \
-H "x-juno-jwt: <token>"
Each transaction in the array includes asset, wallet, account, and card data when available.
Navigating to Strategy Runs
Actionable insights include _links to the strategy and strategy run that generated them. Use these to let users view the full analysis context:
_links.strategy — the strategy configuration
_links.strategy-run — the specific run that produced this insight
These links are present when the insight was generated by a strategy run (i.e. user_strategy_run_id is not null).
Handling Onboarding CTAs
During onboarding, the API may generate insights with OnboardingCtaPayload containing an app_action field. Use this to route the user to the appropriate in-app screen:
app_action | Description |
|---|
open_banking_connect | Navigate to the bank connection flow |
create_budget | Navigate to budget creation |
explore_strategies | Navigate to the strategy catalog |
Error Handling
All error responses follow RFC 7807 Problem Details format:
{
"type": "https://api.sumvin.com/errors/ins-404-001",
"title": "Insight Not Found",
"status": 404,
"detail": "Insight 42 not found",
"instance": "/v0/insights/42",
"error_code": "INS-404-001"
}
| Status | Error Code | Meaning | Recovery |
|---|
| 400 Bad Request | INS-400-001 | Insight is not actionable (passive insights cannot be accepted/dismissed), or invalid status filter | Check call_to_action is true before attempting accept/dismiss |
| 401 Unauthorized | — | Missing or invalid authentication token | Re-authenticate |
| 403 Forbidden | INS-403-001 | Insight belongs to a different user | Verify the insight ID belongs to the authenticated user |
| 404 Not Found | INS-404-001 | Insight does not exist or has been deleted | Verify the insight ID; re-fetch the insight list |
| 409 Conflict | INS-409-001 | Insight has already been accepted or dismissed | Check status before attempting to act; no further action needed |
Reference Tables
Status Values
| Value | Description |
|---|
pending | Insight has not been acted upon |
accepted | User accepted the recommended action |
dismissed | User dismissed the insight |
expired | Insight expired without user action |
HAL Links
| Link | Method | Condition | Description |
|---|
self | GET | Always | Fetch this insight |
user | GET | Always | Fetch the owning user |
transactions | GET | Always | Fetch insight with expanded transactions |
strategy-run | GET | When generated by a strategy run | Fetch the originating strategy run |
strategy | GET | When generated by a strategy | Fetch the strategy configuration |
accept | POST | When call_to_action is true and status is "pending" | Accept the insight |
dismiss | POST | When call_to_action is true and status is "pending" | Dismiss the insight |
Query Parameters (List Endpoint)
| Parameter | Type | Default | Description |
|---|
call_to_action | boolean | — | true for actionable, false for passive |
status | string | — | One of: pending, accepted, dismissed, expired |
offset | integer | 0 | Number of items to skip |
limit | integer | 20 | Items per page (1—100) |
Next Steps