API reference · v1
v1 is the new canonical Aleta API and the default track for every new integration. It runs on a fresh host, returns errors in a different format, and is the first version with proper per-tenant isolation — one customer can no longer impact another's request budget.
Base URL
Every path in v1 is relative to https://api.aleta.ai. v1 lives on its own host, separate from the legacy v2 and v3 prefixes — you can run a migration with both stacks live side-by-side.
https://api.aleta.ai
curl https://api.aleta.ai/v1/clients \
-H "Authorization: Bearer {access_token}"
What changed from v2 / v3
v1 is a clean rewrite, not an incremental bump. The deltas worth knowing before you start.
One surface, one hostReplaces both legacy URL prefixes with a single canonical surface on its own host. No more split-prefix mental model.
RFC 7807 errorsEvery error response is a problem-details payload with the same shape on every endpoint, sent as
Content-Type: application/problem+json. Legacy emits a JSON:API errors envelope; v1 doesn't.Per-tenant isolationRate limiting and concurrency budgets are scoped to the calling tenant, not the global pool. A misbehaving integration can't degrade anyone else's traffic.
Cursor paginationA list answers
data,next_cursorandlimit. Passnext_cursorback ascursorfor the next page; it isnullon the last page. No page-numbered offsets, no shifting results when data changes mid-walk.
Resource groups
v1 is organised around Aleta's domain model rather than generic CRUD slices. Start with Clients — every other resource is scoped to a client.
ClientsClients own the data. Inside a client, the entity structure captures ownership; lenses capture how the client wants to view it. A lens groups cash accounts and custody accounts; cash accounts are where transactions are booked; custody accounts describe where the assets physically sit.
Custodian integrationHow wealth data lands on the platform. Cash accounts and custody accounts are linked to a custodian via an authority granted through an auth flow. The platform owns the consent lifecycle.
DataHoldings, transactions, and performance — what most integrators read out. They operate against lenses, the consolidated views the client has configured.
AssetsEverything you can hold — listed securities, private assets, derivatives, cash positions. Time-series data hangs off each asset.
PlatformWorkspace identity (users, the access grants that say what a user can see, and the groups and modules that say what a user can do there), event delivery (webhook subscriptions), and the tenant itself, which only Aleta closes.
Authentication
An API key (a public_key credential, registered by Aleta) is exchanged for an access token with the OAuth 2.0 JWT-Bearer grant (RFC 7523), the same exchange legacy uses. Sign an assertion JWT with the key's ECDSA P-256 private key (ES256, the key's kid in the header; iss and sub as the credential's issuer and subject; aud https://api.aleta.ai/oauth/token; iat and exp at most 120 seconds apart, exp in the future, iat not in the future; nbf is optional), POST it as a form (application/x-www-form-urlencoded) to https://api.aleta.ai/oauth/token with grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer, and send the access_token as a bearer. No jti is needed: each assertion is taken once, and sending it again answers invalid_grant. The token starts with aleta_at_, lives expires_in seconds (3600) and works on v1, and on v2 and v3 at api.aleta.ai/api/v2 and /api/v3. An assertion with aud https://auth.aleta.io is taken too.
A refused exchange answers 400 with an OAuth error (invalid_request, unsupported_grant_type or invalid_grant, with error_description). Too many exchanges for one key answer 429; a busy token endpoint answers 503 temporarily_unavailable with Retry-After.
A key registered on the legacy platform keeps exchanging at https://auth.aleta.io/oauth/token, and its token works only on platform.aleta.io: api.aleta.ai answers it 401, and api.aleta.ai/oauth/token answers its assertion invalid_grant. To move to v1, ask Aleta for an API key.
The token's scopes are the ceiling of what a call may do. Below it, an admin may do everything, and a user what the user's groups give: each route needs its module at read for a read and read_write for a write, and a route outside them answers 403 forbidden.
curl -X POST https://api.aleta.ai/oauth/token \
-d grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer \
-d assertion=eyJhbGciOiJFUzI1NiIs...
curl https://api.aleta.ai/v1/clients \
-H "Authorization: Bearer aleta_at_..."
Writes
A v1 write (POST, PUT, PATCH, DELETE) can be tried first and sent again safely. One write takes neither option: POST /v1/users, a retired route; it ignores both and always writes. POST /v1/manual-intake/six-price-imports and POST /v1/tags/{id}/preview store nothing, so they need neither.
dry_runbooleanQuery parameter. With
?dry_run=truethe write runs in full, with every check, and is then rolled back. You get the answer the write would have given, with the headerAleta-Dry-Run: true. Nothing is stored and nothing is sent to anyone: no file is saved, no job is queued, and no custodian, webhook or identity provider is called. A machine credential or API key may sendPOST /v1/data-deletion-requestsonly as a dry run, answered200; the request itself needs a person.Idempotency-KeystringRequest header, 1 to 128 characters, chosen by you (a UUID works well). The first write with a key is stored with its answer. Sending the same key and the same request again returns that first answer, byte for byte, with the header
Idempotency-Replay: true, and nothing is written twice. The same key with a different request is refused with409 conflict. A key belongs to the caller and the operation, and is kept for at least 30 days. ADELETEsent again with its key after the row is gone gets the stored answer without a new access check, since the row it would check no longer exists. Only the same caller, with the same request, gets it.Write quotanoteEvery v1 write a machine credential or API key sends counts against its daily write quota, which Aleta sets (
write_quotaon the credential). Dry runs and replays count too. Past the quota the write is refused with429 rate_limitedand aRetry-Afterheader in seconds until 00:00 UTC, when the count starts again. Reads, v2 and v3 calls and writes by people are never counted.ownernoteMarket data comes in two layers. Vendor and Aleta data (SIX, Bloomberg, the banks' feeds, Aleta's manual set) is shared and only Aleta changes it; you add your own rows on top, in the set
external. A row shows which it is asowner:aletaortenant. Every row on an asset you own is yours. A write to analetarow is refused with403(aleta_owned, orforbiddenwith the way to your own set); add your own row instead.
curl -X POST "https://api.aleta.ai/v1/clients?dry_run=true" \
-H "Authorization: Bearer {access_token}" \
-H "Content-Type: application/json" \
-d '{"name": "Family office"}'
curl -X POST https://api.aleta.ai/v1/clients \
-H "Authorization: Bearer {access_token}" \
-H "Idempotency-Key: {uuid}" \
-H "Content-Type: application/json" \
-d '{"name": "Family office"}'
Read dates
Reads of figures that change over time take as_of (YYYY-MM-DD), the date the data is valid on. Left out, it is today. The answer always shows the date it was read at: a list carries as_of next to data, a single resource carries its own as_of.
Legacy ids
Every id v1 returns, and every id inside details or a filter, is a v1 id (a UUID string), or null when the object it names is not in your data. A legacy id never stands in a reference field. An object that has a twin on the legacy platform carries one read-only field, legacy_id: the id v2 or v3 shows for the same object, as text. It is null for an object made in v1 and when legacy has no id for it. Assets, market indices, corporate actions and generated reports have it.
legacy_id is scaffolding for your move from v2 or v3, frozen when the move is done. Use it to find an object (GET /v1/assets?legacy_id=…, also on /v1/market-indices and /v1/corporate-actions), never as an id in a path, and keep the v1 id. Until Aleta has merged duplicate copies of an asset, one legacy_id can match two assets.
Errors
Every v1 error is a problem document (application/problem+json) with type, title, status, code and detail. Match on code, never on detail. Three 409 codes and one 403 code tell you why a write was refused and what to do instead, and 429 tells you when to come back. The checks run in this order: 404, 403 forbidden (you may not write it), 403 aleta_owned, 409 legacy_managed, 409 in_use. A DELETE that archives takes an optional ?reason=, kept as the row's archive_reason. Every code is on Errors, and each problem's type links to its row there.
aleta_owned403The row is shared data owned by Aleta (
owner: aleta), which only Aleta changes. Add your own row instead (sourceexternal).in_use409The resource has had an effect on figures, so it is not deleted.
next_actionnames the call to make instead:next(for examplearchive,end_date,offboard,reverse,anonymize,pause,deleteorask_aleta),method,pathand thefieldto set, if any.ask_aletameans only Aleta can change what blocks the call; the path names that row.next_actionisnullwhen there is no other step.ownership_over_100, ownership_cycle400An ownership would take the economic shares in an entity past 100 %, or would make an entity own itself.
legacy_managed409The row comes from the legacy platform, which still runs your data. It is read-only in v1 until your move to v1 is done, and then every such row opens at once.
period_locked409The write is dated on or before the closing date of an entity whose cash or custody account it touches.
lockedlists each cash or custody account with itskind,id,entity_idandclosed_through, andnext_action(next: unlock) points toPUT /v1/entities/{id}/period-lock: unlock, register, then lock again. See the period lock.rate_limited429Too many calls in a minute, or a key's daily write quota is spent.
Retry-Aftergives the seconds to wait.
{
"type": "https://docs.aleta.ai/developers/api/v1/errors#in_use",
"title": "Conflict",
"status": 409,
"code": "in_use",
"detail": "Cash account 6b1d2f0a-3c4e-4f5a-8b7c-9d0e1f2a3b4c is used by transactions.",
"next_action": {
"next": "end_date",
"method": "PATCH",
"path": "/v1/cash-accounts/6b1d2f0a-3c4e-4f5a-8b7c-9d0e1f2a3b4c",
"field": "valid_to"
}
}
Health and residency
GET /v1/health answers 200 with {"status": "ok"} when the API is up. It takes no token, so a load balancer or a monitor can call it.
GET /v1/residency tells you where your tenant's data lives: region_id (your tenant's region, for example aws-eu-central-1), residency (eu, us or apac) and api_host, the regional host for your tenant. residency and api_host are left out when the region is not one the API knows. Needs platform:read. Call https://api.aleta.ai for every request; api_host is not a host to call.
curl https://api.aleta.ai/v1/residency \
-H "Authorization: Bearer {access_token}"
Not served
Analytics (the analytics query and saved analytics views under /v1/analytics) is not served by v1: those paths answer 404. Document extraction is reserved: its routes answer 501 not_implemented (see documents).
Stability
v1 is the active track. Additive changes ship onto v1 without a new version. Breaking changes — when they eventually need to happen — open a new track that runs in parallel with v1 for at least 12 months before sunset is announced. Routes that leave v1 without a new track are on the list of retired routes, with the headers that announce them.
Migrating from v2 or v3
Both legacy prefixes are kept online for existing integrations. Nothing is forcing a same-day migration — but new integrations should default to v1, and v3 features will not backfill into v1 endpoints. The v3 reference and v2 reference document what's still served on the legacy host.