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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
## v3.4.0 - Sep 1, 2026

OpenAPI 1.21.9:

- Added `workflows->delete()` to delete a workflow.
- Campaign, transactional, and workflow responses now include a `url` field pointing to the resource in the Loops app.

## v3.3.0 - Aug 7, 2026

OpenAPI 1.21.6:
Expand Down
60 changes: 60 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ You can use custom contact properties in API calls. Please make sure to [add cus
- [workflows->create()](#workflows-create)
- [workflows->get()](#workflows-get)
- [workflows->update()](#workflows-update)
- [workflows->delete()](#workflows-delete)
- [workflows->changeMailingList()](#workflows-changemailinglist)
- [workflows->getNode()](#workflows-getnode)
- [workflows->createNode()](#workflows-createnode)
Expand Down Expand Up @@ -917,6 +918,7 @@ $result = $loops->transactional->list(per_page: 15);
"data": [
{
"id": "clfn0k1yg001imo0fdeqg30i8",
"url": "https://app.loops.so/transactional/clfn0k1yg001imo0fdeqg30i8",
"name": "Welcome email",
"draftEmailMessageId": null,
"publishedEmailMessageId": "cly8k3m0n0044jpx2bghepq45",
Expand All @@ -926,6 +928,7 @@ $result = $loops->transactional->list(per_page: 15);
},
{
"id": "cll42l54f20i1la0lfooe3z12",
"url": "https://app.loops.so/transactional/cll42l54f20i1la0lfooe3z12",
"name": "Password reset",
"draftEmailMessageId": "cla3r8s9t0422ua56iqovab01",
"publishedEmailMessageId": "clb4s9t0u0533vb67jrpwbc12",
Expand All @@ -937,6 +940,7 @@ $result = $loops->transactional->list(per_page: 15);
},
{
"id": "clw6rbuwp01rmeiyndm80155l",
"url": "https://app.loops.so/transactional/clw6rbuwp01rmeiyndm80155l",
"name": "Team invite",
"draftEmailMessageId": "clc5t0u1v0644wc78ksqxcd23",
"publishedEmailMessageId": null,
Expand Down Expand Up @@ -979,6 +983,7 @@ $result = $loops->transactional->create(name: 'Welcome email');
```json
{
"id": "clfq6dinn000yl70fgwwyp82l",
"url": "https://app.loops.so/transactional/clfq6dinn000yl70fgwwyp82l",
"name": "Welcome email",
"draftEmailMessageId": "cly8k3m0n0044jpx2bghepq45",
"draftEmailMessageContentRevisionId": "clm9n4o6p0088lrz4dijslt67",
Expand Down Expand Up @@ -1376,6 +1381,7 @@ $result = $loops->campaigns->create(
{
"success": true,
"campaignId": "cln4o7p9q0110msw5ekjtmv78",
"url": "https://app.loops.so/campaigns/cln4o7p9q0110msw5ekjtmv78",
"name": "Spring announcement",
"status": "Draft",
"createdAt": "2025-01-01T00:00:00.000Z",
Expand Down Expand Up @@ -1655,6 +1661,27 @@ $result = $loops->workflows->create(
);
```

#### Response

```json
{
"id": "clw1a3b5c7d9e1f3g5h7i9j1",
"url": "https://app.loops.so/workflows/clw1a3b5c7d9e1f3g5h7i9j1",
"workflowRevisionId": null,
"status": "Draft",
"name": "Welcome series",
"description": "Onboarding emails for new signups",
"mailingListId": null,
"rootNodeId": "clt0u3v5w0232sy31kqvbzs34",
"nodes": {
"clt0u3v5w0232sy31kqvbzs34": {
"typeName": "BlankTrigger",
"nextNodeIds": ["clt0u3v5w0232sy31kqvbzs35"]
}
}
}
```

---

### workflows->get()
Expand Down Expand Up @@ -1706,6 +1733,39 @@ $result = $loops->workflows->update(

---

### workflows->delete()

Delete a workflow.

Successful deletion returns `null` (HTTP 204 No Content). If the workflow is currently sending or has queued contacts, the API returns HTTP 409. Retry with `confirm_delete: true` to delete the workflow, stop sending, and cancel queued contacts.

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

#### Parameters

| Name | Type | Required | Notes |
| ------------------------- | ------- | -------- | --------------------------------------------------------------------- |
| `$workflow_id` | string | Yes | The ID of the workflow. |
| `$expected_revision_id` | string\|null | Yes | The latest workflow revision token. Pass `null` for older workflows. |
| `$confirm_delete` | boolean | No | Set to `true` after a confirmation-required 409 to confirm deleting a sending workflow or a workflow with queued contacts. |

#### Example

```php
$result = $loops->workflows->delete(
workflow_id: 'cls9t2u4v0210rx20jpuary23',
expected_revision_id: 'clrev1s10n2i3d4e5f6g7h8'
);

$result = $loops->workflows->delete(
workflow_id: 'cls9t2u4v0210rx20jpuary23',
expected_revision_id: 'clrev1s10n2i3d4e5f6g7h8',
confirm_delete: true
);
```

---

### workflows->changeMailingList()

Dry run or apply a workflow mailing list change.
Expand Down
4 changes: 4 additions & 0 deletions src/LoopsClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,10 @@ public function query(string $method, string $endpoint, array $options = []): mi
throw new Exceptions\APIError(statusCode: $response->getStatusCode(), json: $json);
}

if ($response->getStatusCode() === 204) {
return null;
}

return json_decode(json: $response->getBody()->getContents(), associative: true);
} catch (\Exception $e) {
// Pass through any exceptions
Expand Down
21 changes: 21 additions & 0 deletions src/Workflows.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,27 @@ public function update(
]);
}

public function delete(
string $workflow_id,
?string $expected_revision_id,
?bool $confirm_delete = null
): mixed {
$payload = array_merge(
['expectedRevisionId' => $expected_revision_id],
Util::omitNull([
'confirmDelete' => $confirm_delete,
])
);

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

public function changeMailingList(
string $workflow_id,
?string $expected_revision_id,
Expand Down
1 change: 1 addition & 0 deletions tests/CampaignsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ public function testCreateCampaign(): void
body: json_encode([
'success' => true,
'campaignId' => 'camp_123',
'url' => 'https://app.loops.so/campaigns/camp_123',
'name' => 'Spring announcement',
'status' => 'Draft',
'createdAt' => '2025-01-01T00:00:00.000Z',
Expand Down
4 changes: 4 additions & 0 deletions tests/TransactionalTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ public function testListTransactionals(): void
'data' => [
[
'id' => 'clfn0k1yg001imo0fdeqg30i8',
'url' => 'https://app.loops.so/transactional/clfn0k1yg001imo0fdeqg30i8',
'name' => 'Welcome email',
'draftEmailMessageId' => null,
'publishedEmailMessageId' => 'msg_abc123',
Expand Down Expand Up @@ -171,6 +172,7 @@ public function testListTransactionals(): void
$this->assertIsArray($result['data']);
$this->assertNotEmpty($result['data']);
$this->assertArrayHasKey('id', $result['data'][0]);
$this->assertArrayHasKey('url', $result['data'][0]);
$this->assertArrayHasKey('name', $result['data'][0]);
$this->assertArrayHasKey('draftEmailMessageId', $result['data'][0]);
$this->assertArrayHasKey('publishedEmailMessageId', $result['data'][0]);
Expand All @@ -194,6 +196,7 @@ public function testCreateTransactional(): void
status: 201,
body: json_encode([
'id' => 'txn_123',
'url' => 'https://app.loops.so/transactional/txn_123',
'name' => 'Welcome email',
'draftEmailMessageId' => 'msg_123',
'draftEmailMessageContentRevisionId' => 'rev_123',
Expand Down Expand Up @@ -244,6 +247,7 @@ public function testGetTransactional(): void
status: 200,
body: json_encode([
'id' => $transactionalId,
'url' => 'https://app.loops.so/transactional/' . $transactionalId,
'name' => 'Welcome email',
'draftEmailMessageId' => 'msg_123',
'publishedEmailMessageId' => 'msg_456',
Expand Down
53 changes: 53 additions & 0 deletions tests/WorkflowsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ public function testCreateWorkflow(): void
status: 200,
body: json_encode([
'id' => 'wf_123',
'url' => 'https://app.loops.so/workflows/wf_123',
'name' => 'Welcome series',
'status' => 'Draft',
])
Expand Down Expand Up @@ -104,6 +105,58 @@ public function testUpdateWorkflow(): void
$this->assertEquals('Updated name', $result['name']);
}

public function testDeleteWorkflow(): void
{
$workflowId = 'wf_123';

$this->mockHttpClient
->expects($this->once())
->method('delete')
->with(
'v1/workflows/' . $workflowId,
$this->callback(function ($options) {
return $options['json'] === [
'expectedRevisionId' => 'rev_123',
];
})
)
->willReturn(new Response(status: 204));

$result = $this->client->workflows->delete(
workflow_id: $workflowId,
expected_revision_id: 'rev_123'
);

$this->assertNull($result);
}

public function testDeleteWorkflowWithConfirmDelete(): void
{
$workflowId = 'wf_123';

$this->mockHttpClient
->expects($this->once())
->method('delete')
->with(
'v1/workflows/' . $workflowId,
$this->callback(function ($options) {
return $options['json'] === [
'expectedRevisionId' => 'rev_123',
'confirmDelete' => true,
];
})
)
->willReturn(new Response(status: 204));

$result = $this->client->workflows->delete(
workflow_id: $workflowId,
expected_revision_id: 'rev_123',
confirm_delete: true
);

$this->assertNull($result);
}

public function testChangeMailingList(): void
{
$workflowId = 'wf_123';
Expand Down
Loading