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
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:
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:
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:
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
| Method | Description |
|---|
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
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>'
});
curl -G https://api.nango.dev/records \
--header 'Authorization: Bearer <ENVIRONMENT-SECRET-KEY>' \
--header 'Provider-Config-Key: <providerConfigKey>' \
--header 'Connection-Id: <connectionId>' \
--data-urlencode 'model=SalesforceContact' \
--data-urlencode 'modified_after=<modifiedAfter>'
Each record includes _nango_metadata with last_action (ADDED, UPDATED, or DELETED), first_seen_at, last_modified_at, and a cursor for 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
| Field | Type | Description |
|---|
description | string | Human-readable description of the sync |
version | string | Semantic version; increment on breaking changes |
frequency | string | Polling interval (e.g., 'every hour', 'every 15 minutes') |
autoStart | boolean | Start automatically when a new connection is created |
trackDeletes | boolean | Detect deleted records in the external API |
checkpoint | z.ZodObject | Schema for progress state saved between runs |
models | Record<string, z.ZodObject> | Named data models this sync produces |
exec | function | The sync implementation |