> ## Documentation Index
> Fetch the complete documentation index at: https://nango.dev/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Deletion detection

> Surface deletes from the external API — even when the API itself doesn't expose them cleanly.

Sometimes you need to know when a record you are syncing has been deleted in the external system. Deletion detection works differently depending on whether your sync function is incremental or full — pick the strategy that matches your sync approach.

For incremental syncs, you must actively tell Nango which IDs have been removed. For full syncs (which fetch all records on every run), Nango can compute deletes for you automatically.

## Detecting deletes in incremental syncs

When your sync only fetches changed data since the previous run, Nango has no built-in way to know which records disappeared on the provider side.

You must actively tell Nango which IDs have been removed by calling `nango.batchDelete()` ([full reference](/reference/functions/functions-sdk#delete-records)) inside the sync function.

### When can you use this?

You can use `nango.batchDelete()` if the external API supports one of the following:

* A dedicated "recently deleted" endpoint (e.g. `GET /entities/deleted?since=...`)
* The ability to filter or sort by a deletion timestamp
* The ability to filter or sort by last-modified timestamp and records include a flag like `is_deleted`, `archived`, etc.

If none of these are available, you cannot detect deletes in an incremental sync. You'll either need to switch to a full sync or skip deletion detection.

### Example

```ts theme={null}
import { createSync } from 'nango';
import * as z from 'zod';

const AccountSchema = z.object({
  id: z.string(),
  name: z.string()
});

export default createSync({
  description: 'Sync Accounts incrementally and handle deletions',
  frequency: 'every 2 hours',
  endpoints: [{ method: 'GET', path: '/accounts', group: 'Accounts' }],
  models: { Account: AccountSchema },
  checkpoint: z.object({
    lastSyncedISO: z.string(),
  }),

  exec: async (nango) => {
    const checkpoint = await nango.getCheckpoint();
    const now = new Date().toISOString();

    // (1) Fetch newly created / updated accounts
    const res = await nango.get({
        endpoint: '/accounts',
        params: { ...(checkpoint && { since: checkpoint.lastSyncedISO }) }
    });
    await nango.batchSave(res.data, 'Account');

    // (2) Fetch deletions since the last run (if this is not the first run)
    if (checkpoint) {
        const deletedRes = await nango.get({
            endpoint: '/accounts/deleted',
            params: { since: checkpoint.lastSyncedISO }
        });

        // (3) Tell Nango which IDs have been deleted in the external system
        const toDelete = deletedRes.data.map((row: any) => ({ id: row.id }));
        if (toDelete.length) {
            await nango.batchDelete(toDelete, 'Account');
        }
    }

    // (4) Save checkpoint for next run
    await nango.saveCheckpoint({ lastSyncedISO: now });
  }
});
```

## Detecting deletes in full syncs

Sync functions that fetch all records on every run can automatically detect deletions.

Nango detects removals by computing the diff between what existed before `trackDeletesStart` and what was saved between `trackDeletesStart` and `trackDeletesEnd` ([full reference](/reference/functions/functions-sdk#detect-deletions-automatically)).

This works whether your full sync uses checkpoints for resilience or not. If it does, `trackDeletesStart` should be called before fetching any data and `trackDeletesEnd` after all records are saved and the checkpoint is cleared.

### Example

```ts theme={null}
import { createSync } from 'nango';
import * as z from 'zod';

const TicketSchema = z.object({
  id: z.string(),
  subject: z.string(),
  status: z.string()
});

export default createSync({
  description: 'Fetch all help-desk tickets',
  frequency: 'every day',
  endpoints: [{ method: 'GET', path: '/tickets', group: 'Tickets' }],
  models: { Ticket: TicketSchema },

  exec: async (nango) => {
    // Mark the start of deletion tracking
    await nango.trackDeletesStart('Ticket');

    const tickets = nango.paginate<{ id: string; subject: string; status: string }>({
      endpoint: '/tickets',
      paginate: { type: 'cursor', cursor_path_in_response: 'next', cursor_name_in_request: 'cursor', response_path: 'tickets' }
    });

    for await (const page of tickets) {
      await nango.batchSave(page, 'Ticket');
    }

    // Detect and mark deleted records
    await nango.trackDeletesEnd('Ticket');
  }
});
```

### How the algorithm works

1. When `trackDeletesStart` is called, Nango marks the beginning of the deletion tracking window for the model.
2. Records saved with `batchSave` between `trackDeletesStart` and `trackDeletesEnd` are tracked.
3. When `trackDeletesEnd` is called, Nango compares what existed before `trackDeletesStart` with what was saved in the window.
4. Any records missing from the new dataset are marked as deleted (soft delete). They remain accessible from the Nango cache, but with `record._metadata.deleted === true`.

<Warning>
  **Be careful with exception handling when using** `trackDeletesStart`/`trackDeletesEnd`

  Nango only performs deletion detection (the "diff") if a sync run completes successfully without any uncaught exceptions.

  Exception handling is critical:

  * If your sync doesn't fetch the full dataset between the two calls (e.g. you catch and swallow an exception), Nango will attempt the diff on an incomplete dataset.
  * This leads to false positives, where valid records are mistakenly considered deleted.

  **What You Should Do**

  If a failure prevents full data retrieval, make sure the sync run fails and `trackDeletesEnd` is not being called:

  * Let exceptions bubble up and interrupt the run.
  * If you're using `try/catch`, re-throw exceptions that indicate incomplete data.
</Warning>

<Tip>
  **How to use** `trackDeletesStart`/`trackDeletesEnd` **safely**

  If some records are incorrectly marked as deleted, you can trigger a full resync (via the UI or API) to restore the correct data state.

  We strongly recommend not performing irreversible destructive actions (like hard-deleting records in your system) based solely on deletions reported by Nango. A full resync should always be able to recover from issues.
</Tip>

## Troubleshooting deletion detection issues

| Symptom                                                                    | Likely cause                                                                                     |
| -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| Records that still exist in the source API are shown as `deleted` in Nango | Sync didn't save all records (silent failures) between `trackDeletesStart` and `trackDeletesEnd` |
| You never see deleted records                                              | Check if deletion detection is implemented for the sync.                                         |

## Related guides

* [Sync functions](/guides/functions/syncs/sync-functions) - implement full and incremental syncs.
* [Records cache](/guides/functions/syncs/records-cache) - understand how deleted records are exposed to your app.
* [Sync efficiency](/guides/functions/syncs/sync-efficiency) - reduce API work during delete detection.
* [Functions SDK reference](/reference/functions/functions-sdk#delete-records) - deletion helper methods.
