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.