Skip to main content
Syncs let you maintain an up-to-date local copy of data from any external API. Nango runs your sync function on a schedule, stores the results in its encrypted cache, detects additions, updates, and deletes, and notifies your backend via webhook whenever new data is available.

When to use syncs

Syncs are ideal when you want to:
  • Store a copy of external API data in your app and keep it up to date
  • Detect changes in APIs that don’t offer webhooks
  • Combine polling and webhooks for a reliable, real-time stream of changes
Common examples include syncing contacts from CRMs (HubSpot, Salesforce, Attio), files from cloud drives (Google Drive, SharePoint, Box), or call transcripts from video tools (Gong, Fathom, Zoom).

Key facts

  • Syncs run in Nango’s infrastructure, scoped to a connection
  • You control the code: which data to fetch, how to transform it, and what the data model looks like
  • All platform features are available: data validation, per-customer config, retries, and rate-limit handling
  • Syncs support checkpoints to save progress, resume after failures, and fetch only changed data
  • Synced records are encrypted at rest and in transit
  • The minimum polling interval is 15 seconds
  • All sync runs and API calls are logged in the Nango dashboard

Build a sync

Step 1 — Set up your integrations folder

If you don’t have a nango-integrations folder yet, follow the functions setup guide.

Step 2 — Start dev mode

nango dev
Keep this terminal open. It continuously compiles your functions and prints syntax errors as you work.

Step 3 — Create the sync file

Sync files live inside a syncs/ folder nested under the integration folder. For a sync that fetches Salesforce contacts:
nango-integrations/
├── .nango
├── .env
├── index.ts
├── package.json
└── salesforce/
    └── syncs/
        └── salesforce-contacts.ts
Paste the following scaffold into your sync file:
salesforce-contacts.ts
import { createSync } from 'nango';
import * as z from 'zod';

const SalesforceContact = z.object({
    id: z.string(),
    first_name: z.string(),
    last_name: z.string(),
    email: z.string(),
    account_id: z.string().nullable(),
    last_modified_date: z.string(),
});

export default createSync({
    description: 'Fetches contacts from Salesforce',
    version: '1.0.0',
    frequency: 'every hour',
    autoStart: true,
    trackDeletes: true,
    checkpoint: z.object({
        lastModifiedISO: z.string(),
    }),
    models: {
        SalesforceContact: SalesforceContact,
    },
    exec: async (nango) => {
        // Integration code goes here.
    },
});
Also import the new file in index.ts:
index.ts
import './salesforce/syncs/salesforce-contacts';

Step 4 — Implement your sync

Write your fetch-and-save logic inside the exec method. Here is a complete Salesforce contacts sync using incremental fetches with checkpoints:
salesforce-contacts.ts
import { createSync } from 'nango';
import * as z from 'zod';

const SalesforceContact = z.object({
    id: z.string(),
    first_name: z.string(),
    last_name: z.string(),
    email: z.string(),
    account_id: z.string().nullable(),
    last_modified_date: z.string(),
});

const sync = createSync({
    description: 'Fetches contacts from Salesforce',
    version: '1.0.0',
    frequency: 'every hour',
    autoStart: true,
    checkpoint: z.object({
        lastModifiedISO: z.string(),
    }),
    models: {
        SalesforceContact: SalesforceContact,
    },
    exec: async (nango) => {
        const checkpoint = await nango.getCheckpoint();
        const query = buildQuery(checkpoint?.lastModifiedISO);

        await fetchAndSaveRecords(nango, query);

        await nango.log('Sync run completed!');
    },
});

export default sync;
export type NangoSyncLocal = Parameters<(typeof sync)['exec']>[0];

function buildQuery(lastModifiedISO?: string): string {
    let q = `SELECT Id, FirstName, LastName, Email, AccountId, LastModifiedDate FROM Contact`;
    if (lastModifiedISO) {
        q += ` WHERE LastModifiedDate > ${lastModifiedISO}`;
    }
    q += ` ORDER BY LastModifiedDate ASC`;
    return q;
}

async function fetchAndSaveRecords(nango: NangoSyncLocal, query: string) {
    let endpoint = '/services/data/v53.0/query';

    while (true) {
        const response = await nango.get({
            endpoint,
            params: endpoint === '/services/data/v53.0/query' ? { q: query } : {}
        });

        const records = response.data.records.map((r: any) => ({
            id: r.Id,
            first_name: r.FirstName,
            last_name: r.LastName,
            email: r.Email,
            account_id: r.AccountId,
            last_modified_date: r.LastModifiedDate,
        }));

        await nango.batchSave(records, 'SalesforceContact');

        await nango.saveCheckpoint({
            lastModifiedISO: records[records.length - 1].last_modified_date
        });

        if (response.data.done) break;
        endpoint = response.data.nextRecordsUrl;
    }
}

SDK methods available in syncs

MethodDescription
nango.get(config)Authenticated GET request
nango.post(config)Authenticated POST request
nango.batchSave(records, model)Persist records to Nango’s cache
nango.batchDelete(records, model)Mark records as deleted
nango.getCheckpoint()Retrieve the last saved checkpoint (returns null on first run)
nango.saveCheckpoint(data)Save progress to resume from on next run
nango.log(message)Write a custom log message to the Nango dashboard
nango.getMetadata()Read per-connection metadata
nango.paginate(config)Iterate through paginated API responses

Step 5 — Test locally

nango dryrun salesforce-contacts '<CONNECTION-ID>'
Because this is a dry run, records are printed to the console rather than saved to Nango’s cache. To see memory and CPU diagnostics:
nango dryrun salesforce-contacts '<CONNECTION-ID>' --diagnostics

Step 6 — Deploy

nango deploy dev
To deploy a single sync:
nango deploy --sync salesforce-contacts dev
Data retention: Records not updated for 30 days have their payload pruned (metadata is retained). Records from syncs not executed for 60 days are permanently deleted. Fetch records from Nango promptly after receiving webhook notifications and store them in your own system.

Use a sync from your backend

Receive Nango webhooks

Set up a webhook endpoint in your app and configure Nango to notify it. When a sync completes, Nango sends a webhook with the payload containing providerConfigKey, connectionId, model, and modifiedAfter.

Fetch records

Use the Node SDK or REST API to fetch the latest records after receiving a webhook:
import { Nango } from '@nangohq/node';

const nango = new Nango({ secretKey: '<ENVIRONMENT-SECRET-KEY>' });

const result = await nango.listRecords({
    providerConfigKey: '<providerConfigKey-from-webhook>',
    connectionId: '<connectionId-from-webhook>',
    model: '<model-from-webhook>',
    modifiedAfter: '<modifiedAfter-from-webhook>'
});
Each record includes _nango_metadata with last_action (ADDED, UPDATED, or DELETED), first_seen_at, last_modified_at, and a cursor for pagination.

Cursor-based pagination

Rather than relying solely on webhook timestamps (which can be missed), track the cursor from the last record you fetched. Pass it on subsequent requests to fetch only records modified after that point:
const result = await nango.listRecords({
    providerConfigKey: 'salesforce',
    connectionId: 'user-123',
    model: 'SalesforceContact',
    cursor: '<cursor-of-last-fetched-record>'
});

Manage syncs from your backend

Trigger a one-off sync run

await nango.triggerSync('salesforce', ['salesforce-contacts'], 'user-123');

Start or pause a sync schedule

// Start the schedule (runs immediately, then on its configured frequency)
await nango.startSync('salesforce', ['salesforce-contacts'], 'user-123');

// Pause the schedule
await nango.pauseSync('salesforce', ['salesforce-contacts'], 'user-123');

Check sync status

const status = await nango.syncStatus('salesforce', ['salesforce-contacts'], 'user-123');

Sync configuration reference

FieldTypeDescription
descriptionstringHuman-readable description of the sync
versionstringSemantic version; increment on breaking changes
frequencystringPolling interval (e.g., 'every hour', 'every 15 minutes')
autoStartbooleanStart automatically when a new connection is created
trackDeletesbooleanDetect deleted records in the external API
checkpointz.ZodObjectSchema for progress state saved between runs
modelsRecord<string, z.ZodObject>Named data models this sync produces
execfunctionThe sync implementation

Build docs developers (and LLMs) love