PHPackages                             loops-so/loops - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [API Development](/categories/api)
4. /
5. loops-so/loops

ActiveLibrary[API Development](/categories/api)

loops-so/loops
==============

The official Loops PHP client.

v3.1.1(1mo ago)129.1k↓20.5%1MITPHPPHP ^8.1

Since Mar 24Pushed 6d agoCompare

[ Source](https://github.com/Loops-so/loops-php)[ Packagist](https://packagist.org/packages/loops-so/loops)[ Docs](https://github.com/Loops-so/loops-php)[ RSS](/packages/loops-so-loops/feed)WikiDiscussions main Synced 2w ago

READMEChangelog (9)Dependencies (6)Versions (14)Used By (0)

Loops PHP Package
=================

[](#loops-php-package)

[![Packagist Total Downloads](https://camo.githubusercontent.com/438832da5480bab52398dfe646dc35e0383b292e969969b73cf120bea3d7b2d3/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f6c6f6f70732d736f2f6c6f6f70733f7374796c653d736f6369616c)](https://packagist.org/packages/loops-so/loops)

Introduction
------------

[](#introduction)

This is the official PHP package for [Loops](https://loops.so), an email platform for modern software companies.

Installation
------------

[](#installation)

Install the Loops package [using Composer](https://packagist.org/packages/loops-so/loops):

```
composer require loops-so/loops
```

Requires PHP 8.1.

Usage
-----

[](#usage)

You will need a Loops API key to use the package.

In your Loops account, go to the [API Settings page](https://app.loops.so/settings?page=api) and click **Generate key**.

Copy this key and save it in your application code (for example, in an environment variable).

See the API documentation to learn more about [rate limiting](https://loops.so/docs/api-reference#rate-limiting) and [error handling](https://loops.so/docs/api-reference#debugging).

To use the package, first initialise the client with your API key, then you can call one of the methods.

API rate limits can be handled with the included `RateLimitExceededError` exception.

```
use Loops\LoopsClient;

$loops = new LoopsClient(env('LOOPS_API_KEY'));

// Test API key
$result = $loops->apiKey->test();

// Create a contact and catch errors
try {
    $result = $loops->contacts->create('user@example.com', [
      'firstName' => 'John'
    ]);
} catch (Loops\Exceptions\APIError $e) {
    // Handle API errors (400, 401, 403, etc)
    echo $e->getMessage();
    $returnedJson = $e->getJson(); // JSON returned by the API
    $statusCode = $e->getStatusCode(); // HTTP status code from the response
} catch (\Exception $e) {
    // Handle any other unexpected errors
    echo "Unexpected error: " . $e->getMessage();
}
```

Handling rate limits
--------------------

[](#handling-rate-limits)

You can use the check for rate limit issues with your requests.

You can access details about the rate limits from the `getLimit` and `getRemaining` functions.

```
try {
    $result = $loops->contacts->create('user@example.com', [
      'firstName' => 'John'
    ]);
} catch (Loops\Exceptions\RateLimitExceededError $e) {
    // Handle rate limiting
    echo "Rate limit hit. Limit: " . $e->getLimit() . ", requests remaining: " . $e->getRemaining();
} catch (Loops\Exceptions\APIError $e) {
    // Handle API errors (400, 401, 403, etc)
    echo $e->getMessage();
} catch (\Exception $e) {
    // Handle any other unexpected errors
    echo "Unexpected error: " . $e->getMessage();
}
```

Default contact properties
--------------------------

[](#default-contact-properties)

Each contact in Loops has a set of default properties. These will always be returned in API results.

- `id`
- `email`
- `firstName`
- `lastName`
- `source`
- `subscribed`
- `userGroup`
- `userId`
- `optInStatus`

Custom contact properties
-------------------------

[](#custom-contact-properties)

You can use custom contact properties in API calls. Please make sure to [add custom properties](https://loops.so/docs/contacts/properties#custom-contact-properties) in your Loops account before using them with the SDK.

Methods
-------

[](#methods)

- [apiKey-&gt;test()](#apikey-test)
- [contacts-&gt;create()](#contacts-create)
- [contacts-&gt;update()](#contacts-update)
- [contacts-&gt;find()](#contacts-find)
- [contacts-&gt;delete()](#contacts-delete)
- [contacts-&gt;checkSuppression()](#contacts-checksuppression)
- [contacts-&gt;removeSuppression()](#contacts-removesuppression)
- [contactProperties-&gt;create()](#contactproperties-create)
- [contactProperties-&gt;list()](#contactproperties-list)
- [mailingLists-&gt;list()](#mailinglists-list)
- [events-&gt;send()](#events-send)
- [transactional-&gt;send()](#transactional-send)
- [transactional-&gt;list()](#transactional-list)
- [transactional-&gt;create()](#transactional-create)
- [transactional-&gt;get()](#transactional-get)
- [transactional-&gt;update()](#transactional-update)
- [transactional-&gt;ensureDraft()](#transactional-ensuredraft)
- [transactional-&gt;publish()](#transactional-publish)
- [dedicatedSendingIps-&gt;list()](#dedicatedsendingips-list)
- [themes-&gt;list()](#themes-list)
- [themes-&gt;get()](#themes-get)
- [themes-&gt;create()](#themes-create)
- [themes-&gt;update()](#themes-update)
- [components-&gt;list()](#components-list)
- [components-&gt;get()](#components-get)
- [components-&gt;create()](#components-create)
- [components-&gt;update()](#components-update)
- [campaigns-&gt;list()](#campaigns-list)
- [campaigns-&gt;create()](#campaigns-create)
- [campaigns-&gt;get()](#campaigns-get)
- [campaigns-&gt;update()](#campaigns-update)
- [campaignGroups-&gt;list()](#campaigngroups-list)
- [campaignGroups-&gt;create()](#campaigngroups-create)
- [campaignGroups-&gt;get()](#campaigngroups-get)
- [campaignGroups-&gt;update()](#campaigngroups-update)
- [audienceSegments-&gt;list()](#audiencesegments-list)
- [audienceSegments-&gt;create()](#audiencesegments-create)
- [audienceSegments-&gt;get()](#audiencesegments-get)
- [workflows-&gt;list()](#workflows-list)
- [workflows-&gt;create()](#workflows-create)
- [workflows-&gt;get()](#workflows-get)
- [workflows-&gt;update()](#workflows-update)
- [workflows-&gt;changeMailingList()](#workflows-changemailinglist)
- [workflows-&gt;getNode()](#workflows-getnode)
- [workflows-&gt;createNode()](#workflows-createnode)
- [workflows-&gt;updateNode()](#workflows-updatenode)
- [workflows-&gt;deleteNode()](#workflows-deletenode)
- [workflows-&gt;addBranch()](#workflows-addbranch)
- [workflows-&gt;rerouteNode()](#workflows-reroutenode)
- [workflows-&gt;deleteNodeRecursive()](#workflows-deletenoderecursive)
- [eventPatterns-&gt;list()](#eventpatterns-list)
- [eventPatterns-&gt;get()](#eventpatterns-get)
- [eventPatterns-&gt;getByName()](#eventpatterns-getbyname)
- [emailMessages-&gt;get()](#emailmessages-get)
- [emailMessages-&gt;update()](#emailmessages-update)
- [emailMessages-&gt;preview()](#emailmessages-preview)
- [emailMessages-&gt;guardian()](#emailmessages-guardian)
- [transactionalGroups-&gt;list()](#transactionalgroups-list)
- [transactionalGroups-&gt;create()](#transactionalgroups-create)
- [transactionalGroups-&gt;get()](#transactionalgroups-get)
- [transactionalGroups-&gt;update()](#transactionalgroups-update)
- [uploads-&gt;upload()](#uploads-upload)

---

### apiKey-&gt;test()

[](#apikey-test)

Test if your API key is valid.

[API Reference](https://loops.so/docs/api-reference/api-key)

#### Parameters

[](#parameters)

None

#### Example

[](#example)

```
$result = $loops->apiKey->test();
```

#### Response

[](#response)

This method will return a success or error message:

```
{
  "success": true,
  "teamName": "Company name"
}
```

```
{
  "error": "Invalid API key"
}
```

---

### contacts-&gt;create()

[](#contacts-create)

Create a new contact.

[API Reference](https://loops.so/docs/api-reference/create-contact)

#### Parameters

[](#parameters-1)

NameTypeRequiredNotes`$email`stringYesIf a contact already exists with this email address, an `APIError` will be thrown.`$properties`arrayNoAn array containing default and any custom properties for your contact.
Please [add custom properties](https://loops.so/docs/contacts/properties#custom-contact-properties) in your Loops account before using them with the SDK.
Values can be of type `string`, `number`, `null` (to reset a value), `boolean` or `date` ([see allowed date formats](https://loops.so/docs/contacts/properties#dates)).`$mailing_lists`arrayNoAn array of mailing list IDs and boolean subscription statuses.#### Examples

[](#examples)

```
$result = $loops->contacts->create("hello@gmail.com");

$contact_properties = [
  'firstName' => "Bob" /* Default property */,
  'favoriteColor' => "Red" /* Custom property */,
];
$mailing_lists = [
  'cm06f5v0e45nf0ml5754o9cix' => TRUE,
  'cm16k73gq014h0mmj5b6jdi9r' => FALSE,
];
$result = $loops->contacts->create(
  email: "hello@gmail.com",
  properties: $contact_properties,
  mailing_lists: $mailing_lists
);
```

#### Response

[](#response-1)

```
{
  "success": true,
  "id": "cll6b3i8901a9jx0oyktl2m4u"
}
```

Error handling is done through the `APIError` class, which provides `getStatusCode()` and `getJson()` methods for retrieving the API's error response details. For implementation examples, see the [Usage section](#usage).

```
// HTTP 400 Bad Request
{
  "success": false,
  "message": "An error message here."
}
```

---

### contacts-&gt;update()

[](#contacts-update)

Update a contact.

Note: To update a contact's email address, the contact requires a `$user_id` value. Then you can make a request with their `$user_id` and an updated email address.

[API Reference](https://loops.so/docs/api-reference/update-contact)

#### Parameters

[](#parameters-2)

NameTypeRequiredNotes`$email`stringNoThe email address of the contact to update. If there is no contact with this email address, a new contact will be created using the email and properties in this request. Required if `$user_id` is not present.`$user_id`stringNoThe contact's unique user ID. If you use `$user_id` without `$email`, this value must have already been added to your contact in Loops. Required if `$email` is not present.`$properties`arrayNoAn array containing default and any custom properties for your contact.
Please [add custom properties](https://loops.so/docs/contacts/properties#custom-contact-properties) in your Loops account before using them with the SDK.
Values can be of type `string`, `number`, `null` (to reset a value), `boolean` or `date` ([see allowed date formats](https://loops.so/docs/contacts/properties#dates)).`$mailing_lists`arrayNoAn array of mailing list IDs and boolean subscription statuses.#### Example

[](#example-1)

```
$result = $loops->contacts->update(
  email: 'hello@gmail.com',
  properties: [
    'firstName' => 'Bob', /* Default property */
    'favoriteColor' => 'Blue' /* Custom property */
  ]
);

// Updating a contact's email address using $user_id
$result = $loops->contacts->update(
  user_id: '1234',
  email: 'newemail@gmail.com'
);

// Subscribe a contact to a mailing list
$result = $loops->contacts->update(
  email: 'hello@gmail.com',
  mailing_lists: [
    'cm06f5v0e45nf0ml5754o9cix' => true /* Subscribe */,
    'cm16k73gq014h0mmj5b6jdi9r' => false /* Unsubscribe */
  ]
);
```

#### Response

[](#response-2)

```
{
  "success": true,
  "id": "cll6b3i8901a9jx0oyktl2m4u"
}
```

Error handling is done through the `APIError` class, which provides `getStatusCode()` and `getJson()` methods for retrieving the API's error response details. For implementation examples, see the [Usage section](#usage).

```
// HTTP 400 Bad Request
{
  "success": false,
  "message": "An error message here."
}
```

---

### contacts-&gt;find()

[](#contacts-find)

Find a contact.

[API Reference](https://loops.so/docs/api-reference/find-contact)

#### Parameters

[](#parameters-3)

You must use one parameter in the request.

NameTypeRequiredNotes`$email`stringNo`$user_id`stringNo#### Examples

[](#examples-1)

```
$result = $loops->contacts->find(email: 'hello@gmail.com');

$result = $loops->contacts->find(user_id: '12345');
```

#### Response

[](#response-3)

This method will return a list containing a single contact object, which will include all default properties and any custom properties.

If no contact is found, an empty list will be returned.

```
[
  {
    "id": "cll6b3i8901a9jx0oyktl2m4u",
    "email": "hello@gmail.com",
    "firstName": "Bob",
    "lastName": null,
    "source": "API",
    "subscribed": true,
    "userGroup": "",
    "userId": "12345",
    "mailingLists": {
      "cm06f5v0e45nf0ml5754o9cix": true
    },
    "optInStatus": null,
    "favoriteColor": "Blue" /* Custom property */
  }
]
```

---

### contacts-&gt;delete()

[](#contacts-delete)

Delete a contact.

[API Reference](https://loops.so/docs/api-reference/delete-contact)

#### Parameters

[](#parameters-4)

You must use one parameter in the request.

NameTypeRequiredNotes`$email`stringNo`$user_id`stringNo#### Example

[](#example-2)

```
$result = $loops->contacts->delete(email: 'hello@gmail.com')

$result = $loops->contacts->delete(user_id: '12345')
```

#### Response

[](#response-4)

```
{
  "success": true,
  "message": "Contact deleted."
}
```

Error handling is done through the `APIError` class, which provides `getStatusCode()` and `getJson()` methods for retrieving the API's error response details. For implementation examples, see the [Usage section](#usage).

```
// HTTP 400 Bad Reuqest
{
  "success": false,
  "message": "An error message here."
}
```

```
// HTTP 404 Not Found
{
  "success": false,
  "message": "An error message here."
}
```

---

### contacts-&gt;checkSuppression()

[](#contacts-checksuppression)

Check if a contact is currently suppressed.

[API Reference](https://loops.so/docs/api-reference/check-contact-suppression)

#### Parameters

[](#parameters-5)

You must use one parameter in the request.

NameTypeRequiredNotes`$email`stringNo`$user_id`stringNo#### Example

[](#example-3)

```
$result = $loops->contacts->checkSuppression(email: 'hello@gmail.com');

$result = $loops->contacts->checkSuppression(user_id: '12345');
```

#### Response

[](#response-5)

```
{
  "contact": {
    "id": "cll6b3i8901a9jx0oyktl2m4u",
    "email": "adam@loops.so",
    "userId": null
  },
  "isSuppressed": true,
  "removalQuota": {
    "limit": 100,
    "remaining": 10
  }
}
```

Error handling is done through the `APIError` class, which provides `getStatusCode()` and `getJson()` methods for retrieving the API's error response details. For implementation examples, see the [Usage section](#usage).

```
// HTTP 400 Bad Request
{
  "success": false,
  "message": "An email or userId is required."
}
```

```
// HTTP 404 Not Found
{
  "success": false,
  "message": "This contact was not found."
}
```

---

### contacts-&gt;removeSuppression()

[](#contacts-removesuppression)

Remove suppression for a contact.

[API Reference](https://loops.so/docs/api-reference/remove-contact-suppression)

#### Parameters

[](#parameters-6)

You must use one parameter in the request.

NameTypeRequiredNotes`$email`stringNo`$user_id`stringNo#### Example

[](#example-4)

```
$result = $loops->contacts->removeSuppression(email: 'hello@gmail.com');

$result = $loops->contacts->removeSuppression(user_id: '12345');
```

#### Response

[](#response-6)

```
{
  "success": true,
  "message": "Email removed from suppression list.",
  "removalQuota": {
    "limit": 100,
    "remaining": 4
  }
}
```

Error handling is done through the `APIError` class, which provides `getStatusCode()` and `getJson()` methods for retrieving the API's error response details. For implementation examples, see the [Usage section](#usage).

```
// HTTP 400 Bad Request
{
  "success": false,
  "message": "This contact is not suppressed."
}
```

```
// HTTP 404 Not Found
{
  "success": false,
  "message": "This contact was not found."
}
```

---

### contactProperties-&gt;create()

[](#contactproperties-create)

Create a new contact property.

[API Reference](https://loops.so/docs/api-reference/create-contact-property)

#### Parameters

[](#parameters-7)

NameTypeRequiredNotes`$name`stringYesThe name of the property. Should be in camelCase, like `planName` or `favouriteColor`.`$type`stringYesThe property's value type.
Can be one of `string`, `number`, `boolean` or `date`.#### Examples

[](#examples-2)

```
$result = $loops->contactProperties->create("planName", "string");
```

#### Response

[](#response-7)

```
{
  "success": true
}
```

Error handling is done through the `APIError` class, which provides `getStatusCode()` and `getJson()` methods for retrieving the API's error response details. For implementation examples, see the [Usage section](#usage).

```
// HTTP 400 Bad Request
{
  "success": false,
  "message": "An error message here."
}
```

---

### contactProperties-&gt;list()

[](#contactproperties-list)

Get a list of your account's contact properties.

[API Reference](https://loops.so/docs/api-reference/list-contact-properties)

#### Parameters

[](#parameters-8)

NameTypeRequiredNotes`$list`stringNoUse "custom" to retrieve only your account's custom properties.#### Example

[](#example-5)

```
$result = $loops->contactProperties->list();

$result = $loops->contactProperties->list(list: "custom");
```

#### Response

[](#response-8)

This method will return a list of contact property objects containing `key`, `label` and `type` attributes.

```
[
  {
    "key": "firstName",
    "label": "First Name",
    "type": "string"
  },
  {
    "key": "lastName",
    "label": "Last Name",
    "type": "string"
  },
  {
    "key": "email",
    "label": "Email",
    "type": "string"
  },
  {
    "key": "notes",
    "label": "Notes",
    "type": "string"
  },
  {
    "key": "source",
    "label": "Source",
    "type": "string"
  },
  {
    "key": "userGroup",
    "label": "User Group",
    "type": "string"
  },
  {
    "key": "userId",
    "label": "User Id",
    "type": "string"
  },
  {
    "key": "subscribed",
    "label": "Subscribed",
    "type": "boolean"
  },
  {
    "key": "createdAt",
    "label": "Created At",
    "type": "date"
  },
  {
    "key": "favoriteColor",
    "label": "Favorite Color",
    "type": "string"
  },
  {
    "key": "plan",
    "label": "Plan",
    "type": "string"
  }
]
```

---

### mailingLists-&gt;list()

[](#mailinglists-list)

Get a list of your account's mailing lists. [Read more about mailing lists](https://loops.so/docs/contacts/mailing-lists)

[API Reference](https://loops.so/docs/api-reference/list-mailing-lists)

#### Parameters

[](#parameters-9)

None

#### Example

[](#example-6)

```
$result = $loops->mailingLists->list();
```

#### Response

[](#response-9)

This method will return a list of mailing list objects containing `id`, `name`, `description` and `isPublic` attributes.

If your account has no mailing lists, an empty list will be returned.

```
[
  {
    "id": "cm06f5v0e45nf0ml5754o9cix",
    "name": "Main list",
    "description": "All customers.",
    "isPublic": true
  },
  {
    "id": "cm16k73gq014h0mmj5b6jdi9r",
    "name": "Investors",
    "description": null,
    "isPublic": false
  }
]
```

---

### events-&gt;send()

[](#events-send)

Send an event to trigger an email in Loops. [Read more about events](https://loops.so/docs/events)

[API Reference](https://loops.so/docs/api-reference/send-event)

#### Parameters

[](#parameters-10)

NameTypeRequiredNotes`$event_name`stringYes`$email`stringNoThe contact's email address. Required if `$user_id` is not present.`$user_id`stringNoThe contact's unique user ID. If you use `$user_id` without `$email`, this value must have already been added to your contact in Loops. Required if `$email` is not present.`$contact_properties`arrayNoAn array containing contact properties, which will be updated or added to the contact when the event is received.
Please [add custom properties](https://loops.so/docs/contacts/properties#custom-contact-properties) in your Loops account before using them with the SDK.
Values can be of type `string`, `number`, `null` (to reset a value), `boolean` or `date` ([see allowed date formats](https://loops.so/docs/contacts/properties#dates)).`$event_properties`arrayNoAn array containing event properties, which will be made available in emails that are triggered by this event.
Values can be of type `string`, `number`, `boolean` or `date` ([see allowed date formats](https://loops.so/docs/events/properties#important-information-about-event-properties)).`$mailing_lists`arrayNoAn array of mailing list IDs and boolean subscription statuses.`$headers`arrayNoAdditional headers to send with the request.#### Examples

[](#examples-3)

```
$result = $loops->events->send(
  event_name: 'signup',
  email: 'hello@gmail.com'
);

$result = $loops->events->send(
  event_name: 'signup',
  email: 'hello@gmail.com',
  event_properties: [
    'username' => 'user1234',
    'signupDate' => '2024-03-21T10:09:23Z'
  ],
  mailing_lists: [
    'cm06f5v0e45nf0ml5754o9cix' => true,
    'cm16k73gq014h0mmj5b6jdi9r' => false
  ]
;

# In this case with both email and user_id present, the system will look for a contact with either a
#  matching `email` or `user_id` value.
# If a contact is found for one of the values (e.g. `email`), the other value (e.g. `user_id`) will be updated.
# If a contact is not found, a new contact will be created using both `email` and `user_id` values.
# Any values added in `contact_properties` will also be updated on the contact.
$result = $loops->events->send(
  event_name: 'signup',
  email: 'hello@gmail.com',
  user_id: '1234567890',
  contact_properties: [
    'firstName' => 'Bob',
    'plan' => 'pro',
  }]
});

# Example with Idempotency-Key header
$result = $loops->events->send(
  event_name: 'signup',
  email: 'hello@gmail.com',
  headers: [
    'Idempotency-Key' => '550e8400-e29b-41d4-a716-446655440000'
  ]
);
```

#### Response

[](#response-10)

```
{
  "success": true
}
```

Error handling is done through the `APIError` class, which provides `getStatusCode()` and `getJson()` methods for retrieving the API's error response details. For implementation examples, see the [Usage section](#usage).

```
// HTTP 400 Bad Request
{
  "success": false,
  "message": "An error message here."
}
```

---

### transactional-&gt;send()

[](#transactional-send)

Send a transactional email to a contact. [Learn about sending transactional email](https://loops.so/docs/transactional/guide)

[API Reference](https://loops.so/docs/api-reference/send-transactional-email)

#### Parameters

[](#parameters-11)

NameTypeRequiredNotes`$transactional_id`stringYesThe ID of the transactional email to send.`$email`stringYesThe email address of the recipient.`$add_to_audience`booleanNoIf `true`, a contact will be created in your audience using the `$email` value (if a matching contact doesn’t already exist).`$data_variables`arrayNoAn array containing data as defined by the data variables added to the transactional email template.
Values can be of type `string` or `number`.`$attachments`array\[\]NoA list of attachments objects.
**Please note**: Attachments need to be enabled on your account before using them with the API. [Read more](https://loops.so/docs/transactional/attachments)`$attachments[]["filename"]`stringNoThe name of the file, shown in email clients.`$attachments[]["contentType"]`stringNoThe MIME type of the file.`$attachments[]["data"]`stringNoThe base64-encoded content of the file.`$headers`arrayNoAdditional headers to send with the request.#### Examples

[](#examples-4)

```
$result = $loops->transactional->send(
  transactional_id: 'clfq6dinn000yl70fgwwyp82l',
  email: 'hello@gmail.com',
  data_variables: [
    'loginUrl' => 'https://myapp.com/login/',
  ]
);

# Example with Idempotency-Key header
$result = $loops->transactional->send(
  transactional_id: 'clfq6dinn000yl70fgwwyp82l',
  email: 'hello@gmail.com',
  data_variables: [
    'loginUrl' => 'https://myapp.com/login/',
  ],
  headers: [
    'Idempotency-Key' => '550e8400-e29b-41d4-a716-446655440000'
  ]
);

# Please contact us to enable attachments on your account.
$result = $loops->transactional->send(
  transactional_id: 'clfq6dinn000yl70fgwwyp82l',
  email: 'hello@gmail.com',
  data_variables: [
    'loginUrl' => 'https://myapp.com/login/',
  ],
  attachments: [
    [
      'filename' => 'presentation.pdf',
      'contentType' => 'application/pdf',
      'data' => base64_encode(file_get_contents('path/to/presentation.pdf'))
    ]
  ]
);
```

#### Response

[](#response-11)

```
{
  "success": true
}
```

Error handling is done through the `APIError` class, which provides `getStatusCode()` and `getJson()` methods for retrieving the API's error response details. For implementation examples, see the [Usage section](#usage).

If there is a problem with the request, a descriptive error message will be returned:

```
// HTTP 400 Bad Request
{
  "success": false,
  "path": "dataVariables",
  "message": "There are required fields for this email. You need to include a 'dataVariables' object with the required fields."
}
```

```
// HTTP 400 Bad Request
{
  "success": false,
  "error": {
    "path": "dataVariables",
    "message": "Missing required fields: login_url"
  },
  "transactionalId": "clfq6dinn000yl70fgwwyp82l"
}
```

---

### transactional-&gt;list()

[](#transactional-list)

Get a paginated list of transactional emails, most recently created first.

[API Reference](https://loops.so/docs/api-reference/list-transactional-emails)

#### Parameters

[](#parameters-12)

NameTypeRequiredNotes`$per_page`integerNoHow many results to return per page. Must be between 10 and 50. Defaults to 20 if omitted.`$cursor`stringNoA cursor, to return a specific page of results. Cursors can be found from the `pagination.nextCursor` value in each response.#### Example

[](#example-7)

```
$result = $loops->transactional->list();

$result = $loops->transactional->list(per_page: 15);
```

#### Response

[](#response-12)

```
{
  "pagination": {
    "totalResults": 23,
    "returnedResults": 20,
    "perPage": 20,
    "totalPages": 2,
    "nextCursor": "clyo0q4wo01p59fsecyxqsh38",
    "nextPage": "https://app.loops.so/api/v1/transactional-emails?cursor=clyo0q4wo01p59fsecyxqsh38&perPage=20"
  },
  "data": [
    {
      "id": "clfn0k1yg001imo0fdeqg30i8",
      "name": "Welcome email",
      "draftEmailMessageId": null,
      "publishedEmailMessageId": "cly8k3m0n0044jpx2bghepq45",
      "createdAt": "2023-11-06T17:48:07.249Z",
      "updatedAt": "2023-11-06T17:48:07.249Z",
      "dataVariables": []
    },
    {
      "id": "cll42l54f20i1la0lfooe3z12",
      "name": "Password reset",
      "draftEmailMessageId": "cla3r8s9t0422ua56iqovab01",
      "publishedEmailMessageId": "clb4s9t0u0533vb67jrpwbc12",
      "createdAt": "2025-01-15T10:00:00.000Z",
      "updatedAt": "2025-02-02T02:56:28.845Z",
      "dataVariables": [
        "confirmationUrl"
      ]
    },
    {
      "id": "clw6rbuwp01rmeiyndm80155l",
      "name": "Team invite",
      "draftEmailMessageId": "clc5t0u1v0644wc78ksqxcd23",
      "publishedEmailMessageId": null,
      "createdAt": "2024-05-14T19:02:52.000Z",
      "updatedAt": "2024-05-14T19:02:52.000Z",
      "dataVariables": [
        "firstName",
        "lastName",
        "inviteLink"
      ]
    },
    ...
  ]
}
```

---

### transactional-&gt;create()

[](#transactional-create)

Create a new transactional email. An empty draft email message is created automatically.

[API Reference](https://loops.so/docs/api-reference/create-transactional-email)

#### Parameters

[](#parameters-13)

NameTypeRequiredNotes`$name`stringYesThe name of the transactional email.`$transactional_group_id`stringNoThe ID of the group to add this transactional email to. Defaults to the team's default group when omitted.#### Example

[](#example-8)

```
$result = $loops->transactional->create(name: 'Welcome email');
```

#### Response

[](#response-13)

```
{
  "id": "clfq6dinn000yl70fgwwyp82l",
  "name": "Welcome email",
  "draftEmailMessageId": "cly8k3m0n0044jpx2bghepq45",
  "draftEmailMessageContentRevisionId": "clm9n4o6p0088lrz4dijslt67",
  "publishedEmailMessageId": null,
  "createdAt": "2025-01-01T00:00:00.000Z",
  "updatedAt": "2025-01-01T00:00:00.000Z",
  "dataVariables": []
}
```

---

### transactional-&gt;get()

[](#transactional-get)

Get a single transactional email by ID.

[API Reference](https://loops.so/docs/api-reference/get-transactional-email)

#### Parameters

[](#parameters-14)

NameTypeRequiredNotes`$transactional_id`stringYesThe ID of the transactional email.#### Example

[](#example-9)

```
$result = $loops->transactional->get(transactional_id: 'clfq6dinn000yl70fgwwyp82l');
```

---

### transactional-&gt;update()

[](#transactional-update)

Update a transactional email.

At least one field alongside `transactional_id` must be provided.

[API Reference](https://loops.so/docs/api-reference/update-transactional-email)

#### Parameters

[](#parameters-15)

NameTypeRequiredNotes`$transactional_id`stringYesThe ID of the transactional email.`$name`stringNoThe updated name.`$transactional_group_id`stringNoThe ID of the group to move this transactional email to.#### Example

[](#example-10)

```
$result = $loops->transactional->update(
  transactional_id: 'clfq6dinn000yl70fgwwyp82l',
  name: 'Updated welcome email'
);
```

---

### transactional-&gt;ensureDraft()

[](#transactional-ensuredraft)

Ensure a transactional email has a draft email message. If a draft already exists it is returned unchanged; otherwise a new empty draft is created.

[API Reference](https://loops.so/docs/api-reference/ensure-transactional-email-draft)

#### Parameters

[](#parameters-16)

NameTypeRequiredNotes`$transactional_id`stringYesThe ID of the transactional email.#### Example

[](#example-11)

```
$result = $loops->transactional->ensureDraft(transactional_id: 'clfq6dinn000yl70fgwwyp82l');
```

---

### transactional-&gt;publish()

[](#transactional-publish)

Publish the transactional email's current draft email message.

[API Reference](https://loops.so/docs/api-reference/publish-transactional-email)

#### Parameters

[](#parameters-17)

NameTypeRequiredNotes`$transactional_id`stringYesThe ID of the transactional email.#### Example

[](#example-12)

```
$result = $loops->transactional->publish(transactional_id: 'clfq6dinn000yl70fgwwyp82l');
```

---

### uploads-&gt;upload()

[](#uploads-upload)

Upload an image asset for use in LMX email content. The returned `finalUrl` can be used in an `` tag in your [LMX content](https://loops.so/docs/creating-emails/lmx).

[API Reference](https://loops.so/docs/api-reference/create-upload)

#### Parameters

[](#parameters-18)

NameTypeRequiredNotes`$path`stringYesPath to an image file. Supported types: JPEG, PNG, GIF, and WebP. Maximum file size is 4,000,000 bytes (4 MB).#### Example

[](#example-13)

```
$result = $loops->uploads->upload(path: '/path/to/image.png');

$imageUrl = $result['finalUrl'];
```

#### Response

[](#response-14)

```
{
  "emailAssetId": "clu1v4w6x0254tz42lrcwat45",
  "finalUrl": "https://cdn.example.com/image.png"
}
```

---

### dedicatedSendingIps-&gt;list()

[](#dedicatedsendingips-list)

Get a list of Loops' dedicated sending IP addresses.

[API Reference](https://loops.so/docs/api-reference/get-dedicated-sending-ips)

#### Parameters

[](#parameters-19)

None

#### Example

[](#example-14)

```
$result = $loops->dedicatedSendingIps->list();
```

#### Response

[](#response-15)

```
["1.2.3.4", "5.6.7.8"]
```

---

### themes-&gt;list()

[](#themes-list)

List email themes.

[API Reference](https://loops.so/docs/api-reference/list-themes)

#### Parameters

[](#parameters-20)

NameTypeRequiredNotes`$per_page`integerNoHow many results to return per page. Must be between 10 and 50. Defaults to 20 if omitted.`$cursor`stringNoA cursor, to return a specific page of results. Cursors can be found from the `pagination.nextCursor` value in each response.#### Example

[](#example-15)

```
$result = $loops->themes->list();

$result = $loops->themes->list(per_page: 15, cursor: 'clyo0q4wo01p59fsecyxqsh38');
```

---

### themes-&gt;get()

[](#themes-get)

Get a single email theme by ID.

[API Reference](https://loops.so/docs/api-reference/get-theme)

#### Parameters

[](#parameters-21)

NameTypeRequiredNotes`$theme_id`stringYesThe ID of the theme.#### Example

[](#example-16)

```
$result = $loops->themes->get(theme_id: 'clo5p8q0r0132ntx6flkunw89');
```

---

### themes-&gt;create()

[](#themes-create)

Create an email theme.

[API Reference](https://loops.so/docs/api-reference/create-theme)

#### Parameters

[](#parameters-22)

NameTypeRequiredNotes`$name`stringYesThe theme name.`$styles`arrayNoStyle attributes matching LMX `` attribute names.#### Example

[](#example-17)

```
$result = $loops->themes->create(
  name: 'Dark mode',
  styles: ['backgroundColor' => '#111827', 'bodyColor' => '#1f2937']
);
```

---

### themes-&gt;update()

[](#themes-update)

Update a theme's name and/or styles. Style changes cascade to emails using the theme.

At least one of `$name` or `$styles` must be provided.

[API Reference](https://loops.so/docs/api-reference/update-theme)

#### Parameters

[](#parameters-23)

NameTypeRequiredNotes`$theme_id`stringYesThe ID of the theme.`$name`stringNoThe updated theme name.`$styles`arrayNoUpdated style attributes.#### Example

[](#example-18)

```
$result = $loops->themes->update(
  theme_id: 'clo5p8q0r0132ntx6flkunw89',
  name: 'Updated theme'
);
```

---

### components-&gt;list()

[](#components-list)

List email components.

[API Reference](https://loops.so/docs/api-reference/list-components)

#### Parameters

[](#parameters-24)

NameTypeRequiredNotes`$per_page`integerNoHow many results to return per page. Must be between 10 and 50. Defaults to 20 if omitted.`$cursor`stringNoA cursor, to return a specific page of results. Cursors can be found from the `pagination.nextCursor` value in each response.#### Example

[](#example-19)

```
$result = $loops->components->list();
```

---

### components-&gt;get()

[](#components-get)

Get a single email component by ID.

[API Reference](https://loops.so/docs/api-reference/get-component)

#### Parameters

[](#parameters-25)

NameTypeRequiredNotes`$component_id`stringYesThe ID of the component.#### Example

[](#example-20)

```
$result = $loops->components->get(component_id: 'clp6q9r1s0154ouy7gmlovx90');
```

---

### components-&gt;create()

[](#components-create)

Create an email component from an LMX body.

[API Reference](https://loops.so/docs/api-reference/create-component)

#### Parameters

[](#parameters-26)

NameTypeRequiredNotes`$name`stringYesThe component name.`$lmx`stringYesThe component body as LMX.#### Example

[](#example-21)

```
$result = $loops->components->create(
  name: 'Header',
  lmx: 'Welcome to Acme'
);
```

---

### components-&gt;update()

[](#components-update)

Update a component's name and/or LMX body. Body changes cascade to emails using the component.

At least one of `$name` or `$lmx` must be provided.

[API Reference](https://loops.so/docs/api-reference/update-component)

#### Parameters

[](#parameters-27)

NameTypeRequiredNotes`$component_id`stringYesThe ID of the component.`$name`stringNoThe updated component name.`$lmx`stringNoThe updated LMX body.#### Example

[](#example-22)

```
$result = $loops->components->update(
  component_id: 'clp6q9r1s0154ouy7gmlovx90',
  lmx: 'Updated header'
);
```

---

### campaigns-&gt;list()

[](#campaigns-list)

List campaigns.

[API Reference](https://loops.so/docs/api-reference/list-campaigns)

#### Parameters

[](#parameters-28)

NameTypeRequiredNotes`$per_page`integerNoHow many results to return per page. Must be between 10 and 50. Defaults to 20 if omitted.`$cursor`stringNoA cursor, to return a specific page of results. Cursors can be found from the `pagination.nextCursor` value in each response.#### Example

[](#example-23)

```
$result = $loops->campaigns->list();
```

---

### campaigns-&gt;create()

[](#campaigns-create)

Create a new draft campaign.

[API Reference](https://loops.so/docs/api-reference/create-campaign)

#### Parameters

[](#parameters-29)

NameTypeRequiredNotes`$name`stringYesThe campaign name.`$campaign_group_id`stringNoThe ID of the group to add this campaign to.`$mailing_list_id`stringNoThe ID of the mailing list to send to.`$audience_segment_id`stringNoThe ID of an audience segment. Setting this without also providing `audience_filter` clears any existing `audience_filter`. If both are provided, the filter is applied on top of the segment's filter.`$audience_filter`arrayNoA tree of audience conditions. See the API reference for the filter schema. Setting this without also providing `audience_segment_id` clears any existing `audience_segment_id`.`$scheduling`arrayNoWhen the campaign should send. Use `['method' => 'now']` or `['method' => 'schedule', 'timestamp' => '...']`.#### Example

[](#example-24)

```
$result = $loops->campaigns->create(name: 'Spring announcement');

$result = $loops->campaigns->create(
  name: 'Spring announcement',
  mailing_list_id: 'cm06f5v0e45nf0ml5754o9cix',
  scheduling: ['method' => 'schedule', 'timestamp' => '2026-06-01T10:00:00Z']
);
```

#### Response

[](#response-16)

```
{
  "success": true,
  "campaignId": "cln4o7p9q0110msw5ekjtmv78",
  "name": "Spring announcement",
  "status": "Draft",
  "createdAt": "2025-01-01T00:00:00.000Z",
  "updatedAt": "2025-01-01T00:00:00.000Z",
  "emailMessageId": "cly8k3m0n0044jpx2bghepq45",
  "emailMessageContentRevisionId": "clm9n4o6p0088lrz4dijslt67"
}
```

---

### campaigns-&gt;get()

[](#campaigns-get)

Get a single campaign by ID.

[API Reference](https://loops.so/docs/api-reference/get-campaign)

#### Parameters

[](#parameters-30)

NameTypeRequiredNotes`$campaign_id`stringYesThe ID of the campaign.#### Example

[](#example-25)

```
$result = $loops->campaigns->get(campaign_id: 'cln4o7p9q0110msw5ekjtmv78');
```

---

### campaigns-&gt;update()

[](#campaigns-update)

Update a draft campaign's name, group, audience, or scheduling.

At least one field must be provided.

[API Reference](https://loops.so/docs/api-reference/update-campaign)

#### Parameters

[](#parameters-31)

NameTypeRequiredNotes`$campaign_id`stringYesThe ID of the campaign.`$name`stringNoThe updated name.`$campaign_group_id`stringNoThe ID of the group to move this campaign to.`$scheduling`arrayNoWhen the campaign should send. Use `['method' => 'now']` or `['method' => 'schedule', 'timestamp' => '...']`.`$mailing_list_id`stringNoThe ID of the mailing list to send to. Pass `null` to clear.`$audience_segment_id`stringNoThe ID of an audience segment. Setting this without also providing `audience_filter` clears any existing `audience_filter`. If both are provided, the filter is applied on top of the segment's filter. Pass `null` to clear.`$audience_filter`arrayNoA tree of audience conditions. See the API reference for the filter schema. Setting this without also providing `audience_segment_id` clears any existing `audience_segment_id`. Pass `null` to clear.#### Example

[](#example-26)

```
$result = $loops->campaigns->update(
  campaign_id: 'cln4o7p9q0110msw5ekjtmv78',
  name: 'Updated name'
);

// Clear the mailing list audience target
$result = $loops->campaigns->update(
  campaign_id: 'cln4o7p9q0110msw5ekjtmv78',
  mailing_list_id: null
);
```

---

### campaignGroups-&gt;list()

[](#campaigngroups-list)

List campaign groups.

[API Reference](https://loops.so/docs/api-reference/list-campaign-groups)

#### Parameters

[](#parameters-32)

NameTypeRequiredNotes`$per_page`integerNoHow many results to return per page. Must be between 10 and 50. Defaults to 20 if omitted.`$cursor`stringNoA cursor, to return a specific page of results. Cursors can be found from the `pagination.nextCursor` value in each response.#### Example

[](#example-27)

```
$result = $loops->campaignGroups->list();
```

---

### campaignGroups-&gt;create()

[](#campaigngroups-create)

Create a campaign group.

[API Reference](https://loops.so/docs/api-reference/create-campaign-group)

#### Parameters

[](#parameters-33)

NameTypeRequiredNotes`$name`stringYesCannot be the reserved name "Unsorted".`$description`stringNoAn optional description for the group.#### Example

[](#example-28)

```
$result = $loops->campaignGroups->create(name: 'Newsletters', description: 'Monthly updates');
```

---

### campaignGroups-&gt;get()

[](#campaigngroups-get)

Get a campaign group by ID.

[API Reference](https://loops.so/docs/api-reference/get-campaign-group)

#### Parameters

[](#parameters-34)

NameTypeRequiredNotes`$campaign_group_id`stringYesThe ID of the campaign group.#### Example

[](#example-29)

```
$result = $loops->campaignGroups->get(campaign_group_id: 'clq7r0s2t0176pvz8hnmpwy01');
```

---

### campaignGroups-&gt;update()

[](#campaigngroups-update)

Update a campaign group's name or description.

At least one field alongside `campaign_group_id` must be provided.

[API Reference](https://loops.so/docs/api-reference/update-campaign-group)

#### Parameters

[](#parameters-35)

NameTypeRequiredNotes`$campaign_group_id`stringYesThe ID of the campaign group.`$name`stringNoCannot be the reserved name "Unsorted".`$description`stringNo#### Example

[](#example-30)

```
$result = $loops->campaignGroups->update(
  campaign_group_id: 'clq7r0s2t0176pvz8hnmpwy01',
  name: 'Updated name'
);
```

---

### audienceSegments-&gt;list()

[](#audiencesegments-list)

List audience segments.

[API Reference](https://loops.so/docs/api-reference/list-audience-segments)

#### Parameters

[](#parameters-36)

NameTypeRequiredNotes`$per_page`integerNoHow many results to return per page. Must be between 10 and 50. Defaults to 20 if omitted.`$cursor`stringNoA cursor, to return a specific page of results. Cursors can be found from the `pagination.nextCursor` value in each response.#### Example

[](#example-31)

```
$result = $loops->audienceSegments->list();
```

---

### audienceSegments-&gt;create()

[](#audiencesegments-create)

Create an audience segment.

[API Reference](https://loops.so/docs/api-reference/create-audience-segment)

#### Parameters

[](#parameters-37)

NameTypeRequiredNotes`$name`stringYesThe segment name. Must be unique within the team.`$filter`arrayYesA tree of audience conditions with `match` and `conditions`.`$description`stringNoAn optional description of the audience segment.#### Example

[](#example-32)

```
$result = $loops->audienceSegments->create(
  name: 'Power users',
  filter: [
    'match' => 'all',
    'conditions' => [
      [
        'type' => 'property',
        'key' => 'plan',
        'operator' => 'equals',
        'value' => 'pro',
      ],
    ],
  ],
  description: 'Contacts on the pro plan'
);
```

---

### audienceSegments-&gt;get()

[](#audiencesegments-get)

Get an audience segment by ID.

[API Reference](https://loops.so/docs/api-reference/get-audience-segment)

#### Parameters

[](#parameters-38)

NameTypeRequiredNotes`$audience_segment_id`stringYesThe ID of the audience segment.#### Example

[](#example-33)

```
$result = $loops->audienceSegments->get(audience_segment_id: 'clr8s1t3u0198qw09iotqzx12');
```

---

### workflows-&gt;list()

[](#workflows-list)

List workflows.

[API Reference](https://loops.so/docs/api-reference/list-workflows)

#### Parameters

[](#parameters-39)

NameTypeRequiredNotes`$per_page`integerNoHow many results to return per page. Must be between 10 and 50. Defaults to 20 if omitted.`$cursor`stringNoA cursor, to return a specific page of results. Cursors can be found from the `pagination.nextCursor` value in each response.#### Example

[](#example-34)

```
$result = $loops->workflows->list();
```

---

### workflows-&gt;create()

[](#workflows-create)

Create a draft workflow with a blank trigger and exit node.

[API Reference](https://loops.so/docs/api-reference/create-workflow)

#### Parameters

[](#parameters-40)

NameTypeRequiredNotes`$name`stringYesThe workflow name.`$description`stringNoThe workflow description.`$mailing_list_id`stringNoThe mailing list the workflow sends to.#### Example

[](#example-35)

```
$result = $loops->workflows->create(
  name: 'Welcome series',
  description: 'Onboarding emails for new signups'
);
```

---

### workflows-&gt;get()

[](#workflows-get)

Get a simplified workflow graph.

[API Reference](https://loops.so/docs/api-reference/get-workflow)

#### Parameters

[](#parameters-41)

NameTypeRequiredNotes`$workflow_id`stringYesThe ID of the workflow.#### Example

[](#example-36)

```
$result = $loops->workflows->get(workflow_id: 'cls9t2u4v0210rx20jpuary23');
```

---

### workflows-&gt;update()

[](#workflows-update)

Update a workflow's display properties.

At least one of `$name` or `$description` must be provided. To change the mailing list, use `workflows->changeMailingList()`.

[API Reference](https://loops.so/docs/api-reference/update-workflow)

#### Parameters

[](#parameters-42)

NameTypeRequiredNotes`$workflow_id`stringYesThe ID of the workflow.`$expected_revision_id`string|nullYesThe latest workflow revision token. Pass `null` for older workflows.`$name`stringNoThe updated workflow name.`$description`stringNoThe updated workflow description.#### Example

[](#example-37)

```
$result = $loops->workflows->update(
  workflow_id: 'cls9t2u4v0210rx20jpuary23',
  expected_revision_id: 'clrev1s10n2i3d4e5f6g7h8',
  name: 'Updated welcome series'
);
```

---

### workflows-&gt;changeMailingList()

[](#workflows-changemailinglist)

Dry run or apply a workflow mailing list change.

If queued contacts would be removed, the API returns `"status": "queuedContactsFound"`. Retry with `queued_contact_policy: "discard"` to apply the change.

[API Reference](https://loops.so/docs/api-reference/change-workflow-mailing-list)

#### Parameters

[](#parameters-43)

NameTypeRequiredNotes`$workflow_id`stringYesThe ID of the workflow.`$expected_revision_id`string|nullYesThe latest workflow revision token. Pass `null` for older workflows.`$mailing_list_id`stringYesThe mailing list to use. Pass `null` to clear.`$dry_run`booleanNoIf `true`, validate without modifying the workflow.`$queued_contact_policy`stringNo`fail` (default) or `discard`.#### Example

[](#example-38)

```
$result = $loops->workflows->changeMailingList(
  workflow_id: 'cls9t2u4v0210rx20jpuary23',
  expected_revision_id: 'clrev1s10n2i3d4e5f6g7h8',
  mailing_list_id: 'cm06f5v0e45nf0ml5754o9cix',
  dry_run: true
);
```

---

### workflows-&gt;getNode()

[](#workflows-getnode)

Get detailed data for a single workflow node.

[API Reference](https://loops.so/docs/api-reference/get-workflow-node)

#### Parameters

[](#parameters-44)

NameTypeRequiredNotes`$workflow_id`stringYesThe ID of the workflow.`$node_id`stringYesThe ID of the workflow node.#### Example

[](#example-39)

```
$result = $loops->workflows->getNode(
  workflow_id: 'cls9t2u4v0210rx20jpuary23',
  node_id: 'clt0u3v5w0232sy31kqvbzs34'
);
```

---

### workflows-&gt;createNode()

[](#workflows-createnode)

Create a new default workflow node.

Use `$insert_mode` to choose placement:

- `between` — insert between `$from_node_id` and `$to_node_id`
- `before` — insert before `$to_node_id`
- `after` — insert after `$from_node_id` (source must have exactly one outgoing connection)

[API Reference](https://loops.so/docs/api-reference/create-workflow-node)

#### Parameters

[](#parameters-45)

NameTypeRequiredNotes`$workflow_id`stringYesThe ID of the workflow.`$expected_revision_id`string|nullYesThe latest workflow revision token. Pass `null` for older workflows.`$insert_mode`stringYes`between`, `before`, or `after`.`$node_type_name`stringYesOne of `AudienceFilter`, `BranchNode`, `ExperimentBranchNode`, `TimerAction`, `SendEmailAction`, `VariantNode`.`$from_node_id`stringCond.Required for `between` and `after`. Not permitted for `before`.`$to_node_id`stringCond.Required for `between`. For `before`, provide this or `$before_node_id` (not both). Not permitted for `after`.`$before_node_id`stringCond.Deprecated. For `before`, provide this or `$to_node_id` (not both).#### Example

[](#example-40)

```
$result = $loops->workflows->createNode(
  workflow_id: 'cls9t2u4v0210rx20jpuary23',
  expected_revision_id: 'clrev1s10n2i3d4e5f6g7h8',
  insert_mode: 'between',
  node_type_name: 'TimerAction',
  from_node_id: 'clt0u3v5w0232sy31kqvbzs34',
  to_node_id: 'clt0u3v5w0232sy31kqvbzs35'
);

$result = $loops->workflows->createNode(
  workflow_id: 'cls9t2u4v0210rx20jpuary23',
  expected_revision_id: 'clrev1s10n2i3d4e5f6g7h8',
  insert_mode: 'after',
  node_type_name: 'TimerAction',
  from_node_id: 'clt0u3v5w0232sy31kqvbzs34'
);

$result = $loops->workflows->createNode(
  workflow_id: 'cls9t2u4v0210rx20jpuary23',
  expected_revision_id: 'clrev1s10n2i3d4e5f6g7h8',
  insert_mode: 'before',
  node_type_name: 'TimerAction',
  to_node_id: 'clt0u3v5w0232sy31kqvbzs35'
);
```

---

### workflows-&gt;updateNode()

[](#workflows-updatenode)

Update workflow-node-owned fields for a single node.

[API Reference](https://loops.so/docs/api-reference/update-workflow-node)

#### Parameters

[](#parameters-46)

NameTypeRequiredNotes`$workflow_id`stringYesThe ID of the workflow.`$node_id`stringYesThe ID of the workflow node.`$expected_revision_id`string|nullYesThe latest workflow revision token. Pass `null` for older workflows.`$payload`arrayYesNode-type-specific fields to update.#### Example

[](#example-41)

```
$result = $loops->workflows->updateNode(
  workflow_id: 'cls9t2u4v0210rx20jpuary23',
  node_id: 'clt0u3v5w0232sy31kqvbzs34',
  expected_revision_id: 'clrev1s10n2i3d4e5f6g7h8',
  payload: ['amount' => 2, 'unit' => 'd']
);
```

---

### workflows-&gt;deleteNode()

[](#workflows-deletenode)

Delete a single workflow node.

If contacts are queued at the node, the API returns `"status": "queuedContactsFound"`. Retry with `queued_contact_policy: "discard"` to delete.

[API Reference](https://loops.so/docs/api-reference/delete-workflow-node)

#### Parameters

[](#parameters-47)

NameTypeRequiredNotes`$workflow_id`stringYesThe ID of the workflow.`$node_id`stringYesThe ID of the workflow node.`$expected_revision_id`string|nullYesThe latest workflow revision token. Pass `null` for older workflows.`$dry_run`booleanNoIf `true`, validate without modifying the workflow.`$queued_contact_policy`stringNo`fail` (default) or `discard`.#### Example

[](#example-42)

```
$result = $loops->workflows->deleteNode(
  workflow_id: 'cls9t2u4v0210rx20jpuary23',
  node_id: 'clt0u3v5w0232sy31kqvbzs34',
  expected_revision_id: 'clrev1s10n2i3d4e5f6g7h8',
  dry_run: true
);
```

---

### workflows-&gt;addBranch()

[](#workflows-addbranch)

Add a branch and child node under an existing `BranchNode` or `ExperimentBranchNode`.

[API Reference](https://loops.so/docs/api-reference/add-workflow-branch)

#### Parameters

[](#parameters-48)

NameTypeRequiredNotes`$workflow_id`stringYesThe ID of the workflow.`$node_id`stringYesThe ID of the branch or experiment node.`$expected_revision_id`string|nullYesThe latest workflow revision token. Pass `null` for older workflows.#### Example

[](#example-43)

```
$result = $loops->workflows->addBranch(
  workflow_id: 'cls9t2u4v0210rx20jpuary23',
  node_id: 'clt0u3v5w0232sy31kqvbzs34',
  expected_revision_id: 'clrev1s10n2i3d4e5f6g7h8'
);
```

---

### workflows-&gt;rerouteNode()

[](#workflows-reroutenode)

Reroute a source node's outgoing connection to another valid target node. The source node must have exactly one outgoing connection. Branch and experiment branch nodes cannot be rerouted with this endpoint.

[API Reference](https://loops.so/docs/api-reference/reroute-node-connection)

#### Parameters

[](#parameters-49)

NameTypeRequiredNotes`$workflow_id`stringYesThe ID of the workflow.`$node_id`stringYesThe ID of the source workflow node.`$expected_revision_id`string|nullYesThe latest workflow revision token. Pass `null` for older workflows.`$new_target_node_id`stringYesThe node that should receive the connection.#### Example

[](#example-44)

```
$result = $loops->workflows->rerouteNode(
  workflow_id: 'cls9t2u4v0210rx20jpuary23',
  node_id: 'clt0u3v5w0232sy31kqvbzs34',
  expected_revision_id: 'clrev1s10n2i3d4e5f6g7h8',
  new_target_node_id: 'cln3c5d7e9f1g3h5i7j9k1l3'
);
```

---

### workflows-&gt;deleteNodeRecursive()

[](#workflows-deletenoderecursive)

Delete a node and its downstream subtree.

[API Reference](https://loops.so/docs/api-reference/delete-workflow-nodes)

#### Parameters

[](#parameters-50)

NameTypeRequiredNotes`$workflow_id`stringYesThe ID of the workflow.`$node_id`stringYesThe root node ID of the subtree to delete.`$expected_revision_id`string|nullYesThe latest workflow revision token. Pass `null` for older workflows.`$dry_run`booleanNoIf `true`, validate without modifying the workflow.`$queued_contact_policy`stringNo`fail` (default) or `discard`.#### Example

[](#example-45)

```
$result = $loops->workflows->deleteNodeRecursive(
  workflow_id: 'cls9t2u4v0210rx20jpuary23',
  node_id: 'clt0u3v5w0232sy31kqvbzs34',
  expected_revision_id: 'clrev1s10n2i3d4e5f6g7h8',
  dry_run: true
);
```

---

### eventPatterns-&gt;list()

[](#eventpatterns-list)

List event patterns available to workflow event trigger nodes.

[API Reference](https://loops.so/docs/api-reference/list-event-patterns)

#### Parameters

[](#parameters-51)

NameTypeRequiredNotes`$per_page`integerNoHow many results to return per page. Must be between 10 and 50. Defaults to 20 if omitted.`$cursor`stringNoA cursor, to return a specific page of results. Cursors can be found from the `pagination.nextCursor` value in each response.#### Example

[](#example-46)

```
$result = $loops->eventPatterns->list();
```

---

### eventPatterns-&gt;get()

[](#eventpatterns-get)

Get an event pattern by ID.

[API Reference](https://loops.so/docs/api-reference/get-event-pattern)

#### Parameters

[](#parameters-52)

NameTypeRequiredNotes`$event_pattern_id`stringYesThe ID of the event pattern.#### Example

[](#example-47)

```
$result = $loops->eventPatterns->get(event_pattern_id: 'cle1v2e3n4t5p6a7t8t9e0r1');
```

---

### eventPatterns-&gt;getByName()

[](#eventpatterns-getbyname)

Get an event pattern by event name. Event names are case-sensitive.

[API Reference](https://loops.so/docs/api-reference/get-event-pattern-by-name)

#### Parameters

[](#parameters-53)

NameTypeRequiredNotes`$event_name`stringYesThe exact event name.#### Example

[](#example-48)

```
$result = $loops->eventPatterns->getByName(event_name: 'signup');
```

---

### emailMessages-&gt;get()

[](#emailmessages-get)

Get an email message, including its compiled LMX content.

[API Reference](https://loops.so/docs/api-reference/get-email-message)

#### Parameters

[](#parameters-54)

NameTypeRequiredNotes`$email_message_id`stringYesThe ID of the email message.#### Example

[](#example-49)

```
$result = $loops->emailMessages->get(email_message_id: 'cly8k3m0n0044jpx2bghepq45');
```

---

### emailMessages-&gt;update()

[](#emailmessages-update)

Update an email message.

At least one field must be provided.

[API Reference](https://loops.so/docs/api-reference/update-email-message)

#### Parameters

[](#parameters-55)

NameTypeRequiredNotes`$email_message_id`stringYesThe ID of the email message.`$expected_revision_id`stringNoSupply a value matching the current `contentRevisionId` to avoid 409 conflicts.`$subject`stringNoThe email subject line.`$preview_text`stringNoThe email preview text.`$from_name`stringNoThe sender name.`$from_email`stringNoThe sender email address (the name before the `@`; your sending domain will be automatically appended).`$reply_to_email`stringNoThe reply-to email address.`$cc_email`stringNoCC email address. Requires the team to have CC/BCC enabled.`$bcc_email`stringNoBCC email address. Requires the team to have CC/BCC enabled.`$language_code`stringNoLanguage code for the email. Requires translation to be enabled for the team.`$email_format`stringNo`styled` or `plain`.`$lmx`stringNoThe LMX content for the email message.`$contact_properties_fallbacks`arrayNoContact property fallback values. Pass `null` as a value to remove an individual fallback entry.`$event_properties_fallbacks`arrayNoEvent property fallback values. Pass `null` as a value to remove an individual fallback entry.`$data_variables_fallbacks`arrayNoData variable fallback values. Pass `null` as a value to remove an individual fallback entry.#### Example

[](#example-50)

```
$result = $loops->emailMessages->update(
  email_message_id: 'cly8k3m0n0044jpx2bghepq45',
  expected_revision_id: 'clm9n4o6p0088lrz4dijslt67',
  subject: 'Updated subject',
  lmx: 'Hello'
);

// Example with contact property fallbacks
$result = $loops->emailMessages->update(
  email_message_id: 'cly8k3m0n0044jpx2bghepq45',
  contact_properties_fallbacks: [
    'firstName' => 'there',      // If firstName is missing, use "there"
    'company' => 'your company', // If company is missing, use "your company"
    'planName' => null           // null removes the fallback for "planName"
  ]
);
```

---

### emailMessages-&gt;preview()

[](#emailmessages-preview)

Send a test preview of an email message to one or more addresses.

[API Reference](https://loops.so/docs/api-reference/send-email-message-preview)

#### Parameters

[](#parameters-56)

NameTypeRequiredNotes`$email_message_id`stringYesThe ID of the email message.`$emails`arrayYesOne or more addresses to send the preview to.`$contact_properties`arrayNoContact property values to render. Accepted for campaign and workflow previews.`$event_properties`arrayNoEvent property values to render. Accepted for workflow previews only.`$data_variables`arrayNoTransactional data variables to render. Accepted for transactional previews only.#### Example

[](#example-51)

```
$result = $loops->emailMessages->preview(
  email_message_id: 'cly8k3m0n0044jpx2bghepq45',
  emails: ['test@example.com'],
  contact_properties: ['firstName' => 'Alex']
);
```

---

### emailMessages-&gt;guardian()

[](#emailmessages-guardian)

Run Guardian content validation on an email message and return errors and warnings.

[API Reference](https://loops.so/docs/api-reference/run-guardian-checks)

#### Parameters

[](#parameters-57)

NameTypeRequiredNotes`$email_message_id`stringYesThe ID of the email message.#### Example

[](#example-52)

```
$result = $loops->emailMessages->guardian(email_message_id: 'cly8k3m0n0044jpx2bghepq45');
```

---

### transactionalGroups-&gt;list()

[](#transactionalgroups-list)

List transactional groups.

[API Reference](https://loops.so/docs/api-reference/list-transactional-groups)

#### Parameters

[](#parameters-58)

NameTypeRequiredNotes`$per_page`integerNoHow many results to return per page. Must be between 10 and 50. Defaults to 20 if omitted.`$cursor`stringNoA cursor, to return a specific page of results. Cursors can be found from the `pagination.nextCursor` value in each response.#### Example

[](#example-53)

```
$result = $loops->transactionalGroups->list();
```

---

### transactionalGroups-&gt;create()

[](#transactionalgroups-create)

Create a transactional group.

[API Reference](https://loops.so/docs/api-reference/create-transactional-group)

#### Parameters

[](#parameters-59)

NameTypeRequiredNotes`$name`stringYesCannot be the reserved name "Unsorted".`$description`stringNoAn optional description for the group.#### Example

[](#example-54)

```
$result = $loops->transactionalGroups->create(name: 'Account emails');
```

---

### transactionalGroups-&gt;get()

[](#transactionalgroups-get)

Get a transactional group by ID.

[API Reference](https://loops.so/docs/api-reference/get-transactional-group)

#### Parameters

[](#parameters-60)

NameTypeRequiredNotes`$transactional_group_id`stringYesThe ID of the transactional group.#### Example

[](#example-55)

```
$result = $loops->transactionalGroups->get(transactional_group_id: 'clv2w3x4y0288xbb0kqrsuv67');
```

---

### transactionalGroups-&gt;update()

[](#transactionalgroups-update)

Update a transactional group's name or description.

At least one field alongside `transactional_group_id` must be provided.

[API Reference](https://loops.so/docs/api-reference/update-transactional-group)

#### Parameters

[](#parameters-61)

NameTypeRequiredNotes`$transactional_group_id`stringYesThe ID of the transactional group.`$name`stringNoCannot be the reserved name "Unsorted".`$description`stringNo#### Example

[](#example-56)

```
$result = $loops->transactionalGroups->update(
  transactional_group_id: 'clv2w3x4y0288xbb0kqrsuv67',
  name: 'Updated name'
);
```

---

Testing
-------

[](#testing)

```
vendor/bin/phpunit
```

---

Contributing
------------

[](#contributing)

Bug reports and pull requests are welcome. Please read our [Contributing Guidelines](CONTRIBUTING.md).

###  Health Score

50

—

FairBetter than 95% of packages

Maintenance95

Actively maintained with recent releases

Popularity31

Limited adoption so far

Community7

Small or concentrated contributor base

Maturity54

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~57 days

Recently: every ~75 days

Total

9

Last Release

49d ago

Major Versions

v0.1.0 → v1.0.02025-05-06

v1.0.2 → v2.0.02025-09-01

v2.1.0 → v3.0.02026-05-21

PHP version history (2 changes)v0.1.0PHP ^8.0

v3.1.0PHP ^8.1

### Community

Maintainers

![](https://www.gravatar.com/avatar/63fd256c61e7fa59f97ef683f6a182d97766b41554e22a53ff1a7c5e9914d7fe?d=identicon)[danrowden](/maintainers/danrowden)

---

Top Contributors

[![danrowden](https://avatars.githubusercontent.com/u/935215?v=4)](https://github.com/danrowden "danrowden (46 commits)")

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/loops-so-loops/health.svg)

```
[![Health](https://phpackages.com/badges/loops-so-loops/health.svg)](https://phpackages.com/packages/loops-so-loops)
```

###  Alternatives

[tencentcloud/tencentcloud-sdk-php

TencentCloudApi php sdk

3661.3M49](/packages/tencentcloud-tencentcloud-sdk-php)[eslazarev/wildberries-sdk

Wildberries OpenAPI clients (generated).

353.6k](/packages/eslazarev-wildberries-sdk)[neuron-core/neuron-ai

The PHP Agentic Framework.

2.0k832.6k55](/packages/neuron-core-neuron-ai)[files.com/files-php-sdk

Files.com PHP SDK

2482.9k](/packages/filescom-files-php-sdk)[volcengine/volcengine-php-sdk

119.5k](/packages/volcengine-volcengine-php-sdk)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
