VRPlatformVRPlatform

Asynchronous Operations

Track queued work through one provider-neutral lifecycle

Some mutations queue work instead of holding the HTTP request open. An endpoint that uses this contract returns the same operation envelope regardless of the worker or execution provider:

{
  "operationId": "750c9ce8-81ed-41b9-b04e-7f9330c583ed",
  "type": "team-duplicate",
  "status": "queued",
  "resource": {
    "type": "team",
    "id": "27cb2c17-ce6a-4072-838a-4835573cdf54",
    "role": "source"
  }
}

Persist operationId. It is the stable public identifier for subsequent status reads. Internal effect records and execution-provider identifiers are not part of the API contract.

Read Status

GET /operations/{id}

Replace {id} with the operationId returned by the initiating endpoint:

curl \
  -H "x-api-key: $VRPLATFORM_API_KEY" \
  "https://api.vrplatform.app/operations/750c9ce8-81ed-41b9-b04e-7f9330c583ed"

The response contains the public operation type and lifecycle timestamps:

{
  "id": "750c9ce8-81ed-41b9-b04e-7f9330c583ed",
  "type": "vri-statement-import",
  "status": "running",
  "resource": {
    "type": "team",
    "id": "27cb2c17-ce6a-4072-838a-4835573cdf54",
    "role": "target"
  },
  "resources": [
    {
      "type": "team",
      "id": "27cb2c17-ce6a-4072-838a-4835573cdf54",
      "role": "target"
    }
  ],
  "failure": null,
  "createdAt": "2026-08-15T13:45:10.123Z",
  "startedAt": "2026-08-15T13:45:12.456Z",
  "finishedAt": null
}

type is a stable public category, independent of the worker implementation. resource is the primary associated domain resource. resources includes all visible source, target, and result associations. resource is null and resources is empty when an operation has no public domain resource. Top-level CSV imports use that shape because uploads and previews are workflow records, not domain resources.

When a completed operation has a file result, that resource includes a stable authenticated url such as /files/{id}/download. The URL is absent until the operation is completed and is not an expiring R2 reference.

StatusMeaning
queuedThe operation is durably recorded and waiting for execution.
runningA worker is executing the operation.
completedEvery required source completed.
failedEvery required source settled and at least one failed.

createdAt is always present. startedAt is the first available execution or subtask timestamp and can be null when the underlying operation does not retain a separate start instant. finishedAt is present only after the operation is completed or failed.

failure is null unless the operation has a safe, actionable public failure. It never contains raw provider errors, payloads, stack traces, or worker data. Failure codes and messages are fixed by the API:

failure.codefailure.message
operationFailedOperation failed
csvPreviewFailed and CSV source failure codesCSV preview failed
csvImportFailedCSV import failed
csvImportRecordFailedCSV import has record failures

Treat an unknown failure code as a generic terminal failure. Do not display or parse internal worker errors.

Poll until the operation reaches completed or failed. Start with a short interval and back off while the status remains non-terminal. The public API currently provides status reads only, not retry or cancellation controls.

List Operations

GET /operations

Use status, type, createdAtFrom, and createdAtTo to filter requester-owned history. Use resourceType and resourceId together to find operations for a team, connection, listing, or another associated resource. The response includes cursor-paginated items, the retained match total, and counts for all four statuses.

Resource list endpoints can embed the same summary so a refreshed UI can show pending or failed work without retaining operation IDs or making one request per row.

Initiating Endpoints

Every public endpoint that creates an operation is linked below. Conditional routes return an operation only in the cases stated.

TypeInitiating endpoints
team-deleteDELETE /team
team-duplicatePOST /team/duplicate
team-initializePOST /team/init
team-demo-dataPOST /team/generate-demo-data
vri-team-migrationPOST /partner/vri-to-vrt
vri-statement-importPOST /partner/vri-to-vrt/statements
calendar-blockPOST /calendar-blocks
calendar-unblockDELETE /calendar-blocks
connection-connectPOST /connections; POST /connections/connect; POST /connections/{id}/connect
connection-archiveDELETE /connections/{id} when archived
connection-syncPOST /connections/{id}/sync
connection-extractPOST /connections/extract
pms-cutover-previewPOST /connections/pms-cutover/preview
pms-cutoverPOST /connections/pms-cutover/apply
csv-import-previewPOST /csv-imports/previews
csv-importPOST /csv-imports/previews/{id}/confirm; POST /connections/{id}/csv-import
ach-verificationPOST /contacts/{id}/payment-methods/ach
provider-paymentPOST /transactions/{id}/pay; POST /statements/pay when dispatched
provider-payment-recoveryPOST /transactions/{id}/provider-payments/{providerPaymentId}/cancel; POST /transactions/{id}/provider-payments/{providerPaymentId}/archive
plaid-initial-syncPOST /plaid/connect/configure
bank-rule-runPOST /bank-rules/{id}/run without dryRun
recurring-transaction-runPOST /transactions/recurring-templates/{id}/run without dryRun
historical-gl-importPOST /general-ledger/historical without dryRun
historical-statement-importPOST /statements/historical without dryRun
reservation-journal-refreshPOST /reservations/journal-entries for multiple IDs or an omitted selection
transaction-journal-refreshPOST /transactions/journal-entries for multi-row work
listing-journal-refreshPOST /listings/ownership-periods; PUT or DELETE /listings/ownership-periods/{id}; PUT or DELETE /listings/{id}/parent, all in GL
webhook-testPOST /webhooks/{id}/test; POST /webhook-deliveries/{id}/replay

POST /flows/{id}/connections/{connectionId}/run creates a flow-run operation but remains excluded from the public API reference. The internal team deletion route also creates team-delete; internal routes remain documented in the engineering RFC instead of the public API reference.

Generated exports

Every supported generated CSV, PDF, and ZIP path exposes two delivery modes:

  • GET takes its selector in query parameters, waits for admitted work, and returns { url, expiresAt }.
  • POST takes the same selector fields in its JSON body, creates a durable export operation, and returns the standard operation acknowledgement.

GET admission limits work to the one-minute HTTP request budget. A successful GET does not create a public operation, including when it waits for Trigger. GET may wait for a Trigger task. GET /statements/pdf is the only generated artifact route that can render directly in the public request Worker. Both methods return metadata to the client, never the artifact bytes.

Choose a client flow

Use GET first when the caller can keep the request open while the artifact is generated:

  1. Send GET with the selector in query parameters.
  2. On 200, download from url before expiresAt.
  3. On 422 EXPORT_REQUIRES_POST, send POST to the same path with the same selector fields in the JSON body.
  4. Persist the returned operationId and poll GET /operations/{id}.
  5. On completed, use the authenticated url on the result file resource. On failed, show the safe failure when present. Use a generic terminal failure when the API has no safe details to expose.

Change from GET to POST only when the response code is EXPORT_REQUIRES_POST. A timeout, an aborted request, a 5xx response, or a different error code does not establish that the selection requires POST. Handle those failures through the Error Contract.

Call POST first when the caller needs a durable operation, background progress, or a result that remains available after the initiating request ends. A queued POST acknowledgement confirms acceptance. It does not confirm that generation completed.

An oversized GET returns this 422 response without creating an operation:

{
  "code": "EXPORT_REQUIRES_POST",
  "message": "Export must be queued",
  "issues": [],
  "context": {
    "recommendedMethod": "POST",
    "reason": "selectionTooLarge"
  }
}

Paths with GET and POST

Every path below supports awaited GET and durable POST.

  • /accounts/csv
  • /bank-records/csv
  • /contacts/csv
  • /listings/csv
  • /reservations/csv
  • /reservations/line-mappings/csv
  • /transactions/csv
  • /statements/csv
  • /statements/detail/csv
  • /statements/by-period/csv
  • /statements/pdf
  • /statements/pdf/batch
  • /reports/guest-balances/csv
  • /reports/profit-and-loss/csv
  • /reports/balance-sheet/csv
  • /reports/trial-balance/csv
  • /reports/journal-entries/csv
  • /reports/manager-statements/csv
  • /reports/manager-statements/detail/csv
  • /reports/manager-statements/detail/pdf
  • /reports/manager-statements/pdf
  • /reports/manager-statements/detail/journal-entries/csv
  • /reports/trust-reconciliation/csv
  • /reports/trust-reconciliation/by-listing/csv
  • /reports/trust-reconciliation/pdf
  • /reports/trust-reconciliation/by-listing/pdf
  • /reports/owner-statement-summaries-per-layout/csv
  • /reports/owner-statement-summaries-per-layout/csv/detailed
  • /reports/owner-statement-summaries-per-layout/details/{contactId}/csv
  • /reports/owner-statement-summaries-per-layout/details/{contactId}/pdf
  • /reports/owner-statement-summaries-per-layout/pdf
  • /reports/sales-tax-liability/csv

Authorization and Privacy

The status route requires operations:read. Existing team and Partner permission bundles grant that scope where operation polling is supported.

An operation belongs to the tenant that owns its public operation record. A Partner migration operation belongs to the requesting Partner. A provider payment recovery operation belongs to the team that owns the payout. Unknown identifiers, unsupported internal action or sync types, malformed recovery records, and identifiers owned by another tenant return 404.

The response never exposes the request payload, raw error details, effect records, or the execution provider's run identifier. Failed operations can expose only a stable code and static safe message. Internal operations without a stable public type are not readable through this endpoint.

Poll Through MCP

Use vrt_api_read:

{
  "path": "/operations/750c9ce8-81ed-41b9-b04e-7f9330c583ed"
}

For API-key authentication, MCP preserves the credential tenant and does not send x-team-id for this read. OAuth users supply the team that initiated the operation through teamId or the MCP connection's team_id.

On this page