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 codecode— 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.