Overview
{productName} offers three methods to detect and track changes in your data. Each method has different capabilities and is suited for specific use cases.
Choosing the Right Method
- updatedAt — Simple periodic syncs without deletion tracking
- Changes API — Complete change history with polling
- Webhooks — Real-time notifications with automatic delivery
Method Comparison
Compare the three change detection methods to choose the best fit for your use case
card: updatedAt
**Advantages**
- Simple to implement
- Low overhead - uses standard query filters
- Good for periodic syncs (hourly, daily)
**Limitations**
- Does not detect deletions
- Requires polling
- May miss rapid changes if polling interval is too long
**Best For:** Simple integrations where deletions are rare or not critical
card: Changes API
**Advantages**
- Captures all operations (create, update, delete)
- Version tracking with sequential IDs
- Reliable - won't miss any changes
- Can track multiple entities
**Limitations**
- Requires polling
- Need to maintain last version ID
- Delay based on polling interval
**Best For:** Robust integrations requiring complete change history and deletion tracking
card: Webhooks
**Advantages**
- Real-time notifications (within 5 seconds)
- No polling required
- Batched delivery (up to 100 changes)
- Built-in retry mechanism
- Supports filtering
**Limitations**
- Requires publicly accessible endpoint
- Must handle webhook reliability
- More complex setup
**Best For:** Real-time integrations requiring immediate action on data changes
Method 1: Using updatedAt [badge:Simple|info]
Track changes using the updatedAt timestamp field available on all entities
**Important Limitation:**
The `updatedAt` method does not detect deletions. If you need to track deleted records, use the Changes API or Webhooks instead.
Query Products Updated Since Last Sync
Retrieve all products that were modified after a specific timestamp
GET /c/{companyId}/products?filter=updatedAt > '2025-11-14T10:00:00Z'&limit=100
// Response
{
"data": [
{
"id": "prod-123",
"name": "Updated Product",
"price": 99.99,
"updatedAt": "2025-11-14T10:15:30Z"
}
// ... more products
]
}
Track Last Sync Time (Correct Method)
Store the timestamp from the last fetched record to avoid missing changes
// Initialize with a timestamp from your last sync
// For first sync, use a timestamp in the past or null
let lastSyncTime = '2025-01-01T00:00:00Z';
// On each sync
const url = `/c/{companyId}/products?filter=${encodeURIComponent(
`updatedAt > '${lastSyncTime}'`
)}&limit=100&sort=updatedAt`;
const response = await fetch(url);
const { data } = await response.json();
if (data.length > 0) {
// Process the changed products
data.forEach(product => {
console.log('Updated:', product.id, product.updatedAt);
});
// ✅ CORRECT: Use updatedAt from the last record
lastSyncTime = data[data.length - 1].updatedAt;
// ❌ WRONG: Never use new Date().toISOString()
// This can miss changes due to:
// - Time between query and new Date() call
// - Clock skew between client and server
}
⚠️ Critical: Time Synchronization
**Never use `new Date().toISOString()` for lastSyncTime!**
This approach has two critical problems:
1. **Race condition:** Changes can occur between fetching data and calling `new Date()`, causing you to miss those changes on the next sync.
2. **Clock skew:** Client and server clocks may not be synchronized. If the client clock is ahead, you'll miss changes. If it's behind, you'll get duplicate data.
✅ Always use the `updatedAt` value from the last record returned by the server.
Method 2: Using Changes API [badge:Complete|success]
Track all changes including deletions using the changes log with version tracking
Key Benefits
- Captures create, update, and delete operations
- Sequential version IDs for reliable tracking
- Query multiple entities in a single request
- No data loss even with long polling intervals
Get Recent Changes for Products
Retrieve all changes (including deletions) for products with version tracking
GET /c/{companyId}/changes?filter=entity = 'products' and id > 100&limit=100
// Response
{
"data": [
{
"id": 101, // version number
"entity": "products",
"rowId": "prod-123",
"operation": "update",
"changeSetId": null,
"createdAt": "2025-11-14T10:15:30Z"
},
{
"id": 102,
"entity": "products",
"rowId": "prod-456",
"operation": "delete",
"changeSetId": null,
"createdAt": "2025-11-14T10:16:45Z"
}
]
}
Poll for Changes with Version Tracking
Continuously poll for new changes using the last known version ID
// Store the last processed version
let lastVersionId = 0;
async function pollForChanges() {
const filter = encodeURIComponent(
`entity = 'products' and id > ${lastVersionId}`
);
const response = await fetch(
`/c/{companyId}/changes?filter=${filter}&limit=100&sort=id`
);
const { data } = await response.json();
if (data.length > 0) {
// Process changes
for (const change of data) {
console.log(
`Change #${change.id}: ${change.operation} on ${change.rowId}`
);
if (change.operation === 'delete') {
// Handle deletion
console.log('Product deleted:', change.rowId);
} else {
// Fetch the updated product
const product = await fetch(
`/c/{companyId}/products/${change.rowId}`
).then(r => r.json());
console.log('Product updated:', product);
}
}
// Update last version ID
lastVersionId = data[data.length - 1].id;
}
}
// Poll every 30 seconds
setInterval(pollForChanges, 30000);
Get Changes for Multiple Entities
Monitor changes across different entity types
const filter = encodeURIComponent(
`(entity = 'products' OR entity = 'categories') and id > ${lastVersionId}`
);
const response = await fetch(
`/c/{companyId}/changes?filter=${filter}&limit=100&sort=id`
);
const { data } = await response.json();
// Group changes by entity
const changesByEntity = data.reduce((acc, change) => {
if (!acc[change.entity]) acc[change.entity] = [];
acc[change.entity].push(change);
return acc;
}, {});
console.log('Products changed:', changesByEntity.products?.length || 0);
console.log('Categories changed:', changesByEntity.categories?.length || 0);
Method 3: Using Webhooks [badge:Real-time|warning]
Receive automatic notifications within 5 seconds of any data change
**Real-time Features**
- Notifications delivered within 5 seconds of changes
- Batched delivery (up to 100 changes per webhook)
- Filter by entity type (specific entity or all entities)
- Filter by row conditions (e.g., "price > 100")
- Filter by changed columns (e.g., ["price", "status"])
- Automatic retry with exponential backoff
Register Webhook for Products
Receive automatic notifications when products change
POST /c/{companyId}/hooks
Content-Type: application/json
{
"entity": "products",
"url": "https://your-api.com/webhooks/products",
"action": "webhook"
}
// Your webhook endpoint will receive:
{
"hookId": "hook-123",
"tenantId": "tenant-789",
"organisationId": "{companyId}",
"changeSetId": null,
"changes": [
{
"id": "change-001",
"entity": "products",
"operation": "update",
"rowId": "prod-123",
"changeSetId": null
}
],
"timestamp": "2025-11-14T10:30:00.000Z"
}
Webhook Handler Example
Process incoming webhook notifications
// Express.js webhook handler
app.post('/webhooks/products', async (req, res) => {
const { changes } = req.body;
for (const change of changes) {
console.log(
`Webhook: ${change.operation} on ${change.rowId}`
);
if (change.operation === 'delete') {
// Handle product deletion
await deleteProductInMySystem(change.rowId);
} else {
// Fetch and process the updated product
const product = await fetch(
`https://api.productsync.com/c/{companyId}/products/${change.rowId}`,
{
headers: {
'Authorization': 'Bearer YOUR_API_KEY'
}
}
).then(r => r.json());
await updateProductInMySystem(product);
}
}
// Always respond with 200 to acknowledge receipt
res.status(200).json({ received: true });
});
Webhook for Specific Entity
Limit notifications to a single entity type
POST /c/{companyId}/hooks
Content-Type: application/json
{
"entity": "products",
"url": "https://your-api.com/webhooks/products",
"action": "webhook"
}
// Only products changes will trigger this webhook
// Changes to categories, units, etc. will be ignored
Webhook for All Entities
Monitor all entity changes in one webhook
POST /c/{companyId}/hooks
Content-Type: application/json
{
"entity": null,
"url": "https://your-api.com/webhooks/all-changes",
"action": "webhook"
}
// This webhook will receive notifications for ALL entity types
// Products, categories, units, customers, orders, etc.
Webhook with Row Condition Filter
Only receive notifications when rows match specific conditions
POST /c/{companyId}/hooks
Content-Type: application/json
{
"entity": "products",
"url": "https://your-api.com/webhooks/expensive-products",
"action": "webhook",
"filter": "price > 100 AND status = 'active'"
}
// You will only receive webhooks when:
// - The entity is 'products'
// - AND the product price is greater than 100
// - AND the product status is 'active'
Webhook with Column Filter
Only trigger when specific columns change
POST /c/{companyId}/hooks
Content-Type: application/json
{
"entity": "products",
"url": "https://your-api.com/webhooks/product-pricing",
"action": "webhook",
"columns": ["price", "discount", "tax"]
}
// Webhook only triggers when price, discount, or tax changes
// Changes to name, description, etc. will be ignored
Webhook with Combined Filters
Combine entity, row conditions, and column filters
POST /c/{companyId}/hooks
Content-Type: application/json
{
"entity": "products",
"url": "https://your-api.com/webhooks/premium-product-pricing",
"action": "webhook",
"filter": "price > 100",
"columns": ["price", "status"]
}
// Triple filtering:
// 1. Only 'products' entity
// 2. Only when price > 100
// 3. Only when price or status columns change
** Webhook Filtering Options**
**1. Entity Filtering**
Set `entity` to limit notifications to a specific entity type (e.g., "products", "categories"). Use `null` to receive notifications for all entities.
**2. Row Condition Filtering**
Use `filter` with SQL-like where clauses to only trigger webhooks when rows match specific conditions (e.g., "price > 100 AND status = 'active'").
**3. Column Change Filtering**
Specify `columns` array to only trigger when those specific columns have changed. Perfect for monitoring price updates, status changes, etc.
**4. Combined Filtering**
Combine all three filters for precise control: entity type + row conditions + column changes. This minimizes unnecessary webhook calls and processing.
Note: For complete webhook documentation including retry logic, filtering options, and payload examples, see the [navigate:/devdoc/webhooks|Webhooks Documentation].
Best Practices
When to Use updatedAt
- Simple integrations with periodic syncs (hourly, daily)
- When deletions are handled separately or not needed
- Low-frequency data updates
- Quick prototypes and MVPs
When to Use Changes API
- Need to track all operations including deletions
- Building a complete data synchronization system
- Maintaining an audit trail of changes
- When webhook infrastructure is not available
- Batch processing of changes
When to Use Webhooks
- Need real-time or near-real-time data updates
- Triggering immediate actions on data changes
- Keeping external systems in sync automatically
- When you can maintain a publicly accessible endpoint
- High-frequency data changes requiring quick response
**Hybrid Approach:**
Consider using webhooks for real-time updates and the Changes API as a fallback to catch any missed changes during downtime or webhook failures. This provides the best of both worlds: real-time updates with guaranteed completeness.