VRPlatformVRPlatform
Run in Production

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

  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 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; and
  • transaction.

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 entityChanges item 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 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, actor.name, actor.email, actor.label
Actionaction.summary, action.code, action.operation
RootrootEntity.type, rootEntity.id, labels
Originsource.type, source.requestId
ChangesentityChanges[].entity, operation, and changes
AccountingaccountingImpact.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; or
  • mixed: 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 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.

On this page