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

# Receive webhooks from Nango

> Reference for the webhooks Nango sends to your app — setup, types, payloads, signature verification, and retries.

Nango POSTs webhooks to your app to notify it of important events: a new connection was created, a sync run finished, an async action completed, or an external webhook was forwarded. This page covers how to receive and verify webhooks from Nango, plus the payloads for auth, sync, and async action webhooks.

<Info>
  To process webhooks coming **from external APIs** (not from Nango), see [Process external webhooks](/getting-started/use-cases/webhooks-from-external-apis). To forward external API webhooks through Nango to your app, see [Webhook forwarding](/guides/platform/webhook-forwarding).
</Info>

## Set up webhooks from Nango

<Note>
  Webhook settings in Nango are specific to each [environment](/guides/platform/environments).
</Note>

To subscribe to Nango webhooks:

1. Set up a `POST` endpoint in your app to receive the Nango webhooks
2. Input the endpoint's URL in your *Environment Settings*, under *Webhook URLs* ([direct link for your dev environment](https://app.nango.dev/dev/environment-settings#notification))
3. Implement [verify incoming webhooks](#verifying-webhooks-from-nango) to ensure sure only authentic Nango webhooks are processed
4. Implement processing logic for each [webhook type](#types-of-nango-webhooks) from Nango you want to handle
   * Make sure your processing logic handles the webhooks you have enabled in the environment settings

You can configure up to two webhook URLs per environment. Nango webhooks will be sent to both. A single connection can [override these with its own URL](#override-webhook-urls-per-connection).

To test webhooks locally, use a webhook forwarding service like [ngrok](https://dev.to/mmascioni/testing-and-debugging-webhooks-with-ngrok-4alp), or [webhook.site](https://webhook.site/).

## Override webhook URLs per connection

A single connection can override the environment's webhook URLs with its own URL. The connection still receives all the same [webhook types](#types-of-nango-webhooks), with the same signing, retries, and logs.

When this override is set, the connection's webhooks are sent **only** to that URL — the environment's primary and secondary webhook URLs are both bypassed for that connection. It does not add a third destination.

Set `webhook_url_override` when creating a [connect session](/reference/backend/http-api/connect/sessions/create). It applies to the connection created by that session:

<Tabs>
  <Tab title="Node">
    ```ts theme={null}
    const session = await nango.createConnectSession({
        allowed_integrations: ['<INTEGRATION-ID>'],
        webhook_url_override: 'https://<your-tunnel>.ngrok.app/webhooks-from-nango'
    });
    ```
  </Tab>

  <Tab title="cURL">
    ```bash theme={null}
    curl --request POST \
      --url https://api.nango.dev/connect/sessions \
      --header 'Authorization: Bearer <NANGO-API-KEY>' \
      --header 'Content-Type: application/json' \
      --data '{
        "allowed_integrations": ["<INTEGRATION-ID>"],
        "webhook_url_override": "https://<your-tunnel>.ngrok.app/webhooks-from-nango"
      }'
    ```
  </Tab>
</Tabs>

You can also set or edit `webhook_url_override` from:

* The **Create connection** flow in the Nango dashboard (advanced configuration), or later on the connection's **Settings** tab
* `PATCH /connections/{connectionId}` with a `webhook_url_override` field (see [Patch a connection](/reference/backend/http-api/connections/patch))

<Note>
  `webhook_url_override` is set by a trusted actor — via connect-session creation, the public API with an API key, or the dashboard. A `webhook_url` passed to `nango.auth()` `params` from the client is ignored.
</Note>

<Tip>
  **Local development:** set a connection's `webhook_url_override` to your local webhook endpoint (for example via [ngrok](https://ngrok.com/) or [webhook.site](https://webhook.site/)) to receive that connection's webhooks on your machine — without changing the shared environment webhook URLs. See [Engineering collaboration](/guides/platform/environments#engineering-collaboration) for the shared-environment workflow.
</Tip>

## Verifying webhooks from Nango

Validate webhooks from Nango by looking at the `X-Nango-Hmac-Sha256` header.

It's an HMAC-SHA256 hash of the webhook payload, using the webhook signing key found in **Environment Settings > Webhooks > Signing key** in the Nango UI.

<Warning>
  Nango webhook requests include an `X-Nango-Hmac-Sha256` header for secure verification. A legacy `X-Nango-Signature` header (using plain SHA-256) is also sent for backwards compatibility but should not be used. If you're currently using `X-Nango-Signature`, migrate to `X-Nango-Hmac-Sha256` for improved security.
</Warning>

The webhook signature can be verified with the following code:

<Warning>
  Verify the signature with the **webhook signing key** from **Environment Settings > Webhooks > Signing key** — not your API secret key. These are two distinct keys. With the Node SDK, pass it as `webhookSigningKey`; for raw HMAC verification, use it as the HMAC secret. Using the API secret key will cause verification to fail on any environment where the keys differ.
</Warning>

<Tabs>
  <Tab title="Node SDK">
    Pass the webhook signing key as `webhookSigningKey` so a single client can make API calls with the API key and verify webhooks with the signing key. `webhookSigningKey` falls back to `secretKey` when omitted.

    ```typescript theme={null}
    const nango = new Nango({
        apiKey: process.env.NANGO_API_KEY, // used for API calls
        webhookSigningKey: process.env.NANGO_WEBHOOK_SIGNING_KEY // used to verify webhooks
    });

    async (req, res) => {
        const isValid = nango.verifyIncomingWebhookRequest(req.body, req.headers);
    }
    ```
  </Tab>

  <Tab title="JavaScript/TypeScript">
    ```typescript theme={null}
    import crypto from 'crypto';

    // From Environment Settings > Webhooks > Signing key
    const webhookSigningKey = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx';
    const body = // raw request body as string
    const hash = crypto.createHmac('sha256', webhookSigningKey).update(body).digest('hex');
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    import hmac
    import hashlib

    # From Environment Settings > Webhooks > Signing key
    webhook_signing_key = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
    body = # raw request body as bytes
    hash = hmac.new(webhook_signing_key.encode('utf-8'), body, hashlib.sha256).hexdigest()
    ```
  </Tab>

  <Tab title="Java">
    ```java theme={null}
    import javax.crypto.Mac;
    import javax.crypto.spec.SecretKeySpec;
    import java.nio.charset.StandardCharsets;

    public class Main {
        public static void main(String[] args) throws Exception {
            // From Environment Settings > Webhooks > Signing key
            String webhookSigningKey = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx";
            String body = // raw request body as string

            Mac mac = Mac.getInstance("HmacSHA256");
            SecretKeySpec secretKey = new SecretKeySpec(webhookSigningKey.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
            mac.init(secretKey);
            byte[] hashBytes = mac.doFinal(body.getBytes(StandardCharsets.UTF_8));

            StringBuilder hexString = new StringBuilder();
            for (byte b : hashBytes) {
                hexString.append(String.format("%02x", b));
            }
            String hash = hexString.toString();
        }
    }
    ```
  </Tab>

  <Tab title="Ruby">
    ```ruby theme={null}
    require 'openssl'

    # From Environment Settings > Webhooks > Signing key
    webhook_signing_key = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
    body = # raw request body
    hash = OpenSSL::HMAC.hexdigest('SHA256', webhook_signing_key, body)
    ```
  </Tab>

  <Tab title="Go">
    ```go theme={null}
    package main

    import (
        "crypto/hmac"
        "crypto/sha256"
        "encoding/hex"
        "io"
        "net/http"
    )

    func main() {
        // From Environment Settings > Webhooks > Signing key
        webhookSigningKey := "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
        var body []byte // raw request body

        h := hmac.New(sha256.New, []byte(webhookSigningKey))
        h.Write(body)
        hash := hex.EncodeToString(h.Sum(nil))
    }
    ```
  </Tab>

  <Tab title="Rust">
    ```rust theme={null}
    use hmac::{Hmac, Mac};
    use sha2::Sha256;

    type HmacSha256 = Hmac<Sha256>;

    // From Environment Settings > Webhooks > Signing key
    let webhook_signing_key = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx";
    let body = // raw request body as string or bytes
    let mut mac = HmacSha256::new_from_slice(webhook_signing_key.as_bytes()).unwrap();
    mac.update(body.as_bytes());
    let hash = format!("{:x}", mac.finalize().into_bytes());
    ```
  </Tab>

  <Tab title="PHP">
    ```php theme={null}
    <?php
    // From Environment Settings > Webhooks > Signing key
    $webhookSigningKey = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx';
    $body = // raw request body as string
    $hash = hash_hmac('sha256', $body, $webhookSigningKey);
    ?>
    ```
  </Tab>
</Tabs>

Only accept a webhook if the `X-Nango-Hmac-Sha256` header value matches the webhook signature.

## Types of Nango webhooks

<Warning>
  New Nango webhook types are added regularly, without considering this a breaking change. Your webhook handling logic should gracefully support receiving new types of webhooks by simply ignoring them.
</Warning>

All webhooks from Nango are POST requests.

The exact webhook type definitions can be found [here](https://github.com/NangoHQ/nango/blob/master/packages/types/lib/webhooks/api.ts).

### Auth webhooks

New connection webhooks have `"type": "auth"` and `"operation": "creation"`. They are sent after a connection has been successfully created.

Payload received following a connection creation:

```json theme={null}
{
    "type": "auth",
    "operation": "creation",
    "connectionId": "<your-connection-id>",
    "authMode": "OAUTH2 | BASIC | API_KEY | ...",
    "providerConfigKey": "<your-integration-id>",
    "provider": "<your-provider-key>",
    "environment": "DEV | PROD | ...",
    "success": true,
    "tags": {
        "end_user_id": "<your-end-user-id>",
        "end_user_email": "<your-end-user-email>",
        "organization_id": "<your-organization-id>"
    }
}
```

<Warning>
  Processing webhooks with `"type": "auth"` and `"operation": "creation"` is **necessary**. After a connection is created, these webhooks give you the generated connection ID which lets you access the connection later on.

  Use `tags` to reconcile and save the connection ID with the user/org/workspace that initiated the connection.
</Warning>

All `authMode` values can be found [here](https://github.com/NangoHQ/nango/blob/master/packages/types/lib/auth/api.ts). The `authMode` value depends on the `provider` value.

All `operation` values are:

* `creation`: a new connection has been created
* `override`: a connection has been re-authorized
* `refresh`: an OAuth connection's access token has failed to refresh

Payload received following a refresh token error:

```json theme={null}
{
    "type": "auth",
    "operation": "refresh",
    "connectionId": "<your-connection-id>",
    "authMode": "OAUTH2 | BASIC | API_KEY | ...",
    "providerConfigKey": "<your-integration-id>",
    "provider": "<your-provider-key>",
    "environment": "DEV | PROD | ...",
    "success": false,
    "tags": {
        "end_user_id": "<your-end-user-id>",
        "end_user_email": "<your-end-user-email>",
        "organization_id": "<your-organization-id>"
    },
    "error": {
        "type": "<string>",
        "description": "<string>"
    }
}
```

<Note>
  Webhooks are only sent for certain connection creation errors. For example, during the OAuth flow, some errors are reported locally in the OAuth modal by the external API. Since Nango does not receive these errors, it cannot trigger a webhook for them.
</Note>

### Sync webhooks

Sync webhooks are sent when a [sync function](/guides/functions/syncs/sync-functions) execution finishes, whether successful or not.

Payload received following a successful sync execution:

```json theme={null}
{
    "type": "sync",
    "connectionId": "<your-connection-id>",
    "providerConfigKey": "<your-integration-id>",
    "syncName": "<your-sync-script-name>",
    "model": "<your-model-name>",
    "syncType": "INCREMENTAL | INITIAL | WEBHOOK", // DEPRECATED — use checkpoints instead
    "success": true,
    "modifiedAfter": "<timestamp>",
    "responseResults": {
        "added": number,
        "updated": number,
        "deleted": number
    },
    // checkpoints itself may be missing or null if the sync doesn't use checkpoints —
    // always guard against that before reading `from`/`to`.
    "checkpoints": {
        "from": { "<key>": "<string | number | boolean>" }, // or null if no prior checkpoint
        "to": { "<key>": "<string | number | boolean>" } // or null if the sync didn't save one
    }
}
```

`modifiedAfter` is an ISO-8601 timestamp marking the start of the last run. To read the changed records, use cursor-based fetching as described in [Records cache → Cursors and sync progress](/guides/functions/syncs/records-cache#cursors-and-sync-progress).

**Distinguishing full vs incremental syncs:** use `checkpoints` instead of the deprecated `syncType` field. `checkpoints.from`/`checkpoints.to` are checkpoint objects (`Record<string, string | number | boolean>`) as saved by the sync function via `saveCheckpoint()` — not strings — and either can be `null`. The `checkpoints` field itself can also be missing or `null`, so guard against that before reading nested fields:

```javascript theme={null}
const { checkpoints } = payload;
const isFullSync = !checkpoints || checkpoints.from === null;
```

* `checkpoints` is missing/`null`, or `checkpoints.from === null` — the run started with no prior state. This happens on the first run, after a manual reset, or when the sync explicitly cleared its checkpoint. Treat this as a full sync and overwrite your data.
* `checkpoints.from` is a non-null checkpoint object — the run continued from a previously saved checkpoint. This is an incremental sync; merge the new records into your existing data.
* **Webhook-triggered runs** (deprecated `syncType: "WEBHOOK"`): always treat these as incremental merges, regardless of `checkpoints.from`. A webhook-triggered run processes a single incoming external event on top of already-synced data — it never represents a full resync.

<Warning>
  **Fetch records promptly**: Due to Nango's [data retention policies](/guides/platform/security#synced-records-retention), you should fetch and store synced records in your own system promptly after receiving webhook notifications. Records not updated for 30 days will have their payload pruned, and sync functions not executed for 60 days will have all records deleted.
</Warning>

By default, Nango sends a webhook even if no modified data was detected in the last sync execution (referred as an "empty" sync), but this is configurable in your *Environment Settings*. In case of an empty sync, the `responseResults` would be:

```json theme={null}
{
    "added": 0,
    "updated": 0,
    "deleted": 0
}
```

Payload received following a failed sync execution:

```json theme={null}
{
    "type": "sync",
    "connectionId": "<your-connection-id>",
    "providerConfigKey": "<your-integration-id>",
    "syncName": "<your-sync-script-name>",
    "model": "<your-model-name>",
    "syncType": "INCREMENTAL | INITIAL | WEBHOOK", // DEPRECATED — use checkpoints instead
    "success": false,
    "error": {
        "type": "<string>",
        "description": "<string>"
    },
    "startedAt": "<timestamp>",
    "failedAt": "<timestamp>",
    // checkpoints itself may be missing or null — see the note above.
    "checkpoints": {
        "from": { "<key>": "<string | number | boolean>" }, // or null
        "to": { "<key>": "<string | number | boolean>" } // or null
    }
}
```

### External webhook forwarding

Forwarded external webhooks use the same webhook URLs, signing, retries, and logs as other Nango webhooks. See [Webhook forwarding](/guides/platform/webhook-forwarding) for setup, routing behavior, and payload shapes.

## Webhook retries & debugging

Nango retries each webhook with non-2xx responses 2 times with exponential backoff (starting delay: 100ms, time multiple: 2, view details in the [code](https://github.com/NangoHQ/nango/blob/master/packages/webhooks/lib/utils.ts)).

Webhooks time out after 20 seconds.

Each webhook attempt is logged in Nango's [logs](/guides/platform/observability).

You can also use the [OpenTelemetry exporter](/guides/platform/observability#opentelemetry-export) to monitor Nango webhooks in your own observability stack.

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

## Related guides

* [Webhook forwarding](/guides/platform/webhook-forwarding) - forward external provider webhooks through Nango.
* [Webhook functions](/guides/functions/webhook-functions) - process provider webhooks inside Nango.
* [Event functions](/guides/functions/event-functions) - react to connection lifecycle events.
* [Observability](/guides/platform/observability) - monitor webhook operations and retries.
