Sync API
The Sync API is designed for third-party systems that want to replicate LetsBuild user data. It provides a change-based synchronization mechanism that allows clients to efficiently track and replicate changes to entities over time.
Overview
The Sync API is a read-only API that does not support write operations. It's built around the concept of change tracking, where each modification to an entity is captured and can be retrieved in chronological order.
Key Concepts
- Change Tracking: Every modification to an entity is recorded with an operation type (created, updated, deleted)
- Cursor-based Pagination: Uses opaque cursors to maintain sync state and paginate through changes
- Eventually Consistent: Changes may take time to propagate through the system
- Entity Snapshots: Each change includes a complete snapshot of the entity at that point in time
Available Sync Queries
The Sync API provides four main sync endpoints, each tracking changes for different entity types:
1. Project Sync
Tracks changes to projects in your accessible projects.
query ProjectSync($cursor: String) {
projectSync(cursor: $cursor) {
changes {
entityId
entityType
operationType
data {
... on Project {
id
name
code
address
city
zipCode
startDate
endDate
isArchived
}
}
}
cursor
hasMore
}
}
2. Form Sync
Tracks changes to forms (quality, safety, environmental) within your projects.
query FormSync($cursor: String) {
formSync(cursor: $cursor) {
changes {
entityId
entityType
operationType
data {
... on Form {
id
subject
code
codeNum
date
type
status
isConform
isArchived
isUrgent
projectId
listId
}
}
}
cursor
hasMore
}
}
3. Point Sync
Tracks changes to points (punch list items) within your projects.
query PointSync($cursor: String) {
pointSync(cursor: $cursor) {
changes {
entityId
entityType
operationType
data {
... on Point {
id
subject
code
codeNum
date
isArchived
isUrgent
dueDate
projectId
listId
status {
name
color
}
}
}
}
cursor
hasMore
}
}
4. Contact Details Sync
📖 Contact Details Sync Reference
Tracks changes to contact details and project participants.
query ContactDetailsSync($cursor: String) {
contactDetailsSync(cursor: $cursor) {
changes {
entityId
entityType
operationType
data {
... on ContactDetails {
id
displayName
company
role
phone
street
city
zip
projectId
userId
isDisabled
accessRightLevel
invitationStatus
}
}
}
cursor
hasMore
}
}
Understanding Sequence Versions
Each entity in the sync system has a sequenceVersion
field that plays a crucial role in maintaining data consistency and ordering.
What is Sequence Version?
The sequenceVersion
is a BigInt
field that represents a monotonically increasing, unique identifier for each entity modification. It's used internally by the sync system to:
- Order Changes: Ensure changes are processed in the correct chronological order
- Detect Conflicts: Identify when multiple changes occur simultaneously
- Optimize Performance: Enable efficient change detection and filtering
- Maintain Uniqueness: Each modification gets a unique sequence number
How Sequence Version Works
- Initial Value: When an entity is created, it gets an initial sequence version
- Monotonic Increment: Each modification receives a higher sequence version than the previous one
- Global Uniqueness: Sequence versions are unique across the entire system
- Change Tracking: The sync system uses sequence versions to determine what changes to include in sync responses
- Cursor Encoding: The cursor internally encodes the last processed sequence version
Using Cursors for Sync
The cursor is the primary mechanism for maintaining sync state and paginating through changes.
Cursor Properties
- Opaque: You should never construct or parse cursors manually
- Stateful: Contains both sync position and pagination state
- Immutable: Each cursor represents a specific point in time
- Forward-Only: Cursors can only move forward in time
Sync Workflow
// Initial sync - no cursor
let cursor = null;
while (true) {
const response = await graphqlClient.request(`
query PointSync($cursor: String) {
pointSync(cursor: $cursor) {
changes {
entityId
entityType
operationType
data { ... }
}
cursor
hasMore
}
}
`, { cursor });
// Process changes
for (const change of response.pointSync.changes) {
await processChange(change);
}
// Update cursor for next iteration
cursor = response.pointSync.cursor;
// If hasMore is true, continue immediately
// If hasMore is false, wait before polling again
if (!hasMore) {
await new Promise(resolve => setTimeout(resolve, 60000)); // Wait 1 minute
}
}
Best Practices for Cursor Usage
- Store Cursors Persistently: Save cursors to your database to resume sync after restarts
- Process Changes Sequentially: Handle changes in the order they're returned
- Handle Interruptions Gracefully: Resume from the last stored cursor if sync is interrupted
- Monitor hasMore: Continue polling until
hasMore
isfalse
, after that poll with larger intervals to avoid rate limiting
Sync API Latency
The Sync Service precomputes a "view of the world" for each user, allowing it to efficiently respond to queries about a user's data at a specific point in time. As a result, there can be a delay between a change made in LetsBuild and when it is reflected in the Sync API.
Consistency Model
- Eventually Consistent: All changes will eventually be reflected
- Typical Latency: Less than 5 minutes
- Maximum Latency: Can be longer in some cases (high load, maintenance)
- No Real-time Updates: Changes are not pushed to clients
FAQ
Why am I getting "00000000-0000-0000-0000-000000000000" (null Guid) for some UUID fields?
This scenario can occur when several related entities are created in LetsBuild within a short time frame. The Sync Service might have processed some changes but not all, resulting in incomplete data. To avoid locking and to return changes as quickly as possible, the service may provide partial data initially.
Solution: This is a rare occurrence. Since the system is eventually consistent, the missing data will be available in the Sync API after a short delay, reflected as an update to the partial entity. No action is required on the client side.
How often should I poll for changes?
The optimal polling frequency depends on your use case:
- Real-time applications: Poll every 30-60 seconds
- Batch processing: Poll every 5-15 minutes
- Daily sync: Poll once per day
What happens if I miss some changes?
The sync system is designed to be resilient. If you miss changes due to network issues or downtime:
- Resume from your last stored cursor
- All missed changes will be included in subsequent sync calls
- No data will be lost
Can I sync multiple entity types simultaneously?
Yes, you can run multiple sync queries in parallel. Each entity type has its own cursor and can be synced independently.
Next Steps
Now that you understand the Sync API:
- API Entities - Learn about the available entities
- GraphQL Guide - Understand GraphQL basics
- API Reference - Browse the complete API schema