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

GET
/api/v1/sync/changesGet entity changes since a timestamp

Query Parameters

Request Parameters

PropertyTypeRequiredDescription
sinceDateTimeISO 8601 timestamp. Returns changes that occurred after this time.
entitiesstringComma-separated entity types to filter: "contacts", "companies", "deals", "activities". Omit for all types.
limitintMaximum 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

PropertyTypeRequiredDescription
idintSequential change ID (monotonically increasing)
entityTypestring"contact", "company", "deal", or "activity"
entityIdGuidThe ID of the entity that changed
actionstring"inserted", "updated", or "deleted"
changedAtDateTimeISO 8601 timestamp of when the change occurred
changedBystring?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. 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. 2

    Store the sync timestamp

    Record the current time (or the changedAt of the latest change) as your high-water mark.

  3. 3

    Poll for changes periodically

    Call GET /api/v1/sync/changes?since={lastSync} at a regular interval (e.g., every 60 seconds).

  4. 4

    Process changes

    For inserted and updated actions, fetch the full record from the entity endpoint. For deleted actions, remove the record from your system.

  5. 5

    Update your stored timestamp

    Set your high-water mark to the changedAt of 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()

Next Steps

Rally Support

We typically reply in a few hours

Hi! 👋 How can we help you today?