Statements
Build owner-statement lifecycle, layout, payout, and editing UI
Mental Model
An owner statement presents owner-side financial activity for one ownership period, month, and currency. Listing plus month is not enough: a mid-month ownership change can produce several statements.
Resources and Lifecycle
Lifecycle
| Status | Journal attachment | Normal owner visibility |
|---|---|---|
draft | No | Team setting can allow it |
inReview | Yes | No |
published | Yes | Yes |
Moving out of draft attaches eligible entries and closes owner-side activity for the period. Returning to draft removes attachment locking while preserving statement identity. Current writes use only these three statuses.
Status Transitions
Use Update statement status (PUT /statements/{id})
for every transition.
Any transition between draft, inReview, and published is allowed in both
directions, subject to these enforced blockers:
| Transition | Blocked when |
|---|---|
| Any status change | A later statement exists for the same ownership period |
To inReview or published | The recomputed statement has error-severity issues |
Draft or inReview to published | A prior month in the publish window has an unfinalized statement or unattached owner activity |
To draft | The caller is a scoped API credential without statements:publish; entries detach and locks lift otherwise |
For scoped API credentials the statements:publish scope gates the published
boundary in both directions: publishing a statement and moving a published
statement back to draft or inReview both require it. Re-posting
Create statement regenerates an existing
statement but returns 409 if it would move a published statement away from
published — status transitions always go through the update endpoint.
Publish order is enforced per ownership period and currency: finalize earlier months before publishing a later one. The status update does not check for an existing payout, so unpublishing a paid statement leaves its payout transaction in place; paying again without an explicit amount is rejected while an active payout exists.
Layouts and Visibility
Layouts define sections, columns, formulas, account groups, and owner/manager visibility. Listing mappings choose the active layout.
Read Model
Render server-calculated detail, totals, rows, source records, issues, locks, and the returned layout for the requested view. Do not recompute allocation, totals, or owner visibility in the browser.
Statement detail includes a Period activity drilldown for the complete
statement and every rendered row. Eligible numeric, non-field cells include a
more specific drilldown; subtotal cells expose one only when they carry an
account scope. Use this metadata as returned instead of rebuilding journal
filters in the browser. If a cached detail response is missing required
drilldowns, the API rebuilds it before returning the statement.
Statement list filters named statementIds, periodIds, listingIds, and
ownerIds accept comma-separated UUIDs only. Display references such as
2026-4360 are not interchangeable with statement IDs; invalid identifier
formats return 400 BAD_REQUEST before any statement query runs. CSV list
exports enforce all four filters, while totals enforce their supported
listingIds and ownerIds filters.
Persisted statements freeze their effective presentation. A later layout change does not silently rewrite old statements.
Each returned payout includes its latest providerPayment, when present. Use
the payout transaction's provider-payment history for older attempts and retry
or unlink actions.
Owner-summary payouts use the payout journal contact when it identifies a member of the ownership period. That owner receives the complete tagged payout. A listing payout tagged to a non-owner payee is shared across the period's owners by their ownership percentages. Transfer rows without any contact remain excluded because they do not provide safe payout attribution. For an allocated payout, use the returned drilldown as-is; it keeps the source payee contact needed to open the supporting journal activity.
Partner Portfolio Counts
Use List partner owner statements (GET /partner/owner-statements)
to build a partner-level statement queue. The response contains every child
team stored in VRT (storageRealm=vrtrust) in the selected regional partition,
including teams with zero statements. Each team includes its identity,
optional logo, creation timestamp, and persisted owner-statement counts for
draft, inReview, and published. Use page and limit, or offset and
limit, to traverse the response; pagination.total counts all matching VRT
teams before pagination. Set sort to draft, inReview, or published to
order by that status count ascending, or prefix the value with - for
descending order. Equal counts retain deterministic team name and ID ordering. Use
search to filter teams by a case-insensitive partial name match; filtering is
applied before pagination.total is calculated.
The counts exclude manager statements and VRI teams. For a multi-region
portfolio, call each region advertised by GET /me and retain region identity
when combining the results.
Decision Table
Payouts and Description Editing
| Decision | Rule |
|---|---|
| Identity | Ownership period + month + currency |
| Owner display | Request owner view; do not hide manager data client-side |
| Manager display | Request manager view |
| Persist history | Use returned frozen layout/detail |
| Refresh layout | Explicit refresh, never automatic on read |
| Owner payout | Pay owner statements with one selection per owner |
| Manager payout | Pay manager statements |
| Summary payout attribution | Owner contact in full; non-owner payee by ownership split |
| Inline description | Require descriptionEdit, no descriptionLock; edit |
Exports
For a single PDF, call GET /statements/pdf with the ownership period and
month. contactId is optional; when provided, it must identify an owner in the
selected ownership period. A manager mismatch returns 400 Selected owner does not belong to this statement; reload the period's current owners before
retrying. Owner credentials still receive an opaque access denial for contacts
or statements outside their scope.
Editability
Non-draft statements create attachment and owner-period locks. Manager-side transaction lines normally do not participate in the owner-statement period lock. Use statement IDs and lock hits, not month alone, to explain coverage.
Row descriptions can have narrower editability than the surrounding financial resource. A row backed by ambiguous source lines is not safely editable.
Preview and Preflight
Statement create, update, delete, pay, manager pay, description mutation, and
historical import all take dryRun where the generated OpenAPI declares it.
Layout mutations do not. Render returned validation and issues before applying
an explicit layout refresh.
Before paying, call POST /statements/pay/preview. It returns each
statement's authoritative owner allocations after rounding, default bank
accounts, ACH payment-method readiness, and typed blocking reasons without
creating payout transactions. Submit the intended owner selections to
validate them, or omit payouts to receive server-computed defaults. The
pay endpoint revalidates the same rules; the preview is advisory.
Offer provider: "ramp" only when the selected account's embedded Ramp
AccountConnection reports capabilities.achPayments.enabled. Keep ineligible
accounts available for provider: null when they are otherwise valid bank
accounts.
Mutation Recipe
- Load statement identity, requested view, detail, status, locks, and issues.
- Render returned totals and frozen layout.
- Determine the exact status action or narrow row edit.
- Dry-run the complete supported mutation.
- Explain new attachment or period-lock consequences.
- Confirm and apply.
- Re-read the same statement ID and view.
- For payouts, follow the returned transaction into its accounting month.
Failure and Recovery
If an owner-side mutation hits a persisted period, identify the locking statement and recovery action; never silently switch attribution. Return a statement to draft only through an explicit authorized workflow. For an ambiguous description row, edit the underlying source instead.
Common Recipes
Publish a statement
Load current detail, dry-run the status update, show issues and lock impact, confirm, publish, then re-read owner view.
PUT /statements/{statementId}?dryRun=true
Content-Type: application/json
{
"status": "published"
}Pay a published statement
Use POST /statements/pay with one explicit selection per ownership member.
The payout line references the paid statement, while its journal entries use
the payout transaction date and can appear in a later statement month.
POST /statements/pay?dryRun=true
Content-Type: application/json
{
"date": "2026-08-08",
"statements": [
{
"statementId": "55555555-5555-4555-8555-555555555555",
"payouts": [
{
"contactId": "77777777-7777-4777-8777-777777777777",
"provider": null,
"bankAccountId": "66666666-6666-4666-8666-666666666666"
}
]
}
]
}provider is the only ACH trigger. provider: "ramp" dispatches an ACH
payment funded through the Ramp account connection of the selected bank
account; provider: null records the payout without dispatching, even when
the bank account is Ramp-backed. Owners of the same statement can mix
methods. Owner ACH details are configured separately; the pay request never
accepts account or routing numbers. Ramp funding identities use the imported
AccountConnection source: its provider bank-account ID must match uniqueRef,
and the source must identify exactly one Ramp entity. Preview and execution
reject missing, stale, or ambiguous routing before creating local payout
transactions or queueing provider work.
The selected bank account supplies only the payout's cash leg and does not
need the payout_distribution assignment. Statement allocation uses the
team's separately assigned payout-distribution account. Adding or removing
that assignment invalidates cached statement reads, so list and detail use the
same current Payouts section. The same payout-distribution account is sent as
the Ramp bill line Category; the selected bank account remains only the cash
and ACH funding source.
Per-statement results are requested (at least one Ramp dispatch),
recorded (book-only), or skipped. Each statement validates and executes
atomically, and a blocked statement never rolls back earlier successful
results. Every result associates each payout transaction and dispatch
syncId with its owner contact. Omitting amount pays the full net income
and requires the statement to have no prior payout; an explicit signed
amount records an additional partial payment or an owner receipt. Retry a
terminal provider failure through the existing payout transaction rather
than creating another full-statement payout.
Webhook Reconciliation
statement.status.changed is emitted only when a persisted owner statement is
created in or transitions among stored statuses, or is deleted. Draft
projections do not emit. The body contains previous/current status and a
monotonic version, but no amounts, rows, owner, payout, or template data.
API Reference
- Get statement detail —
GET /statements/{id} - Update statement status —
PUT /statements/{id} POST /statements/pay— Pay owner statementsPOST /statements/pay/preview— Preview payout plans without writingPOST /statements/manager/pay— Pay manager statementsPOST /statements/{id}/refresh-layout— Refresh a statement layoutPUT /statements/lines/{lineId}/description— Update a line descriptionGET /statements/layouts— List statement layouts
