Skip to main content

Configuration API

Everything a report needs before it can be rendered: identity, reporting period, strategies, fees, disclosure text, and branding. One canonical endpoint (/config) covers all of it; the narrower per-section endpoints below remain available for clients that want a smaller payload. All paths are relative to https://api.renesis.fi/lp-report-studio.
Read Validation and silent failures before writing anything. This API accepts far more than it should: negative fee rates, numbers sent as strings, and unknown field names all return 200, and some of them break every later read.

GET /api/funds

List the reports the caller can read. Admins see everything; other callers see reports they own or are listed on.

Response

These are raw storage records, not the normalized shape. The display name is report_name here, but identity.name in /config. Rows come back newest-updated first, capped at 200.

POST /api/funds

Create a report.

Request body

string
required
The Renesis portfolio that supplies NAV, balances, and transactions. Also accepted as identity.portfolio_id.
string
Display name. Also accepted as identity.name.
Any other field from the unified shape is accepted here too, to pre-populate the report. owner_email is always taken from the credential, never from the body, so a caller cannot create a report owned by someone else.

Response

201 with the freshly created configuration, in the same shape GET /config returns. A missing portfolio_id returns 400; a database rejection also returns 400 with the underlying message.

GET /api/funds/{report_id}/config

Read the unified configuration.

The unified configuration shape

Empty blocks are omitted. Only identity, strategies, layout, and sections are guaranteed present; a fresh report returns a much smaller object than the example above. Code defensively against missing keys rather than assuming the full shape.
Several further blocks are stored and returned as opaque whole objects when set, and are passed through untouched on write: adjustments, recap, pdf_visibility, statement_captions, statement_strategies, and layout_grid. They drive statement layout and manual adjustments. Unless you are replicating the Report Workspace editor, read them and write them back unchanged.

Fields that no longer exist

These were removed from the configuration surface. They are neither returned nor writable, and sending them does nothing:
  • fees.custodian_pct / custodian_fee, fees.supervisory_pct / supervisory_fee. Collected historically but never read by any computation.
  • fees.hurdle_rate_pct. Only the LP Portal share accounting consumes it, and it reads the stored column directly.
  • The whole visibility block (show_fee_breakdown, show_fund_nav_chart, and the rest). Those toggles gate LP Portal responses, not the statement, and are managed there.
  • fund.size, fund.number_of_holdings, fund.unit_pricing, fund.unit_price.

Field notes

  • reporting.currency drives FX. Rates are pulled live and resolved at period_end. Only EUR, CHF, JPY, BTC, and ETH have FX coverage, plus USD which is the base. Any other code, including GBP, silently resolves to a rate of 1.0, producing a statement that prints the wrong currency symbol on unconverted USD values. Validate this field on your side.
  • reporting.aum_basis: current_eom accrues month K’s management fee on month K’s closing AUM. prior_eom accrues it on month K-1’s closing AUM, the convention most institutional mandates specify. The two produce different fee numbers.
  • reporting.crypto_decimals: native precision for crypto pockets. Values below 2 are clamped to 2; there is no upper bound, though the editor only offers 2, 4, and 6.
  • strategies[].mgmt_pct / perf_pct: omitting the key (or null) inherits the fund-level default. 0 is an explicit zero. The two are not the same, and 0 does not suppress the strategy’s fee block: the statement still renders Management, Performance, and Total rows, showing zeros.
  • strategies[].denomination is the strategy’s native currency. It drives whether NAV and fees are displayed in native units (BTC, ETH) or in the reporting currency. Any unrecognized code is treated as non-fiat.
  • strategies[].share_token_contracts pins a strategy to its on-chain vault by contract address, so renaming the strategy or holding a symbol-less token does not break the match.
  • strategies[].fx_to_reporting is legacy and ignored; live FX is used.
  • strategies[].position_group_id, position_group_nav_usd, strategy_type and mandate appear on strategies backed by a position group. The strategy’s NAV, returns and allocation slice then derive from the group’s legs rather than from a single share token. Read, modify in place, and send back what you received: the array replaces wholesale, so dropping these fields detaches the strategy from its group.
  • strategy_mode: single, vault, or manual. Any other string is treated as not-single without error.
  • layout: statement (default), compact (single page), or institutional (four pages). An unrecognized value silently falls back to statement.
  • branding.logo_url is the routable URL to use in an <img>. Do not send branding.logo_path, see the read-only notes below.

PUT /api/funds/{report_id}/config

Partial update. Only the keys present in the body are written; everything else is left alone. Nested objects (fund, contact, fees, content, reporting) deep-merge, so a one-field patch does not clobber its siblings.
The response is the full re-read of the merged configuration, so a client can replace its local state from the response without a second GET. strategies and sharing.email_addresses are arrays and replace wholesale. To remove one entry, send the array without it.

Validation and silent failures

This is the part most likely to burn an integration. The write path is permissive by design and the frontend editor, not the API, enforces most of what looks like a rule.
No numeric validation of any kind. Fee percentages, per-strategy overrides, initial_share_price, and total_shares are stored exactly as sent.
  • Negative values are accepted. A negative mgmt_pct produces negative management-fee rows on a real LP statement. The editor sets min=0 on every fee input; the API does not.
  • Numbers sent as strings are accepted and are a trap. "2%" or "2" writes successfully and returns 200, then makes every subsequent /computed and /render call fail with 500 until the value is corrected. Always send plain JSON numbers.
  • Percentages here are whole numbers. 2.0 means 2%. Computed outputs use decimals (0.02 means 2%). Mixing them up silently produces fees off by 100x.
Unknown keys are silently dropped, not rejected. A misspelled field, or one of the removed fields, writes nothing and returns 200 with the unchanged config. Do not rely on a 400 to catch typos; diff the echoed response against what you sent. The single exception is sharing.invite_codes and sharing.redeemed_invite_codes: sending either returns 400 with unknown lp_reports columns in patch. Long text is truncated silently. Prose is clamped at the write boundary and the request still returns 200: Check the echoed config if you need to detect truncation. Note that text written through the legacy POST /branding is not clamped, and over-budget values stored that way will fail every later render. Dates are not validated. period_start and period_end are accepted in any format and any order. A malformed or reversed pair does not error: period.months comes back empty, and monthly[], fees_summary.by_month_*, and nav_series are empty while recap is null. Headline NAV and FX still resolve at period_end. Renaming strategies. Editing names in place, keeping the array the same length, carries contract pins across and remaps anything keyed by strategy name (adjustments.per_strategy, statement_captions.by_strategy, statement_strategies). Removing and re-adding a strategy in the same request bypasses that remap and orphans those references silently. Rename in place.

Read-only and effectively-managed blocks

Accepted aliases

Legacy names are normalized on write. The response always uses the canonical name. That is the complete list. There are no aliases for the removed custodian and supervisory fields.

DELETE /api/funds/{report_id}

Delete a report. Cascades to its investors and capital events.

Strategy auto-detection

For funds holding on-chain vault positions, the studio can propose the strategy list instead of having you type it.

GET /api/funds/{report_id}/detect-vaults

Preview only, writes nothing. Walks every account linked to the report’s portfolio and returns the vault share tokens it finds.
Only vaults whose reconciliation protocol is Enzyme are proposed today. Other share tokens stay plain holdings and will not appear here.
inception, benchmark, and fee_terms are always empty on detection; fill them yourself.

POST /api/funds/{report_id}/strategies/from-vaults

Replaces strategies with the detected list and sets strategy_mode to vault. Detected strategies come back with mgmt_pct and perf_pct at 0 and fx_to_reporting at 1.0, so set the real fee terms through PUT /config before rendering. Fiat-denominated strategies are ordered first. Body: none.
The recommended wizard flow is: detect-vaults to preview, confirm with the user, strategies/from-vaults to apply, then PUT /config to set fees and benchmarks.
multipart/form-data file part. PNG or JPEG only.
The file is content-sniffed server-side, so renaming a file does not get it past the check. SVG is rejected deliberately: it is XML and can carry scripts, which would be a stored-XSS hazard in a rendered statement.
Errors: 400 with no file, empty filename, unsupported format, or not a valid image. A 500 with image validation unavailable means the server-side image library is missing; retry or report it.

GET /assets/logo/{report_id}

Returns the uploaded image. Read access is sufficient. 404 when no logo has been uploaded.

Templates

A template is a reusable bundle of branding, layout, and fee defaults that is not tied to one report. Templates are personal to the caller; there is no shared or organization-level scope, and the admin role does not bypass ownership here.

POST /api/templates

string
default:"Untitled template"
Display name for the template.
object
A configuration in the canonical shape. Per-report fields are stripped server-side.
string
Snapshot an existing report’s configuration instead of supplying a payload. The caller must be able to read that report.
Provide one or the other. Sending neither returns 400; sending both is not an error, payload wins and source_report_id is ignored. Returns 201 with the created template. PUT with neither name nor payload returns 400 with nothing to update.

POST /api/templates/{template_id}/apply

string
required
The target report.
The caller must own the template and have write access to the target. Merge semantics: only fields that are empty on the target get written. Existing values are never overwritten, so applying a template is safe on a partially configured report. If the template has a logo and the target does not, the logo file is copied across.
applied is the subset that actually got written, useful for showing the user what changed.

Narrower alternatives

These predate /config and stay supported. Use them when you want a smaller payload; use /config for anything new.

PUT /api/funds/{report_id}/fees

Body: any subset of maintenance_fee, performance_fee (whole-number percentages), and hwm (boolean, the high-water-mark toggle). custodian_fee and supervisory_fee are silently ignored. Values are coerced with a float conversion, so a non-numeric value returns 500, not 400. Returns { ok, fees_expenses }.

GET /api/funds/{report_id}/branding

Returns the full rendering-side configuration document, with logo_url attached when a logo exists. Read access is sufficient.

POST /api/funds/{report_id}/branding

Body: any subset of strategies, strategy_mode, vault_decomposition, scope, reporting_ccy, fx_source, share_class, aum_basis, period, period_start, period_end, plus text_blocks. Keys outside that list are ignored. text_blocks deep-merges; every other key replaces. Returns { ok, branding }.
Unlike PUT /config, this endpoint does not clamp text_blocks to the text budgets. Over-budget prose stores successfully here and then fails every later render: 422 on /render-preview, 500 on /render. Prefer /config for any text.