VRPlatformVRPlatform
Run in Production

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

  1. Filter for the history you need. Most views want one entity: entityType=reservation&entityId={reservationId}. The default order=desc returns newest first.
  2. Follow page.nextCursor while you still need older rows. See Page Through Results.
  3. Render from the collection row. It already carries actor, action, root entity, typed field changes, and accounting impact.
  4. 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; and
  • webhookSubscription.

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 attemptedEntityChanges for 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 before or after means that side was not observed;
  • a present property with null means 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:

ColumnEvent fields
WhenoccurredAt
Who or whatactor.type, identity fields, and actor.organization
Actionaction.summary, action.code, action.operation
RootrootEntity.type, rootEntity.id, labels
Originsource.type, source.requestId
ChangesentityChanges and attemptedEntityChanges
Accountingstatus, 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; or
  • mixed: 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:

  • booksClosed with booksClosedAt;
  • statementPeriod with listingId, openFrom, and the determining ownerStatementId; or
  • statementAttachment with ownerStatementId.

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 reviewed changedFields;
  • an optional typed before snapshot;
  • an optional typed after snapshot; and
  • nullable causedBy attribution 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.

On this page