TypeScript client for the Twinfinity Twin Service API. Provides typed access to twins, assets, and twin files with optimistic concurrency control and structured error handling.
bun add @twinfinity/twin
@twinfinity/authentication - provides TwinfinityHttpClient for authenticated HTTP requests@twinfinity/t8-client-model - provides T8 client model types used by loadTwin / loadGeometryInfoimport { TwinClient } from '@twinfinity/twin';
const client = new TwinClient(apiUrl, httpClient);
The baseUrl is the API root (e.g. https://api.twinfinity.com). The client appends /_ps/twins internally.
// List twins (paginated)
const page = await client.listTwins();
// List all twins (auto-paginates)
const allTwins = await client.listAllTwins();
// Get a specific twin
const twin = await client.getTwin('twin-id');
// Get a specific version
const twinV2 = await client.getTwin({ id: 'twin-id', version: 2 });
// Create or update
const updated = await client.createOrUpdateTwin('twin-id', {
name: 'My Twin',
relatedContainerIds: ['container-1'],
etag: twin.etag // required for updates
});
// Soft-delete and restore
const deleted = await client.deleteTwin('twin-id', twin.etag);
const restored = await client.restoreTwin('twin-id', deleted.etag);
The client can download and deserialize processed twin content (T8CM/T8GI formats):
// Load client model with default types
const { id, version, model } = await client.loadTwin('twin-id');
// Load with custom factory functions for domain-specific types
const custom = await client.loadTwinWithFactories(
'twin-id',
myProjectFactory,
myProductFactory,
myEntityTypeFactory,
myUnitFactory,
myConnectProject,
myConnectProduct
);
// Load geometry info
const geometry = await client.loadGeometryInfo('twin-id');
// Load client model with geometry combined
const withGeometry = await client.loadTwinWithGeometry('twin-id');
When no version is specified, the client searches versions (newest first) for one that has the requested content format.
// List assets in a twin
const assets = await client.listAssets('twin-id');
// Include soft-deleted assets
const allAssets = await client.listAssets('twin-id', 0, 100, { includeDeleted: true });
// Create assets
const created = await client.createAssets('twin-id', [{ name: 'Pump A', category: 'HVAC', type: 'Pump' }]);
// Update with optimistic concurrency
const updated = await client.updateAsset('twin-id', 'asset-id', {
etag: asset.etag,
name: 'Pump A (Updated)'
});
// Batch operations (atomic - all succeed or all roll back)
const result = await client.batchAssets('twin-id', [
{ operation: 'upsert', id: 'asset-1', item: { name: 'New Asset' } },
{ operation: 'delete', id: 'asset-2', item: { etag: 'etag-2' } },
{ operation: 'restore', id: 'asset-3', item: { etag: 'etag-3' } }
]);
// Batch get multiple assets by ID
const fetched = await client.batchGetAssets('twin-id', ['id-1', 'id-2']);
// List files (with filtering)
const files = await client.listTwinFiles('twin-id', { checkedIn: true });
// Create or update a file
const file = await client.createOrUpdateTwinFile('twin-id', 'file-id', {
source: { changeId: 'doc-id', changeVersion: 1, sourceType: 'DocumentChange' },
name: 'model.ifc',
quality: 100
});
// Download file content
const blob = await client.getTwinFileContent('twin-id', 'file-id');
All methods throw typed errors for specific HTTP failure codes. Unexpected status codes throw a generic Error.
| Error | HTTP Status | Context Attached |
|---|---|---|
TwinValidationError |
400 | errorBody?: TwinErrorBody |
TwinNotFoundError |
404 | - |
TwinConflictError |
409 | currentTwin: TwinGet |
AssetValidationError |
400 | errorBody?: TwinErrorBody |
AssetForbiddenError |
403 | - |
AssetNotFoundError |
404 | - |
AssetConflictError |
409 | currentAsset: AssetGet |
AssetBatchError |
404, 409 | response: AssetBatchResponse |
TwinFileValidationError |
400 | errorBody?: TwinErrorBody |
TwinFileNotFoundError |
404 | - |
TwinFileConflictError |
409 | currentTwinFile: TwinFileGet |
ProcessorNotFoundError |
404 | - |
Conflict errors (409) carry the current server-side state, enabling retry logic:
import { TwinConflictError } from '@twinfinity/twin';
try {
await client.createOrUpdateTwin('twin-id', twinData);
} catch (e) {
if (e instanceof TwinConflictError) {
// e.currentTwin contains the server's current version
// Use it to merge changes and retry
const merged = mergeChanges(twinData, e.currentTwin);
await client.createOrUpdateTwin('twin-id', {
...merged,
etag: e.currentTwin.etag
});
}
}
TwinValidationError, TwinFileValidationError, and AssetValidationError may include a structured error body:
interface TwinErrorBody {
detail: string;
cause: string;
item?: {
kind: string;
id: string;
version?: number;
field?: string;
};
}
AssetBatchError carries the full batch response with per-item results, including which operations were reverted:
import { AssetBatchError } from '@twinfinity/twin';
try {
await client.batchAssets('twin-id', operations);
} catch (e) {
if (e instanceof AssetBatchError) {
for (const result of e.response.results) {
if (result.status === 'conflicted') {
console.log(`Item ${result.index} conflicted: ${result.error?.detail}`);
}
}
}
}
The package has two export paths:
@twinfinity/twin - Public API: TwinClient, ITwinClient, all types, all error classes@twinfinity/twin/internal/* - Internal modules (no stability guarantees)