Sync & Changes
The Sync API provides a sequential change feed for building reliable data synchronization workflows. Query for all entity changes since a given timestamp to keep external systems in sync with Rally CRM without missing any inserts, updates, or deletes.
Endpoint
/api/v1/sync/changesGet entity changes since a timestampQuery Parameters
Request Parameters
| Property | Type | Required | Description |
|---|---|---|---|
since | DateTime | ✓ | ISO 8601 timestamp. Returns changes that occurred after this time. |
entities | string | — | Comma-separated entity types to filter: "contacts", "companies", "deals", "activities". Omit for all types. |
limit | int | — | Maximum number of change entries to return (default: 1000) |
Response Model
The response is an array of change entries sorted by changedAt in ascending order. Each entry describes a single change to an entity.
Change Entry
| Property | Type | Required | Description |
|---|---|---|---|
id | int | ✓ | Sequential change ID (monotonically increasing) |
entityType | string | ✓ | "contact", "company", "deal", or "activity" |
entityId | Guid | ✓ | The ID of the entity that changed |
action | string | ✓ | "inserted", "updated", or "deleted" |
changedAt | DateTime | ✓ | ISO 8601 timestamp of when the change occurred |
changedBy | string? | — | Email or identifier of the user who made the change |
[
{
"id": 10452,
"entityType": "contact",
"entityId": "c1d2e3f4-a5b6-7890-cdef-1234567890ab",
"action": "inserted",
"changedAt": "2025-02-01T10:15:00Z",
"changedBy": "sarah@acme.com"
},
{
"id": 10453,
"entityType": "deal",
"entityId": "d5e6f7a8-b9c0-1234-5678-90abcdef1234",
"action": "updated",
"changedAt": "2025-02-01T10:18:30Z",
"changedBy": "mike@acme.com"
},
{
"id": 10454,
"entityType": "contact",
"entityId": "a9b8c7d6-e5f4-3210-fedc-ba0987654321",
"action": "deleted",
"changedAt": "2025-02-01T10:22:00Z",
"changedBy": "sarah@acme.com"
}
]Integration Pattern
The recommended workflow for keeping an external system in sync with Rally CRM follows a simple poll-based pattern.
- 1
Initial full sync
Fetch all existing records using the list endpoints:
GET /api/v1/contacts,/companies,/deals,/activities. Page through all results using limit/offset pagination. - 2
Store the sync timestamp
Record the current time (or the
changedAtof the latest change) as your high-water mark. - 3
Poll for changes periodically
Call
GET /api/v1/sync/changes?since={lastSync}at a regular interval (e.g., every 60 seconds). - 4
Process changes
For
insertedandupdatedactions, fetch the full record from the entity endpoint. Fordeletedactions, remove the record from your system. - 5
Update your stored timestamp
Set your high-water mark to the
changedAtof the last change you processed.
Simpler Alternative
If you only need to sync a single entity type and do not need to track deletes, you can use the ?updatedSince= query parameter on any list endpoint (e.g., GET /api/v1/contacts?updatedSince=2025-01-15T00:00:00Z) for simpler incremental fetching.
Examples
Python Sync Loop
import requests
import time
from datetime import datetime, timezone
API_BASE = "https://your-tenant.rallycrm.io/api/v1"
API_KEY = "rk_live_your_api_key_here"
HEADERS = {"X-Api-Key": API_KEY}
POLL_INTERVAL = 60 # seconds
def get_changes(since: str, entities: str = None, limit: int = 1000):
"""Fetch changes since the given ISO 8601 timestamp."""
params = {"since": since, "limit": limit}
if entities:
params["entities"] = entities
response = requests.get(
f"{API_BASE}/sync/changes",
headers=HEADERS,
params=params,
)
response.raise_for_status()
return response.json()
def fetch_entity(entity_type: str, entity_id: str):
"""Fetch the full record for an entity."""
# Map singular type to plural endpoint
endpoint_map = {
"contact": "contacts",
"company": "companies",
"deal": "deals",
"activity": "activities",
}
endpoint = endpoint_map[entity_type]
response = requests.get(
f"{API_BASE}/{endpoint}/{entity_id}",
headers=HEADERS,
)
response.raise_for_status()
return response.json()
def process_changes(changes):
"""Process each change entry."""
for change in changes:
action = change["action"]
entity_type = change["entityType"]
entity_id = change["entityId"]
if action in ("inserted", "updated"):
record = fetch_entity(entity_type, entity_id)
print(f"{action.upper()} {entity_type} {entity_id}")
# Upsert into your local database...
elif action == "deleted":
print(f"DELETED {entity_type} {entity_id}")
# Remove from your local database...
def run_sync_loop():
"""Main sync loop."""
# Start from 24 hours ago (or load from persistent storage)
last_sync = datetime.now(timezone.utc).isoformat()
print(f"Starting sync from: {last_sync}")
while True:
changes = get_changes(since=last_sync)
if changes:
print(f"Processing {len(changes)} changes...")
process_changes(changes)
# Update high-water mark
last_sync = changes[-1]["changedAt"]
print(f"Sync cursor updated to: {last_sync}")
else:
print("No new changes")
time.sleep(POLL_INTERVAL)
if __name__ == "__main__":
run_sync_loop()