onvaca_connect

Dart SDK for the OnVaca Connect Platform — a unified interface for multiple property management system (PMS) providers.

Installation

Add onvaca_connect to your pubspec.yaml:

dependencies:
  onvaca_connect: ^0.2.1

Then run:

dart pub get

Quick Start

import 'package:onvaca_connect/onvaca_connect.dart';

final connect = OnvacaConnect(
  apiKey: 'your-api-key',
  baseUrl: 'https://connect.onvaca.com',
);

// Set up a PMS connection
final setup = await connect.setups.create(
  const CreateSetupInput(
    provider: 'rentalwise',
    userId: 'user-123',
    workspace: 'my-workspace',
    apiKey: 'users-pms-api-key',
  ),
);

// List properties
final properties = await connect.properties.list();
print('Found ${properties.total} properties');

// Create a booking
final booking = await connect.bookings.create({
  'property': '100',
  'daterange': {'start': '2026-06-01', 'end': '2026-06-07'},
  'adults': 2,
});
print('Booking created: ${booking.providerRef}');

// Always close the client when done
connect.close();

Configuration

final connect = OnvacaConnect(
  // Required
  apiKey: 'your-api-key',
  baseUrl: 'https://connect.onvaca.com',

  // Optional with defaults
  basePath: '/api/v1',                      // API version path
  timeout: const Duration(seconds: 15),     // Request timeout
  retries: 3,                               // Auto-retry count (5xx and 429 only)
);

Retry uses exponential backoff (1s, 2s, 4s... capped at 10s) and only triggers on server errors (5xx) and rate limits (429). Client errors (4xx) are never retried.

Resources

Resource Methods Description
properties list(), get(id) Property listings and metadata
bookings list(), get(id), create(data), cancel(id), confirm(id), quote(data) Booking management and quotes
payments list(), get(id), create(data), refund(id) Payment tracking and refunds
messages list(), get(id), send(data) Guest communication
rates get(data), update(data) Rate and availability management
setups create(input), delete(setupId) PMS provider connection setup

Setting Up a PMS Connection

Before accessing entities, create a setup that connects your app to a PMS provider:

final setup = await connect.setups.create(
  const CreateSetupInput(
    provider: 'rentalwise',         // PMS provider slug
    userId: 'user-123',             // Your app's user ID
    workspace: 'my-workspace',      // Your app's workspace/tenant
    apiKey: 'users-pms-api-key',    // The user's PMS API credentials
  ),
);

print(setup.setupId);  // Unique setup identifier
print(setup.linked);   // true if successfully linked
print(setup.provider); // 'rentalwise'

Once linked, the gateway syncs entities (properties, bookings, etc.) automatically.

To disconnect and remove all synced data:

await connect.setups.delete(setup.setupId);

Connections & Operations

Check which providers are connected and what operations they support:

// List all connected providers
final connections = await connect.connections();
for (final conn in connections) {
  print('${conn.label}: ${conn.operations.join(', ')}');
}

// List all available operations
final operations = await connect.operations();
for (final op in operations) {
  print('${op.slug} (${op.category}, ${op.direction})');
}

// Check if a specific operation is supported before calling it
if (await connect.supportsOperation('create-booking')) {
  final result = await connect.bookings.create({...});
}

Pagination & Filtering

List operations return paginated results. Use EntityListOptions to control pagination and apply filters:

// Paginate
final page1 = await connect.properties.list(
  options: const EntityListOptions(page: 1, perPage: 20),
);

print('Total: ${page1.total}');
print('Has next page: ${page1.hasNextPage}');

if (page1.hasNextPage) {
  final page2 = await connect.properties.list(
    options: EntityListOptions(
      page: page1.page + 1,
      perPage: page1.perPage,
    ),
  );
}

// Filter by provider
final rwBookings = await connect.bookings.list(
  options: const EntityListOptions(provider: 'rentalwise'),
);

// Filter by last sync time (incremental sync)
final recentChanges = await connect.properties.list(
  options: EntityListOptions(
    updatedSince: DateTime.now().subtract(const Duration(hours: 1)),
  ),
);

Direct Entity Access

Access entities directly by ID or query across all entity types, bypassing resource classes:

// Get any entity by ID
final entity = await connect.entity('uuid-123');
print(entity.entityType);  // 'property', 'booking', etc.
print(entity.rawData);     // Raw PMS data
print(entity.pmsEntityId); // The provider's native ID

// Query all entity types at once
final allEntities = await connect.entities(
  options: const EntityListOptions(
    provider: 'rentalwise',
    perPage: 100,
  ),
);

Execute Operations

Use execute() for direct operation calls. Resource methods like bookings.create() use this under the hood:

// Direct execute
final result = await connect.execute(
  'create-booking',
  data: {
    'property': '100',
    'daterange': {'start': '2026-06-01', 'end': '2026-06-07'},
    'adults': 2,
  },
);

print(result.success);        // true
print(result.operationSlug);  // 'create-booking'
print(result.data);           // Response data from the PMS
print(result.providerRef);    // External reference ID

Async Operations & Job Polling

For long-running operations, use async mode. The gateway returns a jobId that you can poll:

// Fire-and-forget with async
final result = await connect.execute(
  'sync-rates',
  data: {'property': '100'},
  options: const ExecuteOptions(isAsync: true),
);

// Poll for completion
if (result.jobId != null) {
  JobStatus status;
  do {
    await Future.delayed(const Duration(seconds: 2));
    status = await connect.job(result.jobId!);
    print('Job status: ${status.status}');
  } while (status.status == 'pending');

  print('Job result: ${status.data}');
}

// Or use a callback URL instead of polling
await connect.execute(
  'sync-rates',
  data: {'property': '100'},
  options: const ExecuteOptions(
    isAsync: true,
    callbackUrl: 'https://myapp.com/webhooks/onvaca',
  ),
);

// Idempotency key to prevent duplicate operations
await connect.execute(
  'create-booking',
  data: {...},
  options: const ExecuteOptions(
    idempotencyKey: 'booking-abc-123',
  ),
);

Error Handling

The SDK uses sealed exception classes for exhaustive error handling with Dart 3 pattern matching:

try {
  final booking = await connect.bookings.create({
    'property': '100',
    'daterange': {'start': '2026-06-01', 'end': '2026-06-07'},
    'adults': 2,
  });
} on OnvacaConnectException catch (e) {
  switch (e) {
    case AuthenticationException(:final message):
      // 401 — e.g. "API key has expired"
      print('Auth failed: $message');
    case EntityNotFoundException(:final message, :final code):
      // 404 — e.g. "Setup not found" with code "NOT_FOUND"
      print('Not found ($code): $message');
    case OperationNotSupportedException(:final message):
      // 400 — e.g. "Operation 'create-booking' not supported"
      print('Not supported: $message');
    case RateLimitException(:final retryAfter, :final details):
      // 429 — retryAfter from header or details.retryAfter
      print('Rate limited. Retry after: $retryAfter');
    case ProviderUnavailableException(:final message, :final details):
      // 502/503/504 — details may contain operation, providerType
      print('Provider down: $message');
      print('Operation: ${details?['operation']}');
    case UnknownApiException(:final statusCode, :final message):
      // Unexpected error
      print('Error ($statusCode): $message');
  }
}

All exceptions share these fields:

  • message — the backend's error message (not hardcoded)
  • statusCode — HTTP status code
  • code — error code from the backend (e.g. 'NOT_FOUND', 'UNAUTHORIZED', 'BAD_GATEWAY')
  • details — additional context (e.g. retryAfter, operation, providerType)

The compiler enforces exhaustive handling — if a new exception type is added, your code won't compile until you handle it.

Generic Request

For undocumented or custom gateway endpoints:

final data = await connect.request<Map<String, dynamic>>(
  'GET',
  '/gateway/custom-endpoint',
  queryParameters: {'key': 'value'},
  fromJson: (json) => json,
);

License

MIT

Libraries

onvaca_connect
Dart SDK for the OnVaca Connect Platform.