Audit Events
Read who changed a business entity, which fields changed, and the accounting impact
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
report accountingImpact.status of pending. Query again to see the settled
outcome; 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 administrators of the
selected team can read this collection. Ordinary members, 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.
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;bankRecord;listing;listingOwnershipPeriod;ownerStatement;recurringFee;reservation; andtransaction.
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;
- a source type and optional request ID;
- a stable action and root entity;
- one
entityChangesitem per changed reviewed entity; and - an accounting status plus compact signed net effects.
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.
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, actor.name, actor.email, actor.label |
| Action | action.summary, action.code, action.operation |
| Root | rootEntity.type, rootEntity.id, labels |
| Origin | source.type, source.requestId |
| Changes | entityChanges[].entity, operation, and changes |
| Accounting | accountingImpact.status, netEffect |
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": []
}
],
"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"
}
}
]
}
}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, operation,
and updatedFrom/updatedTo narrow by action kind and time window. The
partner collection also accepts teamId.
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 expose only status and netEffect. There is no collection
expansion for entry detail.
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.
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.
