Saventa

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 like EUR), 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 https and 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.