Audit Events
Read who changed a business entity, which fields changed, and the accounting impact
Last Updated: 2026-09-03 Version: 2.4.0
Work in progress — the contract is not frozen
Response fields, filters, and status values can still change, including in ways that are not backward compatible. Read audit events on demand rather than copying them into your own store, and talk to us before you build a local copy. See Work In Progress.
Audit events answer who or what changed a business entity, which reviewed fields changed, and what the action did to accounting. Reviewed means allowlisted: the Audit Catalog lists every field that can appear in an event, and no other field is ever published. Query the API when you need that history, for example to render an audit table or an entity-history view. Audit events are not a raw journal or an export of VRPlatform's internal job history.
Work In Progress
Treat every shape in this guide as current behavior rather than a long-term guarantee, and check the Changelog before you rely on a detail.
Read audit events on demand instead of copying them into your own store. Direct queries let us correct and extend the contract without stranding a mirror you have already built. If your use case genuinely needs a local copy, talk to us first so we can plan the contract with you.
Read History Directly
- Filter for the history you need. Most views want one entity:
entityType=reservation&entityId={reservationId}. The defaultorder=descreturns newest first. - Follow
page.nextCursorwhile you still need older rows. See Page Through Results. - Render from the collection row. It already carries actor, action, root entity, typed field changes, and accounting impact.
- Fetch
GET /audit/events/{id}only when you need journal-entry evidence for one event.
Accounting is evaluated asynchronously, so a very recent action can still be
waiting for its first public projection or report accountingImpact.status of
pending. This is most noticeable after actions that change thousands of
accounting entries. Query again to see the event or its settled outcome; once
created, the event keeps its id and gains a greater revision.
Poll no faster than you need. Requests beyond the rate limit return 429
with Retry-After.
Choose A Collection
Use List audit events (GET /audit/events) for
one selected team. VRPlatform administrators and active members with
audit:read can read this collection. Team Admin includes that permission.
Assign the Audit Log bundle when another member, such as a bookkeeper, needs
access. Standard Member does not include it. Owner users and embedded sessions
cannot read audit events.
Use List partner audit events
(GET /partner/audit-events) to
read all managed teams in one region. Partner API keys need audit:read,
granted by the stable partner:audit:v1 bundle. Signed-in partner members use
the same collection. Members with all-team reach see the regional portfolio;
members with assigned reach see only assigned and directly joined teams.
teamId can narrow that reach but cannot expand it.
Select the region with x-data-region and query each configured region
separately. The API does not combine regions or provide cross-region
ordering. See
Team Context.
Use Get an audit event
(GET /audit/events/{id}) for paginated
journal-entry evidence. Partner callers select one managed team with
x-team-id before using the detail endpoint.
Audit events are the only audit surface available to external callers. Raw change records, effect attempts, provider payloads, and other forensic data remain restricted to VRPlatform administrators.
Event Model
One event represents one action applied to one root business entity. A batch
that updates ten reservations creates ten events with the same actionId.
Payment-line changes remain inside the reservation event that caused them.
Public root entities are:
account;accountConnection;apiToken;bankRecord;bankRule;connection;contact;emailTemplate;flowMapping;flowSetting;listing;listingOwnershipPeriod;ownerStatement;ownerStatementLayout;ownerStatementLayoutListing;ownerUserAccess;recurringFee;recurringFeeListingSubscription;recurringTransactionTemplate;reservation;reservationLineMapping;taxRate;team;teamMember;transaction; andwebhookSubscription.
An entityChanges item can also identify a paymentLine. Journal entries do
not become root events. Their identity appears only in detail evidence.
Every event contains:
- an event-time actor snapshot for a user, API key, sync, automation, or system;
- the organization represented by that actor when the action occurred, when retained evidence proves it;
- a source type and optional per-request ID;
- a stable action and root entity;
- committed reviewed changes in
entityChanges; - reviewed requested changes in
attemptedEntityChangesfor blocked writes; and - an accounting status, signed net effects, lock evidence, and recovery.
Direct members represent the selected team. Partner-derived users and partner
API keys represent the partner. Embedded actions keep the end user as actor,
identify the embedding partner as the organization, and use
source.type=embedded. PMS reservation and contact imports use the stable
Connection as a sync actor and source.type=sync. Async work retains the
initiating actor, organization, and source.
Actor and organization names are event-time snapshots. A later membership or
user deletion does not rewrite them. Older events return
actor.organization: null when retained evidence cannot prove the affiliation.
Deleting a user does not remove their actor identity from retained audit history. The stored actor ID, name, and email remain available on historical events and authorized forensic audit reads.
Provider credentials, raw provider identifiers, guest data, arbitrary metadata, and secret values are excluded. Bank records and account connections publish particularly short field lists: potentially identifying bank descriptions and holder names are not public audit fields. The Audit Catalog is the complete field list per entity.
Major configuration changes are included from this release forward. This includes team accounting, display, Owner Portal, and reviewed feature settings; tax rates; statement layouts and assignments; member and owner access; bank rules; recurring fees, listing periods, and transaction templates; reservation line mappings; connection, flow-setting, and flow-mapping configuration; API tokens; webhook subscriptions; and email templates.
ownerStatementLayoutListing assigns one listing to one statement layout.
recurringFeeListingSubscription defines when a recurring fee applies to a
listing.
Protected configuration fields expose only their path in redactedFields.
These include credentials, provider mapping values, bank-rule conditions and
lines, recurring formulas and conditions, template bodies and patterns, token
network restrictions, webhook endpoints and secrets, user identifiers, logos,
and nested statement-layout sections. Token secrets are never audit fields.
Other fields absent from the Audit Catalog remain excluded. Existing history
is not backfilled; coverage starts with changes captured after the release.
Interpret Typed Entity Changes
Every public value declares its type. Scalars use string, boolean,
integer, money, date, timestamp, enum, or entity_reference.
Lists declare their item type. Arbitrary nested objects are never returned as
field values.
A money value always carries its currency:
{
"type": "money",
"centTotal": 115000,
"currency": "usd"
}Some historical amounts predate reliable currency capture. When no
trustworthy currency exists for a side, that before or after side is
omitted instead of carrying a guessed currency. An observed null side stays
null. If neither side survives, the money path is omitted and the event's
other changes remain.
Dates use YYYY-MM-DD. Timestamps use the API timestamp convention. Entity
references contain an entity type and UUID.
Property presence is meaningful:
- a missing
beforeoraftermeans that side was not observed; - a present property with
nullmeans the value was observed as database null; and - create and delete operations omit the side that did not exist.
For example, this update observed that the new posting date is null but did not observe the previous value:
{
"path": "generalLedgerPostingAt",
"after": null
}Do not treat missing and null as equivalent. redactedFields names reviewed
fields that participated in the action but whose values were deliberately
withheld.
Build An Audit Table
Collection rows contain everything required for a compact audit table:
| Column | Event fields |
|---|---|
| When | occurredAt |
| Who or what | actor.type, identity fields, and actor.organization |
| Action | action.summary, action.code, action.operation |
| Root | rootEntity.type, rootEntity.id, labels |
| Origin | source.type, source.requestId |
| Changes | entityChanges and attemptedEntityChanges |
| Accounting | status, net effect, lock evidence, and recovery |
action.code joins the root entity type with the operation in past tense:
reservation.updated, ownerStatement.created. Codes are stable API
values; action.summary is display text.
This abbreviated event shows typed reservation and payment-line changes:
{
"id": "9d2a7f80-6e13-4a35-9bb1-6a33a2d96591",
"revision": 2,
"actionId": "f5e5f78b-1142-4e89-ad0b-cba970b15a55",
"action": {
"code": "reservation.updated",
"summary": "Reservation update",
"operation": "update"
},
"rootEntity": {
"type": "reservation",
"id": "20bd0f83-bf5a-4e32-ae93-952a7200b842",
"uniqueRef": "RES-001-0001-001",
"name": null
},
"entityChanges": [
{
"entity": {
"type": "reservation",
"id": "20bd0f83-bf5a-4e32-ae93-952a7200b842",
"uniqueRef": "RES-001-0001-001",
"name": null
},
"operation": "update",
"changes": [
{
"path": "checkOut",
"before": { "type": "date", "value": "2026-08-12" },
"after": { "type": "date", "value": "2026-08-14" }
}
],
"redactedFields": []
},
{
"entity": {
"type": "paymentLine",
"id": "8f90db32-890d-451c-a440-70d833c8189f",
"uniqueRef": "nightly-rate",
"name": "Nightly rate"
},
"operation": "update",
"changes": [
{
"path": "amount",
"before": {
"type": "money",
"centTotal": 100000,
"currency": "usd"
},
"after": {
"type": "money",
"centTotal": 115000,
"currency": "usd"
}
}
],
"redactedFields": []
}
],
"attemptedEntityChanges": [],
"accountingImpact": {
"status": "changed",
"netEffect": [
{
"account": {
"id": "9ee79237-b1f7-431d-a0b3-f5c8d11bfcfb",
"name": "Rental Revenue",
"category": {
"id": "f713ec9a-bfcb-4357-b2ff-1cebe99ebbf2",
"name": "Rental Revenue",
"classification": "revenue"
}
},
"amount": {
"type": "money",
"centTotal": -15000,
"currency": "usd"
}
}
],
"blockedBy": [],
"recovery": []
}
}Filter History And Changes
Use root filters for one entity's history:
GET /audit/events?entityType=reservation&entityId={reservationId}
GET /audit/events?entityType=account&entityId={accountId}Use change filters when the root may differ from the changed child:
GET /audit/events?hasChanges=true
GET /audit/events?changedEntityType=paymentLine&changedPath=accountId
GET /partner/audit-events?accountingStatus=changed,mixed&accountId={accountId}changedPath requires changedEntityType and must be one of the paths the
Audit Catalog lists for that subject. Invalid
subject/path pairs return 400. hasChanges=true means at least one public
entity change or entry change exists; it does not inspect netEffect.
accountId matches either observed side of an entry change. It therefore
finds posting-date and other dimension-only changes even when their net effect
is zero. entityId requires entityType; actorId requires actorType.
actionId selects every event of one batch action; actionCode and
operation narrow by action kind. Use inclusive occurredFrom and
occurredTo for the time the action happened. Use inclusive updatedFrom and
updatedTo for projection revision time. An Activity Log date picker should
use occurrence time. A synchronization client should use updatedFrom,
ascending order, and the returned cursor. The partner collection also accepts
teamId.
occurredFrom cannot be after occurredTo. Occurrence filters are bound into
the cursor but do not change cursor ordering, which remains updatedAt then
event ID so a later revision cannot be missed.
Interpret Accounting Impact
Accounting status is always one of:
not_applicable: the action required no accounting evaluation;pending: accounting work has not reached a terminal outcome;unchanged: accounting completed without an observed public entry change;changed: at least one public entry change was observed;blocked: a lock prevented the accounting change;failed: accounting ended in failure without movement; ormixed: movement and a blocked, failed, or pending outcome coexist.
Collection rows also expose finite blockedBy evidence and supported
recovery actions. There is no collection expansion for entry detail.
For a blocked write, attemptedEntityChanges contains the reviewed requested
change and entityChanges contains no uncommitted values. Lock evidence is one
of:
booksClosedwithbooksClosedAt;statementPeriodwithlistingId,openFrom, and the determiningownerStatementId; orstatementAttachmentwithownerStatementId.
A blocked reservation can include createReservationAdjustment with the
relative adjustment endpoint. mixed events can contain both committed
changes and blocked attempts. Raw request bodies, error text, provider data,
and excluded fields never become attempted changes or lock evidence.
Each netEffect item is the signed effective delta for one account and
currency. Journal signs are preserved: debits are positive and credits are
negative. Before sides are subtracted, after sides are added, and inactive
sides do not contribute. Zero keys are omitted. Results are ordered by
absolute magnitude, then stable account and currency keys.
netEffect: [] does not mean accounting was unchanged. An entry can change
posting date, party, ledger, ownership, statement attachment, or active state
without producing a non-zero account/currency total. Read status and fetch
detail when you need the observed evidence. Historical amounts without a
trustworthy currency contribute nothing rather than a guessed value.
Owner-statement attachment and detachment entry changes belong to the
ownerStatement event that caused them. They do not create reservation or
transaction sibling events. Attachment-only work can have an empty net effect
while its accounting status remains changed.
Read Journal-Entry Evidence
GET /audit/events/{id} adds entryChanges. Each item represents one
observed journal-entry insert, update, or delete. It includes:
- a stable change ID and public
journalEntryId; operation,changedAt, and reviewedchangedFields;- an optional typed
beforesnapshot; - an optional typed
aftersnapshot; and - nullable
causedByattribution when an observed mutation link exists.
Entry snapshots contain account, signed money amount, posting date, ledger,
status, party, ownership period, and owner statement. Inserts omit before;
deletes omit after. A real nullable journal field remains present as null
inside an observed snapshot. A historical change with an untrustworthy amount
can omit that snapshot side while keeping its identity and changed-field
names.
{
"entryChanges": {
"data": [
{
"id": "73c71345-e180-4495-a436-d469b4719f33",
"journalEntryId": "77dd63aa-f3c2-4c3b-93db-450a38ecb7aa",
"operation": "update",
"changedAt": "2026-08-02T10:41:24.210Z",
"changedFields": ["amount", "postingDate"],
"before": {
"account": {
"id": "9ee79237-b1f7-431d-a0b3-f5c8d11bfcfb",
"name": "Rental Revenue",
"category": {
"id": "f713ec9a-bfcb-4357-b2ff-1cebe99ebbf2",
"name": "Rental Revenue",
"classification": "revenue"
}
},
"amount": {
"type": "money",
"centTotal": -100000,
"currency": "usd"
},
"postingDate": { "type": "date", "value": "2026-08-12" },
"ledger": { "type": "enum", "value": "operating" },
"status": { "type": "enum", "value": "active" },
"party": { "type": "enum", "value": "owners" },
"ownershipPeriod": null,
"ownerStatement": null
},
"after": {
"account": {
"id": "9ee79237-b1f7-431d-a0b3-f5c8d11bfcfb",
"name": "Rental Revenue",
"category": {
"id": "f713ec9a-bfcb-4357-b2ff-1cebe99ebbf2",
"name": "Rental Revenue",
"classification": "revenue"
}
},
"amount": {
"type": "money",
"centTotal": -115000,
"currency": "usd"
},
"postingDate": { "type": "date", "value": "2026-08-14" },
"ledger": { "type": "enum", "value": "operating" },
"status": { "type": "enum", "value": "active" },
"party": { "type": "enum", "value": "owners" },
"ownershipPeriod": null,
"ownerStatement": null
},
"causedBy": {
"entity": {
"type": "paymentLine",
"id": "8f90db32-890d-451c-a440-70d833c8189f"
},
"operation": "update",
"changedPaths": ["amount"]
}
}
],
"page": {
"eventRevision": 2,
"limit": 100,
"hasMore": false,
"nextCursor": null
}
}
}Use entryLimit to select up to 100 items. Continue with entryCursor without
changing the authenticated principal, selected team, event, or limit. Entry
order is ascending by changedAt, then ID.
If the event revision changes while a page is being read or between pages, the
API returns 409 AUDIT_EVENT_REVISED with currentRevision in structured
context. Restart detail pagination from the first page. Do not combine pages
from different event revisions.
Page Through Results
Every collection read is paginated. Follow page.nextCursor while
page.hasMore is true, and pass the cursor back unchanged: it is opaque and
encodes the region, principal, order, and filters of the request that issued
it. Changing any of those mid-walk returns 400, as does a cursor issued
before the 2026-07-31 precision repair. Ordering is by updatedAt, then id,
at the database's full timestamp precision, which is finer than the
millisecond timestamps in the response body.
Descending order, the default, is a browse mode rather than a consistent snapshot: an event revised during a multi-page walk can move ahead of the page you are on. Ascending order is stable for walking a bounded window forward, and it excludes approximately the most recent ten seconds so that a concurrently committing revision cannot land behind a cursor you already passed.
Online history is guaranteed for at least 180 days and is currently retained
indefinitely. If a future retention job advances the history watermark, a
cursor or updatedFrom before that watermark returns 410 with
earliestAvailableAt; restart from that timestamp.
Keeping A Local Copy
Mirroring audit events into your own store is not the recommended path while the contract is still moving, and it is the case most likely to break when a field or status changes. Read Work In Progress first, and talk to us before you build one.
If you do maintain a copy, walk forward with order=asc, bound the initial
import with updatedFrom, and keep the last non-null cursor between polls
even when a poll returns no data.
GET /partner/audit-events?order=asc&limit=100&updatedFrom=2026-08-01T00:00:00Z
x-api-key: <partner-api-key>
x-data-region: <data-region>Upsert by id and replace stored data only when the returned revision is
greater. The same event reappears with a greater revision and later
updatedAt whenever it changes, most commonly when accounting settles from
pending to a terminal status. Partner callers synchronize each region
independently.
Subscribe to audit.event.changed when polling alone is unsuitable. Each
delivery identifies the event ID, its public revision, and
/audit/events/{id}. Treat it as an invalidation and fetch the current event.
One delivery is queued for event creation and each higher revision; a
no-change projection emits nothing. See Webhooks.
Audit capture and these APIs remain active for VRTrust and VRI teams. The first built-in Activity Log interface will be limited to VRTrust teams after the completed contract is published; VRI history remains available through the API.
