Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
## v3.2.0 - Aug 3, 2026

OpenAPI 1.21.1:

- Added workflow write APIs: `workflows->create()`, `workflows->update()`, `workflows->changeMailingList()`, `workflows->createNode()`, `workflows->updateNode()`, `workflows->deleteNode()`, `workflows->addBranch()`, and `workflows->deleteNodeRecursive()`.
- Added `eventPatterns->list()`, `eventPatterns->get()`, and `eventPatterns->getByName()`.
- Added `audienceSegments->create()`.
- Added `themes->create()` / `themes->update()` and `components->create()` / `components->update()`.
- Added `emailMessages->guardian()` and extra update fields (`cc_email`, `bcc_email`, `language_code`, `email_format`).
- `campaigns->update()` now rejects empty payloads.

## v3.1.1 - Jun 30, 2026

`$name` is no longer required when updating campaigns.
Expand Down
486 changes: 480 additions & 6 deletions README.md

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions src/AudienceSegments.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,17 @@ public function list(?int $per_page = null, ?string $cursor = null): mixed
]);
}

public function create(string $name, array $filter, ?string $description = null): mixed
{
return $this->client->query(method: 'POST', endpoint: 'v1/audience-segments', options: [
'json' => Util::omitNull([
'name' => $name,
'filter' => $filter,
'description' => $description,
])
]);
}

public function get(string $audience_segment_id): mixed
{
return $this->client->query(method: 'GET', endpoint: 'v1/audience-segments/' . $audience_segment_id);
Expand Down
3 changes: 3 additions & 0 deletions src/Campaigns.php
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ public function update(
'audienceFilter' => $audience_filter,
]),
);
if ($payload === []) {
throw new \InvalidArgumentException(message: 'At least one field must be provided.');
}

return $this->client->query(method: 'POST', endpoint: 'v1/campaigns/' . $campaign_id, options: [
'json' => $payload
Expand Down
25 changes: 25 additions & 0 deletions src/Components.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,33 @@ public function list(?int $per_page = null, ?string $cursor = null): mixed
]);
}

public function create(string $name, string $lmx): mixed
{
return $this->client->query(method: 'POST', endpoint: 'v1/components', options: [
'json' => [
'name' => $name,
'lmx' => $lmx,
]
]);
}

public function get(string $component_id): mixed
{
return $this->client->query(method: 'GET', endpoint: 'v1/components/' . $component_id);
}

public function update(string $component_id, ?string $name = null, ?string $lmx = null): mixed
{
$payload = Util::omitNull([
'name' => $name,
'lmx' => $lmx,
]);
if ($payload === []) {
throw new \InvalidArgumentException(message: 'At least one of name or lmx must be provided.');
}

return $this->client->query(method: 'POST', endpoint: 'v1/components/' . $component_id, options: [
'json' => $payload
]);
}
}
13 changes: 13 additions & 0 deletions src/EmailMessages.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ public function update(
?string $from_name = null,
?string $from_email = null,
?string $reply_to_email = null,
?string $cc_email = null,
?string $bcc_email = null,
?string $language_code = null,
?string $email_format = null,
?string $lmx = null,
?array $contact_properties_fallbacks = null,
?array $event_properties_fallbacks = null,
Expand All @@ -38,6 +42,10 @@ public function update(
'fromName' => $from_name,
'fromEmail' => $from_email,
'replyToEmail' => $reply_to_email,
'ccEmail' => $cc_email,
'bccEmail' => $bcc_email,
'languageCode' => $language_code,
'emailFormat' => $email_format,
'lmx' => $lmx,
'contactPropertiesFallbacks' => $contact_properties_fallbacks,
'eventPropertiesFallbacks' => $event_properties_fallbacks,
Expand Down Expand Up @@ -72,4 +80,9 @@ public function preview(
'json' => $payload
]);
}

public function guardian(string $email_message_id): mixed
{
return $this->client->query(method: 'GET', endpoint: 'v1/email-messages/' . $email_message_id . '/guardian');
}
}
38 changes: 38 additions & 0 deletions src/EventPatterns.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

namespace Loops;

use Loops\LoopsClient;

class EventPatterns
{
private $client;

public function __construct(LoopsClient $client)
{
$this->client = $client;
}

public function list(?int $per_page = null, ?string $cursor = null): mixed
{
return $this->client->query(method: 'GET', endpoint: 'v1/event-patterns', options: [
'query' => Util::omitNull([
'perPage' => $per_page,
'cursor' => $cursor,
])
]);
}

public function get(string $event_pattern_id): mixed
{
return $this->client->query(method: 'GET', endpoint: 'v1/event-patterns/' . $event_pattern_id);
}

public function getByName(string $event_name): mixed
{
return $this->client->query(
method: 'GET',
endpoint: 'v1/event-patterns/by-name/' . rawurlencode($event_name)
);
}
}
2 changes: 2 additions & 0 deletions src/LoopsClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class LoopsClient
public Uploads $uploads;
public AudienceSegments $audienceSegments;
public Workflows $workflows;
public EventPatterns $eventPatterns;
public TransactionalGroups $transactionalGroups;

public function __construct(string $api_key)
Expand Down Expand Up @@ -55,6 +56,7 @@ public function __construct(string $api_key)
$this->uploads = new Uploads(client: $this);
$this->audienceSegments = new AudienceSegments(client: $this);
$this->workflows = new Workflows(client: $this);
$this->eventPatterns = new EventPatterns(client: $this);
$this->transactionalGroups = new TransactionalGroups(client: $this);
}

Expand Down
24 changes: 24 additions & 0 deletions src/Themes.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,32 @@ public function list(?int $per_page = null, ?string $cursor = null): mixed
]);
}

public function create(string $name, ?array $styles = null): mixed
{
return $this->client->query(method: 'POST', endpoint: 'v1/themes', options: [
'json' => array_merge(['name' => $name], Util::omitNull([
'styles' => $styles,
]))
]);
}

public function get(string $theme_id): mixed
{
return $this->client->query(method: 'GET', endpoint: 'v1/themes/' . $theme_id);
}

public function update(string $theme_id, ?string $name = null, ?array $styles = null): mixed
{
$payload = Util::omitNull([
'name' => $name,
'styles' => $styles,
]);
if ($payload === []) {
throw new \InvalidArgumentException(message: 'At least one of name or styles must be provided.');
}

return $this->client->query(method: 'POST', endpoint: 'v1/themes/' . $theme_id, options: [
'json' => $payload
]);
}
}
167 changes: 167 additions & 0 deletions src/Workflows.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,180 @@ public function list(?int $per_page = null, ?string $cursor = null): mixed
]);
}

public function create(
string $name,
?string $description = null,
?string $mailing_list_id = null
): mixed {
return $this->client->query(method: 'POST', endpoint: 'v1/workflows', options: [
'json' => array_merge(['name' => $name], Util::omitNull([
'description' => $description,
'mailingListId' => $mailing_list_id,
]))
]);
}

public function get(string $workflow_id): mixed
{
return $this->client->query(method: 'GET', endpoint: 'v1/workflows/' . $workflow_id);
}

public function update(
string $workflow_id,
?string $expected_revision_id,
?string $name = null,
?string $description = null
): mixed {
$payload = Util::omitNull([
'name' => $name,
'description' => $description,
]);
if ($payload === []) {
throw new \InvalidArgumentException(message: 'At least one of name or description must be provided.');
}
$payload = array_merge(['expectedRevisionId' => $expected_revision_id], $payload);

return $this->client->query(method: 'POST', endpoint: 'v1/workflows/' . $workflow_id, options: [
'json' => $payload
]);
}

public function changeMailingList(
string $workflow_id,
?string $expected_revision_id,
mixed $mailing_list_id,
Comment thread
danrowden marked this conversation as resolved.
?bool $dry_run = null,
?string $queued_contact_policy = null
): mixed {
$payload = array_merge(
[
'expectedRevisionId' => $expected_revision_id,
'mailingListId' => $mailing_list_id,
],
Util::omitNull([
'dryRun' => $dry_run,
'queuedContactPolicy' => $queued_contact_policy,
])
);

return $this->client->query(method: 'POST', endpoint: 'v1/workflows/' . $workflow_id . '/mailing-list', options: [
'json' => $payload
]);
}

public function getNode(string $workflow_id, string $node_id): mixed
{
return $this->client->query(method: 'GET', endpoint: 'v1/workflows/' . $workflow_id . '/nodes/' . $node_id);
}

public function createNode(
string $workflow_id,
?string $expected_revision_id,
string $insert_mode,
string $node_type_name,
?string $from_node_id = null,
?string $to_node_id = null,
?string $before_node_id = null
): mixed {
$payload = [
'expectedRevisionId' => $expected_revision_id,
'insertMode' => $insert_mode,
'nodeTypeName' => $node_type_name,
];

if ($insert_mode === 'between') {
$payload['fromNodeId'] = $from_node_id;
$payload['toNodeId'] = $to_node_id;
} elseif ($insert_mode === 'before') {
$payload['beforeNodeId'] = $before_node_id;
} else {
throw new \InvalidArgumentException(message: 'insert_mode must be "between" or "before".');
}

return $this->client->query(method: 'POST', endpoint: 'v1/workflows/' . $workflow_id . '/nodes', options: [
'json' => $payload
]);
}

public function updateNode(
string $workflow_id,
string $node_id,
?string $expected_revision_id,
array $payload
): mixed {
return $this->client->query(
method: 'POST',
endpoint: 'v1/workflows/' . $workflow_id . '/nodes/' . $node_id,
options: [
'json' => [
'expectedRevisionId' => $expected_revision_id,
'payload' => $payload,
]
]
);
}

public function deleteNode(
string $workflow_id,
string $node_id,
?string $expected_revision_id,
?bool $dry_run = null,
?string $queued_contact_policy = null
): mixed {
$payload = array_merge(
['expectedRevisionId' => $expected_revision_id],
Util::omitNull([
'dryRun' => $dry_run,
'queuedContactPolicy' => $queued_contact_policy,
])
);

return $this->client->query(
method: 'DELETE',
endpoint: 'v1/workflows/' . $workflow_id . '/nodes/' . $node_id,
options: [
'json' => $payload
]
);
}

public function addBranch(
string $workflow_id,
string $node_id,
?string $expected_revision_id
): mixed {
return $this->client->query(
method: 'POST',
endpoint: 'v1/workflows/' . $workflow_id . '/nodes/' . $node_id . '/add-branch',
options: [
'json' => [
'expectedRevisionId' => $expected_revision_id,
]
]
);
}

public function deleteNodeRecursive(
string $workflow_id,
string $node_id,
?string $expected_revision_id,
?bool $dry_run = null,
?string $queued_contact_policy = null
): mixed {
$payload = array_merge(
['expectedRevisionId' => $expected_revision_id],
Util::omitNull([
'dryRun' => $dry_run,
'queuedContactPolicy' => $queued_contact_policy,
])
);

return $this->client->query(
method: 'DELETE',
endpoint: 'v1/workflows/' . $workflow_id . '/nodes/' . $node_id . '/recursive',
options: [
'json' => $payload
]
);
}
}
Loading
Loading