VRPlatformVRPlatform
Build a Product UI

Integration-Sourced Data

Build idempotent batch writes and handle irreversible provider work

Mental Model

Integration writes connect stable external identity to VRPlatform resources. Database mutations and external-system actions have different guarantees and must be presented separately.

Resources and Lifecycle

Connection-scoped writes can use connectionId, sourceId, uniqueRef, and an existing API id to resolve create versus update. Syncs and provider calls continue asynchronously after their initiating request.

Creating or reconnecting a property-management-system connection also queues initialization of missing reservation line mappings. The connection response confirms the provider connection, not completion of that background mapping work. Existing mapping choices and exclusions are preserved.

Hostaway connections also reconcile their provider webhook after credentials are stored. The successful connection response does not wait for webhook creation, update, or duplicate cleanup.

Bank-record batches prefer an exact sourceId match. If an importer rotates that source row, the same uniqueRef on the same bank account still resolves the existing bank record and updates its source identity instead of creating a duplicate.

Read Model

Render batch data[] and item-level issues[], returned platform IDs, connection-scoped identity, source provenance, sync state, and operation progress. Do not infer that one accepted batch means every item or external action completed.

App discovery through GET /apps or GET /apps/{id} returns extension.url only when the app configures a browser-extension sign-in URL. Treat the optional field as absent for apps without that flow. The API does not expose the rest of an app's authentication configuration.

The same app responses expose validated extractor capabilities. A full extractor runs without a configured cursor window, while a cursor extractor uses the integration's configured range or cursor. A stored none value marks a disabled capability and is not returned. Offer an extractor as a manual fetch option only when visible is true. Show date-range inputs only when isDateRangeFetchable is true; the optional dateRangeMethod explains the provider date semantics when the integration supplies it.

Decision Table

SituationRule
External identityStable inside its documented connection scope
Display nameNever use as identity
Dry-run IDDiscard; it was rolled back
Relationship arraySend complete set only for documented replace semantics
Database batchDry-run only when OpenAPI declares support
OAuth, upload, sync, invitation, emailNo generic dry run
Browser-extension credentialsRequire stable unique ref, email, or username
Built-in API connectionServer assigns opaque identity; reconnect preserves it
Migration or cutoverUse dedicated preview and apply workflow

Editability

Source ownership can make selected fields read-only. Team scope, connection scope, permissions, and existing journal locks still apply to imported resources. Never overwrite local fields merely because the external record has the same display name.

Flow contact mapping writes accept only contacts that still exist for the selected team. A stale contact returns 400 BAD_REQUEST with context.contactId. Refresh the available contacts before retrying.

Preview and Preflight

Database-backed contact, listing, reservation, transaction, AccountConnection, and bank-record batches take dryRun wherever the generated OpenAPI declares it. External operations cannot promise rollback. Dedicated migration preview endpoints report affected rows, locks, mappings, and cutover consequences.

For general-ledger teams, each new listing also receives its zero-value opening balance transaction during the listing batch. Journal materialization is queued after the response, so re-read accounting state instead of assuming those journal entries exist when the listing response arrives.

Mutation Recipe

  1. Resolve team and connection.
  2. Normalize and preserve stable source identifiers.
  3. Load existing mappings or platform IDs.
  4. Validate every item and relationship set.
  5. Dry-run a supported database batch or request dedicated preview.
  6. Present item outcomes and blockers.
  7. Apply with the same identity and payload.
  8. Persist returned platform IDs only from the real write.
  9. Monitor asynchronous sync or provider state.

Failure and Recovery

Retry idempotently with the same external identity. Repair failed items from their structured issues instead of renaming references. For an uncertain provider result, read current connection or sync state before issuing another external action.

App connect and reconnect requests wait up to 25 seconds for provider work. If that limit expires, the API returns type: "error" with code: "ERROR_TIMEOUT". The provider task may still be active, so read the current connection state before retrying the external action.

Single-resource POST /connections/{id}/sync requests wait up to 15 seconds total for provider task dispatch and completion. If the limit expires, the API returns HTTP 200 with status: "failed" and message: "Task timed out". The provider task may still finish in the background, so read the returned sync ID before retrying.

For Hostaway, read sync history when diagnosing webhook setup. Credential completion and webhook reconciliation are separate operations, so a successful connection can precede a failed or still-running webhook refresh.

If a PMS connection succeeds but expected reservation line mappings remain missing, rerun team initialization with lineMappings included. This recovery adds eligible missing mappings without replacing existing mapping choices.

If a bank record resolves to an Account that was removed concurrently, that item is skipped and returned in issues[]; other valid items can continue. Treat the issue as a request to refresh Account and AccountConnection identity, not as a reason to retry the stale relationship unchanged.

Banking integrations must return stable external account identity when credentials are completed. VRPlatform stores it as an AccountConnection. The built-in API app has no external account, so CSV bank-record imports use the canonical Account directly and do not require an AccountConnection.

Common Recipes

Upsert a reservation batch

Use stable connection-scoped references, dry-run the exact batch, resolve each item issue, apply, then store real reservation IDs and render calculated state.

POST /reservations/batch?dryRun=true
Content-Type: application/json

{
  "connectionId": "77777777-7777-4777-8777-777777777777",
  "data": [
    {
      "uniqueRef": "pms-reservation-1842",
      "listingId": "11111111-1111-4111-8111-111111111111",
      "status": "booked",
      "currency": "usd",
      "guestName": "Alex Morgan",
      "checkIn": "2026-08-14",
      "checkOut": "2026-08-18",
      "lines": [
        {
          "uniqueRef": "rent",
          "type": "rent",
          "description": "Accommodation",
          "amount": 125000
        }
      ]
    }
  ]
}

Start an OAuth connection

Use the connection flow, persist its state token server-side, handle the callback, and read the resulting connection. Do not offer a dry-run toggle.

POST /connections/connect
Content-Type: application/json

{
  "appId": "<app-id-from-get-apps>",
  "name": "Primary PMS",
  "redirectTo": "https://example.com/integrations/callback",
  "params": {}
}

API Reference

On this page