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

# Webhooks

> Learn about receiving event notifications with webhooks, including setup steps, retries, and payload examples.

Webhooks send data to your website or application when certain events happen in your Loops account.

## Set up webhooks

Go to [Settings -> Webhooks](https://app.loops.so/settings?page=webhooks) and input the URL of your endpoint that will receive events.

You will be provided with a signing secret. You should save this in your project (for example in an environment variable) so you can verify requests when you receive them.

Currently you can only set up one webhook endpoint per Loops account.

Subscribe to the events you want to receive using the toggles. Click the group names to view all events in each.

![Webhooks settings](https://mintcdn.com/loops/aOtgyczWtTw7x5Xz/images/webhooks-setup.png?fit=max\&auto=format\&n=aOtgyczWtTw7x5Xz\&q=85\&s=00d8ab5d846cb8287ceeacd6d29fa9a3)

> [!TIP]
>
> When toggling your endpoint on and off there may be a small delay before this
> setting is reflected on the server. For example, it may take a few seconds
> after toggling on your endpoint for requests to be dispatched.

## Rate limiting

Webhook events will be sent at a maximum rate of 10 per second. Any further events will be queued.

## Retries

If your endpoint doesn't return a `2xx` response, Loops retries the event using exponential backoff.

Up to eight attempts are made over a period of about 28 hours.

Your endpoint should respond within 15 seconds. Any status other than `2xx` is treated as a failure and will be retried.

> [!TIP]
>
> Return a `2xx` as soon as you've received the event, then process it in the background. Use the `Webhook-Id` header to ignore duplicate deliveries, since the same event can be sent more than once.

After all retries are exhausted, the event is marked as failed. You can inspect your endpoint's response in the Webhooks settings page.

If every attempt to your endpoint fails for 5 days, the endpoint is automatically disabled and Loops stops sending events to it. Turn it back on from [Settings -> Webhooks](https://app.loops.so/settings?page=webhooks) once your endpoint is responding successfully.

Disabling your endpoint also stops any in-progress retries.

## Verify requests

Every event is signed so you can check that data sent to your endpoint is sent from Loops.

To verify webhooks, you need to create a signature of the received request and match that to the provided signature in the request's headers.

Here's an example verification function you could use in Next.js:

```typescript lib/verify-webhook.ts theme={"dark"}
import crypto from 'node:crypto';

const SECRET_PREFIX = 'whsec_';
const TOLERANCE_SECONDS = 300;

export class WebhookVerificationError extends Error {
  constructor(
    message: string,
    readonly code:
      | 'MISSING_HEADERS'
      | 'MISSING_SECRET'
      | 'INVALID_TIMESTAMP'
      | 'INVALID_SIGNATURE',
  ) {
    super(message);
    this.name = 'WebhookVerificationError';
  }
}

function timingSafeEqual(a: string, b: string): boolean {
  const left = Buffer.from(a);
  const right = Buffer.from(b);
  if (left.length !== right.length) return false;
  return crypto.timingSafeEqual(left, right);
}

export function verifyWebhook({
  headers,
  rawBody,
  secret = process.env.LOOPS_SIGNING_SECRET,
}: {
  headers: Headers;
  rawBody: string;
  secret?: string;
}): void {
  if (!secret) {
    throw new WebhookVerificationError(
      'Missing LOOPS_SIGNING_SECRET environment variable',
      'MISSING_SECRET',
    );
  }

  const id = headers.get('webhook-id');
  const timestamp = headers.get('webhook-timestamp');
  const signatureHeader = headers.get('webhook-signature');

  if (!id || !timestamp || !signatureHeader) {
    throw new WebhookVerificationError(
      'Missing webhook-id, webhook-timestamp, or webhook-signature header',
      'MISSING_HEADERS',
    );
  }

  const ts = Number(timestamp);
  if (!Number.isFinite(ts)) {
    throw new WebhookVerificationError(
      'webhook-timestamp is not a valid number',
      'INVALID_TIMESTAMP',
    );
  }

  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - ts) > TOLERANCE_SECONDS) {
    throw new WebhookVerificationError(
      'webhook-timestamp is outside the allowed tolerance',
      'INVALID_TIMESTAMP',
    );
  }

  const encoded = secret.startsWith(SECRET_PREFIX)
    ? secret.slice(SECRET_PREFIX.length)
    : secret;
  const key = Buffer.from(encoded, 'base64');

  const expected = crypto
    .createHmac('sha256', key)
    .update(`${id}.${timestamp}.${rawBody}`)
    .digest('base64');

  const matched = signatureHeader.split(/\s+/).some((entry) => {
    const comma = entry.indexOf(',');
    if (comma === -1) return false;
    const version = entry.slice(0, comma);
    const value = entry.slice(comma + 1);
    return version === 'v1' && timingSafeEqual(value, expected);
  });

  if (!matched) {
    throw new WebhookVerificationError('Invalid signature', 'INVALID_SIGNATURE');
  }
}
```

```typescript app/api/webhooks/route.ts theme={"dark"}
import { NextRequest, NextResponse } from 'next/server';
import { verifyWebhook, WebhookVerificationError } from '@/lib/verify-webhook';

export async function POST(req: NextRequest) {
  const rawBody = await req.text();

  try {
    verifyWebhook({ headers: req.headers, rawBody });
  } catch (error) {
    if (error instanceof WebhookVerificationError) {
      const status =
        error.code === 'MISSING_SECRET'
          ? 500
          : error.code === 'INVALID_SIGNATURE' || error.code === 'INVALID_TIMESTAMP'
            ? 401
            : 400;
      return NextResponse.json({ message: error.message }, { status });
    }
    throw error;
  }

  const event = JSON.parse(rawBody);
  // process event...
  return NextResponse.json({ ok: true, eventName: event.eventName });
}
```

If you roll your webhook secret, the previous secret will still be valid for 24 hours. The system sends multiple secrets in the header, so both your old and current secrets will be valid for 24 hours after the change.

## Testing webhooks

From the Webhooks settings page you can send a test request to your endpoint. This allows you to test that your endpoint is working, and that your verification code is OK.

The event name sent during testing is `testing.testEvent`. You can [see the payload below](#testing-testevent).

## Viewing webhook history

Once Loops has started sending webhook events to your endpoint you will be able to see event history in the **Messages** section at the bottom of the Webhooks settings page.

The table shows successful, failed, pending, and retrying events. Clicking on an event reveals the response from your endpoint, which is helpful if there have been any errors.

We retain 30 days of event history.

![Viewing webhook event history](https://mintcdn.com/loops/1DdZd7QTRR4Srm-l/images/webhooks-history.png?fit=max\&auto=format\&n=1DdZd7QTRR4Srm-l\&q=85\&s=9cc28ff8f2e3dfaf2fb548f90e67070e)

## Event data

Every webhook will contain the following data in the request body:

> [!NOTE]
>
> Loops were renamed to Workflows on May 6, 2026. Webhook payloads still use
> `loop` names for compatibility, including `loop.email.sent`, `loopId`,
> `loopName`, and `sourceType: "loop"`.

### `eventName`

The event type. See a [full list of events below](#event-types).

### `webhookSchemaVersion`

Will be `1.0.0` for all events.

### `eventTime`

Unix timestamp of the time the event occurred in Loops.

***

Depending on the context of the event, more data will also be included. Full examples are shown in the [Event types](#event-types) section below.

### `contact`

A full contact object containing a contact's properties.

Contains:

* `id`
* `email`
* `firstName` (nullable string)
* `lastName` (nullable string)
* `source`
* `subscribed` (boolean)
* `userGroup`
* `userId` (nullable string)
* `mailingLists` (object with mailing list IDs as keys and `true` as the value; these are the mailing lists the contact is subscribed to)
* `optInStatus` (nullable string, `"accepted"` or `null`)
  > [!NOTE]
  >
  > This will be `null` for contacts unless they are created via a form and
  > [double opt-in](https://loops.so/docs/contacts/double-opt-in.md) is enabled. `contact.created`
  > events are only sent once contacts have confirmed their subscription, so
  > this value will never be `"pending"` or `"rejected"`.
* plus any custom contact properties

This object is the same as the data returned in the [Find a contact](https://loops.so/docs/api-reference/find-contact.md#response) API endpoint.

### `contactIdentity`

A contact's identifiers. To retrieve the full contact, use the [Find a contact](https://loops.so/docs/api-reference/find-contact.md) API endpoint.

Contains:

* `id`
* `email`
* `userId` (nullable string)

### `email`

Details about an individual email send to a recipient:

* `id` - The unique ID of the email.
* `emailMessageId` - The ID of the sent version of the campaign, workflow or transactional email.
* `subject` - The subject of the sent version of the campaign, workflow or transactional email.

> [!NOTE]
>
> To get the ID of the campaign, workflow or transactional email that relates to the
> Loops dashboard or API, look for a `campaignId`, `loopId`, or
> `transactionalId` in the payload.

### `mailingList`

Details about a mailing list:

* `id`
* `name`
* `description` (nullable string)
* `isPublic` (boolean)

This object is the same as the data returned in the [List mailing lists](https://loops.so/docs/api-reference/list-mailing-lists.md#response) API endpoint.

### `mailingLists`

A list of `mailingList` objects (see above), when an event relates to multiple mailing lists.

### `sourceType`

For `email.*` events, this specifies the type of email.

One of `campaign`, `loop` or `transactional`. Workflow emails use `loop`.

***

### Headers

Headers will include:

* `Webhook-Signature` - A list of request signatures, which can be used to [verify the request](#verify-requests).
* `Webhook-Id` - The unique ID of the event. You can use this to check if you have already saved or processed this specific event.
* `Webhook-Timestamp` - The timestamp of the request (seconds since epoch).

## Event types

### Contacts

#### contact.created

Sent when a new contact is created in your audience.

Contains a `contactIdentity` object plus a full `contact` object, which includes all of the new contact's properties.

> [!TIP]
>
> When [double opt-in](https://loops.so/docs/contacts/double-opt-in.md) is enabled, contact webhooks
> don't fire until the contact is confirmed. The `contact.created` event will
> only be sent for contacts created via forms once the contact has confirmed
> their subscription. Other contact-related webhooks (such as
> `contact.mailingList.subscribed`) will also only fire after confirmation.

```json theme={"dark"}
{
  "eventName": "contact.created",
  "eventTime": 1734425918,
  "webhookSchemaVersion": "1.0.0",
  "contactIdentity": {
    "id": "cm4itta800003ow9hhekzk94o",
    "email": "test+5@loops.so",
    "userId": null
  },
  "contact": {
    "id": "cm4itta800003ow9hhekzk94o",
    "email": "test+5@loops.so",
    "firstName": null,
    "lastName": null,
    "source": "API",
    "subscribed": true,
    "userGroup": "",
    "userId": null,
    "mailingLists": {
      "cm4ittp2k000l12j3lgrzvlxt": true
    },
    "optInStatus": "accepted",
    "favoriteColor": "blue",
    "favoriteNumber": 42
  }
}
```

#### contact.unsubscribed

Sent when

* a contact is unsubscribed from your audience.
* a contact is deleted from your audience (alongside [contact.deleted](#contact-deleted)).

> [!NOTE]
>
> This is not the same as a contact unsubscribing from a mailing list. See
> [contact.mailingList.unsubscribed](#contact-mailingList-unsubscribed).

Contains a `contactIdentity` object.

```json theme={"dark"}
{
  "eventName": "contact.unsubscribed",
  "eventTime": 1734425918,
  "webhookSchemaVersion": "1.0.0",
  "contactIdentity": {
    "id": "cm4ittmhq0011ow9h6fb460yw",
    "email": "test@example.com",
    "userId": null
  }
}
```

#### contact.deleted

Sent when a contact is deleted from your audience.

Contains a `contactIdentity` object.

```json theme={"dark"}
{
  "eventName": "contact.deleted",
  "eventTime": 1734425918,
  "webhookSchemaVersion": "1.0.0",
  "contactIdentity": {
    "id": "cm4ittmhq0011ow9h6fb460yw",
    "email": "test@example.com",
    "userId": null
  }
}
```

#### contact.mailingList.subscribed

Sent when a contact is subscribed to a mailing list.

Contains `contactIdentity` and `mailingList` objects.

```json theme={"dark"}
{
  "eventName": "contact.mailingList.subscribed",
  "eventTime": 1734425918,
  "webhookSchemaVersion": "1.0.0",
  "contactIdentity": {
    "id": "cm4ittmhq0011ow9h6fb460yw",
    "email": "test@example.com",
    "userId": null
  },
  "mailingList": {
    "id": "cm4ittp2k000l12j3lgrzvlxt",
    "name": "test mailing list",
    "description": null,
    "isPublic": true
  }
}
```

#### contact.mailingList.unsubscribed

Sent when a contact is unsubscribed from a mailing list.

> [!NOTE]
>
> This is not the same as a contact unsubscribing from your audience. See
> [contact.unsubscribed](#contact-unsubscribed).

Contains `contactIdentity` and `mailingList` objects.

```json theme={"dark"}
{
  "eventName": "contact.mailingList.unsubscribed",
  "eventTime": 1734425918,
  "webhookSchemaVersion": "1.0.0",
  "contactIdentity": {
    "id": "cm4ittmhq0011ow9h6fb460yw",
    "email": "test@example.com",
    "userId": null
  },
  "mailingList": {
    "id": "cm4ittp2k000l12j3lgrzvlxt",
    "name": "test mailing list",
    "description": null,
    "isPublic": true
  }
}
```

### Email sending

#### campaign.email.sent

Sent when a campaign is sent to a contact.

> [!NOTE]
>
> This event will fire for every campaign send. If you send a campaign to 1,000
> contacts, you will receive 1,000 events.

Contains `campaignId` and `campaignName` values plus `contactIdentity` and `email` objects.

If the campaign was sent to one or more mailing lists, a `mailingLists` list will also be included.

```json theme={"dark"}
{
  "eventName": "campaign.email.sent",
  "eventTime": 1734425918,
  "webhookSchemaVersion": "1.0.0",
  "contactIdentity": {
    "id": "cm4ittmhq0011ow9h6fb460yw",
    "email": "test@example.com",
    "userId": null
  },
  "campaignId": "cm4t1suns001uw6atri87v54s",
  "campaignName": "Test Campaign",
  "email": {
    "id": "cm4t1sv84004yje79hawr1fi1",
    "emailMessageId": "cm4t1suns001ww6atotin3bn1",
    "subject": "Test Subject"
  },
  "mailingLists": [
    {
      "id": "cm4ittp2k000l12j3lgrzvlxt",
      "name": "test mailing list",
      "description": null,
      "isPublic": true
    }
  ]
}
```

#### loop.email.sent

Sent when a workflow email is sent to a contact.

> [!NOTE]
>
> This event keeps its pre-May 6, 2026 `loop` name for compatibility.

> [!NOTE]
>
> This event will fire for every contact in a workflow. If 1,000 contacts get
> sent emails from your workflow, you will receive 1,000 events.

Contains `loopId` and `loopName` values plus `contactIdentity` and `email` objects.

If the workflow was sent to one or more mailing lists, a `mailingLists` list will also be included.

```json theme={"dark"}
{
  "eventName": "loop.email.sent",
  "eventTime": 1734425918,
  "webhookSchemaVersion": "1.0.0",
  "contactIdentity": {
    "id": "cm4ittmhq0011ow9h6fb460yw",
    "email": "test@example.com",
    "userId": null
  },
  "loopId": "cm4t1snfj0052icemfshgqfcw",
  "loopName": "Test Loop",
  "email": {
    "id": "cm4t1socj004mje79e61mgh7d",
    "emailMessageId": "cm4ittv1v001oow9hruou8na8",
    "subject": "Subject of the email"
  },
  "mailingLists": [
    {
      "id": "cm4ittp2k000l12j3lgrzvlxt",
      "name": "test mailing list",
      "description": null,
      "isPublic": true
    }
  ]
}
```

#### transactional.email.sent

Sent when a transactional email is sent.

Contains `transactionalId` and `transactionalName` values plus `contactIdentity` and `email` objects.

```json theme={"dark"}
{
  "eventName": "transactional.email.sent",
  "eventTime": 1734425918,
  "webhookSchemaVersion": "1.0.0",
  "contactIdentity": {
    "id": "cm4ittmhq0011ow9h6fb460yw",
    "email": "test@example.com",
    "userId": null
  },
  "transactionalId": "cm4t1suns001uw6atri87v54s",
  "transactionalName": "Test Transactional",
  "email": {
    "id": "cm4t1sseg004tje7982991nan",
    "emailMessageId": "cm4ittv1v001oow9hruou8na8",
    "subject": "Subject of the email"
  }
}
```

### Email events

#### email.delivered

Sent when an email is delivered to its recipient.

Contains a `sourceType` and a related `campaignId` / `transactionalId` / `loopId` value, plus `contactIdentity` and `email` objects.

```json theme={"dark"}
{
  "eventName": "email.delivered",
  "eventTime": 1734425918,
  "webhookSchemaVersion": "1.0.0",
  "sourceType": "campaign",
  "campaignId": "cm4t1suns001uw6atri87v54s",
  "email": {
    "id": "cm4t1sseg004tje7982991nan",
    "emailMessageId": "cm4ittv1v001oow9hruou8na8",
    "subject": "Subject of the email"
  },
  "contactIdentity": {
    "id": "cm4ittmhq0011ow9h6fb460yw",
    "email": "test@example.com",
    "userId": null
  }
}
```

#### email.softBounced

Sent when an email soft bounces.

> [!TIP]
>
> Soft bounces are temporary email delivery failures, for example a connection
> timing out. Soft bounces are retried multiple times and sometimes the email
> is delivered.

Contains a `sourceType` and a related `campaignId` / `transactionalId` / `loopId` value, plus `contactIdentity` and `email` objects.

```json theme={"dark"}
{
  "eventName": "email.softBounced",
  "eventTime": 1734425918,
  "webhookSchemaVersion": "1.0.0",
  "sourceType": "campaign",
  "campaignId": "cm4t1suns001uw6atri87v54s",
  "email": {
    "id": "cm4t1sseg004tje7982991nan",
    "emailMessageId": "cm4ittv1v001oow9hruou8na8",
    "subject": "Subject of the email"
  },
  "contactIdentity": {
    "id": "cm4ittmhq0011ow9h6fb460yw",
    "email": "test@example.com",
    "userId": null
  }
}
```

#### email.hardBounced

Sent when an email hard bounces.

> [!TIP]
>
> Hard bounces are persistent email delivery failures, for example a mailbox
> that doesn't exist. The email will not be delivered.

In Loops, a hard bounce results in a contact being unsubscribed from your audience so a `contact.unsubscribed` event will also be sent.

Contains a `sourceType` and a related `campaignId` / `transactionalId` / `loopId` value, plus `contactIdentity` and `email` objects.

```json theme={"dark"}
{
  "eventName": "email.hardBounced",
  "eventTime": 1734425918,
  "webhookSchemaVersion": "1.0.0",
  "sourceType": "campaign",
  "campaignId": "cm4t1suns001uw6atri87v54s",
  "email": {
    "id": "9874cm4t1sseg004tje7982991nan8732843",
    "emailMessageId": "cm4ittv1v001oow9hruou8na8",
    "subject": "Subject of the email"
  },
  "contactIdentity": {
    "id": "cm4ittmhq0011ow9h6fb460yw",
    "email": "test@example.com",
    "userId": null
  }
}
```

#### email.opened

Sent when a campaign or workflow email is opened.

Contains a `sourceType` and a related `campaignId` or `loopId` value, plus `contactIdentity` and `email` objects.

> [!NOTE]
>
> This event is not available for transactional emails because email opens are
> [not tracked](https://loops.so/docs/transactional.md#metrics) for transactional emails.

```json theme={"dark"}
{
  "eventName": "email.opened",
  "eventTime": 1734425918,
  "webhookSchemaVersion": "1.0.0",
  "sourceType": "campaign",
  "campaignId": "cm4t1suns001uw6atri87v54s",
  "email": {
    "id": "cm4t1sseg004tje7982991nan",
    "emailMessageId": "cm4ittv1v001oow9hruou8na8",
    "subject": "Subject of the email"
  },
  "contactIdentity": {
    "id": "cm4ittmhq0011ow9h6fb460yw",
    "email": "test@example.com",
    "userId": null
  }
}
```

#### email.clicked

Sent when a link in a campaign or workflow email is clicked.

Contains a `sourceType` and a related `campaignId` or `loopId` value, plus `contactIdentity` and `email` objects.

> [!NOTE]
>
> This event is not available for transactional emails because link clicks are
> [not tracked](https://loops.so/docs/transactional.md#metrics) in transactional emails.

```json theme={"dark"}
{
  "eventName": "email.clicked",
  "eventTime": 1734425918,
  "webhookSchemaVersion": "1.0.0",
  "sourceType": "campaign",
  "campaignId": "cm4t1suns001uw6atri87v54s",
  "email": {
    "id": "cm4t1sseg004tje7982991nan",
    "emailMessageId": "cm4ittv1v001oow9hruou8na8",
    "subject": "Subject of the email"
  },
  "contactIdentity": {
    "id": "cm4ittmhq0011ow9h6fb460yw",
    "email": "test@example.com",
    "userId": null
  }
}
```

#### email.unsubscribed

Sent when a recipient unsubscribes from marketing email or a mailing list using an email's "Unsubscribe" link.

A `contact.unsubscribed` or `contact.mailingList.unsubscribed` event will also be sent depending on whether the email was sent to a mailing list or not.

Contains a `sourceType` and a related `campaignId` or `loopId` value, plus `contactIdentity` and `email` objects.

> [!NOTE]
>
> This event is not available for transactional emails because unsubscribe links
> are [not included or required](https://loops.so/docs/types-of-emails.md#transactional) for
> transactional emails.

```json theme={"dark"}
{
  "eventName": "email.unsubscribed",
  "eventTime": 1734425918,
  "webhookSchemaVersion": "1.0.0",
  "sourceType": "campaign",
  "campaignId": "cm4t1suns001uw6atri87v54s",
  "email": {
    "id": "cm4t1sseg004tje7982991nan",
    "emailMessageId": "cm4ittv1v001oow9hruou8na8",
    "subject": "Subject of the email"
  },
  "contactIdentity": {
    "id": "cm4ittmhq0011ow9h6fb460yw",
    "email": "test@example.com",
    "userId": null
  }
}
```

#### email.resubscribed

Sent when a recipient resubscribes to marketing email from an email's preference center ("Unsubscribe" link).

Contains a `sourceType` and a related `campaignId` or `loopId` value, plus `contactIdentity` and `email` objects.

> [!NOTE]
>
> This event is not available for transactional emails because unsubscribe links
> are [not included or required](https://loops.so/docs/types-of-emails.md#transactional) for
> transactional emails.

```json theme={"dark"}
{
  "eventName": "email.resubscribed",
  "eventTime": 1734425918,
  "webhookSchemaVersion": "1.0.0",
  "sourceType": "campaign",
  "campaignId": "cm4t1suns001uw6atri87v54s",
  "email": {
    "id": "cm4t1sseg004tje7982991nan",
    "emailMessageId": "cm4ittv1v001oow9hruou8na8",
    "subject": "Subject of the email"
  },
  "contactIdentity": {
    "id": "cm4ittmhq0011ow9h6fb460yw",
    "email": "test@example.com",
    "userId": null
  }
}
```

#### email.spamReported

Sent when a recipient reports your email as spam.

Contains a `sourceType` and a related `campaignId` / `transactionalId` / `loopId` value, plus `contactIdentity` and `email` objects.

```json theme={"dark"}
{
  "eventName": "email.spamReported",
  "eventTime": 1734425918,
  "webhookSchemaVersion": "1.0.0",
  "sourceType": "campaign",
  "campaignId": "cm4t1suns001uw6atri87v54s",
  "email": {
    "id": "cm4t1sseg004tje7982991nan",
    "emailMessageId": "cm4ittv1v001oow9hruou8na8",
    "subject": "Subject of the email"
  },
  "contactIdentity": {
    "id": "cm4ittmhq0011ow9h6fb460yw",
    "email": "test@example.com",
    "userId": null
  }
}
```

### Testing

#### testing.testEvent

This is a test event that can be triggered at any time from the [Webhooks settings page](https://app.loops.so/settings?page=webhooks) in Loops.

```json theme={"dark"}
{
  "eventName": "testing.testEvent",
  "eventTime": 1734425918,
  "message": "test",
  "webhookSchemaVersion": "1.0.0"
}
```


## Related topics

- [Incoming webhooks](https://loops.so/docs/integrations/incoming-webhooks.md)
- [Clerk integration](https://loops.so/docs/integrations/clerk.md)
- [Clay integration](https://loops.so/docs/integrations/clay.md)
- [Supabase - Sync contacts](https://loops.so/docs/integrations/supabase/contact-sync.md)
- [Polar](https://loops.so/docs/integrations/polar.md)
