Webhooks is currently in Alpha, please let the team know via a suppport channel if you’d like early access.

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

Set up webhooks

Go to Settings -> 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.

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.

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:

import { NextRequest } from 'next/server';
import crypto from 'crypto';

interface WebhookVerificationError extends Error {
  code: 'MISSING_HEADERS' | 'MISSING_SECRET' | 'INVALID_SIGNATURE' | 'VERIFICATION_FAILED';
}

/**
 * Verifies a webhook request from Loops
 * @param req The incoming Next.js request
 * @throws {WebhookVerificationError} If verification fails
 * @returns {Promise<boolean>} True if verification succeeds
 */
async function verifyWebhook(req: NextRequest): Promise<boolean> {
  try {

    // Get the webhook-related headers directly from req.headers
    // Next.js automatically lowercases header names
    const eventId = req.headers.get('webhook-id');
    const timestamp = req.headers.get('webhook-timestamp');
    const webhookSignature = req.headers.get('webhook-signature');

    // Verify required headers are present
    if (!eventId || !timestamp || !webhookSignature) {
      const error = new Error('Missing required webhook header') as WebhookVerificationError;
      error.code = 'MISSING_HEADERS';
      throw error;
    }
    
    // Read raw body as buffer
    const readable = req.read();
    const buffer = Buffer.from(readable);
    const rawBodyText = buffer.toString();

    const signedContent = `${eventId}.${timestamp}.${rawBodyText}`;

    // Verify secret exists
    const secret = process.env.LOOPS_SIGNING_SECRET;
    if (!secret) {
      const error = new Error(
        'Missing LOOPS_SIGNING_SECRET environment variable'
      ) as WebhookVerificationError;
      error.code = 'MISSING_SECRET';
      throw error;
    }
    // Create a signature from the request data
    const secretBytes = Buffer.from(secret.split('_')[1], 'base64');
    const signature = crypto
      .createHmac('sha256', secretBytes)
      .update(signedContent)
      .digest('base64');

    // Check if the signature matches
    const signatureFound = webhookSignature
      .split(' ')
      .some((sig) => sig.includes(`,${signature}`));

    if (!signatureFound) {
      const error = new Error('Invalid signature') as WebhookVerificationError;
      error.code = 'INVALID_SIGNATURE';
      throw error;
    }

    return true;
  } catch (error) {
    if ((error as WebhookVerificationError).code) {
      throw error;
    }
    const wrappedError = new Error(
      `Webhook verification failed: ${(error as Error).message}`
    ) as WebhookVerificationError;
    wrappedError.code = 'VERIFICATION_FAILED';
    throw wrappedError;
  }
}

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.

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.

Clicking on an event in the table will reveal the response from your endpoint, which is helpful if there have been any errors.

Event data

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

eventName

The event type. See a full list of events below.

webhookSchemaVersion

Will be 1.0.0 for all events.

eventTime

Unix timestamp of the time the event occured in Loops.


Depending on the context of the event, more data will also be included. Full examples are shown in the 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 (list of mailingList objects. See below)
  • plus any custom contact properties

This object is the same as the data returned in the Find a contact API endpoint.

contactIdentity

A contact’s identifiers. To retrieve the full contact, use the Find a contact API endpoint.

Contains:

  • id
  • email
  • userId (nullable string)

email

Details about an individual email send to a recipient:

  • id - The unqiue ID of the email.
  • emailMessageId - The ID of the sent version of the campaign/loop/transactional email.
  • subject - The subject of the sent version of the campaign/loop/transactional email.

To get the ID of the campaign, loop 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 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.


Headers

Headers will include:

  • Webhook-Signature - A list of request signatures, which can be used to verify the request.
  • 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.

{
  "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,
    "favoriteColor": "blue",
    "favoriteNumber": 42,
    "mailingLists": [
      {
        "id": "cm4ittp2k000l12j3lgrzvlxt",
        "name": "test mailing list",
        "description": null,
        "isPublic": true
      }
    ]
  }
}

contact.unsubscribed

Sent when

  • a contact is unsubscribed from your audience.
  • a contact is deleted from your audience (alongside contact.deleted).

This is not the same as a contact unsubscribing from a mailing list.
See contact.mailingList.unsubscribed).

Contains a contactIdentity object.

{
  "eventName": "contact.unsubscribed",
  "eventTime": 1734425918,
  "webhookSchemaVersion": "1.0.0",
  "contactIdentity": {
    "id": "cm4ittmhq0011ow9h6fb460yw",
    "email": "test+1@loops.so",
    "userId": null
  }
}

contact.deleted

Sent when a contact is deleted from your audience.

Contains a contactIdentity object.

{
  "eventName": "contact.deleted",
  "eventTime": 1734425918,
  "webhookSchemaVersion": "1.0.0",
  "contactIdentity": {
    "id": "cm4ittmhq0011ow9h6fb460yw",
    "email": "test+1@loops.so",
    "userId": null
  }
}

contact.mailingList.subscribed

Sent when a contact is subscribed to a mailing list.

Contains contactIdentity and mailingList objects.

{
  "eventName": "contact.mailingList.subscribed",
  "eventTime": 1734425918,
  "webhookSchemaVersion": "1.0.0",
  "contactIdentity": {
    "id": "cm4ittmhq0011ow9h6fb460yw",
    "email": "test+1@loops.so",
    "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.

This is not the same as a contact unsubscribing from your audience.
See contact.unsubscribed.

Contains contactIdentity and mailingList objects.

{
  "eventName": "contact.mailingList.unsubscribed",
  "eventTime": 1734425918,
  "webhookSchemaVersion": "1.0.0",
  "contactIdentity": {
    "id": "cm4ittmhq0011ow9h6fb460yw",
    "email": "test+1@loops.so",
    "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.

This event will fire for every campaign send. If you send a campaign to 1,000 contacts, you will receive 1,000 events.

Contains a campaignId value plus contactIdentity and email objects.

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

{
  "eventName": "campaign.email.sent",
  "eventTime": 1734425918,
  "webhookSchemaVersion": "1.0.0",
  "contactIdentity": {
    "id": "cm4ittmhq0011ow9h6fb460yw",
    "email": "test+1@loops.so",
    "userId": null
  },
  "campaignId": "cm4t1suns001uw6atri87v54s",
  "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 loop email is sent to a contact.

This event will fire for every contact in a loop. If 1,000 contacts get sent emails from your loop, you will receive 1,000 events.

Contains a loopId value plus contactIdentity and email objects.

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

{
  "eventName": "loop.email.sent",
  "eventTime": 1734425918,
  "webhookSchemaVersion": "1.0.0",
  "contactIdentity": {
    "id": "cm4ittmhq0011ow9h6fb460yw",
    "email": "test+1@loops.so",
    "userId": null
  },
  "loopId": "cm4t1snfj0052icemfshgqfcw",
  "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 a transactionalId value plus contactIdentity and email objects.

{
  "eventName": "transactional.email.sent",
  "eventTime": 1734425918,
  "webhookSchemaVersion": "1.0.0",
  "contactIdentity": {
    "id": "cm4ittmhq0011ow9h6fb460yw",
    "email": "test+1@loops.so",
    "userId": null
  },
  "transactionalId": "cm4t1suns001uw6atri87v54s",
  "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.

{
  "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+1@loops.so",
    "userId": null
  }
}

email.softBounced

Sent when an email soft bounces.

Soft bounces are temporary email delivery failures, for example a connection timing out. Soft bounces are retried multiple times and some times the email is delivered.

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

{
  "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+1@loops.so",
    "userId": null
  }
}

email.hardBounced

Sent when an email hard bounces.

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.

{
  "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+1@loops.so",
    "userId": null
  }
}

email.opened

Sent when an email is opened.

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

{
  "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+1@loops.so",
    "userId": null
  }
}

email.clicked

Sent when a link in an email is clicked.

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

{
  "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+1@loops.so",
    "userId": null
  }
}

email.unsubscribed

Sent when a recipient clicks on an unsubscribe link in an email.

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 / transactionalId / loopId value, plus contactIdentity and email objects.

{
  "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+1@loops.so",
    "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.

{
  "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+1@loops.so",
    "userId": null
  }
}

Testing

testing.testEvent

This is a test event that can be triggered at any time from the Webhooks settings page in Loops.

{
  "eventName": "testing.testEvent",
  "eventTime": 1734425918,
  "message": "test",
  "webhookSchemaVersion": "1.0.0"
}

Was this page helpful?