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.
PUT /syncs/{id} accepts queued, started, completed, or failed as status
alongside sync metadata. Persist the authoritative result after provider work
completes; repeating the same terminal status does not create another
status-transition webhook or recipient setup-result event.
To cancel a sync, use POST /syncs/{id}/cancel. It cancels active subtasks, skips
queued changes, and stops the attached Trigger run. Sending status: "canceled"
to the update endpoint returns 400 without changing the sync.
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. Initialization reads the central defaults for that PMS, resolves stable account keys against the team's chart, and inserts only absent mapping rows. 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.
Registered integration flow events are committed with their source mutation before any background wake-up. The response can therefore precede flow start, but a missed wake-up does not lose the event. VRPlatform retries each selected listener from the same durable event and delivery IDs, so one failed listener does not restart listeners already accepted by the flow runner.
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.
First-party Tracking
POST /internal/events accepts authenticated tracking events. A successful response confirms
acceptance. Events from test/demo teams, test users or VRPlatform members are excluded
before storage and delivery to analytics destinations. Team eligibility uses the
computed test classification.
User-authenticated requests derive their team and user from authentication. Sec services
send x-team-id: 7bfec41f-aa3f-4602-95d2-f7996e4f0e59 (the VRPlatform team) to the central
API for routing and authentication. Optional teamId and userId
in the body identify the event target; that team may already be deleted. Other credentials
must keep attribution within their authenticated team.
The tracking worker excludes internal users before clearing missing users or users without membership in the supplied team. Inactive memberships remain valid. Deleted teams require their stored deletion snapshot; the recorded deletion initiator keeps user attribution. Audit evidence is retained independently of tracking eligibility.
Supply the same event ID when retrying the same event. A previously stored event keeps its payload and attribution; retries are filtered using that stored attribution. Reusing its ID with different values does not update the event. Deletion events and operator replays use the same queue. A replay disposition confirms re-enqueue; subsequent delivery failures use the normal retry and dead-letter handling.
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.
Whenever a flow operation accepts both flowId and connectionId, select the
flow from that connection's app. A pair from different apps is rejected before
any flow setting or mapping is read or changed; refresh the connection's flow
list instead of retrying the mismatched IDs.
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
| Situation | Rule |
|---|---|
| External identity | Stable inside its documented connection scope |
| Display name | Never use as identity |
| Dry-run ID | Discard; it was rolled back |
| Relationship array | Send complete set only for documented replace semantics |
| Database batch | Dry-run only when OpenAPI declares support |
| OAuth, upload, sync, invitation, email | No generic dry run |
| Extension credentials | Require stable unique ref, email, or username |
| Built-in API connection | Server assigns opaque identity; reconnect preserves it |
| Migration or cutover | Use 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.
Import Data from CSV
Use the dedicated Import Data from CSV guide for the complete UI flow, example requests and responses, mapping and preset contracts, pagination, and recovery states.
CSV imports use only top-level /csv-imports routes. Existing fixed
templates keep their synchronous Connection-scoped contract.
Mutation Recipe
- Resolve team and connection.
- Normalize and preserve stable source identifiers.
- Load existing mappings or platform IDs.
- Validate every item and relationship set.
- Dry-run a supported database batch or request dedicated preview.
- Present item outcomes and blockers.
- Apply with the same identity and payload.
- Persist returned platform IDs only from the real write.
- 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.
Legacy payment batches apply the same rule to nested lines. A line is stable
within its payment by uniqueRef and line type; concurrent retries update that
logical line even when each request generated a different internal row ID.
App connect and reconnect requests wait up to 25 seconds for provider work.
Provider failures return 502 BAD_GATEWAY; the awaited limit returns
504 GATEWAY_TIMEOUT. The provider task may still be active after a timeout,
so read the current connection state before retrying the external action.
Single-resource POST /connections/{id}/sync requests queue provider work and
return a connection-sync operation acknowledgement. Poll
GET /operations/{id} for the terminal outcome. Do not submit another request
for the same resource while its operation is queued or running. The operation
fails when that targeted sync records a failed resource change. Read
GET /syncs/{id}/changes?status=failed for the failed resource and message.
Failures from earlier syncs do not affect the current operation. Full
connection syncs retain their aggregate completion behavior.
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.
AccountConnection batch create and update groups are atomic. If rows conflict inside one group, the API rolls back that group and returns one structured issue per input instead of persisting a partial result. Correct the duplicate connection-scoped identity and retry the failed inputs.
Calendar block and unblock requests return an operation acknowledgement. Poll
GET /operations/{id} for the provider outcome, then refetch the calendar.
Past-date requests return 400 BAD_REQUEST before dispatch. Provider denials
fail the operation; their display message remains static and PMS details stay
inside the integration that owns their interpretation.
Reconnecting an integration that supports owner calendar blocking retries eligible failed owner blocks for future dates. Recovery requires the original owner's current access and preserves later calendar changes. For example, if an owner blocked October 10–12 during an outage and later unblocked October 11, recovery can retry October 10 and 12. It checks the remaining dates again before sending them to the integration. Repeated reconnects do not queue another copy of the same recovery request. Expired dates, canceled requests, and recognized provider denials are excluded. Requests whose dispatch outcome is still unknown must be reconciled before a new recovery request can run. Failed history remains available; reconnecting does not turn the original failed operation into a successful one.
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.
If the callback request times out, retry the same callback payload. The API reuses the original provider task for one hour, so it does not exchange the provider's one-time authorization code twice.
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
- Import listings — POST /listings/batch
- Import reservations — POST /reservations/batch
- Import account connections — POST /account-connections/batch
- Import bank records — POST /bank-records/batch
- Block calendar — POST /calendar-blocks
- Unblock calendar — DELETE /calendar-blocks
- Connect an app — POST /connections/connect
