Automation: API & webhooks
The Pro plan opens the app up to your own tooling. There are two halves: the API lets your scripts read and write your data on demand, and webhooks push events to you as they happen. Both live under Profile → Developer.
Personal API tokens
Create tokens under Profile → Developer. A token is a long secret starting with fin_pat_, shown once at creation — copy it straight into your script or password manager, because it can never be displayed again. If you lose it, revoke it and create another.
Each token has:
- A name, so you can tell them apart in the list ("Home dashboard", "n8n import job").
- An access level — read only, or read and write. Read-only tokens can fetch data but cannot add entries or run imports. Prefer read-only unless a script genuinely needs to write.
- An optional expiry, 1–3650 days. A token that expires in 90 days limits the damage if it ever leaks.
The list shows when each token was last used, which is the quickest way to spot one you no longer need. Revoking takes effect immediately. You can hold up to 10 active tokens.
Authentication
Send the token as a bearer credential on every request:
curl -H "Authorization: Bearer fin_pat_YOUR_TOKEN" \
https://api.saventa.app/v1/net-worth
Everything a token can reach lives under /v1, and a token only ever sees the account it belongs to. Requests are rate limited per token — 120 requests per minute by default; exceeding it returns 429 with a Retry-After header.
Account-level operations — billing, sessions, profile changes, deleting your account — are deliberately not reachable with a token. Those need a real sign-in, and a token that tries gets 403.
Response conventions
- JSON throughout, camelCase field names, UTF-8.
- Every monetary amount travels with its
currency(an ISO code likeEUR), so nothing is ambiguous. - Dates are
YYYY-MM-DD; timestamps are ISO 8601 UTC. - Fields may be added over time. Existing fields will not be renamed or removed without a new API version, so a script you write today keeps working.
Errors
400 |
Invalid input — check the message in the body. |
401 |
Token missing, unknown, revoked or expired. |
403 |
Endpoint not reachable with a token, write attempted with a read-only token, or your plan no longer includes API access. |
404 |
The resource doesn't exist, or isn't yours. |
429 |
Rate limit exceeded. Wait for Retry-After. |
Reading data
GET /v1/net-worth
Total net worth across every portfolio, plus a one-line summary of each.
currencyId |
integer | Currency to report in. Defaults to your preferred currency; 400 if you have neither. |
asOfDate |
date | Value the portfolios as of this date instead of today. |
{
"currency": "EUR",
"totalValue": 125430.50,
"contributionsValue": 98000.00,
"monthlyIncome": 412.30,
"yearlyIncome": 4947.60,
"returnOfInvestment": 0.28,
"realizedPnl": 5120.00,
"unrealizedPnl": 22310.50,
"portfolios": [ { "id": 12, "name": "Brokerage", "...": "..." } ]
}
GET /v1/portfolios
Every portfolio as a flat summary. Accepts asOfDate.
[
{
"id": 12,
"name": "Brokerage",
"currency": "EUR",
"category": "Brokerage",
"totalValue": 54210.00,
"contributionsValue": 40000.00,
"monthlyIncome": 0,
"yearlyIncome": 0,
"returnOfInvestment": 0.35,
"realizedPnl": 1200.00,
"unrealizedPnl": 13010.00,
"lastModifiedAt": "2026-07-23T09:14:02Z"
}
]
GET /v1/portfolios/{id}
One portfolio, same shape as above. 404 if it isn't yours.
GET /v1/portfolios/{id}/holdings
Positions and totals for a holdings-backed portfolio (brokerage, crypto, pension fund).
currencyId |
integer | Convert all values to this currency. Defaults to the portfolio's own. |
asOfDate |
date | Value the positions as of this date. |
{
"portfolioId": 12,
"currency": "EUR",
"totalMarketValue": 51000.00,
"totalCashValue": 3210.00,
"totalAccountValue": 54210.00,
"totalCostBasis": 40000.00,
"totalUnrealizedGainLoss": 11000.00,
"totalRealizedGainLoss": 1200.00,
"holdings": [
{
"asset": "SPY",
"market": "NYSE",
"quantity": 42,
"averageCostBasis": 380.00,
"totalCostBasis": 15960.00,
"currentPrice": 470.10,
"marketValue": 19744.20,
"unrealizedGainLoss": 3784.20,
"unrealizedGainLossPercent": 0.237,
"realizedGainLoss": 0,
"dividendsReceived": 210.40,
"currency": "USD"
}
]
}
GET /v1/income-calendar
Upcoming income, ordered by date.
monthsInAdvance |
integer | 1 |
How far ahead to look. |
includeDividends |
boolean | true |
Include projected asset dividends alongside portfolio income. |
[
{
"date": "2026-08-01",
"portfolioId": 8,
"portfolioName": "Rental flat",
"eventType": "Rent",
"eventSource": "Portfolio",
"label": "Monthly rent",
"amount": 850.00,
"currency": "EUR",
"asset": null
}
]
Dividend events carry portfolioId: 0 and eventSource: "Asset".
GET /v1/reports
Your generated report snapshots. Optional period — Monthly or Yearly.
[
{
"id": 31,
"period": "Monthly",
"periodStart": "2026-06-01",
"periodEnd": "2026-06-30",
"status": "Final",
"currency": "EUR",
"generatedAt": "2026-07-01T02:04:11Z"
}
]
Writing data
Both endpoints require a read-and-write token; a read-only one gets 403.
POST /v1/portfolios/{id}/entries
Adds a single entry to a portfolio.
type |
integer | 0 investment, 1 reinvestment, 2 payout, 8 withdrawal. |
value |
decimal | Amount, in the portfolio's currency. |
date |
timestamp | When it happened. |
action |
string | Optional label. |
curl -X POST https://api.saventa.app/v1/portfolios/12/entries \
-H "Authorization: Bearer fin_pat_YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"type": 0, "value": 250, "date": "2026-07-01T00:00:00Z"}'
Returns the portfolio's new totals:
{ "portfolioId": 12, "portfolioName": "Brokerage", "totalValue": 54460.00, "currency": "EUR" }
POST /v1/portfolios/{id}/import
Imports a broker statement — the automation counterpart of the import screen. Multipart form upload.
files |
The statement file(s). |
importSource |
Which broker format, e.g. xtb, tradeville, financesapi. |
timezoneId |
Optional IANA timezone for interpreting dates. |
curl -X POST https://api.saventa.app/v1/portfolios/12/import \
-H "Authorization: Bearer fin_pat_YOUR_TOKEN" \
-F "files=@statement.xlsx" \
-F "importSource=xtb"
Returns the same write result as above.
Webhooks
Under Profile → Developer, below your API tokens, you can register URLs to receive the same events your in-app notifications are built from: deposit maturities, savings and rent income, renewals, new reports and feedback updates. Each endpoint subscribes to whichever events you tick, so a single automation only receives what it cares about. You can register up to 5 endpoints.
Every endpoint has:
- A URL. It must be
httpsand reachable from the internet. - A signing secret, shown once when you create the endpoint (rotate it any time to get a new one).
- A pause switch, if you want to stop delivery without deleting the setup.
Send test delivers a synthetic event straight away and tells you exactly what your endpoint answered — a status code, or the error — so you can confirm your consumer end to end without waiting for a real event. Deliveries shows the recent attempts for that endpoint, which is usually enough to debug a misbehaving consumer without leaving the page.
The request we send
A POST with these headers:
X-Finances-Event |
The event type, e.g. DepositApproachingMaturity. |
X-Finances-Delivery |
Delivery id — use it to make your handler idempotent. |
X-Finances-Timestamp |
Unix seconds. |
X-Finances-Signature |
sha256=… — see below. |
{
"version": 1,
"event": "DepositApproachingMaturity",
"test": false,
"createdAt": "2026-07-23T01:30:00Z",
"data": {
"notificationId": 4211,
"title": "Deposit approaching maturity",
"message": "Deposit 'ING 12m' will mature on 2026-08-01.",
"eventDate": "2026-08-01",
"portfolioId": 12,
"portfolioDepositId": 8
}
}
test is true for a "send test event" delivery, which has no notificationId. The data fields present depend on the event — a report event carries no portfolioId, for instance.
Verifying the signature
Verify before trusting a request. Compute an HMAC-SHA256 over {timestamp}.{raw body}, keyed with your signing secret, and compare it to the header. The timestamp is inside the signature so a captured request can't be replayed later.
import hmac, hashlib
def is_valid(secret, timestamp, raw_body, signature):
expected = "sha256=" + hmac.new(
secret.encode(), f"{timestamp}.{raw_body}".encode(), hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
Use the raw request body, exactly as received — re-serializing the JSON changes the bytes and the signature won't match.
Retries and failures
Return a 2xx as soon as you have accepted the event, and do the slow work afterwards. Any other response, or a timeout (10s), is retried with growing gaps — 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours — then the delivery is marked failed.
If an endpoint keeps failing it is switched off automatically and you get an in-app notification about it, so you find out rather than quietly missing events. Fix the endpoint and hit Resume.
Availability
API tokens and webhooks are part of the Pro plan. On Free and Plus both sections show an upgrade prompt. If a Pro subscription lapses, existing tokens stop authenticating and webhook delivery stops — nothing is deleted, and it all resumes when the plan does.