diff --git a/CHANGELOG.md b/CHANGELOG.md
index c22d04a..61028a4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,9 @@
+## v3.0.0 - May 19, 2026
+
+Added support for dedicated sending IPs, themes, components, campaigns, and email messages.
+
+Renamed methods for consistency: single-resource lookups use `get()`, collection endpoints use `list()`. This affects `contactProperties`, `mailingLists`, and `transactional` from prior versions.
+
## v2.1.0 - Apr 8, 2026
Added `contacts->checkSuppression()` and `contacts->removeSuppression()` for managing contact suppressions.
diff --git a/README.md b/README.md
index fccbaa2..42399b7 100644
--- a/README.md
+++ b/README.md
@@ -103,11 +103,22 @@ You can use custom contact properties in API calls. Please make sure to [add cus
- [contacts->checkSuppression()](#contacts-checksuppression)
- [contacts->removeSuppression()](#contacts-removesuppression)
- [contactProperties->create()](#contactproperties-create)
-- [contactProperties->get()](#contactproperties-get)
-- [mailingLists->get()](#mailinglists-get)
+- [contactProperties->list()](#contactproperties-list)
+- [mailingLists->list()](#mailinglists-list)
- [events->send()](#events-send)
- [transactional->send()](#transactional-send)
-- [transactional->get()](#transactional-get)
+- [transactional->list()](#transactional-list)
+- [dedicatedSendingIps->list()](#dedicatedsendingips-list)
+- [themes->list()](#themes-list)
+- [themes->get()](#themes-get)
+- [components->list()](#components-list)
+- [components->get()](#components-get)
+- [campaigns->list()](#campaigns-list)
+- [campaigns->create()](#campaigns-create)
+- [campaigns->get()](#campaigns-get)
+- [campaigns->update()](#campaigns-update)
+- [emailMessages->get()](#emailmessages-get)
+- [emailMessages->update()](#emailmessages-update)
---
@@ -524,7 +535,7 @@ Error handling is done through the `APIError` class, which provides `getStatusCo
---
-### contactProperties->get()
+### contactProperties->list()
Get a list of your account's contact properties.
@@ -539,9 +550,9 @@ Get a list of your account's contact properties.
#### Example
```php
-$result = $loops->contactProperties->get();
+$result = $loops->contactProperties->list();
-$result = $loops->contactProperties->get(list: "custom");
+$result = $loops->contactProperties->list(list: "custom");
```
#### Response
@@ -610,7 +621,7 @@ This method will return a list of contact property objects containing `key`, `la
---
-### mailingLists->get()
+### mailingLists->list()
Get a list of your account's mailing lists. [Read more about mailing lists](https://loops.so/docs/contacts/mailing-lists)
@@ -623,7 +634,7 @@ None
#### Example
```php
-$result = $loops->mailingLists->get();
+$result = $loops->mailingLists->list();
```
#### Response
@@ -830,7 +841,7 @@ If there is a problem with the request, a descriptive error message will be retu
---
-### transactional->get()
+### transactional->list()
Get a list of published transactional emails.
@@ -846,9 +857,9 @@ Get a list of published transactional emails.
#### Example
```php
-$result = $loops->transactional->get();
+$result = $loops->transactional->list();
-$result = $loops->transactional->get(per_page: 15);
+$result = $loops->transactional->list(per_page: 15);
```
#### Response
@@ -892,6 +903,262 @@ $result = $loops->transactional->get(per_page: 15);
---
+### dedicatedSendingIps->list()
+
+Get a list of Loops' dedicated sending IP addresses.
+
+[API Reference](https://loops.so/docs/api-reference/get-dedicated-sending-ips)
+
+#### Parameters
+
+None
+
+#### Example
+
+```php
+$result = $loops->dedicatedSendingIps->list();
+```
+
+#### Response
+
+```json
+["1.2.3.4", "5.6.7.8"]
+```
+
+---
+
+### themes->list()
+
+List email themes.
+
+[API Reference](https://loops.so/docs/api-reference/list-themes)
+
+#### Parameters
+
+| Name | Type | Required | Notes |
+| ----------- | ------- | -------- | ----------------------------------------------------------------------------------------------------------------------------- |
+| `$per_page` | integer | No | How many results to return per page. Must be between 10 and 50. Defaults to 20 if omitted. |
+| `$cursor` | string | No | A cursor, to return a specific page of results. Cursors can be found from the `pagination.nextCursor` value in each response. |
+
+#### Example
+
+```php
+$result = $loops->themes->list();
+
+$result = $loops->themes->list(per_page: 15, cursor: 'cursor123');
+```
+
+---
+
+### themes->get()
+
+Get a single email theme by ID.
+
+[API Reference](https://loops.so/docs/api-reference/get-theme)
+
+#### Parameters
+
+| Name | Type | Required | Notes |
+| ----------- | ------ | -------- | ------------------ |
+| `$theme_id` | string | Yes | The ID of the theme. |
+
+#### Example
+
+```php
+$result = $loops->themes->get(theme_id: 'theme_abc123');
+```
+
+---
+
+### components->list()
+
+List email components.
+
+[API Reference](https://loops.so/docs/api-reference/list-components)
+
+#### Parameters
+
+| Name | Type | Required | Notes |
+| ----------- | ------- | -------- | ----------------------------------------------------------------------------------------------------------------------------- |
+| `$per_page` | integer | No | How many results to return per page. Must be between 10 and 50. Defaults to 20 if omitted. |
+| `$cursor` | string | No | A cursor, to return a specific page of results. Cursors can be found from the `pagination.nextCursor` value in each response. |
+
+#### Example
+
+```php
+$result = $loops->components->list();
+```
+
+---
+
+### components->get()
+
+Get a single email component by ID.
+
+[API Reference](https://loops.so/docs/api-reference/get-component)
+
+#### Parameters
+
+| Name | Type | Required | Notes |
+| --------------- | ------ | -------- | ----------------------- |
+| `$component_id` | string | Yes | The ID of the component. |
+
+#### Example
+
+```php
+$result = $loops->components->get(component_id: 'component_abc123');
+```
+
+---
+
+### campaigns->list()
+
+List campaigns.
+
+[API Reference](https://loops.so/docs/api-reference/list-campaigns)
+
+#### Parameters
+
+| Name | Type | Required | Notes |
+| ----------- | ------- | -------- | ----------------------------------------------------------------------------------------------------------------------------- |
+| `$per_page` | integer | No | How many results to return per page. Must be between 10 and 50. Defaults to 20 if omitted. |
+| `$cursor` | string | No | A cursor, to return a specific page of results. Cursors can be found from the `pagination.nextCursor` value in each response. |
+
+#### Example
+
+```php
+$result = $loops->campaigns->list();
+```
+
+---
+
+### campaigns->create()
+
+Create a new draft campaign.
+
+[API Reference](https://loops.so/docs/api-reference/create-campaign)
+
+#### Parameters
+
+| Name | Type | Required | Notes |
+| ------- | ------ | -------- | ------------------ |
+| `$name` | string | Yes | The campaign name. |
+
+#### Example
+
+```php
+$result = $loops->campaigns->create(name: 'Spring announcement');
+```
+
+#### Response
+
+```json
+{
+ "success": true,
+ "campaignId": "camp_123",
+ "name": "Spring announcement",
+ "status": "Draft",
+ "createdAt": "2025-01-01T00:00:00.000Z",
+ "updatedAt": "2025-01-01T00:00:00.000Z",
+ "emailMessageId": "msg_123",
+ "emailMessageContentRevisionId": "rev_123"
+}
+```
+
+---
+
+### campaigns->get()
+
+Get a single campaign by ID.
+
+[API Reference](https://loops.so/docs/api-reference/get-campaign)
+
+#### Parameters
+
+| Name | Type | Required | Notes |
+| -------------- | ------ | -------- | --------------------- |
+| `$campaign_id` | string | Yes | The ID of the campaign. |
+
+#### Example
+
+```php
+$result = $loops->campaigns->get(campaign_id: 'camp_123');
+```
+
+---
+
+### campaigns->update()
+
+Update a draft campaign's name.
+
+[API Reference](https://loops.so/docs/api-reference/update-campaign)
+
+#### Parameters
+
+| Name | Type | Required | Notes |
+| -------------- | ------ | -------- | --------------------- |
+| `$campaign_id` | string | Yes | The ID of the campaign. |
+| `$name` | string | Yes | The updated name. |
+
+#### Example
+
+```php
+$result = $loops->campaigns->update(
+ campaign_id: 'camp_123',
+ name: 'Updated name'
+);
+```
+
+---
+
+### emailMessages->get()
+
+Get an email message, including its compiled LMX content.
+
+[API Reference](https://loops.so/docs/api-reference/get-email-message)
+
+#### Parameters
+
+| Name | Type | Required | Notes |
+| ------------------- | ------ | -------- | --------------------------- |
+| `$email_message_id` | string | Yes | The ID of the email message. |
+
+#### Example
+
+```php
+$result = $loops->emailMessages->get(email_message_id: 'msg_123');
+```
+
+---
+
+### emailMessages->update()
+
+Update an email message's subject, preview text, sender, or LMX content.
+
+[API Reference](https://loops.so/docs/api-reference/update-email-message)
+
+#### Parameters
+
+| Name | Type | Required | Notes |
+| ------------------- | ----- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `$email_message_id` | string | Yes | The ID of the email message. |
+| `$fields` | array | No | Fields to update. Use API field names: `expectedRevisionId`, `subject`, `previewText`, `fromName`, `fromEmail`, `replyToEmail`, `lmx`. Supply `expectedRevisionId` matching the current `contentRevisionId` to avoid 409 conflicts. |
+
+#### Example
+
+```php
+$result = $loops->emailMessages->update(
+ email_message_id: 'msg_123',
+ fields: [
+ 'expectedRevisionId' => 'rev_123',
+ 'subject' => 'Updated subject',
+ 'lmx' => 'Hello'
+ ]
+);
+```
+
+---
+
## Testing
```bash
diff --git a/src/Campaigns.php b/src/Campaigns.php
new file mode 100644
index 0000000..e4d863c
--- /dev/null
+++ b/src/Campaigns.php
@@ -0,0 +1,49 @@
+client = $client;
+ }
+
+ public function list(?int $per_page = null, ?string $cursor = null): mixed
+ {
+ $query = [];
+ if ($per_page !== null) {
+ $query['perPage'] = $per_page;
+ }
+ if ($cursor) {
+ $query['cursor'] = $cursor;
+ }
+
+ return $this->client->query(method: 'GET', endpoint: 'v1/campaigns', options: [
+ 'query' => $query
+ ]);
+ }
+
+ public function create(string $name): mixed
+ {
+ return $this->client->query(method: 'POST', endpoint: 'v1/campaigns', options: [
+ 'json' => ['name' => $name]
+ ]);
+ }
+
+ public function get(string $campaign_id): mixed
+ {
+ return $this->client->query(method: 'GET', endpoint: 'v1/campaigns/' . $campaign_id);
+ }
+
+ public function update(string $campaign_id, string $name): mixed
+ {
+ return $this->client->query(method: 'POST', endpoint: 'v1/campaigns/' . $campaign_id, options: [
+ 'json' => ['name' => $name]
+ ]);
+ }
+}
diff --git a/src/Components.php b/src/Components.php
new file mode 100644
index 0000000..9f38afa
--- /dev/null
+++ b/src/Components.php
@@ -0,0 +1,35 @@
+client = $client;
+ }
+
+ public function list(?int $per_page = null, ?string $cursor = null): mixed
+ {
+ $query = [];
+ if ($per_page !== null) {
+ $query['perPage'] = $per_page;
+ }
+ if ($cursor) {
+ $query['cursor'] = $cursor;
+ }
+
+ return $this->client->query(method: 'GET', endpoint: 'v1/components', options: [
+ 'query' => $query
+ ]);
+ }
+
+ public function get(string $component_id): mixed
+ {
+ return $this->client->query(method: 'GET', endpoint: 'v1/components/' . $component_id);
+ }
+}
diff --git a/src/ContactProperties.php b/src/ContactProperties.php
index b6cb2b4..eb45062 100644
--- a/src/ContactProperties.php
+++ b/src/ContactProperties.php
@@ -24,7 +24,7 @@ public function create(string $name, string $type = 'string' | 'number' | 'boole
'json' => $payload
]);
}
- public function get(?string $list = null): mixed
+ public function list(?string $list = null): mixed
{
$query = [];
if ($list) {
diff --git a/src/DedicatedSendingIps.php b/src/DedicatedSendingIps.php
new file mode 100644
index 0000000..35a4a89
--- /dev/null
+++ b/src/DedicatedSendingIps.php
@@ -0,0 +1,20 @@
+client = $client;
+ }
+
+ public function list(): mixed
+ {
+ return $this->client->query(method: 'GET', endpoint: 'v1/dedicated-sending-ips');
+ }
+}
diff --git a/src/EmailMessages.php b/src/EmailMessages.php
new file mode 100644
index 0000000..6aa99b3
--- /dev/null
+++ b/src/EmailMessages.php
@@ -0,0 +1,27 @@
+client = $client;
+ }
+
+ public function get(string $email_message_id): mixed
+ {
+ return $this->client->query(method: 'GET', endpoint: 'v1/email-messages/' . $email_message_id);
+ }
+
+ public function update(string $email_message_id, array $fields = []): mixed
+ {
+ return $this->client->query(method: 'POST', endpoint: 'v1/email-messages/' . $email_message_id, options: [
+ 'json' => $fields
+ ]);
+ }
+}
diff --git a/src/LoopsClient.php b/src/LoopsClient.php
index 09ad506..84c505b 100644
--- a/src/LoopsClient.php
+++ b/src/LoopsClient.php
@@ -13,6 +13,11 @@ class LoopsClient
public MailingLists $mailingLists;
public Transactional $transactional;
public ContactProperties $contactProperties;
+ public DedicatedSendingIps $dedicatedSendingIps;
+ public Themes $themes;
+ public Components $components;
+ public Campaigns $campaigns;
+ public EmailMessages $emailMessages;
public function __construct(string $api_key)
{
@@ -31,6 +36,11 @@ public function __construct(string $api_key)
$this->mailingLists = new MailingLists(client: $this);
$this->transactional = new Transactional(client: $this);
$this->contactProperties = new ContactProperties(client: $this);
+ $this->dedicatedSendingIps = new DedicatedSendingIps(client: $this);
+ $this->themes = new Themes(client: $this);
+ $this->components = new Components(client: $this);
+ $this->campaigns = new Campaigns(client: $this);
+ $this->emailMessages = new EmailMessages(client: $this);
}
/**
diff --git a/src/MailingLists.php b/src/MailingLists.php
index 505dc5e..d1bf6d9 100644
--- a/src/MailingLists.php
+++ b/src/MailingLists.php
@@ -13,7 +13,7 @@ public function __construct(LoopsClient $client)
$this->client = $client;
}
- public function get()
+ public function list()
{
return $this->client->query(method: 'GET', endpoint: 'v1/lists');
}
diff --git a/src/Themes.php b/src/Themes.php
new file mode 100644
index 0000000..8c7d6e7
--- /dev/null
+++ b/src/Themes.php
@@ -0,0 +1,35 @@
+client = $client;
+ }
+
+ public function list(?int $per_page = null, ?string $cursor = null): mixed
+ {
+ $query = [];
+ if ($per_page !== null) {
+ $query['perPage'] = $per_page;
+ }
+ if ($cursor) {
+ $query['cursor'] = $cursor;
+ }
+
+ return $this->client->query(method: 'GET', endpoint: 'v1/themes', options: [
+ 'query' => $query
+ ]);
+ }
+
+ public function get(string $theme_id): mixed
+ {
+ return $this->client->query(method: 'GET', endpoint: 'v1/themes/' . $theme_id);
+ }
+}
diff --git a/src/Transactional.php b/src/Transactional.php
index c46b64b..b7220d5 100644
--- a/src/Transactional.php
+++ b/src/Transactional.php
@@ -35,7 +35,7 @@ public function send(
]);
}
- public function get(?int $per_page = 20, ?string $cursor = null): mixed
+ public function list(?int $per_page = 20, ?string $cursor = null): mixed
{
$query = [
diff --git a/tests/CampaignsTest.php b/tests/CampaignsTest.php
new file mode 100644
index 0000000..39ee98d
--- /dev/null
+++ b/tests/CampaignsTest.php
@@ -0,0 +1,130 @@
+mockHttpClient = $this->createMock(\GuzzleHttp\Client::class);
+ $this->client = new LoopsClient('test_api_key');
+ $this->client->setHttpClient($this->mockHttpClient);
+ }
+
+ public function testListCampaigns(): void
+ {
+ $this->mockHttpClient
+ ->expects($this->once())
+ ->method('get')
+ ->with('v1/campaigns')
+ ->willReturn(new Response(
+ status: 200,
+ body: json_encode([
+ 'success' => true,
+ 'pagination' => ['nextCursor' => null],
+ 'data' => []
+ ])
+ ));
+
+ $result = $this->client->campaigns->list();
+
+ $this->assertTrue($result['success']);
+ }
+
+ public function testCreateCampaign(): void
+ {
+ $this->mockHttpClient
+ ->expects($this->once())
+ ->method('post')
+ ->with(
+ 'v1/campaigns',
+ $this->callback(function ($options) {
+ return $options['json']['name'] === 'Spring announcement';
+ })
+ )
+ ->willReturn(new Response(
+ status: 201,
+ body: json_encode([
+ 'success' => true,
+ 'campaignId' => 'camp_123',
+ 'name' => 'Spring announcement',
+ 'status' => 'Draft',
+ 'createdAt' => '2025-01-01T00:00:00.000Z',
+ 'updatedAt' => '2025-01-01T00:00:00.000Z',
+ 'emailMessageId' => 'msg_123',
+ 'emailMessageContentRevisionId' => 'rev_123'
+ ])
+ ));
+
+ $result = $this->client->campaigns->create(name: 'Spring announcement');
+
+ $this->assertEquals('camp_123', $result['campaignId']);
+ }
+
+ public function testFindCampaign(): void
+ {
+ $campaignId = 'camp_123';
+
+ $this->mockHttpClient
+ ->expects($this->once())
+ ->method('get')
+ ->with('v1/campaigns/' . $campaignId)
+ ->willReturn(new Response(
+ status: 200,
+ body: json_encode([
+ 'success' => true,
+ 'campaignId' => $campaignId,
+ 'name' => 'Spring announcement',
+ 'status' => 'Draft',
+ 'createdAt' => '2025-01-01T00:00:00.000Z',
+ 'updatedAt' => '2025-01-01T00:00:00.000Z',
+ 'emailMessageId' => 'msg_123'
+ ])
+ ));
+
+ $result = $this->client->campaigns->get(campaign_id: $campaignId);
+
+ $this->assertEquals($campaignId, $result['campaignId']);
+ }
+
+ public function testUpdateCampaign(): void
+ {
+ $campaignId = 'camp_123';
+
+ $this->mockHttpClient
+ ->expects($this->once())
+ ->method('post')
+ ->with(
+ 'v1/campaigns/' . $campaignId,
+ $this->callback(function ($options) {
+ return $options['json']['name'] === 'Updated name';
+ })
+ )
+ ->willReturn(new Response(
+ status: 200,
+ body: json_encode([
+ 'success' => true,
+ 'campaignId' => $campaignId,
+ 'name' => 'Updated name',
+ 'status' => 'Draft',
+ 'createdAt' => '2025-01-01T00:00:00.000Z',
+ 'updatedAt' => '2025-01-02T00:00:00.000Z',
+ 'emailMessageId' => 'msg_123'
+ ])
+ ));
+
+ $result = $this->client->campaigns->update(
+ campaign_id: $campaignId,
+ name: 'Updated name'
+ );
+
+ $this->assertEquals('Updated name', $result['name']);
+ }
+}
diff --git a/tests/ComponentsTest.php b/tests/ComponentsTest.php
new file mode 100644
index 0000000..b5f5ce0
--- /dev/null
+++ b/tests/ComponentsTest.php
@@ -0,0 +1,65 @@
+mockHttpClient = $this->createMock(\GuzzleHttp\Client::class);
+ $this->client = new LoopsClient('test_api_key');
+ $this->client->setHttpClient($this->mockHttpClient);
+ }
+
+ public function testListComponents(): void
+ {
+ $this->mockHttpClient
+ ->expects($this->once())
+ ->method('get')
+ ->with('v1/components', $this->callback(function ($options) {
+ return $options['query'] === [];
+ }))
+ ->willReturn(new Response(
+ status: 200,
+ body: json_encode([
+ 'success' => true,
+ 'pagination' => ['nextCursor' => null],
+ 'data' => []
+ ])
+ ));
+
+ $result = $this->client->components->list();
+
+ $this->assertTrue($result['success']);
+ }
+
+ public function testFindComponent(): void
+ {
+ $componentId = 'component_abc123';
+
+ $this->mockHttpClient
+ ->expects($this->once())
+ ->method('get')
+ ->with('v1/components/' . $componentId)
+ ->willReturn(new Response(
+ status: 200,
+ body: json_encode([
+ 'success' => true,
+ 'componentId' => $componentId,
+ 'name' => 'Header',
+ 'lmx' => ''
+ ])
+ ));
+
+ $result = $this->client->components->get(component_id: $componentId);
+
+ $this->assertEquals($componentId, $result['componentId']);
+ }
+}
diff --git a/tests/DedicatedSendingIpsTest.php b/tests/DedicatedSendingIpsTest.php
new file mode 100644
index 0000000..833106d
--- /dev/null
+++ b/tests/DedicatedSendingIpsTest.php
@@ -0,0 +1,36 @@
+mockHttpClient = $this->createMock(\GuzzleHttp\Client::class);
+ $this->client = new LoopsClient('test_api_key');
+ $this->client->setHttpClient($this->mockHttpClient);
+ }
+
+ public function testGetDedicatedSendingIps(): void
+ {
+ $this->mockHttpClient
+ ->expects($this->once())
+ ->method('get')
+ ->with('v1/dedicated-sending-ips')
+ ->willReturn(new Response(
+ status: 200,
+ body: json_encode(['1.2.3.4', '5.6.7.8'])
+ ));
+
+ $result = $this->client->dedicatedSendingIps->list();
+
+ $this->assertEquals(['1.2.3.4', '5.6.7.8'], $result);
+ }
+}
diff --git a/tests/EmailMessagesTest.php b/tests/EmailMessagesTest.php
new file mode 100644
index 0000000..028db8c
--- /dev/null
+++ b/tests/EmailMessagesTest.php
@@ -0,0 +1,94 @@
+mockHttpClient = $this->createMock(\GuzzleHttp\Client::class);
+ $this->client = new LoopsClient('test_api_key');
+ $this->client->setHttpClient($this->mockHttpClient);
+ }
+
+ public function testFindEmailMessage(): void
+ {
+ $emailMessageId = 'msg_123';
+
+ $this->mockHttpClient
+ ->expects($this->once())
+ ->method('get')
+ ->with('v1/email-messages/' . $emailMessageId)
+ ->willReturn(new Response(
+ status: 200,
+ body: json_encode([
+ 'success' => true,
+ 'emailMessageId' => $emailMessageId,
+ 'campaignId' => 'camp_123',
+ 'subject' => 'Hello',
+ 'previewText' => '',
+ 'fromName' => 'Loops',
+ 'fromEmail' => 'hello',
+ 'replyToEmail' => '',
+ 'lmx' => '',
+ 'contentRevisionId' => 'rev_123',
+ 'updatedAt' => '2025-01-01T00:00:00.000Z'
+ ])
+ ));
+
+ $result = $this->client->emailMessages->get(email_message_id: $emailMessageId);
+
+ $this->assertEquals($emailMessageId, $result['emailMessageId']);
+ }
+
+ public function testUpdateEmailMessage(): void
+ {
+ $emailMessageId = 'msg_123';
+ $fields = [
+ 'expectedRevisionId' => 'rev_123',
+ 'subject' => 'Updated subject',
+ 'lmx' => 'Hello'
+ ];
+
+ $this->mockHttpClient
+ ->expects($this->once())
+ ->method('post')
+ ->with(
+ 'v1/email-messages/' . $emailMessageId,
+ $this->callback(function ($options) use ($fields) {
+ return $options['json'] === $fields;
+ })
+ )
+ ->willReturn(new Response(
+ status: 200,
+ body: json_encode([
+ 'success' => true,
+ 'emailMessageId' => $emailMessageId,
+ 'campaignId' => 'camp_123',
+ 'subject' => 'Updated subject',
+ 'previewText' => '',
+ 'fromName' => 'Loops',
+ 'fromEmail' => 'hello',
+ 'replyToEmail' => '',
+ 'lmx' => $fields['lmx'],
+ 'contentRevisionId' => 'rev_456',
+ 'updatedAt' => '2025-01-02T00:00:00.000Z'
+ ])
+ ));
+
+ $result = $this->client->emailMessages->update(
+ email_message_id: $emailMessageId,
+ fields: $fields
+ );
+
+ $this->assertEquals('Updated subject', $result['subject']);
+ $this->assertEquals('rev_456', $result['contentRevisionId']);
+ }
+}
diff --git a/tests/ThemesTest.php b/tests/ThemesTest.php
new file mode 100644
index 0000000..b4995b6
--- /dev/null
+++ b/tests/ThemesTest.php
@@ -0,0 +1,72 @@
+mockHttpClient = $this->createMock(\GuzzleHttp\Client::class);
+ $this->client = new LoopsClient('test_api_key');
+ $this->client->setHttpClient($this->mockHttpClient);
+ }
+
+ public function testListThemes(): void
+ {
+ $this->mockHttpClient
+ ->expects($this->once())
+ ->method('get')
+ ->with(
+ 'v1/themes',
+ $this->callback(function ($options) {
+ return $options['query']['perPage'] === 20
+ && $options['query']['cursor'] === 'cursor123';
+ })
+ )
+ ->willReturn(new Response(
+ status: 200,
+ body: json_encode([
+ 'success' => true,
+ 'pagination' => ['nextCursor' => null],
+ 'data' => []
+ ])
+ ));
+
+ $result = $this->client->themes->list(per_page: 20, cursor: 'cursor123');
+
+ $this->assertTrue($result['success']);
+ }
+
+ public function testFindTheme(): void
+ {
+ $themeId = 'theme_abc123';
+
+ $this->mockHttpClient
+ ->expects($this->once())
+ ->method('get')
+ ->with('v1/themes/' . $themeId)
+ ->willReturn(new Response(
+ status: 200,
+ body: json_encode([
+ 'success' => true,
+ 'themeId' => $themeId,
+ 'name' => 'Default',
+ 'styles' => [],
+ 'isDefault' => true,
+ 'createdAt' => '2025-01-01T00:00:00.000Z',
+ 'updatedAt' => '2025-01-01T00:00:00.000Z'
+ ])
+ ));
+
+ $result = $this->client->themes->get(theme_id: $themeId);
+
+ $this->assertEquals($themeId, $result['themeId']);
+ }
+}
diff --git a/tests/TransactionalTest.php b/tests/TransactionalTest.php
index 90bd220..6121b52 100644
--- a/tests/TransactionalTest.php
+++ b/tests/TransactionalTest.php
@@ -146,7 +146,7 @@ public function testGetTransactionals(): void
));
// Make the API call
- $result = $this->client->transactional->get(
+ $result = $this->client->transactional->list(
per_page: $per_page,
cursor: $cursor
);