> ## 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.

# Functions SDK

> Full reference of the SDK available in Nango Functions.

## Examples

<Tabs>
  <Tab title="Sync function">
    ```ts theme={null}
    import { createSync } from 'nango';

    const githubIssueDemoSchema = z.object({ ... });
    type GithubIssueDemo = z.infer<typeof githubIssueDemoSchema>;

    export default createSync({
      description: `Fetches the Github issues from all a user's repositories.`,
      version: '1.0.0',
      frequency: 'every hour',
      autoStart: true,
      metadata: z.void(),
      models: {
        GithubIssueDemo: githubIssueDemoSchema
      },

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

        // Fetch issues from GitHub.
        const res = await nango.get({
            endpoint: '/repos/NangoHQ/interactive-demo/issues?labels=demo&sort=created&direction=asc'
        });

        // Map issues to your preferred schema.
        const issues: GithubIssueDemo[] = res.data.map(({ id, title, html_url }: any) => {
            return { id, title, url: html_url };
        });

        // Persist issues to the Nango cache.
        await nango.batchSave(issues, 'GithubIssueDemo');

        // Detect and mark deleted records
        await nango.trackDeletesEnd('GithubIssueDemo');
      },
    });
    ```
  </Tab>

  <Tab title="Action function">
    ```ts theme={null}
    import { createAction } from 'nango';
    import { GithubCreateIssueInput, GithubCreateIssueResult } from '../../models.js';

    export default createAction({
      description: `Create an issue in GitHub`,
      version: '1.0.0',
      input: GithubCreateIssueInput,
      output: GithubCreateIssueResult,

      exec: async (nango, input) => {
        // Create a GitHub issue.
        const res = await nango.post({
            endpoint: '/repos/NangoHQ/interactive-demo/issues',
            data: {
                title: `[demo] ${input.title}`,
                body: `The body of the issue.`,
                labels: ['automatic']
            }
        });

        // Send response.
        return { url: res.data.html_url, status: res.status };
      },
    });
    ```
  </Tab>
</Tabs>

Read more about [integration functions](/guides/functions/functions-guide) to understand what role they play in Nango.

## Configuration

### `createSync`

<ResponseField name="description" type="string" required>
  The description of the sync.
</ResponseField>

<ResponseField name="exec" type="function" required>
  The function that will be called when the sync is triggered.
</ResponseField>

<ResponseField name="frequency" type="string" required>
  The frequency of the sync.

  ```ts theme={null}
  frequency: 'every 1 minute',
  frequency: 'every hour',
  frequency: 'every 2 days'
  frequency: 'every 3 week'
  ```
</ResponseField>

<ResponseField name="models" type="object" required>
  The models that will be synced by this function.
  You need one endpoint per model.

  ```ts theme={null}
  models: {
      GithubIssue: z.object({
          id: z.string(),
      }),
  },
  ```
</ResponseField>

<ResponseField name="checkpoint" type="ZodSchema">
  Optional schema defining the shape of checkpoint data for this sync.
  Use checkpoints to save progress and avoid re-fetching all data on every run. See the [checkpoints guide](/guides/functions/syncs/checkpoints) for details.

  ```ts theme={null}
  checkpoint: z.object({
      cursor: z.string(),
  }),
  ```
</ResponseField>

<ResponseField name="autoStart" type="boolean">
  If `true`, automatically runs the sync when a new connection is created.
  Otherwise, it needs to be triggered via the API or Nango UI.
</ResponseField>

<ResponseField name="metadata" type="object" default="z.void()">
  The connection's metadata of the action.

  ```ts theme={null}
  metadata: z.object({
      userId: z.string(),
  });
  ```
</ResponseField>

<ResponseField name="onWebhook" type="function">
  The function that will be called when a webhook is received.
</ResponseField>

<ResponseField name="scopes" type="string[]">
  The integration's scopes required by the action.
  This field is for documentation purposes only and currently not enforced by Nango.

  ```ts theme={null}
  scopes: ['read:user', 'write:user'],
  ```
</ResponseField>

<ResponseField name="trackDeletes" type="boolean" deprecated>
  \[DEPRECATED] Instead use `await nango.trackDeletesStart('modelName')` and `await nango.trackDeletesEnd('modelName')` inside the sync `exec` function.

  When `trackDeletes` is set to `true`, Nango automatically detects deleted records **during full syncs only** and marks them as deleted in each record’s metadata (soft delete). These records remain stored in the cache.

  When set to `false`, Nango does not mark missing records as deleted, even if they weren’t returned in the latest full sync—they simply remain in the cache unchanged.

  Defaults to `false`.
</ResponseField>

<ResponseField name="version" type="string">
  The version of the sync.
  Use it to track changes to the sync inside Nango's UI.
</ResponseField>

<ResponseField name="webhookSubscriptions" type="string[]" default="undefined">
  The webhook subscriptions of the sync.
  Specify the types of webhooks the method `onWebhook` will handle.
  If a webhook type is not on the list, it will not be handled.

  ```ts theme={null}
  webhookSubscriptions: ['*'],
  ```
</ResponseField>

<ResponseField name="endpoints" type="object[]" optional deprecated>
  \[DEPRECATED] Use the [`GET /records`](/reference/backend/http-api/sync/records-list) API endpoint to fetch records.
</ResponseField>

### `createAction`

<ResponseField name="description" type="string" required>
  The description of the sync.
</ResponseField>

<ResponseField name="exec" type="function" required>
  The function that will be called when the action is triggered.
</ResponseField>

<ResponseField name="input" type="object" required>
  The input required by the action when triggering it.

  ```ts theme={null}
  input: z.object({
      title: z.string(),
  });
  ```
</ResponseField>

<ResponseField name="output" type="object" required>
  The output of the action.

  ```ts theme={null}
  output: z.object({
      issueId: z.string(),
  });
  ```
</ResponseField>

<ResponseField name="version" type="string">
  The version of the sync.
  Use it to track changes to the sync inside Nango's UI.
</ResponseField>

<ResponseField name="metadata" type="object" default="z.void()">
  The connection's metadata of the action.

  ```ts theme={null}
  metadata: z.object({
      userId: z.string(),
  });
  ```
</ResponseField>

<ResponseField name="scopes" type="string[]">
  The integration's scopes required by the action.
  This field is for documentation purposes only and currently not enforced by Nango.

  ```ts theme={null}
  scopes: ['read:user', 'write:user'],
  ```
</ResponseField>

<ResponseField name="endpoint" type="object" optional deprecated>
  \[DEPRECATED] Use the [`POST /action/trigger`](/reference/backend/http-api/action/trigger) API endpoint to trigger actions.
</ResponseField>

### `createOnEvent`

<ResponseField name="description" type="string" required>
  The description of the sync.
</ResponseField>

<ResponseField name="event" type="'post-connection-creation' | 'pre-connection-deletion' | 'validate-connection'" required>
  The event that will trigger this function.
</ResponseField>

<ResponseField name="exec" type="function" required>
  The function that will be called when the action is triggered.
</ResponseField>

<ResponseField name="version" type="string">
  The version of the onEvent function.
  Use it to track changes to the onEvent function inside Nango's UI.
</ResponseField>

<ResponseField name="metadata" type="object" default="z.void()">
  The connection's metadata of the function.

  ```ts theme={null}
  metadata: z.object({
      userId: z.string(),
  });
  ```
</ResponseField>

## HTTP requests

Makes an HTTP request inside an integration function:

```js theme={null}
import type { ProxyConfiguration } from 'nango';

const config: ProxyConfiguration = { endpoint: '/some-endpoint' };

await nango.get(config); // GET request
await nango.post(config); // POST request
await nango.put(config); // PUT request
await nango.patch(config); // PATCH request
await nango.delete(config); // DELETE request
```

<Tip>
  Note that all HTTP requests benefit from automatic credential injection. Because functions are executed in the context of a specific integration & connection, Nango can automatically retrieve & refresh the relevant API credentials.
</Tip>

**Parameters**

<Expandable>
  <ResponseField name="config" type="object" required>
    <Expandable title="config" defaultOpen>
      <ResponseField name="endpoint" type="string" required>
        The endpoint of the request.
      </ResponseField>

      <ResponseField name="headers" type="Record<string, string>">
        The headers of the request.
      </ResponseField>

      <ResponseField name="params" type="Record<string, string | number>">
        The query parameters of the request.
      </ResponseField>

      <ResponseField name="data" type="unknown">
        The body of the request.
      </ResponseField>

      <ResponseField name="retries" type="number">
        The number of retries in case of failure (with exponential back-off). Optional, default 0.
      </ResponseField>

      <ResponseField name="retryOn" type="number[]">
        Array of HTTP status codes to retry, in addition to those specified [below](#when-a-failed-request-is-retried).
      </ResponseField>

      <ResponseField name="baseUrlOverride" type="string">
        The API base URL. Can be omitted if the base URL is configured for this API in the [providers.yaml](https://github.com/NangoHQ/nango/blob/master/packages/providers/providers.yaml).
      </ResponseField>

      <ResponseField name="decompress" type="boolean">
        Override the decompress option when making requests. Optional, defaults to false
      </ResponseField>

      <ResponseField name="responseType" type="'arraybuffer' | 'blob' | 'document' | 'json' | 'text' | 'stream'">
        The type of the response.
      </ResponseField>
    </Expandable>
  </ResponseField>
</Expandable>

**Response**

<Expandable>
  ```json theme={null}
      {
          data: {}, // the response provided by the server
          status: 200, // the HTTP status code
          headers: {}, // the HTTP headers
          config: {}, // the config provided for the request
          request: {} // the request that generated this response
      }
  ```

  <Tip>
    The response object is an [Axios response](https://axios-http.com/docs/res_schema).
  </Tip>
</Expandable>

## HTTP request retries

Configure behavior with two optional fields on your [HTTP request](#http-requests) config: [`retries`](#param-retries) and [`retryOn`](#param-retry-on).

### When a failed request is retried

Nango retries only when the failure looks **transient**. Exact rules depend on the integration:

| Field                    | Condition for Retry                                                                                                                                                                                                                                                                                                                                                                                                 |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Network Error**        | If code is one of:<br /> `ECONNRESET`, `ETIMEDOUT`, `ECONNABORTED`, `ECONNREFUSED`, `EHOSTUNREACH`, `EAI_AGAIN`                                                                                                                                                                                                                                                                                                     |
| **Transient HTTP Error** | If provider specific `proxy.retry.error_code` is configured in [providers.yaml](https://github.com/NangoHQ/nango/blob/master/packages/providers/providers.yaml), only matching statuses are retried.<br /><br />Otherwise, by default, status codes `401`, `429`, `5xx` are retried. <br /><br /> Additional status specified in [`retryOn`](#param-retry-on) are always retried in addition to the above statuses. |

If nothing in the table applies, the error is **not** retried.

<Note>
  Status `401` is retryable so Nango can recover after refreshing the connection with potentially updated credentials. If a `401` happens and the connection credentials **do not change** on the next fetch, Nango treats that as a definitive authentication failure and stops performing additional retries
</Note>

### Backoff

Between retry attempts Nango waits with **exponential backoff**: first wait **3000ms**, then each wait doubles, capped at **10 minutes**.

## Logging

You can collect logs in integration functions. This is particularly useful when:

* developing, to debug your integration functions
* in production, to collect information about integration function executions & understand issues

Collect logs in integration functions as follows:

```ts theme={null}
await nango.log("This is a log."); // default log level is 'info'
await nango.log("This is an info log", { level: 'info' });
await nango.log("This is a warning", { level: 'warn' });
await nango.log("This is an error", { level: 'error' });
```

### Log Levels

Nango supports the following log levels (from most to least verbose): `debug`, `info`, `warn`, `error`, `off`.

Only logs with a level greater than or equal to the configured logger level will surfaced in the Nango UI Logs tab. For example, if the logger level is set to `warn`, only `warn` and `error` logs will be shown.

**Default log levels:**

* **All cloud environments**: `warn` (only warnings and errors are logged by default)
* **CLI dry-run**: `debug` (all logs are shown in the console)

### Configuring Log Level

You can override the default log level in two ways:

1. **Environment variable**: Set `NANGO_LOGGER_LEVEL` in your environment settings (values: `debug`, `info`, `warn`, `error`, `off`). It will apply to all your functions running in this environment and can be overridden in functions by using `nango.setLogger(...)`

2. **In your function code**:

```ts theme={null}
nango.setLogger({ level: 'debug' }); // All logs will show in your cloud environments. It will apply to all `nango.log()` following this statement.
nango.setLogger({ level: 'off' }); // No logs will show
```

Logs can be viewed & searched in the Nango UI. We plan to make them exportable in the future as well.

You can monitor your usage of custom logs in the [billing tab](https://app.nango.dev/prod/team/billing) of your Nango dashboard.

## Environment variables

Integration functions sometimes need to access sensitive variables that should not be revealed directly in the code.

For this, you can define environment variables in the Nango UI, in the *Environment Settings* tab. Then you can retrieve these environment variables from integration functions with:

```js theme={null}
await nango.getEnvironmentVariables();
```

**Parameters**

No parameters.

**Response**

<Expandable>
  ```json theme={null}
  [
      {
          "name": "MY_SECRET_KEY",
          "value": "SK_373892NSHFNCOWFO..."
      }
  ]
  ```
</Expandable>

## Trigger action

You can call action functions from other integration functions with:

```js theme={null}
await nango.triggerAction('<ACTION-NAME>', { 'custom_key1': 'custom_value1' });
```

**Parameters**

<Expandable>
  <ResponseField name="actionName" type="string" required>
    The name of the action to trigger.
  </ResponseField>

  <ResponseField name="input" type="unkown" required>
    The necessary input for your action's `exec` function.
  </ResponseField>
</Expandable>

**Response**

<Expandable>
  ```json theme={null}
  {
      "your-properties": "The data returned by the action"
  }
  ```
</Expandable>

## Paginate through API responses

Nango provides a helper to automatically paginate endpoint responses.

Similar to [HTTP requests](/reference/functions/functions-sdk#http-requests), the `nango.paginate()` method takes in a `ProxyConfiguration` parameter.

Use the `paginate` field to of the `ProxyConfiguration` to specify how the endpoint's pagination work. Here's an example for a Jira endpoint:

```ts theme={null}
const config: ProxyConfiguration = {
    // https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-projects/#api-rest-api-3-project-search-get
    endpoint: `/ex/jira/${cloud.cloudId}/rest/api/3/project/search`,
    params: {
        properties: properties
    },
    paginate: {
        type: 'offset',
        offset_name_in_request: 'startAt',
        response_path: 'values',
        limit_name_in_request: 'maxResults',
        limit: 50,
        on_page: async ({ nextPageParam, response }) => {
            await nango.log(`Next offset value = ${nextPageParam}`);
            await nango.log(`Fetched ${response.data.total} records`);
        },
    },
    headers: {
        'X-Atlassian-Token': 'no-check'
    },
    retries: 10
};

for await (const projects of nango.paginate<JiraProjectResponse>(config)) {
    const projectsToSave = toProjects(projects, cloud.baseUrl);
    await nango.batchSave(projectsToSave, 'Project');
}
```

As shown in the example above, use a `for` loop to iterate through the paginated results.

<Tip>
  Nango has pre-configured the pagination settings for some popular APIs, so you don't have to specify them in functions.

  You can view the pre-configured pagination settings for all APIs in the [providers.yaml](https://github.com/NangoHQ/nango/blob/master/packages/providers/providers.yaml) file.

  Please note that some APIs have diverging pagination strategies per endpoint, so you might still need to override pre-configured pagination settings at times.
</Tip>

The pagination helper supports 3 types of pagination: `cursor`, `link` or `offset` with the following settings:

<Expandable>
  <ResponseField name="config" type="object" required>
    <Expandable title="config" defaultOpen>
      <ResponseField name="paginate" type="object" required>
        <Expandable title="paginate" defaultOpen>
          <ResponseField name="limit_name_in_request" type="string" required>
            *For all pagination types.*

            The name of the parameter containing the number of items per page, in the request. Inserted in the query parameters for `GET`/`DELETE`, in the body for `POST`/`PUT`/`PATCH`.
          </ResponseField>

          <ResponseField name="limit" type="number">
            *For all pagination types.*

            The maximum number of items per page. If omitted, no limit will be sent to the external endpoint, relying on the endpoint's default limit.
          </ResponseField>

          <ResponseField name="response_path" type="string">
            *For all pagination types.*

            The path of the field containing the results, in the response. If omitted or empty string, it defaults to the root. Use `.` for nested fields, e.g. `"results.contacts"`.
          </ResponseField>

          <ResponseField name="type" type="'cursor' | 'link' | 'offset'" required>
            *For all pagination types.*

            The pagination strategy.
          </ResponseField>

          <ResponseField name="cursor_path_in_response" type="string">
            *For cursor pagination only (required).*

            The path of the field containing the cursor for the next page, in the response. Use `.` for nested fields, e.g. `"pagination.cursor"`.
          </ResponseField>

          <ResponseField name="cursor_name_in_request" type="string">
            *For cursor pagination only (required).*

            The name of the parameter containing the cursor for the next page, in the request. Inserted in the query parameters for `GET`/`DELETE`, in the body for `POST`/`PUT`/`PATCH`.
          </ResponseField>

          <ResponseField name="link_rel_in_response_header" type="string">
            *`For link pagination (required unless link_path_in_response_body is specified).`*

            The header containing the link to the next page, in the response.
          </ResponseField>

          <ResponseField name="link_path_in_response_body" type="string">
            *`For link pagination (required unless link_rel_in_response_header is specified).`*

            The path of the field containing the link to the next page, in the response. Use `.` for nested fields, e.g. `"pagination.link"`.
          </ResponseField>

          <ResponseField name="offset_name_in_request" type="string">
            *For offset pagination only (required).*

            The name of the parameter containing the offset for the next page, in the request. Inserted in the query parameters for `GET`/`DELETE`, in the body for `POST`/`PUT`/`PATCH`.
          </ResponseField>

          <ResponseField name="offset_start_value" type="number">
            *For offset pagination only (optional).*

            The initial offset. Defaults to 0, but some APIs start at 1.
          </ResponseField>

          <ResponseField name="offset_calculation_method" type="'per-page' | 'by-response-size'">
            *For offset pagination only (optional).*

            The offset calculation method. `by-response-size` (default) means the offset is incremented by the number of results. `per-page` means the offset is incremented by one for each page.
          </ResponseField>

          <ResponseField name="on_page" type="(paginationState: { nextPageParam?: string | number | undefined; response: AxiosResponse }) => Promise<void>">
            *For all pagination types (optional).*

            A callback function that is called after each page is fetched. Useful for logging or tracking pagination progress. The callback receives the next page parameter and the full Axios response object, which includes the response data, status, headers, and request configuration.
          </ResponseField>
        </Expandable>
      </ResponseField>
    </Expandable>
  </ResponseField>

  <Info>
    You can find details on the pagination [types](https://github.com/NangoHQ/nango/blob/master/packages/runner-sdk/models.d.ts) and [logic](https://github.com/NangoHQ/nango/blob/master/packages/runner-sdk/lib/paginate.service.ts) in the code.
  </Info>
</Expandable>

## Get Integration

Returns the current integration information

```js theme={null}
await nango.getIntegration();
```

With credentials

```js theme={null}
await nango.getIntegration({ include: ['credentials'] });
```

**Parameters**

See `GET /integrations/{uniqueKey}` query parameters: [documentation](/reference/backend/http-api/integration/get)

**Response**

See `GET /integrations/{uniqueKey}` response: [documentation](/reference/backend/http-api/integration/get)

## Manage connection metadata

### Get connection metadata

Returns the connection's metadata.

```js theme={null}
await nango.getMetadata();
```

Better, you can specify the type of the metadata;

```ts theme={null}
interface CustomMetadata {
    anyKey: Record<string, string>;
}
const myTypedMetadata = await nango.getMetadata<CustomMetadata>();
```

**Parameters**

No parameters.

**Example Response**

<Expandable>
  ```json theme={null}
  {
      "custom_key1": "custom_value1"
  }
  ```
</Expandable>

### Set connection metadata

Set custom metadata for the connection (overrides existing metadata).

```js theme={null}
await nango.setMetadata({ 'CUSTOM_KEY1': 'CUSTOM_VALUE1' });
```

**Parameters**

<Expandable>
  <ResponseField name="metadata" type="Record<string, any>" required>
    The custom metadata to store in the connection.
  </ResponseField>
</Expandable>

**Response**

Empty response.

### Edit connection metadata

Edit custom metadata for the connection. Only overrides & adds specified properties, not the entire metadata.

```js theme={null}
await nango.updateMetadata({ 'CUSTOM_KEY1': 'CUSTOM_VALUE1' });
```

**Parameters**

<Expandable>
  <ResponseField name="metadata" type="Record<string, any>" required>
    The custom metadata to store in the connection.
  </ResponseField>
</Expandable>

**Response**

Empty response.

## Get the connection credentials

Returns a specific connection with credentials.

```js theme={null}
await nango.getConnection();
```

<Info>
  The response content depends on the API authentication type (OAuth 2, OAuth 1, API key, Basic auth, etc.).
</Info>

<Tip>
  When you fetch the connection with this API endpoint, Nango will check if the access token has expired. If it has, it will refresh it.

  We recommend not caching tokens for longer than 5 minutes to ensure they are fresh.
</Tip>

**Parameters**

<Expandable>
  <ResponseField name="providerConfigKeyOverride" type="string">
    An optional integration ID to override the one from the function context.
  </ResponseField>

  <ResponseField name="connectionIdOverride" type="string">
    An optional connection ID to override the one from the function context.
  </ResponseField>

  <ResponseField name="options" type="object">
    <Expandable>
      <ResponseField name="forceRefresh" type="boolean">
        Defaults to `false`. If `false`, the token will only be refreshed if it expires within 15 minutes. If `true`, a token refresh attempt will happen on each request. This is only useful for testing and should not be done at high traffic.
      </ResponseField>

      <ResponseField name="refreshToken" type="boolean">
        Defaults to `false`. If `false`, the refresh token is not included in the response, otherwise it is. In production, it is not advised to return the refresh token, for security reasons, since only the access token is needed to sign requests.
      </ResponseField>

      <ResponseField name="refreshGithubAppJwtToken" type="boolean">
        Defaults to `false`. If true, this will refresh the JWT token for GitHub App / Github App OAuth connections
      </ResponseField>
    </Expandable>
  </ResponseField>
</Expandable>

**Example Response**

<Expandable>
  ```json theme={null}
  {
      "id": 18393,
      "created_at": "2023-03-08T09:43:03.725Z",
      "updated_at": "2023-03-08T09:43:03.725Z",
      "provider_config_key": "github",
      "connection_id": "1",
      "credentials": {
          "type": "OAUTH2",
          "access_token": "gho_tsXLG73f....",
          "refresh_token": "gho_fjofu84u9....",
          "expires_at": "2024-03-08T09:43:03.725Z",
          "raw": { // Raw token response from the OAuth provider: Contents vary!
              "access_token": "gho_tsXLG73f....",
              "refresh_token": "gho_fjofu84u9....",
              "token_type": "bearer",
              "scope": "public_repo,user"
          }
      },
      "connection_config": {
          "subdomain": "myshop",
          "realmId": "XXXXX",
          "instance_id": "YYYYYYY"
      },
      "account_id": 0,
      "metadata": {
          "myProperty": "yes",
          "filter": "closed=true"
      }
  }
  ```
</Expandable>

## Sync-specific helper methods

Sync functions persist data updates to the Nango cache, which your app later fetches. See the [sync functions section](/guides/functions/syncs/sync-functions).

### Checkpoints

Checkpoints allow syncs to save their progress and resume from where they left off. This is useful for:

* Resuming after failures without re-fetching all data
* Tracking pagination state across sync runs
* Storing custom cursor/offset values

To use checkpoints, define a `checkpoint` schema in your sync configuration (see [Configuration](#createsync)). For a complete guide on implementing incremental syncs with checkpoints, see the [checkpoints guide](/guides/functions/syncs/checkpoints). If you are migrating from `nango.lastSyncDate`, see the [migration guide](/guides/platform/migrations/migrate-to-checkpoints).

#### getCheckpoint()

Retrieves the current checkpoint. Returns `null` on first run or after a reset.

```ts theme={null}
const checkpoint = await nango.getCheckpoint();
if (checkpoint) {
    // Resume from checkpoint
    startCursor = checkpoint.cursor;
}
```

**Response**

Returns the checkpoint object matching your schema, or `null` if no checkpoint exists.

#### saveCheckpoint()

Saves the current progress. Call this after processing each batch of data to enable resumption if the sync fails.

```ts theme={null}
await nango.saveCheckpoint({ cursor: nextCursor });
```

**Parameters**

<Expandable>
  <ResponseField name="checkpoint" type="object" required>
    The checkpoint data matching your defined schema.
  </ResponseField>
</Expandable>

#### clearCheckpoint()

Clears the checkpoint. Use at the end of a full sync so the next run starts from the first page. Checkpoints are also automatically cleared when triggering a sync with `reset: true`.

```ts theme={null}
await nango.clearCheckpoint();
```

### Save records

Upserts records to the Nango cache (i.e. create new records, update existing ones). Each record needs to contain a unique `id` field used to dedupe records.

```js theme={null}
const githubIssues: GitHubIssue[] = ...; // Fetch issues from GitHub API.

await nango.batchSave(githubIssues, 'GitHubIssue');
```

**Parameters**

<Expandable>
  <ResponseField name="recordList" type="Model[]" required>
    The list of records to persist.
  </ResponseField>

  <ResponseField name="modelType" type="string" required>
    The model type of the records to persist.
  </ResponseField>
</Expandable>

### Delete records

Marks records as deleted in the Nango cache. Deleted records are still returned when you fetch them, but they are marked as deleted in the record's metadata (i.e. soft delete).

<Tip>
  This does not remove cached payloads.

  `nango.batchDelete()` is used to mark records as deleted in Nango because they were deleted on the external API. Nango may still keep the last-known payload so your customer can react to the deletion event.

  If you want to permanently remove data from Nango storage for cost or compliance reasons, use [record pruning instead](/reference/backend/http-api/sync/prune-records).
</Tip>

To implement deletion detection in your syncs, [follow this guide](/guides/functions/syncs/deletion-detection).

The only field that needs to be present in each record when calling `batchDelete` is the unique `id`; the other fields are ignored.

```js theme={null}
const githubIssuesToDelete: { id: string }[] = ...; // Fetch issues to delete from GitHub API.

await nango.batchDelete(githubIssuesToDelete, 'GitHubIssue');
```

**Parameters**

<Expandable>
  <ResponseField name="recordList" type="{ id: string }[]" required>
    The list of records to delete.
  </ResponseField>

  <ResponseField name="modelType" type="string" required>
    The model type of the records to delete.
  </ResponseField>
</Expandable>

### Detect deletions automatically

Automatically detects and marks records as deleted by comparing what existed before `trackDeletesStart` with what was saved between `trackDeletesStart` and `trackDeletesEnd`.

<Tip>
  This does not remove cached payloads.

  `nango.trackDeletesStart()`/`nango.trackDeletesEnd()` are used to mark records as deleted in Nango because they were not returned by the external API. Nango may still keep the last-known payload so your customer can react to the deletion event.

  If you want to permanently remove data from Nango storage for cost or compliance reasons, use [record pruning instead](/reference/backend/http-api/sync/prune-records).
</Tip>

```js theme={null}
await nango.trackDeletesStart('ModelName');

// ... fetch and save all records ...

await nango.trackDeletesEnd('ModelName');
```

Call `trackDeletesStart` at the beginning of your sync execution, before fetching any data. Call `trackDeletesEnd` after all records have been saved with `batchSave`. Nango will compare the records that existed before `trackDeletesStart` with those saved in the window and mark any missing records as deleted.

**Parameters** (both functions)

<Expandable>
  <ResponseField name="modelType" type="string" required>
    The model type to detect deletions for.
  </ResponseField>
</Expandable>

**Important considerations:**

* Only use within syncs that fetch the complete dataset between `trackDeletesStart` and `trackDeletesEnd`
* If your sync fails or doesn't fetch the complete dataset, avoid calling `trackDeletesEnd` as it may cause false deletions
* Records are soft deleted (marked with `_metadata.deleted = true`) and remain in the cache

<Note>
  `deleteRecordsFromPreviousExecutions` is deprecated. Use `trackDeletesStart`/`trackDeletesEnd` instead.
</Note>

For more details on deletion detection strategies, see the [detecting deletes guide](/guides/functions/syncs/deletion-detection).

### Update records

Updates records in the Nango cache by merging the given data into the existing record. The `id` field is required in each record and used to determine what existing record to merge into.

`batchUpdate` is primarily useful in webhook sync functions, where you receive partial updates from a webhook and want to merge them into the existing records.

The merge algorithm used is a deep merge. Nested objects are merged recursively, while arrays always use the new value for the array. Any fields not present in the update record are left unchanged.

```ts theme={null}
// Create partial GitHub Issue update records with only id and state.
const githubIssues: Pick<GitHubIssue, "id" | "state">[] = ...;

await nango.batchUpdate(githubIssues, 'GitHubIssue');
```

**`Take special care when using batchUpdate with records containing arrays.`** The merge algorithm does not attempt to merge arrays, but rather always uses the value of the new array.

```ts theme={null}
// given a an existing record:
// { id: '1', tags: [{id: 12, name: 'Dev'}, {id: 13, name: "QA"}] }
const updates: Pick<Issue, "id" | "tags">[] = [
    { id: '1', tags: [{id: 14, name: 'UX'}] }
];

// after the update, the record will be:
// { id: '1', tags: [{id: 14, name: "UX"}] }
await nango.batchUpdate(updates, 'Issue');
```

**Parameters**

<Expandable>
  <ResponseField name="recordList" type="Pick<Model, 'id' | ...>[]" required>
    The list of partial records to persist.
  </ResponseField>

  <ResponseField name="modelType" type="string" required>
    The model type of the records to persist.
  </ResponseField>
</Expandable>

### Get records

Fetches records from the Nango cache by ID. Returns a Map where the keys are the requested IDs, and the values are the corresponding records. Any records that are not found will simply be absent from the map.

Example usage:

```ts theme={null}
const records = await nango.getRecordsById<string, Issue>(['1', '2', '3'], 'Issue');

if (records.has('1')) {
    const record = records.get('1');
    await nango.log(record.title);
} else {
    await nango.log('Record with id 1 not found.');
}
```

<Tip>
  Fetching records by ID is useful when you need to update specific records with a more granular approach than [`nango.batchUpdate()`](/reference/functions/functions-sdk#update-records), which performs a deep merge. Note that `nango.batchUpdate()` is more performant than using `nango.getRecordsById()`, followed by `nango.batchSave()`.

  A common use case is when handling external webhooks, where only a partial update of a record is received from an API.
</Tip>

### Variant

If you are using sync variants, you can access the current variant name via the `nango.variant` property.

```ts theme={null}
export default createSync({
  exec: async (nango) => {
    await nango.log(`Running sync with variant: ${nango.variant}`);

    // Customize sync behavior based on variant
    const res = await nango.get({
        endpoint: `/spreadsheet/${nango.variant}`
    });

    // Rest of sync implementation...
  },
});
```

## Action-specific helper methods

### `ActionError`

You can use `ActionError` in an action function to return a descriptive error to your app when needed:

```ts theme={null}

export default createAction({
  exec: async (nango) => {
    // Something went wrong...

    throw new ActionError({ any_key: 'any_value' });
  },
});
```

In this case, the response to the trigger action call will be:

```json theme={null}
{
  "error_type": "action_script_failure",
  "payload": {
    "any_key": "any_value"
  }
}
```

## Relative imports in functions

You can import relative files into your functions to allow for code abstraction and to
maintain DRY (Don't Repeat Yourself) principles. This means you can reuse code across
different functions by importing it. The imported file must live in the `nango-integrations`
directory and can be imported in the following way:

```ts theme={null}
import { issueMapper } from '../mappers/issue-mappper';

export default createSync({
  exec: async (nango) => {
    // Fetch issues from GitHub.
    const res = await nango.get({
        endpoint: '/repos/NangoHQ/interactive-demo/issues?labels=demo&sort=created&direction=asc'
    });

    // Persist issues to the Nango cache.
    await nango.batchSave(issueMapper(res.data), 'GithubIssueDemo');
  },
});
```

Note that you cannot import third-party modules at this time. Additionally, if there is a compilation error in an imported file, the entry point file will also fail to compile.

## Pre-included Dependencies

Some libraries are pre-included for usage in functions:

* [zod](https://github.com/colinhacks/zod)
* [crypto / node:crypto](https://nodejs.org/api/crypto.html#crypto)
* [url / node:url](https://nodejs.org/api/url.html#url)

Please reach out in the [community](https://nango.dev/slack) if you would like to request additional ones.

<Tip>
  **Questions, problems, feedback?** Please reach out in the [Slack community](https://nango.dev/slack).
</Tip>
